সরাসরি পর্বে যান
পর্ব ০৬ · ৬২-এর মধ্যে · মডিউল ১

ডেটার বিট-রূপ — অক্ষর, Unicode, endianness আর bit manipulation

Data Representation — Characters, Unicode, Endianness & Bit Manipulation

Taught in Bangla, this lesson reads one 32-bit pattern as an integer, a float, text or an instruction, covers bytes, words, alignment and struct padding checked with sizeof and offsetof, ASCII's design, Unicode code points, the UTF-8 bit layout, UTF-16 surrogate pairs and Bangla conjuncts and graphemes, little- and big-endian memory with safe detection and byte swapping, and bit manipulation with masks, field extraction, shifts and popcount, using live C programs.

এই পর্বে যা শিখবেন

  • একটি bit pattern-কে চারভাবে পড়া, আর কেন “type” আসলে পড়ার নিয়ম
  • byte, word, alignment আর padding-এর নিয়ম — sizeof আর offsetof দিয়ে যাচাই
  • ASCII-র নকশার কৌশল, UTF-8-এর bit layout, UTF-16-এর surrogate pair আর বাংলার যুক্তাক্ষর
  • little-endian আর big-endian মেমরি, নিরাপদে endianness শনাক্ত করা আর byte উল্টানো
  • hardware-স্তরের bit কৌশল: mask, field extract-insert, shift-এর দুই রকম, x & (x − 1) আর popcount

১ · সবকিছুই বিট — একই ৩২ বিট, চার রকম অর্থ

মহল্লার একটি বাড়ির দেয়ালে লেখা “১২”. পিয়নের কাছে এটি বাড়ির নম্বর, বাসের যাত্রীর কাছে রুট নম্বর, আর দোকানির কাছে হয়তো ডিমের দাম। লেখাটি একই, অর্থ ঠিক করে কে পড়ছে আর কোন উদ্দেশ্যে। কম্পিউটারের মেমরিতেও ঠিক এমনই ঘটে। মেমরিতে থাকে শুধু bit — 0 আর 1. সেখানে কোথাও লেখা থাকে না “এটি একটি সংখ্যা” বা “এটি একটি অক্ষর”।

পর্ব ০৪-এ আপনি binary আর hexadecimal দেখেছেন, আর পর্ব ০৫-এ দেখেছেন integer কীভাবে বিটে বসে। এবার একটি চমকপ্রদ পরীক্ষা করি। নিচের ৩২টি বিট নিন:

0100 0001 0100 0010 0100 0011 0011 0111
hex-এ 0x41424337

এই একই বিটগুলো চারজন আলাদা “পাঠক” চারভাবে পড়ে:

কে পড়ছেকীভাবে পড়েফল
unsigned integer (পর্ব ০৫)প্রতিটি বিটের ঘরের মান যোগ1094861623
IEEE 754 float (পর্ব ২৩)sign 0, exponent 1000 0010 = ১৩০ অর্থাৎ ২৩, বাকি ২৩ বিট ভগ্নাংশপ্রায় 12.141410
চারটি ASCII অক্ষরপ্রতিটি byte একটি অক্ষর, মেমরির ক্রমে (little-endian মেশিনে 37 43 42 41)"7CBA"
RISC-V CPU (পর্ব ২৭)নিচের ৭ বিট 0110111 = LUI, বিট ১১–৭ = 00110 = x6, ওপরের ২০ বিট immediatelui t1, 0x41424

প্রতিটি পড়াই “সঠিক”। CPU যদি program counter দিয়ে এই word-টি আনে, এটি instruction; একটি flw (float load) নির্দেশ আনলে এটি দশমিক সংখ্যা; printf("%s") আনলে এটি লেখা। অর্থাৎ type কোনো বিটের গায়ে লেখা থাকে না — type হলো পড়ার নিয়ম, আর সেই নিয়ম থাকে instruction-এর ভেতরে। High-level ভাষায় compiler type মনে রাখে আর ঠিক instruction বেছে দেয়; মেশিনের স্তরে কেবল বিট।

মূল ধারণা

ডেটার অর্থ = বিট + ব্যাখ্যার নিয়ম। একই নিয়ম সবাই মেনে চললে (একটি “চুক্তি” বা standard) তবেই এক মেশিনের লেখা আরেক মেশিন পড়তে পারে। এই পর্বের বাকি সবকিছু — ASCII, UTF-8, endianness, alignment — আসলে এমনই কিছু চুক্তি।

এই ভাবনার একটি ঐতিহাসিক মূল আছে। ১৯৪০-এর দশকে stored-program ধারণা (পর্ব ০১ আর ০২-এ দেখেছেন) বলেছিল, প্রোগ্রাম আর ডেটা একই মেমরিতে থাকবে। এর অর্থই হলো — instruction আর সংখ্যা মেমরিতে আলাদা করে চেনার কোনো উপায় নেই। এই নমনীয়তা থেকেই compiler, loader আর JIT সম্ভব হয়েছে (একটি প্রোগ্রাম আরেকটি প্রোগ্রামের instruction “ডেটা” হিসেবে লিখতে পারে)। আবার এখান থেকেই এসেছে বহু নিরাপত্তা-সমস্যা: ডেটা হিসেবে ঢোকানো বিট যদি instruction হিসেবে চলে যায়। আধুনিক CPU তাই page-এ “execute নিষেধ” বিট (NX বা XN) রাখে — সেটি পর্ব ৫০-এ আসবে।

২ · Bit, nibble, byte আর word — মাপের একক

বাজারে চাল মাপা হয় ছটাক, সের, মণে। বিটেরও তেমন কয়েকটি প্রচলিত মাপ আছে:

এককবিটকী বোঝায়
bit১একটি সুইচ — চালু বা বন্ধ
nibble৪ঠিক একটি hex অঙ্ক (0–F); BCD-তে একটি দশমিক অঙ্ক
byte৮আজকের প্রায় সব মেশিনে ঠিকানার সবচেয়ে ছোট একক — প্রতিটি byte-এর আলাদা ঠিকানা
wordISA-ভেদেCPU যে মাপের ডেটা স্বাভাবিকভাবে একবারে নিয়ে কাজ করে

byte সবসময় ৮ বিট ছিল না। ১৯৬০-এর দশকে ৬ বা ৯ বিটের “byte”-ও ছিল; IBM System/360 (১৯৬৪) ৮ বিটের byte জনপ্রিয় করে, আর পরে এটিই নিয়ম হয়ে যায়। নেটওয়ার্কের RFC-গুলো তাই সাবধানে “octet” (ঠিক ৮ বিট) শব্দটি ব্যবহার করে। C ভাষাও নিশ্চয়তা দেয় শুধু “char অন্তত ৮ বিট” (CHAR_BIT).

Word — যে শব্দের মানে ISA-ভেদে বদলায়

“Word” শব্দটি সবচেয়ে বিভ্রান্তিকর, কারণ প্রতিটি ISA-র নিজের পরিভাষা আছে:

ISA১৬ বিট৩২ বিট৬৪ বিট
RISC-V (এই কোর্সের মূল ISA)halfword (lh, sh)word (lw, sw)doubleword (ld, sd) — RV64-এও word = ৩২ বিট
x86 / x86-64WORD (8086-এর উত্তরাধিকার)DWORDQWORD
ARM (AArch32 আর AArch64)halfwordworddoubleword

তাহলে “৬৪-বিট CPU” মানে কী? সাধারণত এর অর্থ — general-purpose register ৬৪ বিট চওড়া, আর memory address-ও ৬৪ বিটের register-এ রাখা হয়। ফলে একটি pointer ৮ byte. তবে বাস্তবে x86-64 CPU-গুলো সাধারণত ৪৮ বিটের (নতুন কিছুতে ৫৭ বিটের) virtual address ব্যবহার করে — বাকি বিটগুলোর নিয়ম পর্ব ৫০-এ।

C ভাষায় int, long-এর মাপও ISA আর OS মিলে ঠিক হয়, একে বলে data model. Linux আর macOS-এর ৬৪-বিট প্রোগ্রাম LP64 মানে: long আর pointer ৮ byte, int ৪ byte. কিন্তু ৬৪-বিট Windows LLP64: সেখানে long মাত্র ৪ byte, শুধু long long আর pointer ৮ byte. একই C কোড দুই OS-এ ভিন্ন আচরণ করতে পারে — তাই বিট-গোনা কাজে uint32_t, uint64_t-এর মতো নির্দিষ্ট মাপের type (<stdint.h>) ব্যবহার করাই পেশাদার নিয়ম।

byte হলো রেলস্টেশনের প্ল্যাটফর্মে আঁকা প্রতিটি দাগ-কাটা ঘর, আর word হলো একটি বগি। যাত্রী (ডেটা) যেকোনো ঘরে দাঁড়াতে পারেন, কিন্তু ট্রেন থামে বগির হিসাবে — বগির দরজা যদি ঘরের দাগের সাথে মেলে, ওঠানামা এক ধাক্কায় শেষ। পরের অংশে দেখব, দরজা না মিললে কী ঝামেলা হয়।

৩ · Alignment আর padding — struct কেন বড় হয়ে যায়

মেমরি আর cache (পর্ব ৪৫) ডেটা দেয় নির্দিষ্ট মাপের টুকরো ধরে — যেমন ৮ byte-এর bus, ৬৪ byte-এর cache line. একটি ৪-byte int যদি ঠিকানা ৪-এর গুণিতকে থাকে, সেটি কখনো দুটি টুকরোর সীমানা পার হয় না: এক বারে পড়া যায়। একেই বলে natural alignment: N byte-এর একটি সাধারণ মান (N = ১, ২, ৪, ৮) এমন ঠিকানায় বসবে, যা N-এর গুণিতক।

ঠিকানা ৬ থেকে শুরু হওয়া একটি ৪-byte মান ৬, ৭, ৮, ৯ — চারটি byte নেয়। ৮-byte-এর টুকরো ধরে আনলে ৬–৭ পড়ে প্রথম টুকরোয় (০–৭), আর ৮–৯ পড়ে দ্বিতীয়টিতে (৮–১৫)। ফলে দুবার আনতে হয়, তারপর জোড়া লাগাতে হয়। ISA-ভেদে এর ফল আলাদা:

  • x86-64: misaligned access চলে, তবে cache line বা page-এর সীমানা পার হলে ধীর হতে পারে। আর atomic instruction যদি দুটি cache line জুড়ে থাকে (“split lock”), পুরো সিস্টেমের গতিতে বড় ধাক্কা লাগে।
  • ARMv8 (AArch64): সাধারণ মেমরিতে বেশিরভাগ load/store misaligned হলেও চলে, কিন্তু atomic/exclusive instruction আর device memory-তে alignment বাধ্যতামূলক।
  • RISC-V: spec বলে misaligned access hardware নিজে সামলাতে পারে, অথবা একটি exception তুলে software-কে দিয়ে সামলাতে পারে — দ্বিতীয় ক্ষেত্রে এটি শতগুণ ধীর হতে পারে। তাই RISC-V-তে aligned ডেটাই নিরাপদ।
  • পুরোনো অনেক RISC (যেমন MIPS) misaligned load-এ সরাসরি exception দিত।

Struct layout-এর তিনটি নিয়ম

Compiler তাই struct-এর member-দের মাঝে ফাঁকা byte বসিয়ে দেয় — একে বলে padding. x86-64 Linux-এর ABI (System V) অনুযায়ী নিয়মগুলো এমন:

  1. প্রতিটি member বসে এমন offset-এ, যা তার নিজের alignment-এর গুণিতক (char ১, short ২, int ৪, double/long/pointer ৮)।
  2. পুরো struct-এর alignment = member-দের মধ্যে সবচেয়ে বড় alignment.
  3. sizeof struct-এর alignment-এর গুণিতক পর্যন্ত বাড়ানো হয় (শেষে tail padding).

