এই পর্বে যা শিখবেন
- বিটের অবস্থান, LSB/MSB আর hex দিয়ে বিট দ্রুত পড়া
- ছয়টি বিটওয়াইজ অপারেটরের আসল অর্থ — পরীক্ষা, চালু, বন্ধ, উল্টানো, সরানো
- একটি সংখ্যায় অনেকগুলো হ্যাঁ/না রাখা — ফাইলের অনুমতি, অ্যাপের নোটিফিকেশন, হাজিরা
- প্রচলিত কৌশল: জোড়-বিজোড়, ২-এর ঘাত, কয়টি বিট 1 — আর কোথায় কোথায় undefined behavior লুকিয়ে থাকে
- নেটওয়ার্ক, গ্রাফিক্স, হার্ডওয়্যার ও AI-এর লেখা বিট-কোড যাচাইয়ের চেকলিস্ট
১ · একটি সংখ্যা মানে এক সারি সুইচ
পর্ব ০৫-এ দেখেছেন, কম্পিউটারের মেমরিতে প্রতিটি সংখ্যা আসলে কয়েকটি bit-এর সারি। প্রতিটি বিট একটি ছোট সুইচ — 1 মানে অন, 0 মানে অফ। পর্ব ০৭-এ বিটওয়াইজ অপারেটরের সাথে প্রথম পরিচয়ও হয়েছে। এই পর্বে আমরা সেই পরিচয়কে দক্ষতায় বদলাব: একটি সংখ্যার ভেতরের যেকোনো একটি সুইচ খুঁজে বের করা, চালু করা, বন্ধ করা, উল্টে দেওয়া — বাকি সুইচে হাত না দিয়েই।
প্রথমে ছোট একটি পুনরালোচনা। Binary-তে ডান দিক থেকে প্রতিটি ঘরের মান আগের ঘরের দ্বিগুণ: ১, ২, ৪, ৮, ১৬, ৩২, ৬৪, ১২৮। ঘরগুলোর নম্বর গোনা হয় ডান দিক থেকে, ০ দিয়ে শুরু করে। তাই “বিট ০” মানে সবচেয়ে ডানের ঘর, যার মান ১; আর “বিট k”-এর মান ২k. সবচেয়ে ডানের বিটকে বলে LSB (least significant bit — সবচেয়ে কম ওজনের বিট), আর সবচেয়ে বাঁয়েরটিকে MSB (most significant bit — সবচেয়ে বেশি ওজনের বিট)।
bit position : 7 6 5 4 3 2 1 0
weight : 128 64 32 16 8 4 2 1
x = 166 : 1 0 1 0 0 1 1 0 = 128 + 32 + 4 + 2
MSB LSB
৮টি 0/1 পড়তে চোখ ক্লান্ত হয়ে যায়। তাই প্রোগ্রামাররা hexadecimal ব্যবহার করেন — প্রতিটি hex অঙ্ক ঠিক ৪টি বিট। ৪ বিটের এই দলটির একটি নাম আছে: nibble. ১৬৬-কে দুটি nibble-এ ভাগ করলে 1010 আর 0110, অর্থাৎ hex-এ A আর 6 — একসাথে 0xA6. বিপরীত দিকেও একই সহজ: 0xF0 দেখামাত্র বুঝবেন “বাঁয়ের চারটি বিট অন, ডানের চারটি অফ।” দশমিক ২৪০ দেখে সেটি বোঝা কঠিন।
| ৪ বিট | hex | ৪ বিট | hex |
|---|---|---|---|
0000 | 0 | 1000 | 8 |
0001 | 1 | 1001 | 9 |
0011 | 3 | 1010 | A |
0101 | 5 | 1011 | B |
0110 | 6 | 1100 | C |
0111 | 7 | 1111 | F |
ঢাকার একটি মেসের বৈদ্যুতিক সুইচবোর্ড ভাবুন। আটটি সুইচ পাশাপাশি — ফ্যান, টিউবলাইট, বারান্দার বাতি, পানির পাম্প… বোর্ডের পুরো অবস্থা একটি ৮ বিটের সংখ্যা দিয়েই লেখা যায়। কেউ যদি বলে “অবস্থা ১৬৬”, তাতে কিছু বোঝা যায় না। কিন্তু “1010 0110” বললে সাথে সাথে বোঝা যায় কোন কোন সুইচ অন। এই পর্বের সব কৌশল আসলে এই সুইচবোর্ড চালানোর নিয়ম।
সাধারণ অপারেটর (+, *) পুরো সংখ্যাকে একটি মান হিসেবে দেখে। Bitwise operator দেখে আলাদা আলাদা সুইচ — একই নম্বরের বিট জোড়ায় জোড়ায় মিলিয়ে, এক ঘরের ফল পাশের ঘরে কখনো “হাতে থাকে না” (carry নেই)। এজন্যই এগুলো CPU-র সবচেয়ে সস্তা ও দ্রুত কাজগুলোর মধ্যে পড়ে।
২ · ছয়টি অপারেটর — প্রতিটির আসল কাজ
পর্ব ০৭-এ truth table দেখেছেন। এখানে নিয়মগুলো একসাথে রাখছি, তবে সাথে যোগ করছি সবচেয়ে জরুরি তথ্যটি — বাস্তবে প্রতিটি অপারেটর দিয়ে কী করা হয়। ধরা যাক একটি বিট b, আর তার সাথে মেলানো হচ্ছে মাস্কের একটি বিট m:
| অপারেটর | প্রতিটি বিটে নিয়ম | m = 1 হলে b-এর কী হয় | m = 0 হলে | বাস্তব কাজ |
|---|---|---|---|---|
& AND | দুটোই 1 হলে 1 | যেমন ছিল তেমন থাকে | 0 হয়ে যায় | পরীক্ষা ও ছাঁকা (mask) |
| OR | যেকোনো একটি 1 হলে 1 | 1 হয়ে যায় | যেমন ছিল | চালু করা (set) |
^ XOR | দুটো আলাদা হলে 1 | উল্টে যায় | যেমন ছিল | উল্টানো (toggle) |
~ NOT | একটি সংখ্যারই সব বিট উল্টায় | মাস্ক নেই — সব বিট উল্টে যায় | “এটি বাদে সব” মাস্ক বানানো | |
<< বাঁয়ে শিফট | সব বিট k ঘর বাঁয়ে, ডানে 0 ঢোকে | — | মাস্ক বানানো, ×২k | |
>> ডানে শিফট | সব বিট k ঘর ডানে, ডানের বিট পড়ে যায় | — | টুকরো নিচে নামানো, ÷২k (unsigned) | |
তৃতীয় আর চতুর্থ কলাম দুটি মন দিয়ে দেখুন — এটাই পুরো রহস্য। মাস্কে যেখানে 0, সেখানে | আর ^ কিছুই বদলায় না; মাস্কে যেখানে 1, সেখানে & কিছুই বদলায় না। এই কারণেই একটি ঠিক মাস্ক বানাতে পারলে আপনি শুধু নির্দিষ্ট সুইচে হাত দিতে পারেন। আর “বন্ধ করা”র জন্য আলাদা কোনো অপারেটর নেই — & আর ~ মিলিয়ে কাজটি হয়: ~mask মানে “এই বিট ছাড়া সব 1”, তার সাথে & করলে শুধু ওই বিটটি 0 হয়।
এখন হাতে-কলমে দেখি। x = 1010 0110 (১৬৬) নিয়ে তার ২ নম্বর বিটটিকে পরীক্ষা, বন্ধ, চালু আর উল্টানো — প্রতিটি ধাপে আটটি বিটই দেখানো হলো, যাতে বোঝা যায় বাকি সাতটি সত্যিই অপরিবর্তিত থাকে:
| x (বাইনারি) | |
| দশমিক | |
| hex | |
| বিট ২ |
position 7 6 5 4 3 2 1 0
x 1 0 1 0 0 1 1 0
1u 0 0 0 0 0 0 0 1 1u << 2 0 0 0 0 0 1 0 0 = 4
x 1 0 1 0 0 1 1 0 x >> 2 0 0 1 0 1 0 0 1 ← বিট ২ নেমে এলো ঘর ০-তে & 1 0 0 0 0 0 0 0 1 ফল 1 → বিটটি অন
~mask 1 1 1 1 1 0 1 1 (নিচের ৮ বিট) x 1 0 1 0 0 1 1 0 & ----------------- x 1 0 1 0 0 0 1 0 = 0xA2 = 162
x 1 0 1 0 0 0 1 0 mask 0 0 0 0 0 1 0 0 | ----------------- x 1 0 1 0 0 1 1 0 = 0xA6 = 166
x 1 0 1 0 0 1 1 0 mask 0 0 0 0 0 1 0 0 ^ ----------------- x 1 0 1 0 0 0 1 0 = 162 (আবার ^ দিলে 166)
- শুরুর অবস্থা।
x = 0xA6, অর্থাৎ1010 0110. ঘর গুনুন ডান থেকে, ০ দিয়ে শুরু করে: বিট ০ = 0, বিট ১ = 1, বিট ২ = 1. আমাদের লক্ষ্য শুধু এই একটি সুইচ। - মাস্ক বানানো।
1uমানে unsigned সংখ্যা ১ — শুধু বিট ০ অন। তাকে ২ ঘর বাঁয়ে সরালে শুধু বিট ২ অন:0000 0100= ৪। যেকোনো k-এর জন্য একই সূত্র —1u << k. শেষেরuকেন জরুরি, তা অংশ ৬-এ দেখবেন। - পরীক্ষা। x-কে ২ ঘর ডানে সরালে বিট ২ নেমে আসে ঘর ০-তে, তারপর
& 1বাকি সব মুছে দেয়। ফল ১, অর্থাৎ বিটটি অন। খেয়াল করুন, x নিজে বদলায়নি — আমরা শুধু একটি নতুন মান হিসাব করেছি।x & (1u << 2)লিখলেও চলে, তবে তখন ফল ১ নয়, ৪ — “শূন্য নয়” মানেই অন। - বন্ধ।
~maskসব বিট উল্টে বানায় “বিট ২ ছাড়া সব অন”। এবার&করলে যেখানে মাস্কে 1, সেখানে x যেমন ছিল তেমন থাকে; শুধু বিট ২ হয় 0. x হলো1010 0010= ১৬২ — ঠিক ৪ কম, কারণ বিট ২-এর ওজন ৪। - চালু।
|দিয়ে মাস্ক জুড়লে বিট ২ আবার 1. বাকি সাতটি বিটে মাস্ক 0, তাই সেগুলো অপরিবর্তিত। বিটটি আগে থেকেই অন থাকলেও|=কোনো ক্ষতি করে না — এটি নিরাপদে বারবার চালানো যায়। - উল্টানো।
^দিয়ে বিট ২ অন থেকে অফ হলো (১৬২)। আরেকবার^=দিলে আবার অন (১৬৬)। মোবাইলের ফ্ল্যাশলাইট বোতামের মতো — একবার চাপলে জ্বলে, আবার চাপলে নেভে। চারটি সূত্রই এখন আপনার হাতে।
শিফট মানে ২ দিয়ে গুণ-ভাগ — কিন্তু শুধু unsigned-এ নিশ্চিত
Bit shift সব বিটকে একসাথে সরায়। দশমিকে ডানে একটি শূন্য বসালে মান ১০ গুণ হয়; বাইনারিতে একই কাজে মান ২ গুণ। তাই unsigned x-এর জন্য x << k = x × ২k (যতক্ষণ বিট বাঁ দিক দিয়ে পড়ে না যায়), আর x >> k = x ÷ ২k, ভাগশেষ বাদ দিয়ে। 13u >> 1 = ৬, কারণ 1101 ডানে সরে 0110 হয় — ডানের 1-টি পড়ে গেল, সেটাই ভাগশেষ। তবে পর্ব ০৭-এ যেমন বলা হয়েছে, গুণ-ভাগের জন্য হাতে শিফট লেখার দরকার নেই; compiler নিজেই x * 8-কে শিফটে বদলে নেয়। শিফটের আসল কাজ মাস্ক বানানো আর টুকরো সরানো।
৩ · মাস্ক দিয়ে টুকরো বের করা — রঙের কোড
ওয়েবসাইটে রঙ লেখা হয় #39B549-এর মতো ছয় অঙ্কের hex দিয়ে — এই পাতার সবুজটি ঠিক এই রঙ। এখানে আসলে তিনটি সংখ্যা পাশাপাশি বসানো: লাল (Red), সবুজ (Green), নীল (Blue), প্রতিটি এক byte (০–২৫৫)। একসাথে একটি ২৪ বিটের সংখ্যা: 0xRRGGBB. প্রশ্ন হলো, পুরো সংখ্যা থেকে শুধু সবুজ অংশটি কীভাবে বের করবেন?
কৌশলটি দুই ধাপের, আর এটি প্রায় সব “টুকরো বের করা” সমস্যায় খাটে: প্রথমে শিফট করে টুকরোটিকে একেবারে ডানে নামান, তারপর & দিয়ে বাকি সব মুছে দিন।
| কাজ | |
| ফল |
Red Green Blue hex 39 B5 49 bits 0011 1001 1011 0101 0100 1001 23 ... 16 15 ... 8 7 ... 0
(empty) Red Green bits 0000 0000 0011 1001 1011 0101 hex 00 39 B5 → 0x0039B5
value 0000 0000 0011 1001 1011 0101
0xFF 0000 0000 0000 0000 1111 1111
& ---------------------------------
0000 0000 0000 0000 1011 0101 → 0xB5
B5 = 11 x 16 + 5 = 181 red = (c >> 16) & 0xFF = 0x39 = 57 green = (c >> 8) & 0xFF = 0xB5 = 181 blue = c & 0xFF = 0x49 = 73
57u << 16 = 0x390000
| 181u << 8 = 0x00B500
| 73u = 0x000049
----------------------
0x39B549
- তিনটি byte এক সংখ্যায়।
0x39B549-এ প্রতি দুটি hex অঙ্ক এক byte: লাল39, সবুজB5, নীল49. সবুজ বসে আছে বিট ৮ থেকে ১৫-এ। আমাদের তাকে ঘর ০ থেকে ৭-এ আনতে হবে। - ৮ ঘর ডানে।
c >> 8সব বিটকে ৮ ঘর ডানে সরায়। নীল byte-টি ডান দিক দিয়ে পড়ে হারিয়ে গেল, সবুজ নেমে এলো সবচেয়ে নিচে। কিন্তু তার বাঁয়ে লাল এখনো বসে আছে —0x0039B5. & 0xFFদিয়ে ছাঁকা।0xFFমানে নিচের ৮টি বিট অন, বাকি সব অফ।&করলে মাস্কের 0-ওয়ালা ঘরগুলো (লাল সহ) মুছে যায়, শুধু সবুজ টিকে থাকে:0xB5. চালুনিতে চাল ছাঁকার মতো — মাস্ক ঠিক করে কোনটি থাকবে।- দশমিকে ১৮১।
B= ১১, তাই0xB5= ১১ × ১৬ + ৫ = ১৮১। একই সূত্রে লাল = ৫৭ (১৬ ঘর শিফট), নীল = ৭৩ (শিফট লাগেই না)। CSS-এrgb(57, 181, 73)লিখলে হুবহু একই রঙ দেখাবে। - উল্টো পথে জোড়া। তিনটি টুকরো থেকে আবার রঙ বানাতে প্রতিটিকে নিজের জায়গায় শিফট করুন, তারপর
|দিয়ে জুড়ুন। টুকরোগুলো আলাদা আলাদা ঘরে থাকে বলে একটির বিট আরেকটির সাথে মেশে না। একে বলে bit packing — ছবির ফাইল, নেটওয়ার্ক প্যাকেট, গেমের সেভ ফাইল সবখানে এটি চলে।
মাস্কের প্রস্থ সবসময় টুকরোর প্রস্থের সমান হবে। ৮ বিটের টুকরোর জন্য 0xFF, ৪ বিটের জন্য 0xF, ১২ বিটের জন্য 0xFFF. সাধারণ সূত্র: n বিটের মাস্ক = (1u << n) - 1 — যেমন n = ৫ হলে 100000 - 1 = 011111. (n যদি টাইপের পুরো প্রস্থের সমান হয়, এই সূত্র নিজেই undefined behavior — অংশ ৬ দেখুন।)
৪ · একটি সংখ্যায় অনেক হ্যাঁ/না — ফ্ল্যাগ
অনেক তথ্য আসলে শুধু হ্যাঁ/না: এই ফাইল কি পড়া যাবে? এই ব্যবহারকারী কি SMS পেতে চান? ছাত্রটি কি সোমবার উপস্থিত ছিল? প্রতিটি হ্যাঁ/না-র জন্য আলাদা ভ্যারিয়েবল না রেখে একটি সংখ্যার একেকটি বিটে রাখা যায়। এমন প্রতিটি বিটকে বলে bit flag, আর যে সংখ্যাটি দিয়ে নির্দিষ্ট ফ্ল্যাগটি বাছাই করা হয়, সেটি bit mask.
Linux-এর chmod 755 — তিন দলের তিনটি করে বিট
পর্ব ০৭-এ দেখেছেন, Linux-এ প্রতিটি ফাইলের অনুমতি r (পড়া), w (লেখা), x (চালানো) — তিনটি বিট। এবার পুরো ছবিটি দেখি: মালিক (owner), গ্রুপ (group) আর বাকি সবাই (others) — তিন দল, প্রত্যেকের তিনটি বিট, মোট ৯টি বিট। কমান্ডে যে তিন অঙ্ক লেখা হয়, সেটি আসলে octal (৮-ভিত্তিক) সংখ্যা, কারণ প্রতিটি octal অঙ্ক ঠিক ৩টি বিট — hex যেমন ৪টি।
| দেখতে যেমন | |
| ৯টি বিট | |
| দশমিক মান |
digit 7 5 5 group owner group others
r w x r w x r w x weight 4 2 1 4 2 1 4 2 1 7 = 4+2+1 1 1 1 5 = 4+1 1 0 1 1 0 1
bit 8 7 6 5 4 3 2 1 0
mode 1 1 1 1 0 1 1 0 1
0755 = 7 x 64 + 5 x 8 + 5 = 493
mode 1 1 1 1 0 1 1 0 1
letters r w x r - x r - x
-rwxr-xr-x deploy.sh
mode 1 1 1 1 0 1 1 0 1 0002 0 0 0 0 0 0 0 1 0 (S_IWOTH) & ----------------------- 0 0 0 0 0 0 0 0 0 = 0 → না mode & 0100 (S_IXUSR) = 0100 → মালিক চালাতে পারে
- তিন অঙ্ক, তিন দল।
chmod 755 deploy.sh— প্রথম অঙ্ক মালিকের, দ্বিতীয়টি গ্রুপের, তৃতীয়টি বাকি সবার। cPanel বা কোনো সার্ভারে ওয়েবসাইটের ফোল্ডারে যে “৭৫৫” দেখেন, সেটি এটাই। - প্রতিটি অঙ্ক ভেঙে ৩ বিট। r-এর ওজন ৪, w-এর ২, x-এর ১। ৭ = ৪ + ২ + ১, তাই
111— সব অনুমতি। ৫ = ৪ + ১, তাই101— পড়া আর চালানো, লেখা নয়। এজন্যই অনুমতির সংখ্যা কখনো ৮ বা ৯ হয় না: ৩ বিটে সর্বোচ্চ ৭। - ৯টি বিট এক সংখ্যায়। তিন দলকে পাশাপাশি বসালে
111 101 101. C-তে সংখ্যার শুরুতে0লিখলে সেটি octal ধরা হয়, তাই এই মান C-তে0755, দশমিকে ৪৯৩। সাবধান:chmod(path, 755)লিখলে সেটি দশমিক ৭৫৫ — সম্পূর্ণ ভিন্ন বিট! - বিট থেকে অক্ষর। প্রতিটি বিট 1 হলে তার অক্ষর (r, w বা x), 0 হলে ড্যাশ। ফল
rwxr-xr-x—ls -l-এ ঠিক এটাই দেখায় (সামনের-মানে এটি সাধারণ ফাইল, ফোল্ডার হলে সেখানেdথাকে)। &দিয়ে প্রশ্ন। বাকি সবাই কি লিখতে পারে?mode & 0002= ০, অর্থাৎ না — ভালো, অপরিচিত কেউ আপনার স্ক্রিপ্ট বদলাতে পারবে না। মালিক কি চালাতে পারে?mode & 0100শূন্য নয়, তাই হ্যাঁ। Linux-এর<sys/stat.h>ফাইলে এই মাস্কগুলোর নামই দেওয়া আছে:S_IWOTH,S_IXUSRইত্যাদি।
বাংলাদেশি উদাহরণ: সপ্তাহের হাজিরা এক byte-এ
একটি কোচিং সেন্টার সপ্তাহের ৭ দিনের হাজিরা রাখতে চায়। প্রতিটি ছাত্রের জন্য ৭টি আলাদা int রাখলে (প্রতিটি সাধারণত ৪ byte) লাগে ২৮ byte. অথচ প্রতিটি দিন শুধু হ্যাঁ/না — তাই একটি unsigned char-এর ৭টি বিটই যথেষ্ট, মাত্র ১ byte. বিট ০ = শনিবার, বিট ১ = রবিবার, … বিট ৬ = শুক্রবার:
bit : 7 6 5 4 3 2 1 0
day : - Fri Thu Wed Tue Mon Sun Sat
rafi : 0 0 1 1 1 0 1 1 = 0x3B
present on Monday? (rafi >> 2) & 1 -> 0 (absent)
days present popcount(rafi) -> 5
দশ হাজার ছাত্রের প্রতিষ্ঠানে এক বছরের (৫২ সপ্তাহ) হাজিরা: int দিয়ে প্রায় ১ কোটি ৪৬ লক্ষ byte, বিট দিয়ে মাত্র ৫ লক্ষ ২০ হাজার byte — প্রায় ২৮ ভাগের এক ভাগ। আর “এই সপ্তাহে কে কে পুরো ৫ দিন এসেছে” — এমন প্রশ্নের উত্তরও কয়েকটি বিট-অপারেশনেই পাওয়া যায়।
অ্যাপের নোটিফিকেশন সেটিং — কোডে
একটি মোবাইল ব্যাংকিং অ্যাপের কথা ভাবুন, যেখানে ব্যবহারকারী ঠিক করেন কোন পথে বার্তা পাবেন: SMS, ইমেইল, push নোটিফিকেশন, আর অফারের প্রচার-বার্তা। চারটি ফ্ল্যাগ, প্রতিটির নাম দেওয়া মাস্ক, আর বিটগুলো চোখে দেখার জন্য একটি ছোট print_bits ফাংশন:
#include <stdio.h>
#define SMS (1u << 0)
#define EMAIL (1u << 1)
#define PUSH (1u << 2)
#define PROMO (1u << 3)
static void print_bits(const char *label, unsigned x) {
printf("%-9s", label);
for (int k = 7; k >= 0; k--)
putchar(((x >> k) & 1u) ? '1' : '0');
printf(" (%u)\n", x);
}
int main(void) {
unsigned s = SMS | PUSH | PROMO; print_bits("start", s);
s &= ~PROMO; print_bits("no promo", s);
s |= EMAIL; print_bits("+email", s);
s ^= PUSH; print_bits("toggle", s);
printf("email on? %d\n", (s & EMAIL) != 0);
return 0;
}আউটপুট হবে 00001101 (13), তারপর প্রচার বন্ধ করে 00000101 (5), ইমেইল চালু করে 00000111 (7), push উল্টে 00000011 (3), শেষে email on? 1. খেয়াল করুন: প্রতিটি মাস্ক 1u << k দিয়ে বানানো আর বন্ধনীতে মোড়া — #define নামগুলো হুবহু বসিয়ে দেয় (পর্ব ২৭-এ বিস্তারিত), তাই বন্ধনী না থাকলে precedence-এর গোলমাল হতে পারত। print_bits-এর লুপটি অংশ ২-এর “পরীক্ষা” সূত্রটিই, বিট ৭ থেকে ০ পর্যন্ত। আর (s & EMAIL) != 0 লেখার কারণ — s & EMAIL নিজে ২ দিত, আমরা চাই সোজা ১ বা ০।
৫ · প্রতিদিনের কৌশল — যে সূত্রগুলো বারবার লাগে
বিট নিয়ে কাজ করা কোডে কয়েকটি ছোট সূত্র বারবার ফিরে আসে। এগুলো মুখস্থ করার চেয়ে কেন কাজ করে বোঝা জরুরি — তাহলে AI-এর দেওয়া কোডে ভুল থাকলে চোখে পড়বে।
| কাজ | সূত্র (x unsigned) | কেন কাজ করে |
|---|---|---|
| বিট k অন কি না | (x >> k) & 1u | বিট k-কে ঘর ০-তে এনে বাকি মুছে ফেলা |
| বিট k চালু | x |= 1u << k | OR-এ 1 মানে অবশ্যই 1 |
| বিট k বন্ধ | x &= ~(1u << k) | “k ছাড়া সব” মাস্কের সাথে AND |
| বিট k উল্টানো | x ^= 1u << k | XOR-এ 1 মানে উল্টো |
| জোড় না বিজোড় | x & 1u | বিজোড় সংখ্যার বিট ০ সবসময় 1 |
| সবচেয়ে নিচের 1-টি মুছে ফেলা | x & (x - 1) | ১ বিয়োগে নিচের 1 হয় 0, তার নিচের 0-গুলো হয় 1 |
| ২-এর ঘাত কি না | x && !(x & (x - 1)) | ২-এর ঘাতে ঠিক একটি 1 — সেটি মুছলেই ০ |
| নিচের n বিটের মাস্ক | (1u << n) - 1 | 1000 - 1 = 0111 |
x & (x - 1) — ছোট কিন্তু গভীর সূত্র
১ বিয়োগ করলে কী হয়, বাইনারিতে দেখুন। x = 0110 1000 (১০৪)। সবচেয়ে ডানের 1-টি আছে বিট ৩-এ। ১ বিয়োগ করলে সেই 1 হয় 0, আর তার ডানের সব 0 হয়ে যায় 1: x - 1 = 0110 0111. এবার দুটির AND: 0110 0000 — ঠিক ওই একটি বিট মুছে গেল, বাকি সব অক্ষত।
এখান থেকেই দুটি কাজের সূত্র আসে। প্রথম: ২-এর ঘাতে (১, ২, ৪, ৮…) ঠিক একটিই 1 থাকে। সেটি মুছলে ০ পাওয়া যায়। তাই !(x & (x - 1)) সত্য হলে x ২-এর ঘাত — তবে x = ০ হলেও সূত্রটি সত্য দেয়, সেজন্য সামনে x &&. দ্বিতীয়: বারবার নিচের 1 মুছতে থাকলে যতবার মুছতে পারলেন, ততগুলো বিট অন ছিল। কয়টি বিট 1 — এই হিসাবকে বলে popcount, পুরো নাম population count. সহজ লুপে প্রতিটি বিট দেখতে হয়; এই কৌশলে লুপ ঘোরে শুধু যতগুলো 1 আছে ততবার। ঐতিহাসিকভাবে কৌশলটি Brian Kernighan-এর নামে পরিচিত হলেও এটি তারও আগে থেকে প্রচলিত ছিল।
#include <stdio.h>
static unsigned count_ones(unsigned x) {
unsigned n = 0;
while (x != 0) {
n += x & 1u; /* look at bit 0 */
x >>= 1; /* next bit moves down */
}
return n;
}
static int is_pow2(unsigned x) { return x && !(x & (x - 1)); }
int main(void) {
unsigned v[] = {0, 1, 6, 64, 96, 255, 0x39B549};
for (unsigned i = 0; i < sizeof v / sizeof v[0]; i++)
printf("%8u ones=%2u builtin=%2d pow2=%s\n", v[i], count_ones(v[i]),
__builtin_popcount(v[i]), is_pow2(v[i]) ? "yes" : "no");
return 0;
}count_ones প্রতিবার বিট ০ দেখে যোগ করে, তারপর পুরো সংখ্যাকে এক ঘর ডানে সরায় — যতক্ষণ না সংখ্যাটি ০ হয়। ফল: ০ → ০টি, ১ → ১টি, ৬ (110) → ২টি, ৬৪ → ১টি, ৯৬ (0110 0000) → ২টি, ২৫৫ → ৮টি, আর রঙের কোড 0x39B549 (দশমিকে 3781961) → ১২টি। __builtin_popcount হলো GCC ও Clang-এর একটি builtin ফাংশন — standard C-র অংশ নয়, কিন্তু আধুনিক CPU-তে এক নির্দেশেই হিসাব করতে পারে। দুই কলাম মিলছে কি না দেখুন — নিজের ফাংশনকে builtin দিয়ে যাচাই করা একটি ভালো অভ্যাস। ২-এর ঘাত শুধু ১ আর ৬৪; ০-কে x && অংশটি বাদ দিয়েছে।
পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়ের পরীক্ষা, সরকারি-ব্যাংকের প্রোগ্রামার পদের লিখিত পরীক্ষা আর কোডিং ইন্টারভিউতে এই ধরনের প্রশ্ন আসতে পারে:
5 & 3,5 | 3,5 ^ 3-এর মান?101আর011মিলিয়ে: ১, ৭, ৬।int-এ~5-এর মান কত? −৬, কারণ two's complement-এ~x=-x - 1.- একটি সংখ্যা ২-এর ঘাত কি না, লুপ ছাড়া কীভাবে বুঝবেন?
x && !(x & (x - 1)). - একটি সংখ্যার কয়টি বিট 1, দক্ষভাবে গুনুন।
x &= x - 1লুপ — শুধু 1-এর সংখ্যার সমান বার ঘোরে। - অ্যারেতে সব সংখ্যা দুবার, একটি মাত্র একবার — সেটি খুঁজুন। সবগুলোর XOR; জোড়াগুলো কেটে যায় (অংশ ৭)।
৬ · বিপদের জায়গা — যেখানে বিট-কোড চুপচাপ ভুল করে
বিটওয়াইজ অপারেটর ছোট আর দ্রুত — কিন্তু C-র নিয়মে এখানে বেশ কয়েকটি গর্ত আছে। কিছু জায়গায় ফল implementation-defined (compiler ঠিক করে, তবে নথিতে লিখে রাখে), আর কিছু জায়গায় সরাসরি undefined behavior — যেখানে C কোনো প্রতিশ্রুতিই দেয় না, আর optimizer যা খুশি তাই করতে পারে। পর্ব ০৭-এ শিফটের কয়েকটি সীমা দেখেছেন; নিচে সেগুলোর সাথে আরও কয়েকটি ফাঁদ এক টেবিলে রাখা হলো।
| কোড | সমস্যা | নিরাপদ বিকল্প |
|---|---|---|
1 << 31 (৩২ বিটের int) | Signed overflow — undefined behavior | 1u << 31 |
x << 32 (৩২ বিটের x), x >> 40 | টাইপের প্রস্থের সমান বা বেশি শিফট — undefined behavior. x86-এ প্রায়ই শিফটই হয় না! | শিফটের পরিমাণ আগে যাচাই করুন |
x << -1 | ঋণাত্মক পরিমাণ — undefined behavior, উল্টো দিকে সরে না | দিক আলাদা করে লিখুন |
-8 >> 1 | ঋণাত্মক signed সংখ্যা ডানে — implementation-defined (GCC চিহ্ন-বিট কপি করে) | unsigned-এ কাজ করুন |
-1 << 2 | ঋণাত্মক signed সংখ্যা বাঁয়ে — undefined behavior | unsigned-এ কাজ করুন |
1u << 40 (৬৪ বিট চাই) | 1u সাধারণত ৩২ বিট — প্রস্থের বাইরে, undefined behavior | UINT64_C(1) << 40 বা (uint64_t)1 << 40 |
if (x & 1 == 0) | == আগে কাজ করে: x & (1 == 0) = x & 0 = সবসময় ০ | if ((x & 1) == 0) |
~c == 0x59 (unsigned char c = 0xA6) | ~-এর আগে c বড় হয়ে int হয়, ফল 0xFFFFFF59-এর মতো, তাই তুলনা মিথ্যা | (unsigned char)~c == 0x59 |
শেষ সারিটির পেছনে আছে integer promotion — পর্ব ০৮-এর লুকানো রূপান্তর। int-এর চেয়ে ছোট টাইপ (char, short) নিয়ে যেকোনো হিসাবের আগে C তাকে int-এ বড় করে নেয়। ফলে ৮ বিটের ~ আসলে ৩২ বিটের ~ হয়ে যায়, আর বাঁয়ের ২৪টি নতুন বিটও 1 হয়ে ওঠে। ৮ বিটের মাস্ক নিয়ে কাজ করলে ফলকে আবার ৮ বিটে ফেরত আনতে হয়।
precedence-এর ফাঁদটি সবচেয়ে বেশি ধরা খায়। C-তে &, ^, |-এর অগ্রাধিকার == আর !=-এর চেয়ে কম — এটি ১৯৭০-এর দশকের একটি ঐতিহাসিক সিদ্ধান্ত, যা Dennis Ritchie নিজেও পরে ভুল বলে স্বীকার করেছেন, কিন্তু পুরোনো কোড ভেঙে যাবে বলে আর বদলানো যায়নি। আবার শিফট (<<, >>) বাঁধে +-এর চেয়েও দুর্বলভাবে: 1 << n - 1 মানে 1 << (n - 1). নিয়ম একটাই — বিটের প্রতিটি এক্সপ্রেশনে বন্ধনী দিন। সুখবর হলো, -Wall চালু থাকলে GCC x & 1 == 0 দেখে সতর্ক করে: suggest parentheses around comparison in operand of '&'.
বিটের কাজে unsigned, নির্দিষ্ট প্রস্থ আর বন্ধনী। <stdint.h>-এর uint8_t, uint32_t, uint64_t ব্যবহার করুন — তাহলে কত বিট নিয়ে কাজ করছেন, তা কোডেই লেখা থাকে। আর অনুমান থাকলে সেটি compiler-কে দিয়ে যাচাই করিয়ে নিন: _Static_assert(sizeof(unsigned) * CHAR_BIT == 32, "need 32-bit unsigned"); — অনুমান ভুল হলে প্রোগ্রাম compile-ই হবে না। এই কৌশলের নাম static assert.
৭ · XOR-এর জাদু — আর তার সীমা
XOR-এর তিনটি বৈশিষ্ট্য তাকে বিশেষ করেছে: a ^ 0 = a, a ^ a = 0, আর ক্রম বদলালেও ফল একই। তাই কোনো মান দিয়ে দুবার XOR করলে আগের মানে ফিরে আসা যায়: (a ^ k) ^ k = a. এখান থেকেই কয়েকটি বিখ্যাত কৌশল।
অস্থায়ী ভ্যারিয়েবল ছাড়া swap — এবং কেন ব্যবহার করবেন না
a = 5 (0101), b = 3 (0011)
a ^= b; // a = 0110
b ^= a; // b = 0011 ^ 0110 = 0101 = 5
a ^= b; // a = 0110 ^ 0101 = 0011 = 3 -> swapped
ইন্টারভিউতে প্রশ্নটি আসতে পারে, তাই জানা দরকার। কিন্তু আসল কোডে XOR swap লিখবেন না। কারণ তিনটি: (১) দুটি পয়েন্টার যদি একই ভ্যারিয়েবল দেখায়, প্রথম লাইনেই a ^ a = 0 — মান চিরতরে মুছে যায়। (২) তিনটি লাইনের প্রতিটি আগেরটির ফলের অপেক্ষায় থাকে, তাই আধুনিক CPU এগুলো একসাথে চালাতে পারে না; সাধারণ tmp দিয়ে swap প্রায়ই সমান বা বেশি দ্রুত, কারণ compiler সেটিকে register-এর ভেতরেই সেরে ফেলে। (৩) পড়তে কঠিন — পরের প্রোগ্রামারকে থেমে ভাবতে হয়।
Parity আর checksum — ভুল ধরার সবচেয়ে সস্তা উপায়
কয়েকটি বিটের XOR করলে জানা যায় 1-এর সংখ্যা জোড় না বিজোড় — একে বলে parity. পাঠানোর সময় একটি বাড়তি parity বিট জুড়ে দিলে, পথে যেকোনো একটি বিট উল্টে গেলে প্রাপক টের পান। একইভাবে অনেকগুলো byte-এর XOR নিয়ে একটি ছোট checksum বানানো যায়। RAID 5 নামের ডিস্ক-ব্যবস্থা এই ধারণার বড় রূপ: কয়েকটি ডিস্কের ডেটার XOR একটি parity অংশে রাখা হয়। একটি ডিস্ক নষ্ট হলে বাকিগুলোর XOR করলেই হারানো ডেটা ফিরে আসে — কারণ A ^ B ^ (A ^ B) = 0, তাই B = A ^ P.
XOR “এনক্রিপশন” — নিরাপদ নয়
একটি গোপন চাবি দিয়ে বার্তার প্রতিটি byte XOR করলে লেখাটি দুর্বোধ্য দেখায়, আর একই চাবিতে আবার XOR করলে ফিরে আসে। অনেক টিউটোরিয়াল (আর AI) একে “এনক্রিপশন” বলে চালিয়ে দেয়। কিন্তু ছোট চাবি বারবার ব্যবহার করা এমন XOR cipher খুব সহজে ভাঙা যায়: বার্তার কোনো অংশ অনুমান করা গেলে (যেমন প্রতিটি JSON {" দিয়ে শুরু হয়) cipher ^ plain করেই চাবি বেরিয়ে আসে। পাসওয়ার্ড বা টাকা-পয়সার তথ্য রক্ষায় সবসময় পরীক্ষিত লাইব্রেরির AES বা ChaCha20 ব্যবহার করুন — নিজে বানানো “এনক্রিপশন” নয়। নিরাপত্তার এই দিকটি বিস্তারিত আছে Cybersecurity কোর্সে।
১৯৪৯ সালে Claude Shannon গাণিতিকভাবে প্রমাণ করেন, চাবি যদি বার্তার সমান লম্বা, সত্যিকারের এলোমেলো এবং মাত্র একবার ব্যবহৃত হয়, তবে XOR দিয়ে করা এনক্রিপশন (one-time pad) একেবারে ভাঙা অসম্ভব। কিন্তু শর্ত ভাঙলেই সব শেষ: শীতল যুদ্ধের সময় সোভিয়েত গোয়েন্দা বার্তার কিছু চাবি একাধিকবার ব্যবহার হয়েছিল, আর মার্কিন Venona প্রকল্প ঠিক সেই ভুল কাজে লাগিয়ে হাজার হাজার বার্তার অংশবিশেষ পড়ে ফেলে।
৮ · বিট কোথায় কাজে লাগে — নেটওয়ার্ক থেকে গেম
নেটওয়ার্ক: subnet mask আসলে একটি AND
একটি IP address (IPv4) আসলে ৩২ বিটের একটি সংখ্যা, পড়ার সুবিধার জন্য চার byte আলাদা করে লেখা: 192.168.1.130. এর বাঁয়ের অংশ নেটওয়ার্কের পরিচয়, ডানের অংশ সেই নেটওয়ার্কের ভেতরে নির্দিষ্ট যন্ত্রের। কোথায় ভাগ, তা বলে subnet mask. বাসার রাউটারে সাধারণত দেখবেন /24 = 255.255.255.0 — অর্থাৎ বাঁয়ের ২৪টি বিট 1, শেষ ৮টি 0. এবার একটু ছোট নেটওয়ার্ক, /26, দিয়ে পুরো হিসাবটি দেখি:
| বিষয় | |
| ফল |
IP 11000000.10101000.00000001.10000010
192 168 1 130
mask 11111111.11111111.11111111.11000000
255 255 255 192
IP 11000000.10101000.00000001.10000010
mask 11111111.11111111.11111111.11000000
& -----------------------------------
net 11000000.10101000.00000001.10000000 = 192.168.1.128
~mask 00000000.00000000.00000000.00111111
IP 11000000.10101000.00000001.10000010
& -----------------------------------
host 00000000.00000000.00000000.00000010 = 2
net 11000000.10101000.00000001.10000000
~mask 00000000.00000000.00000000.00111111
| -----------------------------------
bcast 11000000.10101000.00000001.10111111 = 192.168.1.191
usable hosts = 2^6 - 2 = 62 (.129 to .190)
- IP মানে ৩২টি বিট। প্রতিটি অংশ এক byte: ১৯২ =
11000000, ১৬৮ =10101000, ১ =00000001, ১৩০ =10000010. বিন্দুগুলো শুধু মানুষের পড়ার সুবিধার জন্য। /26মানে ২৬টি 1. প্রথম তিন byte পুরো 1 (২৫৫), চতুর্থ byte-এ বাঁয়ের দুটি বিট 1:11000000= ১২৮ + ৬৪ = ১৯২। তাই মাস্ক255.255.255.192./24হলে চতুর্থ byte পুরোটাই 0 —255.255.255.0.- AND = নেটওয়ার্ক ঠিকানা। মাস্কের 1-ওয়ালা ঘরে IP-র বিট থাকে, 0-ওয়ালা ঘরে মুছে যায়। শেষ byte:
10000010 & 11000000 = 10000000= ১২৮। নেটওয়ার্ক192.168.1.128. রাউটার প্রতিটি প্যাকেটের জন্য ঠিক এই AND করে ঠিক করে প্যাকেটটি কোন দিকে পাঠাবে। - উল্টো মাস্ক = যন্ত্রের নম্বর।
~maskশুধু ডানের ৬টি বিট রাখে।130 & 63= ২ — অর্থাৎ এই নেটওয়ার্কের ২ নম্বর যন্ত্র। একই ছাঁকুনি, শুধু উল্টো দিক থেকে। - OR = শেষ ঠিকানা। নেটওয়ার্কের যন্ত্র-অংশের সব বিট 1 করলে পাওয়া যায় broadcast ঠিকানা
192.168.1.191. যন্ত্র-অংশে ৬টি বিট, তাই ২৬ = ৬৪টি ঠিকানা; প্রথমটি (নেটওয়ার্ক) আর শেষটি (broadcast) বাদে ৬২টি যন্ত্র বসানো যায়। নেটওয়ার্ক ইঞ্জিনিয়ারদের পুরো subnetting হিসাব এই তিনটি অপারেশন।
প্যাকেটের header-এও বিট গাদাগাদি করে রাখা হয়। IPv4 header-এর প্রথম byte সাধারণত 0x45: বাঁয়ের nibble ৪ মানে IPv4, ডানের nibble ৫ মানে header-এর দৈর্ঘ্য ৫ × ৪ = ২০ byte. TCP header-এ SYN, ACK, FIN-এর মতো ফ্ল্যাগগুলো একটি byte-এর আলাদা আলাদা বিট — SYN = 0x02, ACK = 0x10, তাই সংযোগ শুরুর উত্তরে আসে 0x12. পর্ব ৫০-এ সকেট প্রোগ্রামিংয়ে এগুলো আবার দেখবেন; আরও গভীরে যেতে Computer Networks কোর্স।
আরও কিছু জায়গা
হার্ডওয়্যার register
Microcontroller-এর register-এ প্রতিটি বিট একটি পিন বা সেটিং। PORTB |= 1u << 5 দিয়ে একটি LED জ্বলে, পাশের পিনে হাত পড়ে না।
হ্যাশ ফাংশন
FNV বা djb2-এর মতো hash function XOR, শিফট আর গুণ মিলিয়ে বিটগুলোকে ভালোভাবে এলোমেলো করে।
পর্ব ৩৭ · হ্যাশ টেবিলBitset ও bitmask DP
হাজার হাজার হ্যাঁ/না একটি bitset-এ; আর ২০টি শহরের কোনগুলো ঘোরা হয়েছে — একটি সংখ্যার ২০টি বিটে।
পর্ব ৪২ · DP, পর্ব ৪৪ · কনটেস্টCompression
ZIP বা ছবির ফাইলে কোড শব্দগুলো byte-এর সীমা মানে না — কোনোটি ৩ বিট, কোনোটি ৯ বা ১২ বিট। লেখা-পড়া হয় শিফট আর মাস্কে।
Huffman coding · পর্ব ৪৩৯ · AI-এর যুগে বিট-কোড যাচাই
AI বিট-কোড খুব দ্রুত লেখে, আর দেখতেও পেশাদার লাগে। কিন্তু এই জায়গার ভুলগুলো সাধারণত compile হয়ে যায়, ছোট পরীক্ষায় ঠিকঠাক চলে, আর ভাঙে কেবল বিশেষ মানে — বিট ৩১, ঋণাত্মক সংখ্যা, ৬৪ বিটের সিস্টেম। এজন্য এখানে যাচাইয়ের দায়িত্ব আরও বেশি।
| AI প্রায়ই যা লেখে | কখন ভাঙে | যা চাইবেন |
|---|---|---|
flags |= 1 << k; (int) | k = ৩১ হলে undefined behavior | 1u << k বা UINT32_C(1) << k |
int ভ্যারিয়েবলে >> | ঋণাত্মক মানে ফল compiler-নির্ভর | uint32_t |
if (x & MASK == MASK) | সবসময় — precedence ভুল | বন্ধনী: (x & MASK) == MASK |
unsigned long mask = 1 << 40; | ডানদিকের হিসাব ৩২ বিটের int-এ হয়, বাঁয়ের টাইপ সাহায্য করে না | UINT64_C(1) << 40 |
#define FLAG 1 << 3 | x & FLAG লিখলে precedence গোলমাল | #define FLAG (1u << 3) |
| XOR দিয়ে “পাসওয়ার্ড এনক্রিপশন” | সবসময় — সহজেই ভাঙা যায় | পরীক্ষিত crypto লাইব্রেরি |
বিট-কোড চাওয়ার সময় শর্তগুলো প্রম্পটেই বলে দিন — প্রস্থ, টাইপ, আর যাচাইয়ের উপায়। শেষ লাইনটি সবচেয়ে কাজের: AI-কে নিজের ফল হাতে-হিসাবের মান দিয়ে প্রমাণ করতে বলা।
AI-এর বিট-কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১কম্পিউটার কেন দশমিক নয়, বাইনারিতে চলে? সব যন্ত্রই কি তাই ছিল?
না, সব যন্ত্র বাইনারি ছিল না। ১৯৪০-এর দশকের বিখ্যাত ENIAC ছিল দশমিক যন্ত্র — প্রতিটি অঙ্ক রাখতে দশটি অবস্থার একটি রিং-কাউন্টার ব্যবহার হতো। ১৯৫৮ সালে মস্কো স্টেট বিশ্ববিদ্যালয়ে তৈরি হয় Setun, যা চলত তিন-অবস্থার (ternary) যুক্তিতে। তবু শেষ পর্যন্ত জয় হয়েছে বাইনারির। কেন?
- শব্দ-দূষণের বিরুদ্ধে সহনশীলতা: একটি তারে ভোল্টেজ কখনো ঠিক নির্দিষ্ট থাকে না — তাপমাত্রা, পাশের তারের প্রভাব, বিদ্যুতের ওঠানামায় একটু সরে যায়। শুধু দুটি অবস্থা হলে “অর্ধেকের বেশি = 1, কম = 0” — মাঝখানে বড় একটি নিরাপদ ফাঁক। দশটি অবস্থা হলে প্রতিটির মাঝে ফাঁক অনেক সরু, ভুল পড়ার ঝুঁকি বেশি।
- Transistor-এর স্বভাব: Transistor সবচেয়ে নির্ভরযোগ্যভাবে কাজ করে পুরো অন অথবা পুরো অফ অবস্থায়, আর তখন বিদ্যুৎও কম খরচ হয়।
- গণিত আগে থেকেই তৈরি ছিল: ১৯৩৭ সালে Claude Shannon তাঁর মাস্টার্স থিসিসে দেখান, George Boole-এর সত্য/মিথ্যার বীজগণিত দিয়েই সুইচের সার্কিট নকশা করা যায়। ফলে যোগ, তুলনা, এমনকি পুরো CPU — সবকিছু AND, OR, NOT গেট দিয়ে বানানো সম্ভব হলো।
এই পর্বের প্রতিটি অপারেটর আসলে সেই গেটগুলোরই সরাসরি প্রতিচ্ছবি: & একটি AND গেটের সারি, | OR গেটের সারি। তাই এগুলো এত দ্রুত — CPU-র ভেতরে কোনো “হিসাব” নেই, শুধু বিদ্যুৎ গেট পার হয়। মজার ব্যাপার, ফ্ল্যাশ মেমরি (আপনার ফোনের স্টোরেজ) আজ একটি ঘরে ৩ বা ৪ বিট রাখতে একাধিক ভোল্টেজ-স্তর ব্যবহার করে — জায়গা বাঁচাতে শব্দ-দূষণের ঝুঁকি মেনে নিয়ে, আর ভুল সংশোধনের কোড দিয়ে সেটি সামলে।
মূল উপলব্ধি: বাইনারি কোনো গাণিতিক “সৌন্দর্য” থেকে আসেনি, এসেছে প্রকৌশলের নির্ভরযোগ্যতা থেকে। দুটি অবস্থা মানে সবচেয়ে বড় নিরাপত্তা-ফাঁক — আর বিটওয়াইজ অপারেটর সেই হার্ডওয়্যারের ভাষায় সরাসরি কথা বলা।
প্র ০২দাবার ইঞ্জিন কীভাবে ৬৪ বিটের একটি সংখ্যা দিয়ে পুরো বোর্ড ভাবে?
দাবার বোর্ডে ঠিক ৬৪টি ঘর, আর আধুনিক CPU-র একটি register-এ ঠিক ৬৪টি বিট — এই মিলটিই bitboard কৌশলের ভিত্তি। প্রতিটি ঘর একটি বিট। “সাদা ঘোড়াগুলো কোথায়” — একটি ৬৪ বিটের সংখ্যা। “কালো সৈন্যগুলো কোথায়” — আরেকটি। মোট ১২ ধরনের ঘুঁটির জন্য ১২টি সংখ্যাতেই পুরো বোর্ড।
এবার প্রশ্নগুলো কত সহজ হয়ে যায় দেখুন:
- সব সাদা ঘুঁটি: ছয়টি সাদা bitboard-এর OR.
- কোন ঘর ফাঁকা: সব ঘুঁটির OR-এর
~. - সাদা সৈন্য এক ঘর এগোলে: পুরো bitboard-কে ৮ ঘর শিফট — আটটি সৈন্য একসাথে, এক নির্দেশে! তারপর ফাঁকা ঘরের সাথে AND করলেই বৈধ চাল।
- কয়টি ঘুঁটি আছে: popcount — আধুনিক CPU-তে এক নির্দেশ।
লুপ চালিয়ে ৬৪টি ঘর আলাদা করে দেখার বদলে একটি অপারেশনে ৬৪টি ঘরের হিসাব হয় — এটি এক ধরনের সমান্তরাল গণনা, যাকে কখনো “SWAR” (SIMD within a register) বলা হয়। বিশ্বের অন্যতম শক্তিশালী ওপেন-সোর্স দাবা ইঞ্জিন Stockfish এই bitboard-এর ওপরই দাঁড়িয়ে আছে। মন্ত্রী বা নৌকার মতো লম্বা পথে চলা ঘুঁটির চাল দ্রুত বের করতে “magic bitboard” নামে আরও চতুর একটি কৌশল আছে, যেখানে একটি বিশেষ সংখ্যা দিয়ে গুণ করে বোর্ডের অবস্থাকে একটি টেবিলের সূচকে রূপান্তর করা হয়।
একই চিন্তা দাবার বাইরেও চলে: গেমের মানচিত্রে কোন টাইল দেয়াল, ছবিতে কোন পিক্সেল নির্বাচিত, Sudoku-র কোন ঘরে কোন সংখ্যা বসতে পারে — সব জায়গায় bitboard-ধাঁচের সমাধান।
মূল উপলব্ধি: ডেটার আকার যখন হার্ডওয়্যারের প্রস্থের সাথে মিলে যায়, তখন একটি নির্দেশেই অনেক কাজ হয়। ভালো ডেটা-বিন্যাস প্রায়ই চতুর অ্যালগরিদমের চেয়েও বেশি গতি আনে।
প্র ০৩Bitset আর Bloom filter কীভাবে এত মেমরি বাঁচায়? এর দাম কী?
একটি bool বা char সাধারণত পুরো এক byte নেয় — অথচ তথ্য মাত্র এক বিট। ১০ কোটি সংখ্যার মধ্যে কোনগুলো আছে, তা বিট দিয়ে রাখলে লাগে ১০ কোটি বিট ≈ ১২.৫ MB; char দিয়ে রাখলে ১০০ MB, আর int দিয়ে ৪০০ MB. এই হলো bitset: k নম্বর জিনিসটি আছে কি না, তা bits[k / 8] byte-এর k % 8 নম্বর বিটে। Sieve of Eratosthenes-এ মৌলিক সংখ্যা খোঁজার সময় এভাবেই কোটি সংখ্যা মেমরিতে আঁটানো হয়। মেমরি কম মানে CPU cache-এ বেশি ডেটা আঁটে — তাই অনেক সময় গতিও বাড়ে।
১৯৭০ সালে Burton Bloom আরও এক ধাপ এগোলেন। ধরুন জানতে চান একটি ওয়েবসাইট কালো তালিকায় আছে কি না, কিন্তু তালিকায় কোটি কোটি ঠিকানা। Bloom filter-এ একটি বড় bitset থাকে আর কয়েকটি আলাদা hash function. একটি ঠিকানা যোগ করতে প্রতিটি hash যে বিট দেখায়, সেগুলো চালু করুন। খোঁজার সময় একই বিটগুলো দেখুন:
- কোনো একটি বিটও বন্ধ থাকলে — নিশ্চিতভাবে নেই।
- সবগুলো চালু থাকলে — সম্ভবত আছে; অন্য ঠিকানাগুলো মিলে কাকতালীয়ভাবে বিটগুলো চালু করে থাকতে পারে — একে বলে false positive.
এই দাম মেনে নিয়ে বিশাল সাশ্রয়: আসল ঠিকানাগুলো না রেখে প্রতিটির জন্য মাত্র কয়েকটি বিট। অনেক ডেটাবেস ইঞ্জিন ডিস্কে ফাইল খোলার আগে Bloom filter দেখে নেয় — “নিশ্চিতভাবে নেই” হলে ধীর ডিস্ক-পড়াটাই বাঁচে। ভুল “সম্ভবত আছে” হলে কেবল একবার বাড়তি যাচাই লাগে।
দাম আরও আছে: Bloom filter থেকে সাধারণভাবে কিছু মোছা যায় না (একটি বিট হয়তো অন্য জিনিসেরও), আর মেমরি কম দিলে ভুলের হার বাড়ে।
মূল উপলব্ধি: প্রকৌশলে “একদম নির্ভুল” সবসময় জরুরি নয়। কোন ভুল সহ্য করা যায় (মাঝে মাঝে বাড়তি যাচাই) আর কোনটা যায় না (কিছু হারিয়ে ফেলা) — সেটি বুঝে নকশা করলে বিট-স্তরের ছোট কৌশলও বড় সিস্টেম বাঁচায়।
প্র ০৪এত অপারেটর থাকতে XOR কেন ক্রিপ্টোগ্রাফি আর RAID-এর কেন্দ্রে?
XOR-এর একটি অসাধারণ গুণ আছে: এটি নিজেই নিজের উল্টো। (a ^ k) ^ k = a. যোগের উল্টো বিয়োগ, গুণের উল্টো ভাগ — কিন্তু XOR-এর উল্টো XOR নিজেই। তাই একই সার্কিট দিয়ে তালা দেওয়া আর খোলা দুটোই হয়।
দ্বিতীয় গুণটি আরও গভীর: তথ্য লুকিয়ে ফেলা। AND-এর ক্ষেত্রে ফল 1 হলে আপনি নিশ্চিত দুটো ইনপুটই 1 ছিল — তথ্য ফাঁস হয়। কিন্তু XOR-এ চাবির বিটটি যদি সত্যিকারের এলোমেলো হয়, তবে ফল 0 বা 1 দেখে মূল বিট সম্পর্কে কিছুই জানা যায় না — দুটো সম্ভাবনাই সমান। Shannon-এর one-time pad প্রমাণ এই সহজ সত্যের ওপর দাঁড়িয়ে।
- আধুনিক stream cipher: ChaCha20 একটি গোপন চাবি থেকে লম্বা এলোমেলো-দেখতে বিটের স্রোত বানায়, তারপর বার্তার সাথে XOR. কঠিন কাজটি হলো ভালো স্রোত বানানো; XOR শুধু শেষ ধাপ।
- AES: প্রতিটি রাউন্ডে ডেটার সাথে রাউন্ড-চাবি XOR করা হয় (AddRoundKey ধাপ)।
- RAID 5: ডিস্ক A, B, C-র ডেটার XOR রাখা হয় parity হিসেবে:
P = A ^ B ^ C. B নষ্ট হলেB = A ^ C ^ P— একই অপারেশন, উল্টো দিকে। ডেটাসেন্টারে প্রতিদিন এই হিসাব চলে।
কিন্তু মনে রাখুন — XOR নিজে নিরাপত্তা দেয় না, নিরাপত্তা আসে চাবির মান থেকে। চাবি ছোট হলে, পুনরাবৃত্তি হলে বা অনুমানযোগ্য হলে XOR কিছুই লুকায় না, যেমন অংশ ৭-এ দেখেছেন। Venona-র ঘটনা দেখায়, গাণিতিকভাবে নিখুঁত ব্যবস্থাও মানুষের একটি শর্টকাটে ভেঙে পড়ে।
মূল উপলব্ধি: সবচেয়ে শক্তিশালী ব্যবস্থাগুলো প্রায়ই সবচেয়ে সরল অংশ দিয়ে তৈরি — কিন্তু সেই অংশগুলো ঠিক কোন শর্তে কাজ করে, সেটি না বুঝে ব্যবহার করলে সরলতাই বিপদ হয়ে দাঁড়ায়।
প্র ০৫CPU কীভাবে যেকোনো পরিমাণ শিফট এক ধাপেই করে ফেলে?
সহজ চিন্তায় মনে হয়, ১৩ ঘর শিফট মানে ১৩ বার এক ঘর করে সরানো। প্রথম দিকের কিছু প্রসেসর সত্যিই প্রায় এভাবে কাজ করত — যেমন Intel 8086-এ শিফটের পরিমাণ যত বেশি, নির্দেশটি তত বেশি clock cycle নিত। আধুনিক CPU-তে কাজটি করে barrel shifter নামের একটি সার্কিট, যা যেকোনো পরিমাণ শিফট একটি ধাপেই শেষ করে।
কৌশলটি বাইনারিরই সৌন্দর্য। শিফটের পরিমাণ নিজেও একটি বাইনারি সংখ্যা। ১৩ = 01101 = ৮ + ৪ + ১। তাই সার্কিটে কয়েকটি স্তর রাখা হয় — প্রথম স্তর চাইলে ১ ঘর সরায়, দ্বিতীয়টি ২ ঘর, তৃতীয়টি ৪, চতুর্থটি ৮, পঞ্চমটি ১৬। প্রতিটি স্তর শিফট-পরিমাণের একটি বিট দেখে ঠিক করে “সরাব, না সোজা পার করে দেব।” ৩২ বিটের যন্ত্রে মাত্র ৫টি স্তর (২৫ = ৩২) — ০ থেকে ৩১ পর্যন্ত যেকোনো শিফট একবারে, বিদ্যুৎ শুধু পাঁচটি স্তর পার হয়।
এখান থেকে দুটি মজার সংযোগ বেরিয়ে আসে:
- কেন
x << 32undefined: x86 CPU শিফট-পরিমাণের শুধু নিচের ৫টি বিট দেখে (৩২ বিটের ক্ষেত্রে)। ৩২ =100000, নিচের ৫ বিট ০ — তাই কিছুই সরে না! অন্য CPU হয়তো সব বিট শূন্য করে দেয়। C সব CPU-তে চলতে চায়, তাই কোনো একটি আচরণের প্রতিশ্রুতি দেয়নি। - ARM-এর বিশেষত্ব: ARM প্রসেসরের অনেক সাধারণ নির্দেশে দ্বিতীয় মানটি barrel shifter দিয়ে বিনা খরচে শিফট করে নেওয়া যায় — যেমন এক নির্দেশেই
a + (b << 2). এজন্যই compiler অনেক গুণকে শিফট-যোগে ভেঙে দেয়।
এই নকশার দাম হলো জায়গা — অনেকগুলো multiplexer লাগে, চিপে জায়গা নেয়। কিন্তু শিফট এত বেশি ব্যবহৃত হয় যে এই খরচ পুরোপুরি সার্থক। সার্কিটের ভেতরের এই ধরনের নকশা নিয়ে আরও জানতে পারবেন Computer Architecture কোর্সে।
মূল উপলব্ধি: একটি সমস্যাকে ২-এর ঘাতে ভেঙে ফেলা (এখানে শিফটের পরিমাণ) — কম্পিউটিংয়ের সবচেয়ে পুনরাবৃত্ত কৌশল। Binary search, fast power, barrel shifter — সবখানে একই চিন্তা: n ধাপের বদলে log n ধাপ।
অনুশীলন
খাতা-কলম নিন — বাইনারিতে হাতে হিসাব করাই এখানে আসল অনুশীলন।
-
হাতে বিট পড়ুন:
x = 0xA6. (ক)(x >> 5) & 1কত? (খ)(x >> 4) & 1কত? (গ)x | 0x0Fআরx & 0xF0hex-এ কত?x = 1010 0110. (ক) ৫ ঘর ডানে সরালে101, শেষ বিট ১ — উত্তর ১. (খ) ৪ ঘর সরালে1010, শেষ বিট ০ — উত্তর ০. (গ)| 0x0Fডানের nibble-এর সব বিট চালু করে:1010 1111=0xAF.& 0xF0ডানের nibble মুছে দেয়:1010 0000=0xA0. খেয়াল করুন, hex-এ ভাবলে উত্তর প্রায় চোখ বুলিয়েই পাওয়া যায় — এজন্যই প্রোগ্রামাররা মাস্ক hex-এ লেখেন। -
অনুমতি পড়ুন: (ক)
chmod 640দিলেls -l-এ কী দেখাবে? (খ) অনেকে সমস্যা না বুঝে ওয়েবসাইটের ফোল্ডারে777দিয়ে দেন — বিটের ভাষায় এতে কী হয়, আর কেন বিপজ্জনক?(ক) ৬ =
110=rw-, ৪ =100=r--, ০ =000=---. মোটrw-r-----: মালিক পড়তে-লিখতে পারেন, গ্রুপ শুধু পড়তে, বাকিরা কিছুই না। কনফিগ ফাইলে পাসওয়ার্ড থাকলে এমন অনুমতি ভালো।(খ) ৭৭৭ =
111 111 111— তিন দলেরই সব বিট চালু, অর্থাৎ others-এর w বিটও চালু। সার্ভারে চলা অন্য যেকোনো প্রোগ্রাম বা অন্য ব্যবহারকারী ফাইল বদলাতে বা ক্ষতিকর ফাইল রাখতে পারে। সঠিক সমাধান সাধারণত ফোল্ডারে ৭৫৫, ফাইলে ৬৪৪ — আর সমস্যা থাকলে মালিকানা ঠিক করা, সব বিট খুলে দেওয়া নয়। -
Subnet হিসাব: (ক)
10.0.5.77/16-এর নেটওয়ার্ক ঠিকানা কী? (খ) একই IP যদি/28হয়, মাস্ক আর নেটওয়ার্ক ঠিকানা কী?(ক)
/16=255.255.0.0. শেষ দুই byte পুরো মুছে যায়: নেটওয়ার্ক10.0.0.0.(খ)
/28মানে ২৮টি 1 — শেষ byte-এ বাঁয়ের ৪টি বিট:11110000= ২৪০, মাস্ক255.255.255.240. শেষ byte: ৭৭ =0100 1101;0100 1101 & 1111 0000 = 0100 0000= ৬৪। নেটওয়ার্ক10.0.5.64, আর এতে ২৪ − ২ = ১৪টি যন্ত্র বসানো যায়,.65থেকে.78পর্যন্ত। -
কোড লিখুন — হাজিরা: অংশ ৪-এর নিয়মে (বিট ০ = শনিবার … বিট ৬ = শুক্রবার) একটি
uint8_tহাজিরা নিয়ে একটি ফাংশন লিখুন, যা কয় দিন উপস্থিত আর কোন কোন দিন অনুপস্থিত, তা ছাপে।0x7Bদিয়ে পরীক্ষা করুন।0x7B=0111 1011: বিট ২ ছাড়া ০ থেকে ৬ সব চালু, তাই ৬ দিন উপস্থিত, অনুপস্থিত শুধু সোমবার। একটি সম্ভাব্য সমাধান:static const char *day[7] = {"Sat","Sun","Mon","Tue","Wed","Thu","Fri"}; void report(uint8_t att) { unsigned present = 0; for (unsigned k = 0; k < 7; k++) { if ((att >> k) & 1u) present++; else printf("absent: %s\n", day[k]); } printf("present %u of 7 days\n", present); }যাচাই করুন: বিট ৭ চালু থাকলেও (যেমন
0xFB) ফল বদলাবে না, কারণ লুপ শুধু ০–৬ দেখে — অপ্রয়োজনীয় বিটে ভুল ডেটা থাকলে সেটি উপেক্ষা হচ্ছে। চাইলে শুরুতেatt &= 0x7Fদিয়ে স্পষ্টভাবে মুছে নিতে পারেন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমx-এর বিট k বন্ধ করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
1uনেওয়া — শুধু বিট ০ অন<< k— অন বিটটিকে k নম্বর ঘরে সরানো~— সব বিট উল্টে “k ছাড়া সব অন” মাস্কx &মাস্ক — শুধু বিট k শূন্য হয়- ফলটি আবার x-এ রাখা (
&=)
13 & 6-এর মান কত?
- 4
- 15
- 11
- 2
বাকি বিট না বদলে x-এর বিট ৩ চালু করার সঠিক লাইন কোনটি?
- x = 1u << 3;
- x &= 1u << 3;
- x |= 1u << 3;
- x ^= 1u << 3;
if (x & 1 == 0) লাইনটিতে কী সমস্যা?
- এটি compile হবে না
- == আগে কাজ করে, তাই শর্তটি সবসময় মিথ্যা
- x signed হলে কাজ করে না, unsigned হলে ঠিক আছে
- কোনো সমস্যা নেই, x জোড় হলে সত্য হয়
(0x39B549 >> 16) & 0xFF-এর মান কত?
- 73
- 181
- 0x39B5
- 57
৩২ বিটের int-এর সিস্টেমে 1 << 31-এর বদলে 1u << 31 কেন লেখা উচিত?
- signed int-এ বিট ৩১ পর্যন্ত সরানো undefined behavior, unsigned-এ নিয়ম স্পষ্ট
- 1u দিয়ে শিফট বেশি দ্রুত হয়
- 1u লিখলে ফল ৬৪ বিটের হয়ে যায়
- শুধু দেখতে সুন্দর লাগে, অন্য কোনো কারণ নেই
একটি অ্যাপ ব্যবহারকারীর পাসওয়ার্ড একটি ছোট গোপন চাবি দিয়ে XOR করে ডেটাবেসে রাখে। এ সম্পর্কে কোনটি সঠিক?
- চাবি গোপন থাকলে এটি যথেষ্ট নিরাপদ
- দুবার XOR করলে নিরাপত্তা দ্বিগুণ হয়
- এটি সহজে ভাঙা যায়; পরীক্ষিত crypto লাইব্রেরি ব্যবহার করা উচিত
- এটি one-time pad, তাই গাণিতিকভাবে অভেদ্য
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- একটি সংখ্যা মানে এক সারি সুইচ; ঘর গোনা হয় ডান থেকে, বিট ০ = LSB. প্রতিটি hex অঙ্ক ৪ বিট, প্রতিটি octal অঙ্ক ৩ বিট।
- পরীক্ষা
&, চালু|, বন্ধ& ~, উল্টানো^; মাস্ক বানান1u << kদিয়ে। - টুকরো বের করতে আগে শিফট, পরে মাস্ক — রঙ, ফাইলের অনুমতি, IP ঠিকানা সব একই নিয়মে।
x & (x - 1)সবচেয়ে নিচের 1 মুছে দেয় — ২-এর ঘাত যাচাই আর popcount-এর ভিত্তি।- বিটের কাজে unsigned, নির্দিষ্ট প্রস্থ, বন্ধনী; প্রস্থের সমান/বেশি বা ঋণাত্মক শিফট নয়; XOR এনক্রিপশন নয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১০ · ইনপুট-আউটপুট — printf ও scanfপরবর্তী পর্বFormat specifier, hex ছাপা, সাজানো টেবিল আর নিরাপদ ইনপুট।
- C প্রোগ্রামিং · কোর্স হোম৫৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Computer Architecture & Digital Logicগভীরে যানAND/OR গেট, ALU আর barrel shifter — বিটওয়াইজ অপারেটরের হার্ডওয়্যার রূপ।
- Computer Networksপাশের ট্র্যাকSubnetting, প্যাকেট header আর TCP ফ্ল্যাগ বিস্তারিত।
- Digital Signal Processingপাশের ট্র্যাকশব্দ ও ছবির ডেটায় বিট-স্তরের গণনা কোথায় লাগে।