তৃতীয় নিয়মটি কেন? একটি array struct S a[2] ভাবুন। a[1] শুরু হয় ঠিক sizeof(struct S) byte পরে। a[0] যদি aligned ঠিকানা A-তে থাকে (A হলো alignment k-এর গুণিতক), তবে a[1]-এর ঠিকানা A + sizeof. এটি k-এর গুণিতক হবে কেবল তখনই, যখন sizeof নিজে k-এর গুণিতক। অর্থাৎ array-র প্রতিটি উপাদান aligned রাখতে হলে tail padding অপরিহার্য — এটি একটি ছোট গাণিতিক প্রমাণ।

এবার হিসাব করি। struct {char c; int i;}: c offset ০-তে (১ byte), i-কে ৪-এর গুণিতকে যেতে হবে, তাই ১, ২, ৩ — তিনটি byte padding, i বসে ৪-এ, শেষ হয় ৮-এ। মোট ৮ byte, যদিও ডেটা মাত্র ৫ byte. নিচের প্রোগ্রাম চালিয়ে compiler-এর কাছ থেকেই সত্যিটা জেনে নিন — offsetof বলে দেয় প্রতিটি member কোথায় বসেছে:

#include <stdio.h>
#include <stddef.h>
#include <stdalign.h>

struct A    { char c; int i; };
struct Bad  { char a; double d; char b; };
struct Good { double d; char a; char b; };

int main(void) {
    printf("char %zu, short %zu, int %zu, long %zu, double %zu, pointer %zu\n",
           sizeof(char), sizeof(short), sizeof(int), sizeof(long),
           sizeof(double), sizeof(void *));
    printf("struct A    size %2zu align %zu | c@%zu i@%zu\n",
           sizeof(struct A), alignof(struct A),
           offsetof(struct A, c), offsetof(struct A, i));
    printf("struct Bad  size %2zu align %zu | a@%zu d@%zu b@%zu\n",
           sizeof(struct Bad), alignof(struct Bad), offsetof(struct Bad, a),
           offsetof(struct Bad, d), offsetof(struct Bad, b));
    printf("struct Good size %2zu align %zu | d@%zu a@%zu b@%zu\n",
           sizeof(struct Good), alignof(struct Good), offsetof(struct Good, d),
           offsetof(struct Good, a), offsetof(struct Good, b));
    printf("1000 x Bad = %zu bytes, 1000 x Good = %zu bytes\n",
           1000 * sizeof(struct Bad), 1000 * sizeof(struct Good));
    return 0;
}

Wandbox-এর gcc (Linux x86-64) আউটপুট:

char 1, short 2, int 4, long 8, double 8, pointer 8
struct A    size  8 align 4 | c@0 i@4
struct Bad  size 24 align 8 | a@0 d@8 b@16
struct Good size 16 align 8 | d@0 a@8 b@9
1000 x Bad = 24000 bytes, 1000 x Good = 16000 bytes

struct Bad-এ a-র পরে ৭ byte ফাঁকা (যাতে d ৮-এ বসে), আর b-র পরে আরও ৭ byte tail padding — ১০ byte ডেটার জন্য ২৪ byte! একই তিনটি member বড় থেকে ছোট ক্রমে সাজালে (Good) a আর b পাশাপাশি ৮ আর ৯-এ বসে, শেষে ৬ byte tail padding, মোট ১৬. হাজারটি রেকর্ডে ৮,০০০ byte বাঁচল — আর বেশি রেকর্ড এক cache line-এ আঁটে বলে গতিও বাড়ে। প্রথম লাইনে long 8 খেয়াল করুন: এটি LP64; Windows-এ একই কোড long 4 দেখাবে।

নিয়মটি মনে রাখার সহজ উপায়: member-দের alignment-এর বড় থেকে ছোট ক্রমে সাজান — padding সাধারণত সবচেয়ে কমে। উল্টো দিকে, নেটওয়ার্ক packet বা ফাইল-ফরম্যাটের header হুবহু মেলাতে অনেকে __attribute__((packed)) দিয়ে padding বন্ধ করেন। তখন member misaligned হতে পারে — x86-এ চলে, কিন্তু কিছু ISA-তে ধীর বা ভুল। C-র দিক থেকে struct-এর পুরো আলোচনা আছে C কোর্সের struct আর union পর্বে; এখানে আমরা দেখলাম hardware-এর কারণটি।

৪ · অক্ষরের কোড — ASCII, control code আর পুরোনো code page

লেখা রাখার কৌশল একটাই: প্রতিটি অক্ষরকে একটি নম্বর দিন, তারপর নম্বরটি বিটে রাখুন। প্রাথমিক কথাগুলো — ASCII-র টেবিল, BCD, EBCDIC — বিস্তারিত আছে Computer Fundamentals কোর্সের কোড পর্বে। এখানে আমরা দেখব নকশার ভেতরের কৌশল, যেগুলো hardware আর low-level কোডে কাজে লাগে।

ASCII (১৯৬৩ সালে প্রথম প্রকাশিত) ৭ বিটের — ১২৮টি ঘর। প্রথম ৩২টি (০–৩১) আর শেষেরটি (১২৭) control code: এগুলো ছাপার অক্ষর নয়, যন্ত্রকে নির্দেশ। যেমন ৭ = BEL (টেলিপ্রিন্টারে ঘণ্টা বাজানো), ৯ = TAB, ১০ = LF (নতুন লাইন), ১৩ = CR (ছাপার মাথা বাঁ দিকে ফেরানো), ২৭ = ESC. Windows-এর ফাইলে লাইনের শেষে CR LF দুটিই থাকে, Linux-এ শুধু LF — পুরোনো টেলিপ্রিন্টারের সেই দুই আলাদা যান্ত্রিক কাজের উত্তরাধিকার।

টেবিলের ভেতরে লুকানো বিট-কৌশল

অক্ষরদশমিকbinaryযা খেয়াল করবেন
'A'৬৫100 0001বড় হাতের অক্ষরে বিট ৫ = 0
'a'৯৭110 0001শুধু বিট ৫ (মান ৩২) আলাদা
'0'৪৮011 0000নিচের nibble-ই অঙ্কের মান
'9'৫৭011 1001c & 0x0F = ৯
DEL১২৭111 1111সাতটি বিটই 1

'A' আর 'a'-র পার্থক্য ঠিক ৩২ — এটি কাকতালীয় নয়। ৩২ = ২৫, অর্থাৎ বড় আর ছোট হাতের অক্ষর কেবল বিট ৫-এ আলাদা। তাই একটি XOR (c ^ 0x20) অক্ষরের case উল্টে দেয়, আর c & ~0x20 ছোট হাতের অক্ষরকে বড় করে (শুধু ইংরেজি অক্ষরের ক্ষেত্রে; অন্য চিহ্নে এটি ভুল ফল দেবে)। পুরোনো টেলিপ্রিন্টার আর কীবোর্ডের hardware এভাবে একটি তার চালু-বন্ধ করেই Shift-এর কাজ সারত।

আরেকটি মজার উদাহরণ: টার্মিনালে Ctrl + কোনো অক্ষর চাপলে ঐতিহ্যগতভাবে সেই অক্ষরের নিচের ৫ বিট পাঠানো হয় (c & 0x1F). 'C' = 0x43, তাই Ctrl+C = ৩ (ETX, “লেখা শেষ”) — যা দিয়ে আপনি প্রোগ্রাম থামান। Ctrl+[ দেয় ২৭, অর্থাৎ ESC. আর DEL কেন ১২৭? কাগজের টেপে ভুল অক্ষরের ওপর সাতটি ছিদ্রই করে দিলে সেটি “মুছে” যেত — সব বিট 1.

৮ম বিট আর code page-এর বিশৃঙ্খলা

byte যখন ৮ বিটের হলো, ASCII-র বাইরে আরও ১২৮টি ঘর (১২৮–২৫৫) খালি থাকল। প্রতিটি কোম্পানি আর দেশ সেগুলো নিজের মতো ভরল — একেকটি তালিকার নাম code page. ISO-8859-1 (Latin-1)-এ byte 0xE9 হলো é, আর পুরোনো IBM PC-র CP437-এ একই byte হলো Θ. ফাইলটি কোন code page-এ লেখা, সেটি ফাইলের ভেতরে লেখা থাকত না। ফলে ভুল তালিকা দিয়ে পড়লে আসত আবর্জনা লেখা — জাপানিরা এর নাম দিয়েছিল mojibake.

বাংলার ক্ষেত্রে আরও অদ্ভুত একটি পথ নেওয়া হয়েছিল। Unicode জনপ্রিয় হওয়ার আগে বিজয়ের মতো বহুল প্রচলিত পদ্ধতিতে ফাইলে আসলে ইংরেজি বা ANSI কোডই রাখা হতো, আর একটি বিশেষ font সেই কোডগুলোর জায়গায় বাংলা আকৃতি এঁকে দিত। সেই font না থাকলে লেখাটি দেখায় অর্থহীন ইংরেজি অক্ষরের সারি — আর সার্চ, সাজানো বা অন্য সফটওয়্যারে পাঠানো প্রায় অসম্ভব। পুরোনো সরকারি নথি বা পত্রিকার আর্কাইভ রূপান্তরে আজও এই সমস্যা দেখা যায়। সমাধান এসেছে Unicode দিয়ে।

৫ · Unicode — code point আর encoding

Unicode (সংস্করণ ১.০ প্রকাশিত ১৯৯১ সালে) পৃথিবীর প্রায় সব লিপির প্রতিটি অক্ষরকে একটি স্থায়ী নম্বর দেয় — code point, লেখা হয় U+ আর hex দিয়ে। পরিসর U+0000 থেকে U+10FFFF, অর্থাৎ ২১ বিটে আঁটে। কিন্তু নম্বর পেলেই কাজ শেষ নয়: নম্বরটি byte-এ কীভাবে লিখবেন, সেটি আলাদা প্রশ্ন — তার উত্তর হলো encoding. মূল তিনটি encoding: UTF-8, UTF-16 আর UTF-32.

UTF-8-এর bit layout

UTF-8 একটি code point-কে ১ থেকে ৪ byte-এ লেখে। প্রথম byte-এর শুরুর বিটগুলো বলে দেয় মোট কয়টি byte, আর বাকি প্রতিটি byte শুরু হয় 10 দিয়ে:

Code point-এর পরিসরbytebit patternডেটা-বিট
U+0000 – U+007F১0xxxxxxx৭
U+0080 – U+07FF২110xxxxx 10xxxxxx১১
U+0800 – U+FFFF৩1110xxxx 10xxxxxx 10xxxxxx১৬
U+10000 – U+10FFFF৪11110xxx 10xxxxxx 10xxxxxx 10xxxxxx২১

বাংলার পুরো ব্লক U+0980–U+09FF তৃতীয় সারিতে পড়ে, তাই প্রতিটি বাংলা code point UTF-8-এ ৩ byte. এবার “ক” (U+0995)-কে হাতে encode করে দেখি — প্রতিটি ধাপে “পরে” চাপুন:

ধাপ ১পরিসর দেখে byte সংখ্যা
code point = U+0995
0x0800 ≤ 0x0995 ≤ 0xFFFF   -> 3 bytes
template   = 1110xxxx 10xxxxxx 10xxxxxx
             (4 + 6 + 6 = 16 payload bits)
ধাপ ২১৬ বিটের binary
hex      0    9    9    5
bits  0000 1001 1001 0101
ধাপ ৩ডান দিক থেকে ৬-৬-৪ ভাগ
0000 1001 1001 0101
->  0000 │ 100110 │ 010101
    4 bits  6 bits   6 bits
ধাপ ৪ছাঁচে বসানো
1110 xxxx  ->  1110 0000
10 xxxxxx  ->  10 100110
10 xxxxxx  ->  10 010101
ধাপ ৫hex-এ পড়া
1110 0000  =  E0
1010 0110  =  A6
1001 0101  =  95
"ক" in UTF-8 = E0 A6 95
ধাপ ৬মাঝখান থেকে পড়া শুরু হলে
stream:  41 E0 A6 95 E0 A6 BE
reader starts here:  ^ A6 = 10100110 (continuation)
skip 10xxxxxx bytes -> next lead E0 = 1110xxxx
-> back in sync after at most 3 bytes
  1. কয়টি byte লাগবে? 0x0995 পড়ে U+0800 থেকে U+FFFF পরিসরে, তাই ৩ byte-এর ছাঁচ 1110xxxx 10xxxxxx 10xxxxxx. ছাঁচে ফাঁকা ঘর (x) মোট ৪ + ৬ + ৬ = ১৬টি।
  2. Binary-তে লেখা। প্রতিটি hex অঙ্ক একটি nibble: 0 = 0000, 9 = 1001, 9 = 1001, 5 = 0101. মোট ১৬ বিট — ছাঁচের ১৬টি ঘরের সাথে হুবহু মেলে।
  3. ডান দিক থেকে ভাগ। শেষ ৬ বিট 010101, তার আগের ৬ বিট 100110, আর বাকি ৪ বিট 0000. ডান দিক থেকে ভাগ করার কারণ — নিচের বিটগুলোই সবসময় শেষ byte-এ যায়।
  4. ছাঁচে বসানো। প্রথম byte-এ 1110-এর পরে 0000, দ্বিতীয়তে 10-এর পরে 100110, তৃতীয়তে 10-এর পরে 010101. শুরুর “1110” বলে “আমি ৩ byte-এর অক্ষরের প্রথম byte”, আর প্রতিটি “10” বলে “আমি আগের অক্ষরের ধারাবাহিক অংশ”।
  5. Hex-এ পড়া। 1110 0000 = E0, 1010 0110 = A6, 1001 0101 = 95. অর্থাৎ “ক” = E0 A6 95 — এটি নিচের JavaScript simulator আর ব্রাউজারের TextEncoder দুটোই নিশ্চিত করবে।
  6. Self-synchronizing. ধরুন একটি প্রোগ্রাম ভুল করে মাঝখানের A6 থেকে পড়া শুরু করল। A6 শুরু হয় 10 দিয়ে, তাই সে জানে এটি কোনো অক্ষরের শুরু নয়; সর্বোচ্চ ৩টি byte বাদ দিলেই পরের প্রথম byte (E0) পাওয়া যায়। একটি byte নষ্ট হলে ক্ষতি সীমিত থাকে একটি অক্ষরে।

UTF-8 কেন এত ভালো নকশা

  • ASCII-compatible: U+007F পর্যন্ত অক্ষর হুবহু ASCII-র byte. পুরোনো ইংরেজি ফাইল কোনো পরিবর্তন ছাড়াই বৈধ UTF-8.
  • Self-synchronizing: প্রথম byte (0xxxxxxx, 11xxxxxx) আর ধারাবাহিক byte (10xxxxxx) কখনো এক রকম দেখায় না — উপরের ধাপ ৬.
  • ০ byte নেই: U+0000 ছাড়া কোনো অক্ষরের encoding-এ 0x00 আসে না, তাই C-র '\0'-শেষ string ভাঙে না। আর ASCII-র / বা " কখনো বহু-byte অক্ষরের ভেতরে লুকিয়ে থাকে না।
  • Endianness-মুক্ত: একক হলো byte, তাই byte-ক্রমের প্রশ্নই নেই (অংশ ৭)।
  • সাজানো: UTF-8 byte-গুলোকে সংখ্যার মতো তুলনা করলে যে ক্রম আসে, code point-এর ক্রমও তাই।

একটি নিয়ম কঠোর: প্রতিটি code point-কে সবচেয়ে ছোট সম্ভাব্য রূপে লিখতে হবে। যেমন / (U+002F) কে দুই byte-এ C0 AF লেখা “overlong” encoding — অবৈধ। ২০০০-এর আশেপাশে কিছু web server এমন byte যাচাই না করেই গ্রহণ করত, আর আক্রমণকারী ../-কে ছদ্মবেশে পাঠিয়ে নিষিদ্ধ ফোল্ডারে ঢুকতে পারত। তাই decoder-কে অবশ্যই overlong, surrogate (U+D800–U+DFFF) আর U+10FFFF-এর চেয়ে বড় মান বাতিল করতে হয়।

UTF-16 আর surrogate pair, UTF-32

UTF-16-এর একক ১৬ বিট। U+FFFF পর্যন্ত অক্ষর (বাংলা সহ) একটি ১৬-বিটের একক। তার ওপরের অক্ষর, যেমন ইমোজি, লাগে দুটি একক — surrogate pair. নিয়মটি পুরোপুরি বিট-কৌশল:

  1. code point থেকে 0x10000 বাদ দিন — ফল ২০ বিটে আঁটে।
  2. ওপরের ১০ বিট + 0xD800 = high surrogate; নিচের ১০ বিট + 0xDC00 = low surrogate.

😀 = U+1F600: 0x1F600 − 0x10000 = 0xF600. ওপরের ১০ বিট 0x3D, তাই D800 + 3D = D83D; নিচের ১০ বিট 0x200, তাই DC00 + 200 = DE00. U+D800–U+DFFF পরিসরটি এজন্যই কোনো অক্ষরকে দেওয়া হয়নি। UTF-16-এ একক দুই byte-এর, তাই byte-ক্রমের প্রশ্ন আছে: ফাইলের শুরুতে BOM (U+FEFF) থাকলে FE FF মানে big-endian, FF FE মানে little-endian. Windows-এর API, Java আর JavaScript-এর string ভেতরে UTF-16.

UTF-32 প্রতিটি code point সরাসরি ৪ byte-এ রাখে — সরল, কিন্তু ইংরেজির জন্য চারগুণ জায়গা। Linux-এ C-র wchar_t ৪ byte (UTF-32), Windows-এ ২ byte (UTF-16) — আরেকটি “একই নাম, ভিন্ন মাপ”-এর উদাহরণ।

৬ · বাংলা Unicode-এ — যুক্তাক্ষর, grapheme আর string-এর “দৈর্ঘ্য”

বাংলা লিপি পেয়েছে U+0980 থেকে U+09FF — ১২৮টি ঘরের একটি ব্লক। ক = U+0995, া (আ-কার) = U+09BE, ং = U+0982, ্ (হসন্ত, Unicode-এর নামে virama) = U+09CD. বাংলা অঙ্ক ০–৯ বসে U+09E6–U+09EF-এ, পরপর — ঠিক ASCII-র অঙ্কের মতো, তাই অঙ্কের মান = code point − 0x09E6.

কিন্তু বাংলায় সবচেয়ে গুরুত্বপূর্ণ কথা: যুক্তাক্ষরের জন্য আলাদা code point নেই। “ক্ষ” আসলে তিনটি code point — ক (U+0995) + ্ (U+09CD) + ষ (U+09B7). ফাইলে থাকে এই তিনটি, ৯ byte. পর্দায় একটি যুক্ত আকৃতি আঁকার কাজ করে font আর text shaping engine (যেমন HarfBuzz), যারা font-এর ভেতরের নিয়ম দেখে তিনটিকে একটি glyph-এ বদলায়। হসন্ত দৃশ্যমান রাখতে চাইলে মাঝে ZWNJ (U+200C) বসানো হয় — তখন দেখায় ক্‌ষ।

মানুষ যাকে “একটি অক্ষর” বলে দেখে, Unicode তার নাম দিয়েছে grapheme (আরও নির্দিষ্ট করে “extended grapheme cluster”). তাই একটি string-এর “দৈর্ঘ্য” চারভাবে মাপা যায়, আর বাংলায় চারটি উত্তর প্রায়ই চার রকম:

লেখাUTF-8 byteUTF-16 একক (JS .length)code pointgrapheme
A১১১১
ক৩১১১
বাংলা১৫৫৫ (ব া ং ল া)২ (বাং, লা)
ক্ষ৯৩৩১*
😀৪২১১

* যুক্তাক্ষরকে এক grapheme ধরার নিয়ম Unicode যোগ করেছে সংস্করণ ১৫.১-এ (২০২৩); পুরোনো সফটওয়্যার “ক্ষ”-কে ২টি grapheme গুনতে পারে। আর “বাংলা”-য় ং আলাদা grapheme নয়, কারণ Unicode একে আগের অক্ষরের সাথে জুড়ে থাকা চিহ্ন (spacing mark) হিসেবে দেখে।

এর বাস্তব ফল অনেক। মোবাইলের SMS-এ ইংরেজি বার্তা ১৬০ অক্ষর পর্যন্ত এক SMS, কিন্তু বাংলা থাকলে বার্তা যায় ১৬-বিটের UCS-2-এ, তখন এক SMS-এ মাত্র ৭০টি একক — বাংলায় একই কথা লিখতে বেশি SMS লাগে। Database-এ VARCHAR(10) byte গোনে না code point — ডেটাবেস-ভেদে আলাদা; MySQL-এর পুরোনো utf8 আসলে সর্বোচ্চ ৩ byte রাখে, তাই ইমোজি রাখতে লাগে utf8mb4. আর কোনো লেখা byte গুনে মাঝখান থেকে কাটলে একটি বাংলা অক্ষরের ৩ byte-এর মাঝে কাটা পড়তে পারে — পর্দায় আসে � (U+FFFD, “replacement character”).

হাতের হিসাব যন্ত্রে মিলিয়ে নিন। নিচের simulator নিজে UTF-8 encode আর decode করে, প্রতিটি লেখার code point, UTF-8 byte, UTF-16 একক আর grapheme গোনে, আর ব্রাউজারের নিজস্ব TextEncoder-এর সাথে মিলিয়ে দেখে। Run চাপুন:

// Hand-made UTF-8 encoder/decoder, checked against the browser's TextEncoder.
function encode(cp) {                       // one code point -> 1..4 bytes
  if (cp < 0x80) return [cp];                                   // 0xxxxxxx
  if (cp < 0x800) return [0xC0 | (cp >> 6), 0x80 | (cp & 63)];  // 110xxxxx 10xxxxxx
  if (cp < 0x10000)
    return [0xE0 | (cp >> 12), 0x80 | ((cp >> 6) & 63), 0x80 | (cp & 63)];
  return [0xF0 | (cp >> 18), 0x80 | ((cp >> 12) & 63),
          0x80 | ((cp >> 6) & 63), 0x80 | (cp & 63)];
}
function decode(bytes) {                    // bytes -> code points
  const out = [];
  for (let i = 0; i < bytes.length; ) {
    const b = bytes[i], n = b < 0x80 ? 1 : b < 0xE0 ? 2 : b < 0xF0 ? 3 : 4;
    let cp = n === 1 ? b : b & (0xFF >> (n + 1));    // payload of lead byte
    for (let k = 1; k < n; k++) cp = (cp << 6) | (bytes[i + k] & 63);
    out.push(cp); i += n;
  }
  return out;
}
const hex = (a, w) => a.map(v => v.toString(16).toUpperCase().padStart(w, "0")).join(" ");
const seg = typeof Intl.Segmenter === "function"
  ? new Intl.Segmenter("bn", { granularity: "grapheme" }) : null;
const samples = { A: "A", ka: "ক", bangla: "বাংলা",
                  ksha: "ক্ষ", emoji: "\u{1F600}" };
for (const [name, s] of Object.entries(samples)) {
  const cps = [...s].map(c => c.codePointAt(0));
  const mine = cps.flatMap(encode);
  const ref = [...new TextEncoder().encode(s)];
  const units = Array.from({ length: s.length }, (_, i) => s.charCodeAt(i));
  const same = hex(mine, 2) === hex(ref, 2) && hex(decode(mine), 4) === hex(cps, 4);
  console.log(`${name.padEnd(6)} U+${hex(cps, 4).replace(/ /g, " U+")}`);
  console.log(`       UTF-8  ${hex(mine, 2)}  (${mine.length} bytes, TextEncoder ${same ? "agrees" : "DIFFERS"})`);
  console.log(`       UTF-16 ${hex(units, 4)}  (.length = ${s.length})`);
  console.log(`       code points ${cps.length}, graphemes ${seg ? [...seg.segment(s)].length : "n/a"}`);
}

আউটপুট (নির্ধারিত — প্রতিবার একই; grapheme সংখ্যা ব্রাউজারের Unicode সংস্করণের ওপর নির্ভর করে):

A      U+0041
       UTF-8  41  (1 bytes, TextEncoder agrees)
       UTF-16 0041  (.length = 1)
       code points 1, graphemes 1
ka     U+0995
       UTF-8  E0 A6 95  (3 bytes, TextEncoder agrees)
       UTF-16 0995  (.length = 1)
       code points 1, graphemes 1
bangla U+09AC U+09BE U+0982 U+09B2 U+09BE
       UTF-8  E0 A6 AC E0 A6 BE E0 A6 82 E0 A6 B2 E0 A6 BE  (15 bytes, TextEncoder agrees)
       UTF-16 09AC 09BE 0982 09B2 09BE  (.length = 5)
       code points 5, graphemes 2
ksha   U+0995 U+09CD U+09B7
       UTF-8  E0 A6 95 E0 A7 8D E0 A6 B7  (9 bytes, TextEncoder agrees)
       UTF-16 0995 09CD 09B7  (.length = 3)
       code points 3, graphemes 1
emoji  U+1F600
       UTF-8  F0 9F 98 80  (4 bytes, TextEncoder agrees)
       UTF-16 D83D DE00  (.length = 2)
       code points 1, graphemes 1

হাতে করা “ক” = E0 A6 95 মিলে গেল, আর প্রতিটি লাইনে TextEncoder agrees. “বাংলা”: ৫টি code point, প্রতিটি ৩ byte, তাই ১৫ byte; JavaScript-এর .length = ৫, কারণ সেটি UTF-16 একক গোনে; কিন্তু চোখে দেখা “অক্ষর” মাত্র ২টি। ইমোজির .length = ২ — কারণ surrogate pair D83D DE00. আর “ক্ষ” ৩টি code point হলেও এই ব্রাউজারে ১টি grapheme. decode()-এ লক্ষ করুন 0xFF >> (n + 1): ২ byte-এর অক্ষরে প্রথম byte থেকে নিচের ৫ বিট, ৩ byte-এ ৪ বিট, ৪ byte-এ ৩ বিট রাখে — ঠিক উপরের ছাঁচের x-গুলো।

টার্মিনালেও একই সত্য দেখা যায়। Linux-এ od একটি ফাইল বা লেখার কাঁচা byte দেখায়:

$ printf 'বাংলা' | od -An -tx1
 e0 a6 ac e0 a6 be e0 a6 82 e0 a6 b2 e0 a6 be
$ printf 'বাংলা' | wc -c
15

৭ · Endianness — byte কোন ক্রমে বসে

একটি ৪-byte মান 0x12345678 ভাবুন। এর সবচেয়ে “ভারী” byte হলো 12 (most significant byte, MSB), আর সবচেয়ে হালকা 78 (least significant byte, LSB). মেমরিতে প্রতিটি byte-এর আলাদা ঠিকানা। প্রশ্ন: ঠিকানা 0x1000 থেকে 0x1003 — এই চারটি ঘরে কোন byte কোথায় বসবে? এর দুটি প্রচলিত উত্তর, আর সেই পছন্দের নামই endianness.

মানচারটি byte, ভারী থেকে হালকা
value = 0x12345678
        12   34   56   78
        MSB             LSB
addresses: 0x1000 0x1001 0x1002 0x1003
Big-endianভারী byte আগে (ছোট ঠিকানায়)
address : 0x1000 0x1001 0x1002 0x1003
byte    :   12     34     56     78
reads like the number written on paper
Little-endianহালকা byte আগে (ছোট ঠিকানায়)
address : 0x1000 0x1001 0x1002 0x1003
byte    :   78     56     34     12
x86-64, RISC-V, ARM (in practice)
প্রথম byte পড়াঠিকানা 0x1000-এর একটি byte
little-endian : byte at 0x1000 = 0x78
big-endian    : byte at 0x1000 = 0x12
little-endian bonus: 16-bit read at 0x1000 = 0x5678
                     (the low half, same address)
নেটওয়ার্কে পাঠানোwire-এ big-endian
x86 memory      : 78 56 34 12
htonl(x)        : byte swap
memory to send  : 12 34 56 78
on the wire     : 12 34 56 78  (network byte order)
receiver ntohl  : back to its own order
  1. চারটি byte. 0x12345678-এ ১২ সবচেয়ে ভারী (MSB), ৭৮ সবচেয়ে হালকা (LSB). মেমরিতে রাখার জন্য চারটি পরপর ঠিকানা 0x1000–0x1003 পাওয়া গেছে। এখন সিদ্ধান্ত — কোন byte প্রথম ঘরে?
  2. Big-endian: ভারী byte (১২) সবচেয়ে ছোট ঠিকানায়, তারপর ৩৪, ৫৬, ৭৮. মেমরি-dump বাঁ থেকে ডানে পড়লে ঠিক কাগজে লেখা সংখ্যার মতোই দেখায়। নেটওয়ার্ক protocol, Java-র ক্লাস ফাইল, PNG ছবি — এরা এই ক্রম মানে।
  3. Little-endian: হালকা byte (৭৮) সবচেয়ে ছোট ঠিকানায়, অর্থাৎ ক্রম উল্টো: ৭৮ ৫৬ ৩৪ ১২. x86 আর x86-64, RISC-V আর বাস্তবে প্রায় সব ARM সিস্টেম (Android, iPhone, Apple Silicon Mac) এভাবে রাখে।
  4. প্রথম byte দিয়ে চেনা। ঠিকানা 0x1000 থেকে শুধু একটি byte পড়লে little-endian-এ আসে 0x78, big-endian-এ 0x12 — endianness শনাক্ত করার কৌশল এটাই। little-endian-এর একটি সুবিধাও দেখা যাচ্ছে: একই ঠিকানা থেকে ছোট মাপে (১৬ বিট) পড়লে মানের নিচের অর্ধেক (0x5678) পাওয়া যায়, ঠিকানা বদলাতে হয় না।
  5. নেটওয়ার্কে পাঠানো। TCP/IP header-এর সব সংখ্যা big-endian — একে বলে network byte order. তাই x86 মেশিন পাঠানোর আগে htonl/htons দিয়ে byte উল্টে নেয়, আর প্রাপক ntohl/ntohs দিয়ে নিজের ক্রমে ফেরায়। big-endian মেশিনে এই ফাংশনগুলো কিছুই করে না।

নামটি এসেছে একটি ব্যঙ্গ-উপন্যাস থেকে। Jonathan Swift-এর Gulliver's Travels (১৭২৬)-এ লিলিপুট আর ব্লেফুস্কুর মানুষেরা যুদ্ধ করে — সেদ্ধ ডিম কোন দিক থেকে ভাঙতে হবে, বড় দিক (Big-Endians) না ছোট দিক (Little-Endians), তা নিয়ে। ১৯৮০ সালে Danny Cohen তাঁর বিখ্যাত নোট “On Holy Wars and a Plea for Peace”-এ এই নাম দুটি byte-ক্রমের বিতর্কে ব্যবহার করেন বলে সাধারণভাবে জানা যায়, আর সেখান থেকেই পরিভাষাটি ছড়িয়ে পড়ে। তাঁর মূল কথা ছিল: দুটির কোনোটিই আসলে “ভালো” নয়, দরকার শুধু একটি চুক্তি।

কে কোন দিকে

  • Little-endian: Intel 8080 থেকে আজকের x86-64; RISC-V (মূলত); বেশিরভাগ ARM সিস্টেম; ফাইল-ফরম্যাটে BMP আর ZIP.
  • Big-endian: Motorola 68000, ঐতিহ্যগত SPARC আর PowerPC, IBM-এর mainframe (z); নেটওয়ার্ক protocol (network byte order); PNG আর JPEG ফাইল।
  • Bi-endian: ARM, PowerPC আর MIPS hardware দুই ক্রমেই চালানো যায় — কোনটি চলবে, তা সাধারণত চালু (boot) হওয়ার সময় hardware-এর সেটিং, firmware বা OS ঠিক করে দেয়। যেমন PowerPC-তে আজকের Linux বিতরণ প্রায়ই little-endian “ppc64le”; MIPS-এর দুই রূপের নাম mips আর mipsel.

নিচের প্রোগ্রামটি একটি int, একটি float আর একটি ছোট string-এর byte মেমরির ক্রমে ছাপে, তারপর অংশ ১-এর 0x41424337-কে একই বিট রেখে float আর অক্ষর হিসেবে পড়ে। খেয়াল করুন, type punning (এক type-এর বিট আরেক type হিসেবে পড়া) এখানে memcpy দিয়ে করা হয়েছে — pointer cast করে *(float *)&w লিখলে C-র strict aliasing নিয়ম ভাঙে, যা undefined behavior:

#include <stdio.h>
#include <string.h>
#include <stdint.h>

static void dump(const char *label, const void *p, size_t n) {
    const unsigned char *b = p;            /* any object can be viewed as bytes */
    printf("%-16s", label);
    for (size_t i = 0; i < n; i++) printf(" %02x", b[i]);
    printf("\n");
}

int main(void) {
    uint32_t x = 0x12345678u;
    float f = 1.0f;
    const char s[] = "Hi!";
    dump("int 0x12345678", &x, sizeof x);
    dump("float 1.0f", &f, sizeof f);
    dump("char[] \"Hi!\"", s, sizeof s);

    unsigned char first;
    memcpy(&first, &x, 1);                 /* lowest address first */
    printf("first byte 0x%02x -> %s-endian\n", first,
           first == 0x78 ? "little" : "big");

    uint32_t w = 0x41424337u;              /* one 32-bit pattern ... */
    float wf;
    char wc[5] = {0};
    memcpy(&wf, &w, sizeof wf);            /* ... read as a float */
    memcpy(wc, &w, 4);                     /* ... read as 4 chars */
    printf("0x%08x: unsigned %u, float %.6f, chars \"%s\"\n",
           (unsigned)w, (unsigned)w, (double)wf, wc);
    return 0;
}

Wandbox-এর gcc (x86-64 Linux) আউটপুট:

int 0x12345678   78 56 34 12
float 1.0f       00 00 80 3f
char[] "Hi!"     48 69 21 00
first byte 0x78 -> little-endian
0x41424337: unsigned 1094861623, float 12.141410, chars "7CBA"

প্রথম লাইন: ৭৮ আগে — little-endian. দ্বিতীয় লাইন: 1.0f-এর বিট 0x3F800000 (sign 0, exponent ১২৭, ভগ্নাংশ ০ — পর্ব ২৩-এ বিস্তারিত), মেমরিতে উল্টো ক্রমে 00 00 80 3f. তৃতীয় লাইন: string-এর ক্ষেত্রে ক্রম উল্টায়নি — কারণ এটি চারটি আলাদা ১-byte মান, কোনো বহু-byte সংখ্যা নয়; শেষের 00 হলো '\0'. endianness শুধু একটি বহু-byte মানের ভেতরের byte-ক্রমের প্রশ্ন। শেষ লাইনে অংশ ১-এর টেবিল হুবহু মিলল: একই ৪ byte unsigned হিসেবে ১০৯৪৮৬১৬২৩, float হিসেবে প্রায় ১২.১৪, অক্ষর হিসেবে "7CBA" (মেমরির ক্রমে 37 43 42 41).

Portable কোড — endianness নিয়ে না ভাবার কৌশল

সবচেয়ে ভালো কৌশল হলো মেমরিকে সরাসরি int হিসেবে না পড়ে, byte ধরে ধরে shift করে মান বানানো। এতে কোডটি যেকোনো মেশিনে একই ফল দেয়, আর আধুনিক compiler এই ধাঁচ চিনে একটি load আর একটি byte-swap instruction-এ নামিয়ে আনে:

#include <stdint.h>

uint32_t read_be32(const unsigned char *p) {
    return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
           ((uint32_t)p[2] << 8)  |  (uint32_t)p[3];
}
; x86-64                         ; AArch64
read_be32:                         read_be32:
    mov     eax, DWORD PTR [rdi]       ldr     w0, [x0]
    bswap   eax                        rev     w0, w0
    ret                                ret

x86-এর bswap (৮০৪৮৬ থেকে), ARM-এর rev আর RISC-V-র Zbb extension-এর rev8 — সবাই এক instruction-এ byte-ক্রম উল্টায়। Compile-এর সময় endianness জানতে gcc আর clang __BYTE_ORDER__ macro দেয়, আর C23 মানে <stdbit.h>-এ __STDC_ENDIAN_NATIVE__ যোগ হয়েছে। Socket প্রোগ্রামে htons-এর ব্যবহার দেখতে Computer Networks কোর্সের TCP socket পর্ব দেখুন।

৮ · Bit manipulation — hardware-এর ভাষায় কথা বলা

একটি বাড়ির মেইন সুইচবোর্ডের কথা ভাবুন: আটটি সুইচ পাশাপাশি — প্রতিটি একটি বাতি বা পাখার জন্য। একটি বাতি জ্বালাতে আপনি পুরো বোর্ড বদলান না, শুধু সেই সুইচটি টেপেন; অন্যগুলো যেমন ছিল তেমন থাকে। Hardware-এর সাথে কথা বলা ঠিক এমন — একটি peripheral-এর control register-এর প্রতিটি বিট বা বিটের গুচ্ছ (field) আলাদা কিছু নিয়ন্ত্রণ করে। C কোর্সের বিটওয়াইজ অপারেশন পর্বে অপারেটরগুলো ভাষার দিক থেকে দেখেছেন; এখানে দেখব hardware-এর দিক থেকে, আর CPU এগুলো কোন instruction-এ করে।

কাজCRISC-Vx86-64
বিট k চালু (set)x | (1u << k)or / orior, bts
বিট k বন্ধ (clear)x & ~(1u << k)and / andiand, btr
বিট k উল্টানো (toggle)x ^ (1u << k)xor / xorixor, btc
বিট k পরীক্ষা (test)(x >> k) & 1usrl + andibt, test
বাঁয়ে shiftx << ksll / sllishl
ডানে logical shiftu >> k (unsigned)srl / srlishr
ডানে arithmetic shifts >> k (signed, সাধারণত)sra / sraisar

প্রতিটি কাজে একটি mask লাগে — যে সংখ্যার কেবল দরকারি বিটগুলো 1. নিয়মটি মনে রাখুন: OR দিয়ে চালু, AND দিয়ে বন্ধ বা ছাঁকা, XOR দিয়ে উল্টানো। RISC-V-র Zbs extension-এ একক-বিটের জন্য সরাসরি bset, bclr, binv, bext instruction-ও আছে।

Bit field বের করা আর বসানো

ছোট LCD-ওয়ালা যন্ত্রে (যেমন microcontroller-এর ডিসপ্লে) রঙ প্রায়ই রাখা হয় ১৬ বিটের RGB565 ফরম্যাটে: ওপরের ৫ বিট লাল (বিট ১৫–১১), মাঝের ৬ বিট সবুজ (বিট ১০–৫), নিচের ৫ বিট নীল (বিট ৪–০)। সবুজ বেশি বিট পায়, কারণ মানুষের চোখ সবুজের পার্থক্যে বেশি সংবেদনশীল। এই সাইটের সবুজ রঙ (৫৭, ১৮১, ৭৩) RGB565-এ হয় 0x3DA9. এবার একটি register-এর মতো এই মান থেকে বিট ১০..৫ — সবুজ bit field — বের করি:

registerRGB565 = 0x3DA9
bit  : 15 .. 11 │ 10 ..... 5 │ 4 ... 0
field:   red    │   green    │  blue
value:  00111   │  101101    │ 01001
all  : 0011 1101 1010 1001  = 0x3DA9
shiftx >> 5
x      : 0011 1101 1010 1001
x >> 5 : 0000 0001 1110 1101  = 0x01ED
                    └ 10 1101 now at bits 5..0
blue (01001) fell off the right edge
mask& 0x3F
x >> 5 : 0000 0001 1110 1101
0x3F   : 0000 0000 0011 1111
AND    : 0000 0000 0010 1101  = 0x2D = 45
সাধারণ সূত্রশুরু s, চওড়া w
field = (x >> s) & ((1u << w) - 1)
s = 5, w = 6 -> mask = (1 << 6) - 1 = 63 = 0x3F
green 6-bit = 45 -> 8-bit = (45 << 2) │ (45 >> 4)
                       = 180 + 2 = 182  (was 181)
insertgreen = 63 বসানো
clear  : x & ~(0x3F << 5) = 0x3DA9 & 0xF81F = 0x3809
         0011 1000 0000 1001
set    : 0x3809 │ (63 << 5) = 0x3809 │ 0x07E0
result : 0011 1111 1110 1001  = 0x3FE9
  1. Field চেনা। 0x3DA9 = 0011 1101 1010 1001. বিট ১৫–১১ = 00111 (লাল = ৭), বিট ১০–৫ = 101101 (সবুজ), বিট ৪–০ = 01001 (নীল = ৯)। আমাদের লক্ষ্য মাঝের ৬ বিট।
  2. ডানে ৫ ঘর shift. x >> 5 করলে নীলের ৫টি বিট ডান দিয়ে পড়ে যায়, আর সবুজ নেমে আসে বিট ৫..০-তে: 0x01ED. কিন্তু তার বাঁয়ে লালের বিটগুলো এখনো আছে।
  3. Mask দিয়ে ছাঁকা। 0x3F-এ কেবল নিচের ৬ বিট 1. AND করলে লালের বিট মুছে যায়, থাকে 10 1101 = ৪৫. এটিই সবুজ field-এর মান।
  4. সাধারণ সূত্র। শুরু s, চওড়া w হলে mask = ২w − 1, আর field = (x >> s) & mask. ৬ বিটের ৪৫-কে ৮ বিটে ফেরাতে বাঁয়ে ২ ঘর সরিয়ে ওপরের ২ বিট নিচে জুড়ে দিই: ১৮০ + ২ = ১৮২. মূল মান ছিল ১৮১ — নিচের ২ বিট প্যাক করার সময়ই হারিয়েছে।
  5. নতুন মান বসানো (insert). আগে field-এর জায়গা মুছি: ~(0x3F << 5) = 0xF81F, AND করে পাই 0x3809. তারপর নতুন সবুজ ৬৩-কে ৫ ঘর বাঁয়ে সরিয়ে (0x07E0) OR করি: ফল 0x3FE9. অন্য দুই field অক্ষত — hardware register লেখার নিয়ম ঠিক এটাই, যার নাম read-modify-write.

Hardware-এ কিন্তু এই কাজের কোনো খরচই নেই। Verilog-এ (পর্ব ১৪) field বের করা মানে কয়েকটি তার আলাদা করে টেনে নেওয়া — কোনো gate লাগে না:

module rgb565_unpack (
    input  wire [15:0] px,
    output wire [4:0]  r,
    output wire [5:0]  g,
    output wire [4:0]  b
);
    assign r = px[15:11];
    assign g = px[10:5];
    assign b = px[4:0];
endmodule

Software-এ একই কাজে লাগে shift আর AND — দুটি instruction. এই পার্থক্যই বোঝায় কেন নেটওয়ার্ক কার্ড বা ভিডিও decoder-এর মতো যন্ত্র bit field ভাঙার কাজ hardware-এ করে। x86-এর BMI2 extension-এর pext instruction যেকোনো ছড়ানো বিট-গুচ্ছ এক instruction-এ বের করতে পারে।

Logical বনাম arithmetic right shift

ডানে shift করলে বাঁ দিকে খালি ঘর তৈরি হয়। Logical shift সেখানে 0 বসায়; arithmetic shift বসায় sign bit-এর কপি, যাতে ঋণাত্মক সংখ্যা ঋণাত্মকই থাকে (পর্ব ০৫-এর two's complement). ধরা যাক ৩২-বিটের −২০ = 0xFFFFFFEC:

  • arithmetic: -20 >> 2 = −৫. সাধারণভাবে arithmetic shift k ঘর = ⌊x / ২k⌋, অর্থাৎ −∞-এর দিকে গোল করা।
  • logical: ওপরের দুই ঘরে 0 ঢোকে, ফল 0x3FFFFFFB = ১০৭৩৭৪১৮১৯ — একটি বিশাল ধনাত্মক সংখ্যা।

সূক্ষ্ম একটি ফাঁদ: C-র / শূন্যের দিকে গোল করে, তাই -5 / 2 = −২, কিন্তু -5 >> 1 = −৩. Compiler তাই signed সংখ্যাকে ২-এর ঘাত দিয়ে ভাগ করতে শুধু shift দেয় না, আগে একটি সংশোধন যোগ করে। আর C মানে ঋণাত্মক signed সংখ্যার ডান shift-এর ফল implementation-defined — gcc আর clang arithmetic shift করে, তবে নিশ্চিত logical shift চাইলে unsigned type ব্যবহার করুন। (C++20 থেকে C++-এ এটি arithmetic হিসেবে নির্ধারিত।)

প্রতিদিনের কয়েকটি কৌশল আর তাদের প্রমাণ

  • x & (x − 1) সবচেয়ে নিচের 1-বিটটি মুছে দেয়। কারণ: x − 1 করলে সবচেয়ে নিচের 1 হয়ে যায় 0, আর তার নিচের সব 0 হয়ে যায় 1 (ধার নেওয়া); ওপরের বিট বদলায় না। AND করলে ওই অংশ পুরো 0, ওপরের অংশ অক্ষত। যেমন 1011 0100 → 1011 0000.
  • ২-এর ঘাত পরীক্ষা: x != 0 && (x & (x − 1)) == 0. ২-এর ঘাতে ঠিক একটি 1 থাকে, সেটি মুছলে শূন্য।
  • Popcount (কয়টি বিট 1): x &= x − 1 যতবার করা যায় ততবার গুনুন — লুপ চলে শুধু 1-এর সংখ্যার সমান বার (Brian Kernighan-এর নামে পরিচিত কৌশল)। Hardware-এ সরাসরি instruction আছে: x86-এর popcnt, RISC-V Zbb-এর cpop, ARM64-এ SIMD cnt.
  • x & -x সবচেয়ে নিচের 1-বিটটি একা রাখে (two's complement-এ −x = ~x + 1).
  • XOR swap (a ^= b; b ^= a; a ^= b;) তৃতীয় ভ্যারিয়েবল ছাড়াই অদলবদল করে — কিন্তু আজ কাজের নয়। a আর b একই জায়গা হলে দুটোই 0 হয়ে যায়; তিনটি instruction একটির ওপর আরেকটি নির্ভর করে, তাই আধুনিক out-of-order CPU (পর্ব ৪০) সমান্তরালে চালাতে পারে না; আর সাধারণ temp-ভিত্তিক swap compiler register-এর নাম বদলেই সেরে ফেলে, প্রায়ই কোনো instruction ছাড়াই।

এবার পুরো toolkit একসাথে চালিয়ে দেখুন — set, clear, toggle, test, x & (x − 1), popcount, ২-এর ঘাত, RGB565 প্যাক-আনপ্যাক আর দুই রকম shift:

#include <stdio.h>
#include <stdint.h>

static void show(const char *label, unsigned v) {     /* print 8 bits */
    printf("%-26s ", label);
    for (int k = 7; k >= 0; k--) putchar(((v >> k) & 1u) ? '1' : '0');
    printf("  0x%02X\n", v & 0xFFu);
}
static unsigned popcount(uint32_t x) { unsigned n = 0; while (x) { x &= x - 1; n++; } return n; }
static int is_pow2(uint32_t x) { return x != 0 && (x & (x - 1)) == 0; }

int main(void) {
    unsigned x = 0xB4;                                 /* 1011 0100 */
    show("x", x);
    show("set bit 0:    x | (1<<0)", x | (1u << 0));
    show("clear bit 2:  x & ~(1<<2)", x & ~(1u << 2));
    show("toggle bit 7: x ^ (1<<7)", x ^ (1u << 7));
    printf("%-26s %u\n", "test bit 4:   (x>>4) & 1", (x >> 4) & 1u);
    show("x & (x-1)", x & (x - 1));
    printf("popcount %u (builtin %d), pow2: 64->%d 96->%d 0->%d\n",
           popcount(x), __builtin_popcount(x), is_pow2(64), is_pow2(96), is_pow2(0));

    unsigned r = 57, g = 181, b = 73;                  /* RGB565: rrrrr gggggg bbbbb */
    uint16_t px = (uint16_t)(((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3));
    unsigned g6 = (px >> 5) & 0x3Fu;                   /* extract bits 10..5 */
    printf("RGB565(57,181,73) = 0x%04X, green field %u -> 8-bit %u\n",
           (unsigned)px, g6, (g6 << 2) | (g6 >> 4));
    px = (uint16_t)((px & ~(0x3Fu << 5)) | (63u << 5)); /* insert green = 63 */
    printf("after inserting green=63: 0x%04X\n", (unsigned)px);

    int32_t neg = -20;                                 /* 0xFFFFFFEC */
    printf("-20 >> 2 as int32_t  = %d (gcc: arithmetic)\n", (int)(neg >> 2));
    printf("-20 >> 2 as uint32_t = %u (logical)\n", (unsigned)((uint32_t)neg >> 2));
    return 0;
}

Wandbox-এর gcc আউটপুট:

x                          10110100  0xB4
set bit 0:    x | (1<<0)   10110101  0xB5
clear bit 2:  x & ~(1<<2)  10110000  0xB0
toggle bit 7: x ^ (1<<7)   00110100  0x34
test bit 4:   (x>>4) & 1   1
x & (x-1)                  10110000  0xB0
popcount 4 (builtin 4), pow2: 64->1 96->0 0->0
RGB565(57,181,73) = 0x3DA9, green field 45 -> 8-bit 182
after inserting green=63: 0x3FE9
-20 >> 2 as int32_t  = -5 (gcc: arithmetic)
-20 >> 2 as uint32_t = 1073741819 (logical)

প্রতিটি লাইন একটি সুইচ টেপা: বিট ০ চালু (B4 → B5), বিট ২ বন্ধ (B4 → B0), বিট ৭ উল্টানো (B4 → 34), বিট ৪ পরীক্ষা = 1. x & (x-1)-এ নিচের 1 (বিট ২) মুছে B0. popcount-এ আমাদের লুপ আর gcc-র built-in দুটোই ৪. RGB565-এর মান 0x3DA9, সবুজ ৪৫, আর নতুন সবুজ বসানোর পরে 0x3FE9 — উপরের ধাপের সাথে হুবহু মিলল। শেষ দুই লাইনে একই বিট (0xFFFFFFEC) signed হিসেবে arithmetic shift-এ −৫, unsigned হিসেবে logical shift-এ ১০৭৩৭৪১৮১৯. সব mask 1u দিয়ে লেখা, যাতে signed overflow-এর undefined behavior না হয় (যেমন 1 << 31).

Register-এ বিট — embedded জগতের রোজকার কাজ

একটি microcontroller-এর GPIO বা UART-এর control register ঠিক এমন field-এ ভাগ করা: কোনো বিট “চালু/বন্ধ”, কোনো ২–৩ বিট “mode”, কোনো ৮ বিট “baud rate-এর ভাজক”। এই register-গুলো মেমরির নির্দিষ্ট ঠিকানায় বসানো থাকে (memory-mapped I/O, পর্ব ৫১), তাই C কোডে সেগুলো একটি pointer দিয়ে পড়া-লেখা হয় — compiler যেন পড়া-লেখা বাদ না দেয়, তার জন্য volatile. datasheet-এ লেখা থাকে কোন বিটের কী কাজ, আর একটি ভুল mask মানে হয়তো ভুল pin-এ ভোল্টেজ। এই বিষয়টি পর্ব ৫৬-এ (embedded সিস্টেম) আবার আসবে। বিট উল্টে যাওয়া ধরার জন্য একটি বাড়তি parity bit বা ECC কীভাবে কাজ করে, তা আছে Computer Networks কোর্সের error detection পর্বে।

আরও গভীরে: UTF-8 যাচাই আজ SIMD (পর্ব ৪২) দিয়ে করা হয় — simdutf বা simdjson-এর মতো library একবারে ১৬–৬৪ byte পরীক্ষা করে প্রতি সেকেন্ডে কয়েক GB লেখা যাচাই করতে পারে, শুধু বিট-mask আর table lookup দিয়ে। x86-এর BMI2-এর pext/pdep instruction (Intel Haswell, ২০১৩ থেকে) যেকোনো mask অনুযায়ী বিট জড়ো বা ছড়াতে পারে; তবে কিছু পুরোনো AMD CPU-তে এগুলো microcode-এ চলত বলে খুব ধীর ছিল — তাই “instruction আছে” মানেই “দ্রুত” নয়, মেপে দেখতে হয়। আর RISC-V তার বিট-কাজগুলো ঐচ্ছিক extension-এ (Zba, Zbb, Zbs) ভাগ করেছে, যাতে ছোট chip-এ এগুলো বাদ দিয়ে জায়গা বাঁচানো যায়।

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • 0x12345678 little-endian মেশিনে ঠিকানা A থেকে কীভাবে বসে? — A: 78, A+1: 56, A+2: 34, A+3: 12.
  • x86-64-এ struct {char c; int i;}-এর sizeof কত, কেন? — ৮; int-কে ৪-এর গুণিতকে বসাতে ৩ byte padding.
  • U+0995-এর UTF-8? — ৩ byte ছাঁচ 1110xxxx 10xxxxxx 10xxxxxx, ফল E0 A6 95.
  • একটি সংখ্যা ২-এর ঘাত কি না এক লাইনে? — x && !(x & (x - 1)).
  • মেশিন little- না big-endian, C-তে কীভাবে জানবেন? — একটি uint32_t = 1-এর প্রথম byte unsigned char বা memcpy দিয়ে পড়ুন; 1 হলে little-endian.

৯ · AI-এর যুগে — বিট-স্তরের দাবি যাচাই

বিট-স্তরের প্রশ্নে AI-এর উত্তর শুনতে খুব আত্মবিশ্বাসী লাগে, কিন্তু ভুলের হার বেশি — কারণ সঠিক উত্তরের জন্য গুনে গুনে বিট মেলাতে হয়, আর ভাষা-মডেল সেখানে প্রায়ই “দেখতে ঠিক” উত্তর বানায়। সুখবর হলো, এই বিষয়ের প্রায় সব দাবি কয়েক লাইনের কোডে যাচাই করা যায়।

AI প্রায়ই যা বলেসত্যিটাযাচাইয়ের উপায়
“struct-এর মাপ = member-দের মাপের যোগফল”padding যোগ হয়; ABI-ভেদে আলাদাsizeof, offsetof ছাপুন
বাংলা অক্ষরের ভুল UTF-8 byte বা “বাংলা ২ byte”বাংলা ব্লকের প্রতিটি code point ৩ byteTextEncoder, od -tx1
little-endian dump-এ byte-ক্রম উল্টো লেখাছোট ঠিকানায় LSBunsigned char * দিয়ে byte ছাপুন
“C-তে >> সবসময় arithmetic”ঋণাত্মক signed-এ implementation-definedunsigned ব্যবহার; compiler-এর ডকুমেন্টেশন
type punning-এ *(int *)&fstrict aliasing ভাঙে; memcpy নিরাপদ-Wall -O2 আর sanitizer দিয়ে চালান
JS-এ "বাংলা".length = “অক্ষরের সংখ্যা”এটি UTF-16 একক (৫); চোখের অক্ষর ২Intl.Segmenter

AI-কে বিট-স্তরের প্রশ্ন করার সময় তাকে দিয়েই যাচাইয়ের পথ বের করিয়ে নিন। নিচের কাঠামোটি কাজে লাগবে:

আমি x86-64 Linux-এ gcc ব্যবহার করি। নিচের C struct-এর memory layout বের করো: struct Packet { char type; uint32_t len; uint16_t port; char flag; }; ১. প্রতিটি member-এর offset আর মোট sizeof কত, কোথায় কত byte padding — একটি টেবিলে দাও। ২. member-দের ক্রম বদলে মাপ কমানো যায় কি না দেখাও। ৩. তোমার উত্তর যাচাই করার জন্য sizeof আর offsetof ছাপে এমন একটি ছোট C প্রোগ্রাম দাও — আমি নিজে চালিয়ে মেলাব। ৪. এই struct নেটওয়ার্কে পাঠালে endianness আর padding নিয়ে কী সমস্যা হবে, সংক্ষেপে বলো।

AI-এর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১নেটওয়ার্ক big-endian, অথচ আজকের প্রায় সব CPU little-endian — এমন হলো কেন, আর কোনটি আসলে ভালো?

ইতিহাসটা দুটি আলাদা ধারার। Little-endian এসেছে মূলত ছোট, সস্তা প্রসেসরের দুনিয়া থেকে। ১৯৭০-এর দশকের ৮-বিট CPU (যেমন Intel 8080) একবারে একটি byte যোগ করত। বড় সংখ্যা যোগ করতে হয় নিচের byte থেকে শুরু করে carry ওপরে তুলে — তাই নিচের byte ছোট ঠিকানায় থাকলে ঠিকানা বাড়াতে বাড়াতেই যোগ এগোয়। আরেকটি সুবিধা: একই ঠিকানা থেকে ৮, ১৬ বা ৩২ বিট পড়লে সবসময় মানের নিচের অংশ পাওয়া যায়, তাই type ছোট করতে (cast) ঠিকানা বদলাতে হয় না। Intel 8086 এই ধারা নিয়ে এগোয়, আর IBM PC-র সাফল্যে x86 পুরো পৃথিবীতে ছড়ায়।

Big-endian এসেছে বড় কম্পিউটার আর মানুষের পড়ার সুবিধা থেকে — IBM System/360, Motorola 68000, পরে SPARC আর PowerPC. মেমরি-dump-এ সংখ্যা কাগজের মতো পড়া যায়, আর দুটি big-endian সংখ্যাকে byte ধরে বাঁ থেকে তুলনা করলেই বড়-ছোট বোঝা যায়। ১৯৮০-র দশকের শুরুতে TCP/IP-এর নির্দেশিকা লেখার সময় big-endian মেশিনের প্রভাব বেশি ছিল, আর protocol-এর header-এ এই ক্রমই চুক্তি হয়ে যায়: network byte order.

মাঝখানে অদ্ভুত একটি অধ্যায়ও আছে। DEC PDP-11-এ ১৬-বিটের word ছিল little-endian, কিন্তু ৩২-বিটের মান রাখা হতো দুটি word-কে big-endian ক্রমে — ফলে “UNIX” লেখা ভুল ক্রমে পড়লে হয়ে যেত “NUXI”. এই “NUXI problem” Unix-কে অন্য মেশিনে নেওয়ার সময় বিখ্যাত হয়েছিল। Apple-এর Mac-ও ঘুরেছে: 68000 (big) → PowerPC (big) → ২০০৬ সালে Intel (little) → ২০২০ সালে নিজের ARM chip (little). ARM আর PowerPC নিজেরা bi-endian হলেও বাজার little-endian-এর দিকে গেছে, কারণ x86-এর জন্য লেখা বিশাল সফটওয়্যার আর ডেটা ফরম্যাট সহজে আনা যায়।

কোনটি ভালো? Danny Cohen-এর মূল যুক্তিটিই আজও সত্য — প্রযুক্তিগতভাবে পার্থক্য সামান্য, আসল ক্ষতি হয় যখন দুই পক্ষ ভিন্ন চুক্তি মানে। একটি byte-swap instruction-এ এক চক্রের মতো সময় লাগে; কিন্তু ভুল ক্রমে পড়া একটি ফাইল-header বা packet length মারাত্মক বাগ বা নিরাপত্তা-ত্রুটি তৈরি করতে পারে।

মূল উপলব্ধি: endianness একটি চুক্তি, গুণমানের প্রশ্ন নয়। ভালো প্রকৌশলী সীমানায় (নেটওয়ার্ক, ফাইল, hardware register) ক্রমটি স্পষ্ট লিখে রাখেন আর portable কোড লেখেন — মেশিনের ক্রমের ওপর ভরসা করেন না।

প্র ০২UTF-8 কেন ওয়েবে জিতে গেল, অথচ Windows, Java আর JavaScript আজও ভেতরে UTF-16 — এই বিভাজন কোথা থেকে?

শুরুতে Unicode-এর পরিকল্পনা ছিল সরল: ১৬ বিট, অর্থাৎ ৬৫,৫৩৬টি ঘরেই পৃথিবীর সব অক্ষর আঁটবে। ১৯৯১ সালে Unicode ১.০ এই ধারণা নিয়েই আসে, আর প্রতিটি অক্ষর একটি ১৬-বিটের একক — এই নির্দিষ্ট-মাপের রূপের নাম ছিল UCS-2. সে সময়ের নতুন সিস্টেমগুলো এটিকে ভবিষ্যৎ ভেবে গ্রহণ করে: Windows NT (১৯৯৩) তার পুরো API ১৬-বিটের অক্ষরে বানায়, Java (১৯৯৫) তার char-কে ১৬ বিট করে, আর JavaScript-এর string-ও ১৬-বিটের একক ধরে।

কিন্তু দ্রুতই বোঝা গেল ৬৫,৫৩৬ যথেষ্ট নয় — চীনা-জাপানি-কোরিয়ান লিপির বিরল অক্ষর, ঐতিহাসিক লিপি, গাণিতিক চিহ্ন, পরে ইমোজি। ১৯৯৬ সালে Unicode ২.০ পরিসর বাড়িয়ে U+10FFFF পর্যন্ত নেয়, আর ১৬-বিটের সিস্টেমগুলোকে বাঁচাতে আনা হয় surrogate pair. এভাবে UCS-2 পরিণত হলো UTF-16-এ — কিন্তু তখন আর এটি নির্দিষ্ট-মাপের নয়, ফলে UTF-8-এর তুলনায় এর প্রধান সুবিধাটিই হারিয়ে গেল। তবু Windows, Java আর JavaScript-এর বিশাল কোডভান্ডার আর পুরোনো আচরণ বদলানো সম্ভব ছিল না; তাই আজও JS-এ ইমোজির .length = ২।

অন্যদিকে, ১৯৯২ সালে Bell Labs-এর Ken Thompson আর Rob Pike তাঁদের Plan 9 অপারেটিং সিস্টেমের জন্য UTF-8-এর বর্তমান নকশা তৈরি করেন — প্রচলিত গল্প অনুযায়ী এক সন্ধ্যায় রেস্তোরাঁর কাগজের ওপর। এর গুণগুলো Unix আর ইন্টারনেটের জন্য নিখুঁত ছিল: পুরোনো ASCII ফাইল ও টুল বদলাতে হয় না, C-র string ভাঙে না, byte-ক্রমের ঝামেলা নেই, আর নষ্ট byte থেকে দ্রুত সামলে ওঠা যায়। ২০০৩ সালে RFC 3629 UTF-8-কে সর্বোচ্চ ৪ byte আর U+10FFFF-এ বেঁধে দেয়। আজ ওয়েবের অধিকাংশ পাতা, Linux আর macOS-এর ফাইলনাম, JSON — সব UTF-8; এমনকি Windows-ও এখন প্রোগ্রামকে UTF-8 code page-এ চলার সুযোগ দেয়।

বাংলার জন্য একটি মজার হিসাব: খাঁটি বাংলা লেখা UTF-16-এ প্রতি অক্ষরে ২ byte, UTF-8-এ ৩ byte — তাই শুধু বাংলা লেখা UTF-16-এ ছোট। কিন্তু ওয়েব পাতায় HTML tag, URL আর ইংরেজি মিশে থাকে, সেগুলো UTF-8-এ ১ byte; সব মিলিয়ে UTF-8 সাধারণত জিতে যায়, আর compression-এর পরে পার্থক্য আরও কমে।

মূল উপলব্ধি: প্রথম দিনের নকশা-ধারণা (“১৬ বিটেই হবে”) ভুল প্রমাণিত হলেও তার ওপর গড়া সিস্টেম দশকের পর দশক থেকে যায়। ভালো নকশা আগে থেকেই বৃদ্ধির জায়গা রাখে — UTF-8 ঠিক সেটাই করেছিল।

প্র ০৩দুটি বাংলা শব্দ পর্দায় হুবহু এক দেখায়, তবু সার্চে মেলে না — কেন হয়, আর সফটওয়্যার কীভাবে সামলায়?

কারণ Unicode-এ একই দৃশ্যমান লেখা একাধিক code point-ক্রমে লেখা যায়। বাংলায় এর পরিচিত উদাহরণ আছে। “য়” একটি আলাদা code point (U+09DF) হিসেবেও আছে, আবার “য” (U+09AF) + নুক্তা “়” (U+09BC) জুড়েও লেখা যায়। একইভাবে “ড়” (U+09DC) = ড + ়, আর “ো”-কার (U+09CB) = “ে” (U+09C7) + “া” (U+09BE). পর্দায় সব একই, কিন্তু byte আলাদা — ফলে byte-ধরে তুলনা করা সার্চ “মেলেনি” বলে দেয়।

Unicode এই সমস্যার সমাধান রেখেছে normalization নামে। Standard বলে দেয় কোন ক্রমগুলো “canonically equivalent” — অর্থে সমান। তুলনার আগে সব লেখাকে একটি নির্দিষ্ট রূপে আনতে হয়: NFC (যতটা সম্ভব জোড়া রূপ) বা NFD (সব ভাঙা রূপ)। মজার ব্যাপার, য়, ড়, ঢ়-এর জোড়া রূপগুলো “composition exclusion” তালিকায় আছে — অর্থাৎ NFC-তেও এগুলো ভাঙা রূপে (য + ়) থাকে। তাই বাংলায় সার্চ, ডেটাবেসের unique key বা পাসওয়ার্ড তুলনার আগে normalization না করলে একই নাম দুবার নিবন্ধিত হতে পারে।

ইতিহাসও জটিল। Unicode-এ বাংলা ব্লক প্রথম থেকেই ছিল, ভারতের ISCII মানের ধাঁচে সাজানো। খণ্ড-ত “ৎ” প্রথমে আলাদা code point ছিল না — হসন্ত আর ZWJ দিয়ে লেখার নিয়ম ছিল; পরে ২০০৫ সালের দিকে Unicode ৪.১-এ U+09CE হিসেবে যোগ হয়। ফলে পুরোনো আর নতুন ফাইলে একই শব্দ ভিন্ন ক্রমে থাকতে পারে। বাংলাদেশে ২০০০-এর দশকে Unicode-ভিত্তিক বাংলা লেখা জনপ্রিয় করতে বিনামূল্যের কীবোর্ড সফটওয়্যার (যেমন অভ্র) বড় ভূমিকা রেখেছে বলে ব্যাপকভাবে মনে করা হয়; তার আগে বেশিরভাগ লেখা ছিল font-নির্ভর পুরোনো পদ্ধতিতে, যার রূপান্তর আজও চলছে।

প্রকৌশলের দিক থেকে শিক্ষা হলো, “string সমান কি না” প্রশ্নটি শুনতে যতটা সরল, আসলে ততটা নয়। byte সমান, code point সমান, normalization-এর পরে সমান, নাকি মানুষের চোখে সমান — প্রতিটি আলাদা প্রশ্ন। আন্তর্জাতিক সফটওয়্যার ICU-এর মতো library দিয়ে এই কাজগুলো করে, নিজে লেখে না।

মূল উপলব্ধি: লেখা = code point-এর ক্রম, আর একই চেহারার একাধিক ক্রম থাকতে পারে। বাংলা নিয়ে কাজ করা যেকোনো সিস্টেমে তুলনা ও সার্চের আগে normalization বাধ্যতামূলক ধাপ হওয়া উচিত।

প্র ০৪Hardware misaligned access কেন অপছন্দ করে, আর packed struct ব্যবহার করলে কী কী ঝুঁকি নেওয়া হয়?

Hardware-এর দৃষ্টিতে মেমরি কখনো “একটি করে byte” দেয় না। L1 cache একবারে একটি পুরো cache line (সাধারণত ৬৪ byte) রাখে, আর load unit তার ভেতর থেকে একটি aligned টুকরো বেছে নেয়। একটি aligned ৮-byte মান সবসময় একটি cache line-এর ভেতরে থাকে। কিন্তু misaligned মান দুটি cache line জুড়ে থাকতে পারে — তখন দুবার cache-এ যেতে হয়, দুটি অংশ shift করে জোড়া দিতে হয়; আর page-এর সীমানা পার হলে দুটি আলাদা TLB lookup (পর্ব ৫০) লাগে, দুটি page-এর অনুমতি যাচাই হয়, এমনকি একটি অংশের জন্য page fault-ও হতে পারে। এর জন্য বাড়তি circuit লাগে, যা প্রতিটি সাধারণ load-কেও জটিল করে।

তাই প্রথম দিকের RISC নকশা (MIPS, পুরোনো ARM) সিদ্ধান্ত নিয়েছিল — misaligned access সমর্থনই করবে না; হয় exception, নয়তো অদ্ভুত ফল (পুরোনো কিছু ARM-এ misaligned load ঘোরানো বা rotated ডেটা ফেরত দিত)। x86 প্রথম থেকেই সমর্থন দিয়েছে, তবে মূল্য দিয়ে। আজকের x86-এ একটি সূক্ষ্ম বিপদ হলো split lock: lock দেওয়া atomic instruction যদি দুটি cache line জুড়ে থাকে, CPU-কে কার্যত পুরো memory bus আটকে রাখতে হয় — অন্য core-গুলোও থমকে যায়। Linux এখন এমন ঘটনা শনাক্ত করে সতর্ক করতে বা প্রোগ্রামটিকে ধীর করে দিতে পারে। SIMD-তেও নিয়ম আছে: SSE-র movaps ১৬-byte-aligned ঠিকানা না পেলে fault দেয়, আর movups যেকোনো ঠিকানা মানে।

__attribute__((packed)) দিয়ে padding বন্ধ করলে struct ছোট হয় আর নেটওয়ার্ক বা ফাইল header-এর সাথে হুবহু মেলে — এই সুবিধার জন্যই অনেকে ব্যবহার করেন। ঝুঁকিগুলো: (১) member misaligned হলে কিছু ISA-তে ধীর বা exception; (২) packed member-এর ঠিকানা নিয়ে সাধারণ int *-তে রাখলে compiler আর জানে না সেটি misaligned — gcc এ নিয়ে সতর্কবার্তা দেয় (-Waddress-of-packed-member); (৩) endianness তবু সমস্যা থেকে যায় — packed struct byte-ক্রম ঠিক করে না। তাই পেশাদার নিয়ম: বাইরের ডেটা byte buffer হিসেবে পড়ুন, আর read_be32-এর মতো ফাংশন দিয়ে field আলাদা করুন।

মূল উপলব্ধি: alignment হলো software আর memory hardware-এর মধ্যে একটি নীরব চুক্তি। চুক্তি মানলে প্রতিটি load এক ধাপে; ভাঙলে কোথাও না কোথাও কেউ — hardware, OS বা আপনি — বাড়তি দাম দেয়।

প্র ০৫Compiler তো এখন অনেক বুদ্ধিমান — তবু bit manipulation শেখা আজও কেন জরুরি?

সত্যি, compiler অনেক কৌশল নিজেই করে। x * 8 লিখলে সে x << 3 বানায়, Kernighan-এর popcount লুপ চিনে উপযুক্ত flag (যেমন -mpopcnt) থাকলে একটি popcnt instruction-এ নামিয়ে আনে, আর byte-ধরে পড়া কোডকে bswap-এ বদলায়। তাই “গুণের বদলে shift লিখে কোড দ্রুত করা” আজ বেশিরভাগ ক্ষেত্রে অপ্রয়োজনীয় — এমনকি পড়তে কঠিন বলে ক্ষতিকর।

কিন্তু compiler জানে না ডেটার layout কী হবে — সেটি ঠিক করে বাইরের চুক্তি। একটি UART register-এর বিট ৩ মানে “parity চালু”, IPv4 header-এর প্রথম byte-এর ওপরের nibble version আর নিচের nibble header length, RGB565-এর মাঝের ৬ বিট সবুজ — এগুলো datasheet বা RFC বলে দেয়, আর মানুষকেই mask আর shift লিখতে হয়। Firmware, driver, নেটওয়ার্ক stack, ফাইল ফরম্যাট, compression আর cryptography — সবখানে এই কাজ রোজকার।

দ্বিতীয়ত, কিছু অ্যালগরিদমের পুরো দক্ষতাই বিটে। দাবা-ইঞ্জিন ৬৪টি ঘরকে একটি ৬৪-বিটের সংখ্যায় রাখে (bitboard) — একটি AND-এ সব আক্রমণযোগ্য ঘর বের হয়। Bloom filter, Linux kernel-এর CPU-mask, ডেটাবেসের bitmap index, গ্রাফ অ্যালগরিদমের subset DP — সব এক ধাঁচের। Cryptography-তে আরেকটি কারণ আছে: constant-time কোড। গোপন চাবির ওপর নির্ভর করে if লিখলে branch predictor (পর্ব ৩৭) আর সময়ের পার্থক্য দিয়ে তথ্য ফাঁস হতে পারে, তাই নিরাপদ কোড mask দিয়ে শর্ত ছাড়াই মান বাছে।

ইতিহাসেও এর শিকড় গভীর। ১৯৬০-এর দশকের CDC 6600-এর মতো supercomputer-এ population count instruction ছিল, আর এ নিয়ে একটি জনপ্রিয় (অনিশ্চিত) গল্প আছে যে গোয়েন্দা সংস্থার code-breaking-এর চাহিদা থেকে এটি রাখা হতো। Henry Warren-এর বই Hacker's Delight (২০০২) এমন শত শত কৌশল প্রমাণসহ সংকলন করেছে, আর আধুনিক ISA-গুলো (x86-এর BMI, RISC-V-র Zb*) সেগুলোর অনেককে instruction বানিয়েছে।

মূল উপলব্ধি: compiler “কীভাবে দ্রুত করব” জানে, কিন্তু “কোন বিটের কী অর্থ” জানে না। hardware আর বাইরের ফরম্যাটের সাথে চুক্তি রক্ষা করা প্রকৌশলীর কাজ — আর সেই ভাষা হলো mask আর shift.

অনুশীলন

কাগজ-কলম নিন — প্রথম তিনটি হাতে করার, শেষটি কোড লেখার।

  1. হাতে encode-decode: (ক) “আ” (U+0986) আর “€” (U+20AC)-কে UTF-8-এ লিখুন। (খ) E0 A7 A8 — এই তিনটি byte কোন code point, কোন অক্ষর?

    (ক) দুটিই U+0800–U+FFFF পরিসরে, তাই ৩ byte. 0x0986 = 0000 1001 1000 0110 → 0000 │ 100110 │ 000110 → 1110 0000, 10 100110, 10 000110 = E0 A6 86. 0x20AC = 0010 0000 1010 1100 → 0010 │ 000010 │ 101100 → E2 82 AC.

    (খ) E0 = 1110 0000 → ৩ byte-এর অক্ষর, payload 0000; A7 = 10 100111 → 100111; A8 = 10 101000 → 101000. জোড়া দিলে 0000 1001 1110 1000 = U+09E8. বাংলা অঙ্ক শুরু U+09E6 (০) থেকে, তাই 0x09E8 − 0x09E6 = ২ — অক্ষরটি “২”।

  2. আউটপুট অনুমান: uint16_t a[2] = {0x1234, 0xABCD}; — little-endian মেশিনে এর ৪টি byte মেমরির ক্রমে কী? একই ৪ byte memcpy দিয়ে একটি uint32_t-এ নিলে কী মান ছাপবে? big-endian মেশিনে?

    Little-endian: প্রতিটি ১৬-বিটের মান আলাদাভাবে উল্টায় — 34 12 CD AB. এই ৪ byte-কে little-endian ৩২-বিট হিসেবে পড়লে সবচেয়ে ছোট ঠিকানার byte সবচেয়ে হালকা: 0xABCD1234.

    Big-endian: byte 12 34 AB CD, ৩২-বিট মান 0x1234ABCD. লক্ষ করুন, array-র উপাদানের ক্রম (a[0] আগে) কখনো বদলায় না — endianness শুধু প্রতিটি উপাদানের ভেতরের byte-ক্রম।

  3. Struct layout (x86-64 Linux): struct S { char a; short b; char c; int d; char e; }; — প্রতিটি member-এর offset আর sizeof বের করুন। তারপর member-দের ক্রম বদলে সবচেয়ে ছোট মাপ কত করা যায়?

    a@০ (১ byte), ১ byte padding, b@২ (২ byte, শেষ ৪-এ), c@৪, ৩ byte padding, d@৮ (শেষ ১২-তে), e@১২ — ডেটা শেষ ১৩-তে. struct-এর alignment ৪ (সবচেয়ে বড় int), তাই tail padding দিয়ে sizeof = ১৬.

    বড় থেকে ছোট সাজালে: int d; short b; char a, c, e; → d@০, b@৪, a@৬, c@৭, e@৮, ডেটা শেষ ৯-এ, ৪-এর গুণিতকে বাড়িয়ে sizeof = ১২. ডেটা আসলে ৯ byte, কিন্তু array-তে alignment রাখতে ১২ লাগবেই। নিজের compiler-এ offsetof দিয়ে যাচাই করে নিন।

  4. কোড লিখুন: একটি C ফাংশন লিখুন uint32_t set_field(uint32_t reg, unsigned s, unsigned w, uint32_t v), যা reg-এর বিট s থেকে w-টি বিটে v বসাবে, বাকি বিট অক্ষত রাখবে। set_field(0x3DA9, 5, 6, 63) কত হওয়া উচিত?
    uint32_t set_field(uint32_t reg, unsigned s, unsigned w, uint32_t v) {
        uint32_t mask = (w >= 32) ? 0xFFFFFFFFu : ((1u << w) - 1u);
        return (reg & ~(mask << s)) | ((v & mask) << s);
    }

    প্রথমে mask (w-টি 1) বানানো; w == 32 হলে 1u << 32 undefined behavior, তাই আলাদা করে ধরা হয়েছে (আর s + w ≤ ৩২ হতে হবে)। তারপর field-এর জায়গা মুছে (& ~(mask << s)), v-কে mask করে (যাতে বেশি বড় v পাশের field নষ্ট না করে) জায়গায় বসানো। set_field(0x3DA9, 5, 6, 63) = 0x3FE9 — উপরের stepper-এর ধাপ ৫-এর সাথে মেলে।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি অক্ষরকে হাতে UTF-8-এ encode করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. অক্ষরের code point বের করা (যেমন ক = U+0995)
  2. পরিসর দেখে byte সংখ্যা আর ছাঁচ ঠিক করা
  3. code point-কে binary-তে লেখা
  4. ডান দিক থেকে ৬ বিট করে ভাগ করা
  5. ভাগগুলো ছাঁচের x-ঘরে বসানো
  6. প্রতিটি byte hex-এ পড়া

বাংলা অক্ষর “ক” (U+0995) UTF-8-এ কয়টি byte নেয়?

  • ১
  • ২
  • ৩
  • ৪

একটি little-endian মেশিনে ৩২-বিটের মান 0x12345678 ঠিকানা A থেকে রাখা হলো। ঠিকানা A-তে কোন byte আছে?

  • 0x12
  • 0x78
  • 0x34
  • 0x56

x86-64 Linux-এ (int = ৪ byte, natural alignment) struct {char c; int i;}-এর sizeof কত?

  • ৫
  • ৬
  • ৮
  • ১৬

x = 1011 0100 (binary) হলে x & (x - 1) কত?

  • 1011 0000
  • 1011 0011
  • 0000 0100
  • ০

নিচের কোনটি UTF-8-এর বৈশিষ্ট্য নয়?

  • ASCII-compatible
  • Self-synchronizing
  • Endianness-এর ওপর নির্ভর করে না
  • প্রতিটি অক্ষর নির্দিষ্ট ৪ byte

বাংলা যুক্তাক্ষর “ক্ষ” Unicode-এ কয়টি code point দিয়ে লেখা হয়?

  • ১
  • ৩
  • ২
  • ৯

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. বিটের নিজের অর্থ নেই; type = পড়ার নিয়ম, যা instruction ঠিক করে। 0x41424337 একসাথে int, float, "7CBA" আর lui t1, 0x41424.
  2. Natural alignment: N byte-এর মান N-এর গুণিতক ঠিকানায়; তাই struct-এ padding, আর বড় থেকে ছোট সাজালে জায়গা বাঁচে।
  3. UTF-8: ১–৪ byte, ASCII-compatible, self-synchronizing; বাংলা প্রতি code point ৩ byte; “বাংলা” = ১৫ byte, ৫ code point, ২ grapheme.
  4. Endianness: little-endian-এ ছোট ঠিকানায় LSB (x86, RISC-V, ARM বাস্তবে), নেটওয়ার্ক big-endian; portable কোড byte ধরে shift করে।
  5. Bit কৌশল: OR-এ চালু, AND-এ বন্ধ/ছাঁকা, XOR-এ উল্টানো; field = (x >> s) & mask; x & (x − 1) নিচের 1 মোছে; নিশ্চিত logical shift-এর জন্য unsigned.

পরের পর্বে: এতক্ষণ বিটকে আমরা ডেটা হিসেবে দেখলাম। এবার বিট দিয়ে যুক্তি — Boolean algebra-র সূত্র, De Morgan আর SOP-POS, যা দিয়ে প্রতিটি digital circuit নকশা করা হয়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান