এই পর্বে যা শিখবেন
- CPU-তে ALU-র কাজ আর তার interface — ইনপুট, control আর আউটপুট
- AND, OR, full adder আর mux দিয়ে ১-বিট ALU slice; Ainvert আর Binvert দিয়ে বিয়োগ ও NOR
- Set-on-less-than (slt) কীভাবে কাজ করে, overflow হলে কেন ভুল হয় আর SLT = N ⊕ V কেন ঠিক — প্রমাণসহ
- ৩২টি slice জুড়ে ripple ALU, ALU control-এর টেবিল, আর Z, N, C, V flag তৈরির circuit
- x86/ARM-এর condition code বনাম RISC-V-এর flag-হীন শাখা; barrel shifter-এর নকশা; বাস্তবের ALU আর 74181-এর ইতিহাস
১ · ALU কী করে — CPU-র ক্যালকুলেটর আর তার interface
পর্ব ০১-এ (fetch–decode–execute চক্র) দেখেছেন, CPU বারবার তিনটি কাজ করে: মেমরি থেকে instruction আনে, সেটি বোঝে, তারপর কাজটি করে। এই “কাজটি করা” ধাপের বেশিরভাগ হিসাব যে অংশে হয়, তার নাম ALU — Arithmetic Logic Unit. add, sub, and, or, তুলনা — সবই ALU-র কাজ। এমনকি load বা store instruction-এও মেমরির ঠিকানা (base + offset) যোগ করে ALU.
দোকানের একটি সাধারণ ক্যালকুলেটরের কথা ভাবুন। আপনি দুটি সংখ্যা টাইপ করেন, তারপর একটি বোতাম চাপেন — +, −, × বা ÷. ক্যালকুলেটরের ভেতরের circuit একই, শুধু বোতাম ঠিক করে দেয় কোন কাজটি হবে। পর্দায় ফল আসে, আর কোণে ছোট্ট একটি “E” বা “−” চিহ্ন জ্বলে ওঠে। ALU ঠিক এমন: দুটি সংখ্যা (A, B), একটি “বোতাম” (operation select), একটি ফল, আর কয়েকটি ছোট্ট সংকেত-বাতি — যাদের বলে flag.
ALU-র interface (বাইরের সংযোগ) মনে রাখুন এভাবে:
| সংযোগ | দিক | প্রস্থ (৩২-বিট ALU) | কাজ |
|---|---|---|---|
| A, B | ইনপুট | ৩২ বিট করে | দুটি operand — সাধারণত register file থেকে, বা B-র জায়গায় instruction-এর immediate |
| ALU control | ইনপুট | কয়েক বিট (যেমন ৪) | কোন কাজ হবে — AND, OR, add, sub, slt…; আসে control unit থেকে |
| Result | আউটপুট | ৩২ বিট | ফল — register-এ লেখা হয় বা মেমরির ঠিকানা হিসেবে যায় |
| Flag (Z, N, C, V) | আউটপুট | ১ বিট করে | ফল সম্পর্কে তথ্য: শূন্য কি না, ঋণাত্মক কি না, carry বেরোল কি না, signed overflow হলো কি না |
একটি গুরুত্বপূর্ণ কথা: ALU একটি combinational circuit — এতে কোনো মেমরি নেই, ঘড়ির (clock) অপেক্ষাও নেই। ইনপুট বদলালে কিছুক্ষণ পরে (gate-গুলোর delay পেরিয়ে) আউটপুট আপনা থেকে বদলে যায়। ফল ধরে রাখার কাজ register-এর, যা মডিউল ৩-এ আসবে। আর পুরো CPU-তে ALU কোথায় বসে, register file আর মেমরির সাথে কীভাবে জোড়া থাকে — সেই datapath দেখবেন পর্ব ৩২ আর ৩৩-এ।
ALU সব কাজ একসাথে করে, তারপর একটি বেছে নেয়। ভেতরে AND, OR আর adder একই সময়ে চলে; শেষে একটি multiplexer (mux) control সংকেত দেখে যেকোনো একটির ফল বাইরে পাঠায়। Hardware-এ “আগে দেখি কোন কাজ, তারপর করি” নয় — “সব করি, দরকারিটা রাখি।”
২ · ১-বিটের ALU slice — AND, OR আর full adder, mux দিয়ে বাছাই
৩২-বিটের ALU সরাসরি আঁকা কঠিন। Patterson ও Hennessy-র পাঠ্যবইয়ের বিখ্যাত পদ্ধতি হলো আগে এক বিটের একটি ALU বানানো — একে বলে ALU slice বা bit-slice — তারপর সেটির ৩২টি কপি পাশাপাশি বসানো। গার্মেন্টস কারখানার লাইনে যেমন একই রকম অনেকগুলো মেশিন পাশাপাশি থাকে, প্রতিটি একটি করে অংশ সেলাই করে, এখানেও প্রতিটি slice একটি করে বিট সামলায়।
বিট i-এর slice-এ ইনপুট ai, bi আর ডান পাশের slice থেকে আসা CarryIn. ভেতরে তিনটি অংশ:
- AND gate: ai · bi
- OR gate: ai + bi (এই পর্বে Boolean-এ + মানে OR, ⊕ মানে XOR, আর A′ মানে NOT A; কোডে
~,|,^) - Full adder: sum = a ⊕ b ⊕ cin, CarryOut = a·b + a·cin + b·cin — এর gate-স্তরের নকশা পর্ব ১১-এ (Adder)
তিনটির আউটপুট যায় একটি mux-এ। Mux-এর select লাইন হলো ২-বিটের Operation সংকেত: 00 হলে AND, 01 হলে OR, 10 হলে adder-এর sum. (Mux কীভাবে বানানো হয়, তা পর্ব ১২-এ — Multiplexer, decoder আর encoder.) পুরো slice-টি, পরের দুই অংশের বাড়তি অংশসহ, এক ছবিতে:
১-বিট ALU slice (Patterson–Hennessy ধাঁচে)
Ainvert, Binvert, AND, OR, full adder, Less — and a 4-to-1 mux chooses the result
Ainvert ─┐ Operation (2 bits)
▼ │
a ──►[ a or a' ]──┬──►[ AND ]──► 0 ┐ ▼
│ ├──[ 4:1 ]──► Result
b ──►[ b or b' ]──┼──►[ OR ]──► 1 ┤ [ MUX ]
▲ │ │
Binvert ─┘ └──►[ FULL ]──► 2 ┤
CarryIn ─►[ADDER] │
│ Less ► 3 ┘
▼
CarryOut
এখনো এই slice শুধু তিনটি কাজ জানে: AND, OR আর যোগ। পরের দুটি অংশে মাত্র তিনটি ছোট সংযোজনে আমরা পাব বিয়োগ, NOR আর তুলনা।
৩ · বিয়োগ বিনা খরচে — Binvert আর CarryIn = 1
পর্ব ০৫-এ (two's complement) দেখেছেন, −B = B′ + 1, অর্থাৎ সব বিট উল্টে ১ যোগ করলে B-এর ঋণাত্মক পাওয়া যায়। তাহলে
এটি hardware-এ কতটা সস্তা, দেখুন। B′ পেতে প্রতিটি slice-এর b ইনপুটে একটি XOR gate বসানো হয়: b ⊕ Binvert. Binvert = 0 হলে b যেমন আছে তেমন যায়, Binvert = 1 হলে উল্টে যায়। আর “+ 1”? Adder-এর একদম ডানের slice (বিট ০)-এর একটি CarryIn ইনপুট আছে, যা যোগের সময় এমনিতেই ০ থাকে। সেখানে ১ দিলেই +1 হয়ে গেল — একটি বাড়তি adder-ও লাগল না।
তাই Patterson–Hennessy-র নকশায় একটিই সংকেত দুই জায়গায় যায়: Bnegate = সব slice-এর Binvert + বিট ০-এর CarryIn. Bnegate = 1 মানে “B-কে negate করা হবে”। বীজগণিতে যাচাই (n বিটে, mod 2n):
এখান থেকে একটি বাড়তি তথ্যও বেরোয়, যা অংশ ৬-এ খুব কাজে লাগবে: যোগফল 2n + (A − B). A ≥ B (unsigned) হলে এটি 2n-এর সমান বা বেশি, তাই MSB থেকে carry বেরোয় (CarryOut = 1). A < B হলে যোগফল 2n-এর কম, carry বেরোয় না (CarryOut = 0) — অর্থাৎ বিয়োগে borrow লেগেছে। বিয়োগের পর CarryOut = NOT borrow.
নিচের stepper-এ ৪-বিটের ALU-তে 5 − 3 ধাপে ধাপে চলছে। “পরে” চাপুন:
A = 0101 (5) B = 0011 (3) Ainvert = 0 Bnegate = 1 -> every Binvert = 1, CarryIn(bit 0) = 1 Operation= 10 -> mux picks the adder's sum
bit 3 2 1 0 b 0 0 1 1 b xor 1 1 1 0 0 = B' (one's complement of 3) CarryIn(0) = 1
bit 3 2 1 0 carry-in . . . 1 a 0 1 0 1 b' 1 1 0 0 result . . . 0 carry to bit 1 = 1
bit 3 2 1 0 carry-in . . 1 1 a 0 1 0 1 b' 1 1 0 0 result . . 1 0 carry to bit 2 = 0
bit 3 2 1 0 carry-in . 0 1 1 a 0 1 0 1 b' 1 1 0 0 result . 0 1 0 carry to bit 3 = 1
bit 3 2 1 0 carry-in 1 0 1 1 a 0 1 0 1 b' 1 1 0 0 result 0 0 1 0 = 2 CarryOut = 1 V = Cin(bit3) xor Cout = 1 xor 1 = 0
- Control সংকেত বসানো। বিয়োগের জন্য control unit পাঠায় Ainvert = 0, Bnegate = 1, Operation = 10. Bnegate একসাথে দুটি কাজ করে: সব slice-এর B উল্টায়, আর বিট ০-এর CarryIn-এ ১ দেয়।
- B উল্টে গেল। প্রতিটি slice-এর XOR gate b ⊕ 1 = b′ দেয়। 0011 হয়ে গেল 1100. এর সাথে CarryIn-এর ১ মিলে আসলে যোগ হবে 1100 + 1 = 1101, যা ৪-বিটে −3.
- Slice 0. a0 = 1, b′0 = 0, CarryIn = 1. 1 + 0 + 1 = 10 (binary), তাই ফলের বিট ০ = 0, আর বিট ১-এ carry যায় ১.
- Slice 1. a1 = 0, b′1 = 0, carry 1. 0 + 0 + 1 = 1 — ফলের বিট ১ = 1, পরের slice-এ carry 0. Carry ডান থেকে বাঁয়ে এক slice করে এগোচ্ছে — এটাই ripple.
- Slice 2. a2 = 1, b′2 = 1, carry 0. 1 + 1 + 0 = 10 — ফলের বিট ২ = 0, আর বিট ৩-এ carry ১.
- Slice 3 আর ফল। 0 + 1 + 1 = 10 — ফলের বিট ৩ = 0, CarryOut = 1. পুরো ফল 0010 = 2, অর্থাৎ 5 − 3 = 2. CarryOut = 1 মানে borrow লাগেনি (unsigned-এ 5 ≥ 3), আর MSB-তে ঢোকা ও বেরোনো carry দুটিই ১, তাই V = 0 — signed অর্থেও ফল ঠিক।
এজন্যই বলা হয় two's complement বিয়োগকে “বিনা খরচে” দেয়। Sign-magnitude-এ বিয়োগের জন্য আলাদা subtractor আর চিহ্ন তুলনার circuit লাগত (পর্ব ০৫-এর অংশ ৩)। Two's complement-এ লাগে প্রতি বিটে একটি XOR আর একটি তার (CarryIn) — বাকিটা পুরোনো adder-ই করে দেয়। পর্ব ২০-এ একই চিন্তাকে software আর ISA-র দিক থেকে (adder-subtractor, status flag) আবার দেখবেন।
৪ · Ainvert, NOR আর set-on-less-than — overflow-এর সূক্ষ্ম ফাঁদ
Ainvert দিয়ে NOR
B-এর মতো A-এর ইনপুটেও একটি XOR (বা ২:১ mux) বসানো হয়, যার সংকেতের নাম Ainvert. কেন? De Morgan-এর সূত্র (পর্ব ০৭) মনে করুন: (A + B)′ = A′ · B′. অর্থাৎ A আর B দুটিকেই উল্টে AND করলে পাওয়া যায় NOR. তাই Ainvert = 1, Bnegate = 1 আর Operation = 00 (AND) দিলে ALU NOR করে — নতুন কোনো gate ছাড়াই। একই কৌশলে Ainvert = 1, Binvert = 1, Operation = 01 (OR) দিলে পাওয়া যায় A′ + B′ = (A·B)′, অর্থাৎ NAND. (Bnegate একই সাথে CarryIn-এও ১ দেয়, কিন্তু AND বা OR বেছে নিলে adder-এর ফল mux-এ বাদ পড়ে, তাই এতে ক্ষতি নেই।)
Set-on-less-than — তুলনার ফল একটি সংখ্যা হিসেবে
MIPS আর RISC-V-তে একটি instruction আছে slt rd, rs1, rs2 — set-on-less-than: rs1 < rs2 (signed) হলে rd = 1, নাহলে rd = 0. এর ফল সবসময় 0000…0001 বা 0000…0000. অর্থাৎ উপরের ৩১টি বিট সবসময় ০, শুধু বিট ০-এ তুলনার উত্তর।
এটি ALU-তে বানানোর কৌশল চমৎকার। Mux-এর চতুর্থ ইনপুট (Operation = 11) হলো Less. বিট ১ থেকে ৩১-এর slice-এ Less-এ সরাসরি ০ বাঁধা থাকে। বিট ০-এর Less আসে MSB-র slice থেকে একটি তারে, যার নাম Set. আর ALU সেই সময় ভেতরে A − B হিসাব করে (Bnegate = 1). ভাবনাটা সরল: A < B হলে A − B ঋণাত্মক, তাই ফলের চিহ্ন-বিট (MSB) ১ — এটাই Set. সুতরাং slt-এর control: Ainvert 0, Bnegate 1, Operation 11 → 0111.
কিন্তু overflow হলে চিহ্ন-বিট মিথ্যা বলে। ৮ বিটে A = −128, B = 1 নিন। আসল A − B = −129, যা ৮ বিটের সীমার (−128 … 127) বাইরে। Adder wrap করে দেয় 0111 1111 = +127 — চিহ্ন-বিট ০! সরল নিয়মে slt বলবে “−128 < 1 মিথ্যা”, যা স্পষ্টতই ভুল। পাঠ্যবইয়ের প্রথম সরল ছবিতে Set সরাসরি adder-এর MSB থেকে নেওয়া হয়; সঠিক নকশায় এর সাথে overflow মেলাতে হয়।
সঠিক নিয়ম: SLT = N ⊕ V — প্রমাণ
ধরুন n বিটের adder A − B-এর যে ফল দেয় তার চিহ্ন-বিট N, আর signed overflow flag V. আসল (অসীম নির্ভুল) পার্থক্য D = A − B. আমরা চাই: D < 0 হলে ১.
- V = 0 হলে: D সীমার ভেতরে, তাই adder-এর ফলই D-এর সঠিক two's complement রূপ। তখন D < 0 ⇔ N = 1. অর্থাৎ উত্তর = N = N ⊕ 0.
- V = 1 হলে: D সীমার বাইরে। পর্ব ০৫-এ দেখেছেন, overflow হয় কেবল তখনই যখন ফল ঠিক এক “পাক” (2n) ঘুরে উল্টো চিহ্নের অর্ধেকে পড়ে — তাই ফলের চিহ্ন আসল চিহ্নের ঠিক উল্টো। উত্তর = N′ = N ⊕ 1.
দুই ক্ষেত্র মিলিয়ে Less = N ⊕ V. Hardware-এ খরচ মাত্র একটি XOR gate: MSB slice-এর Set = sumn−1 ⊕ V, আর V আসে V = cn ⊕ cn−1 থেকে (cn = MSB থেকে বেরোনো carry, cn−1 = MSB-তে ঢোকা carry). ARM আর x86-এর “signed less than” শর্তও ঠিক এটাই: N ≠ V (x86-এ SF ≠ OF) — অংশ ৭-এ আবার আসবে।
A = 1000 0000 (-128) B = 0000 0001 (+1) ALU computes A + B' + 1 inside
1000 0000 A
+ 1111 1110 B'
+ 1 CarryIn
-----------
1 0111 1111 sum = +127 (wrapped), CarryOut = 1
carry into bit 7 c7 = 0 carry out of bit 7 c8 = 1 V = c8 xor c7 = 1 (overflow!) N = sum bit 7 = 0
naive Set = N = 0 -> result 0000 0000 (WRONG) correct Set = N xor V = 0 xor 1 = 1 result = 0000 0001 (-128 < 1 is true)
- দুটি operand. A = −128 (৮ বিটের সবচেয়ে ছোট signed সংখ্যা), B = +1. Control 0111: ALU ভেতরে A − B হিসাব করবে, আর mux বেছে নেবে Less ইনপুট।
- ভেতরের বিয়োগ। B′ = 1111 1110, CarryIn = 1. যোগফল 1 0111 1111. ৮ বিট রাখলে ফল 0111 1111 = +127, অথচ আসল উত্তর −129 — ৮ বিটে ধরে না, তাই wrap হয়েছে।
- Carry দুটি দেখা। বিট ০ থেকে ৬ পর্যন্ত কোনো carry ওঠে না (বিট ০-এ 0 + 0 + 1 = 1, তারপর প্রতি ঘরে 0 + 1 + 0 = 1), তাই MSB-তে ঢোকা carry c7 = 0. MSB-তে 1 + 1 + 0 = 10, তাই বেরোনো carry c8 = 1. V = 1 ⊕ 0 = 1 — signed overflow; আর N = 0.
- Set বিট। সরল নিয়মে Set = N = 0, ফল 0 — ভুল। সঠিক নিয়মে Set = N ⊕ V = 1, ফল 0000 0001 — −128 সত্যিই 1-এর চেয়ে ছোট। উল্টো ফাঁদও আছে: 127 slt −1-এ পার্থক্য +128 wrap করে −128 হয়, N = 1, V = 1, সঠিক উত্তর 0.
Unsigned তুলনা — sltu
RISC-V-তে sltu (set-on-less-than unsigned) দুটি সংখ্যাকে unsigned হিসেবে তুলনা করে। অংশ ৩-এর হিসাব মনে করুন: A + B′ + 1-এর CarryOut = 1 ঠিক তখনই, যখন A ≥ B (unsigned). তাই sltu-এর ফল = NOT CarryOut — অর্থাৎ borrow লেগেছে কি না। চিহ্ন-বিট বা V এখানে অপ্রাসঙ্গিক। একই বিট-প্যাটার্নে দুটি তুলনা ভিন্ন উত্তর দিতে পারে: ৮ বিটে 1111 1111 আর 0000 0001 — signed-এ −1 < 1 সত্য, unsigned-এ 255 < 1 মিথ্যা।
৫ · ৩২টি slice জুড়ে ৩২-বিট ALU আর ALU control-এর টেবিল
এবার slice-গুলো জোড়া যাক। বিট i-এর CarryOut যায় বিট i + 1-এর CarryIn-এ, ঠিক পর্ব ১১-এর ripple carry adder-এর মতো। সব slice-এর Ainvert একটি তারে, সব Binvert আরেকটি তারে (যা বিট ০-এর CarryIn-ও), আর Operation-এর দুটি তার সব slice-এ একসাথে যায়। শেষ slice (বিট ৩১) একটু বিশেষ: এতে বাড়তি থাকে Set আউটপুট আর overflow নির্ণয়ের gate.
Ainvert Bnegate Operation (each goes to all 32 slices)
│ │ │
a0,b0 ─►[ ALU0 ]◄─ CarryIn = Bnegate Less0 ◄── Set (from ALU31)
│ c1 ──► Result0
a1,b1 ─►[ ALU1 ] Less1 = 0 ──► Result1
│ c2
... ... (carry ripples upward)
│ c31
a31,b31►[ ALU31 ] Less31 = 0 ──► Result31
│ c32 Set = sum31 xor V, Overflow V = c32 xor c31
▼
CarryOut
Zero = NOR(Result31 ... Result0)
সমস্যা একটিই: গতি। CarryIn থেকে ফল পেতে carry-কে ৩২টি slice পেরোতে হয়। প্রতিটি full adder-এর carry পথে মোটামুটি দুই স্তর gate থাকলে সবচেয়ে খারাপ ক্ষেত্রে প্রায় ৬৪ স্তরের delay, তারপর mux আর Set-এর পথ। বাস্তব CPU তাই ripple ব্যবহার করে না; ব্যবহার করে carry-lookahead বা parallel-prefix adder, যেখানে carry প্রায় log2 n স্তরে হিসাব হয় — বিস্তারিত পর্ব ১১-এ। ALU-র বাকি গঠন একই থাকে; শুধু adder অংশটি দ্রুত adder দিয়ে বদলানো হয়।
ALU control-এর টেবিল
চারটি control বিট (Ainvert, Bnegate, Operation1, Operation0) একসাথে একটি ৪-বিটের কোড। Patterson–Hennessy-র MIPS সংস্করণে এই কোডগুলোই ব্যবহার হয়, আর বিশ্ববিদ্যালয় ও চাকরির পরীক্ষায় এই টেবিল বারবার আসে:
| ALU control | Ainvert | Bnegate | Operation | কাজ | কেন |
|---|---|---|---|---|---|
0000 | 0 | 0 | 00 | AND | a · b |
0001 | 0 | 0 | 01 | OR | a + b |
0010 | 0 | 0 | 10 | add | a + b + 0 |
0110 | 0 | 1 | 10 | subtract | a + b′ + 1 |
0111 | 0 | 1 | 11 | set-on-less-than | A − B-এর N ⊕ V বিট ০-এ |
1100 | 1 | 1 | 00 | NOR | a′ · b′ = (a + b)′ |
RISC-V-তে কী? RV32I-এর ALU-কে মূলত এই কাজগুলো করতে হয়: add, sub, and, or, xor, sll, srl, sra, slt, sltu (আর এদের immediate রূপ, যেমন addi, slti). লক্ষ করুন, RISC-V-তে nor instruction নেই (MIPS-এ ছিল), কিন্তু xor আর shift আছে — তাই RISC-V-এর ALU-তে XOR আর shifter-এর পথ লাগে। আর ALU control-এর ৪-বিট কোড ISA-র অংশ নয়; এটি CPU-র ভেতরের নকশাকারের পছন্দ (implementation-specific). পাঠ্যবইয়ের RISC-V সংস্করণ (আর এই কোর্সের পর্ব ৩২–৩৩) প্রথম চারটি কোড একই রাখে — 0000 AND, 0001 OR, 0010 add, 0110 sub; বাস্তব কোনো chip-এর কোড আলাদা হতে পারে।
নিচের simulator এই পুরো নকশাটি ৮ বিটে চালায়: প্রতিটি slice-এ দুটি XOR, AND, OR, full adder আর mux; প্রথমে carry chain চলে, তারপর MSB থেকে Set = sum7 ⊕ V নিয়ে বিট ০-এর Less-এ দেওয়া হয়। তিন জোড়া operand-এ ছয়টি কাজ — Run চাপুন, তারপর নিজের সংখ্যা বসিয়ে দেখুন:
// 8-bit ripple ALU built from 1-bit slices (Patterson-Hennessy style).
// control = Ainvert, Bnegate, Operation (00 AND, 01 OR, 10 add, 11 less)
const OPS = { AND: "0000", OR: "0001", add: "0010", sub: "0110", slt: "0111", NOR: "1100" };
function slice(a, b, cin, less, ainv, bneg, op) {
const x = a ^ ainv, y = b ^ bneg; // the two inverters (XOR)
const sum = x ^ y ^ cin, cout = (x & y) | (x & cin) | (y & cin);
return { r: [x & y, x | y, sum, less][op], sum, cout };
}
function alu8(A, B, ctrl) {
const ainv = +ctrl[0], bneg = +ctrl[1], op = parseInt(ctrl.slice(2), 2);
let c = bneg, cins = [], sum7 = 0, res = []; // CarryIn of bit 0 = Bnegate
for (let i = 0; i < 8; i++) { // pass 1: the carry chain
cins.push(c);
const s = slice((A >> i) & 1, (B >> i) & 1, c, 0, ainv, bneg, op);
sum7 = s.sum; c = s.cout;
}
const V = cins[7] ^ c, set = sum7 ^ V; // corrected SLT bit
for (let i = 0; i < 8; i++) // pass 2: mux picks result
res.push(slice((A >> i) & 1, (B >> i) & 1, cins[i], i === 0 ? set : 0,
ainv, bneg, op).r);
const R = res.reduce((v, bit, i) => v | (bit << i), 0);
return { R, Z: +(R === 0), N: R >> 7, C: c, V, naive: sum7 };
}
const s8 = p => (p & 0x80 ? p - 256 : p), b8 = p => p.toString(2).padStart(8, "0");
for (const [x, y] of [[100, 50], [-128, 1], [5, 5]]) {
console.log(`A=${x} (${b8(x & 255)}) B=${y} (${b8(y & 255)})`);
for (const [name, ctrl] of Object.entries(OPS)) {
const { R, Z, N, C, V, naive } = alu8(x & 255, y & 255, ctrl);
const cv = ctrl[2] === "1" ? `C=${C} V=${V}` : "C=- V=-"; // adder ops only
const note = name === "slt" ? ` naive sign bit=${naive}` : "";
console.log(` ${name.padEnd(3)} ${ctrl} ${b8(R)} = ${String(s8(R)).padStart(4)} Z=${Z} N=${N} ${cv}${note}`);
}
}আউটপুট (নির্ধারিত — প্রতিবার একই):
A=100 (01100100) B=50 (00110010)
AND 0000 00100000 = 32 Z=0 N=0 C=- V=-
OR 0001 01110110 = 118 Z=0 N=0 C=- V=-
add 0010 10010110 = -106 Z=0 N=1 C=0 V=1
sub 0110 00110010 = 50 Z=0 N=0 C=1 V=0
slt 0111 00000000 = 0 Z=1 N=0 C=1 V=0 naive sign bit=0
NOR 1100 10001001 = -119 Z=0 N=1 C=- V=-
A=-128 (10000000) B=1 (00000001)
AND 0000 00000000 = 0 Z=1 N=0 C=- V=-
OR 0001 10000001 = -127 Z=0 N=1 C=- V=-
add 0010 10000001 = -127 Z=0 N=1 C=0 V=0
sub 0110 01111111 = 127 Z=0 N=0 C=1 V=1
slt 0111 00000001 = 1 Z=0 N=0 C=1 V=1 naive sign bit=0
NOR 1100 01111110 = 126 Z=0 N=0 C=- V=-
A=5 (00000101) B=5 (00000101)
AND 0000 00000101 = 5 Z=0 N=0 C=- V=-
OR 0001 00000101 = 5 Z=0 N=0 C=- V=-
add 0010 00001010 = 10 Z=0 N=0 C=0 V=0
sub 0110 00000000 = 0 Z=1 N=0 C=1 V=0
slt 0111 00000000 = 0 Z=1 N=0 C=1 V=0 naive sign bit=0
NOR 1100 11111010 = -6 Z=0 N=1 C=- V=-
প্রথম জোড়ায় add দেখায় 100 + 50 = −106 সঙ্গে V=1 — আসল 150 ৮-বিট signed-এ ধরে না; unsigned চোখে 150 ঠিক, তাই C=0. দ্বিতীয় জোড়াটিই এই অংশের মূল কথা: sub দেয় 127 সঙ্গে V=1 (−129 সীমার বাইরে), তাই slt-এ সরল চিহ্ন-বিট 0 হলেও সংশোধিত ফল 00000001 — সঠিক। তৃতীয় জোড়ায় 5 − 5 = 0, তাই Z=1; এখানে C=1 মানে borrow লাগেনি। খেয়াল করুন, slt সারির Z আর N এসেছে slt-এর ০/১ ফল থেকে, আর C ও V ভেতরের বিয়োগ থেকে; বাস্তবের RISC-V-তে slt কোনো flag রাখেই না। আর NOR-এর ফল 10001001 হলো OR-এর ফল 01110110-এর ঠিক উল্টো।
৬ · Flag — Zero, Negative, Carry আর Overflow
ফলের পাশাপাশি ALU চারটি ১-বিটের তথ্য দেয়। ARM এদের একসাথে বলে NZCV; x86-এ নাম ZF, SF, CF, OF. এরা CPU-র একটি status flag register-এ জমা হতে পারে (x86, ARM), অথবা একদম না-ও জমতে পারে (RISC-V — অংশ ৭)। প্রতিটির circuit:
| Flag | মানে | Circuit | কোন চশমায় কাজের |
|---|---|---|---|
| Z (Zero) | ফল পুরোপুরি ০ | সব ফল-বিটের NOR: Z = (r31 + … + r0)′ — বাস্তবে OR-এর গাছ, শেষে NOT | দুটিতেই (সমান কি না) |
| N (Negative) | ফল ঋণাত্মক | শুধু ফলের MSB-র তার: N = rn−1 | Signed |
| C (Carry) | যোগে carry বেরোল / বিয়োগে borrow-এর তথ্য | MSB slice-এর CarryOut (x86-এ বিয়োগে উল্টো করে রাখা হয় — নিচে দেখুন) | Unsigned |
| V (Overflow) | signed ফল সীমার বাইরে | V = cn ⊕ cn−1 — একটি XOR | Signed |
বিয়োগের পর C — দুই ঘরানা। দুটি বড় আর্কিটেকচার বিয়োগের পর carry flag-কে উল্টো অর্থে রাখে, আর এটি পরীক্ষা ও ইন্টারভিউর প্রিয় ফাঁদ। ARM (আর পুরোনো 6502-এর মতো কিছু নকশা) adder-এর CarryOut যেমন আছে তেমন রাখে: SUBS/CMP-এর পর C = 1 মানে borrow হয়নি (unsigned A ≥ B). x86 উল্টো করে রাখে: SUB/CMP-এর পর CF = 1 মানে borrow হয়েছে (unsigned A < B). Hardware-এ পার্থক্য শুধু একটি NOT gate; কিন্তু assembly পড়ার সময় কোন CPU, তা না জানলে C-এর অর্থ উল্টে যায়। যোগের বেলায় দুটিতেই C = CarryOut. (Logic instruction বা shift-এর পর C আর V কী হয়, তা আবার প্রতিটি ISA-তে আলাদা — manual দেখে নিতে হয়।)
Z flag-এর circuit নিয়ে একটি কথা: ৩২টি বিটের NOR একটি বিশাল gate হিসেবে বানানো যায় না (CMOS-এ বেশি ইনপুটের gate ধীর, পর্ব ০৮)। তাই ২ বা ৪ ইনপুটের OR gate-এর একটি গাছ বানানো হয় — ৩২ বিটে ২-ইনপুট gate দিয়ে log2 32 = ৫ স্তর — শেষে একটি NOT. এবার একটি বিয়োগে চারটি flag একটি একটি করে তৈরি হতে দেখুন:
A = 0111 0000 0x70 (unsigned 112, signed +112)
B = 1001 0000 0x90 (unsigned 144, signed -112)
B' = 0110 1111
0111 0000 + 0110 1111 + 1 = 1110 0000 (0xE0)
carries: c7 (into bit 7) = 1, c8 (out) = 0
result 1110 0000
OR of all bits = 1
Z = NOT 1 = 0 (result is not zero)
result 1110 0000 N = bit 7 = 1 (as signed: -32)
CarryOut c8 = 0 ARM C = 0 (0 = a borrow happened) x86 CF = 1 (1 = a borrow happened) unsigned check: 112 < 144, so a borrow is right
V = c8 xor c7 = 0 xor 1 = 1
signed check: 112 - (-112) = 224 > 127 -> overflow
result -32 is wrong as a signed answer
signed A < B ? N xor V = 1 xor 1 = 0 -> no (112 > -112) unsigned A < B ? borrow (ARM C = 0) = yes (112 < 144)
- বিয়োগটি চালানো। A = 0x70, B = 0x90. ALU যোগ করে A + B′ + 1 = 0111 0000 + 0110 1111 + 1 = 1110 0000 (0xE0). নিচের চার বিট থেকে ওঠা carry বিট ৪, ৫, ৬ হয়ে MSB-তে ঢোকে (c7 = 1), কিন্তু MSB-তে 0 + 0 + 1 = 1, তাই বাইরে carry বেরোয় না (c8 = 0).
- Z flag. ফলের সব বিট OR করলে ১ (কয়েকটি বিট ১), NOT করলে Z = 0 — ফল শূন্য নয়, অর্থাৎ A ≠ B.
- N flag. ফলের MSB ১, তাই N = 1. Signed হিসেবে 0xE0 = −32.
- C flag — দুই ঘরানা। CarryOut = 0. ARM এটিকেই C হিসেবে রাখে (C = 0 মানে borrow হয়েছে); x86 উল্টে রাখে CF = 1 (borrow হয়েছে)। দুটির অর্থ একই: unsigned 112 < 144.
- V flag. V = c8 ⊕ c7 = 0 ⊕ 1 = 1. আসল signed পার্থক্য 112 − (−112) = 224, যা ৮ বিটের সীমা 127 ছাড়িয়েছে — তাই −32 signed অর্থে ভুল ফল।
- Flag থেকে সিদ্ধান্ত। Signed প্রশ্ন “A < B?” — N ⊕ V = 0, তাই না; সত্যিই +112 > −112. Unsigned প্রশ্ন — borrow হয়েছে, তাই হ্যাঁ; সত্যিই 112 < 144. একই বিয়োগ, একই চার flag, দুই প্রশ্নের দুই সঠিক উত্তর।
৭ · Flag দিয়ে শাখা — x86/ARM-এর condition code বনাম RISC-V-এর compare-and-branch
if (a < b) লিখলে CPU-কে দুটি কাজ করতে হয়: তুলনা আর শাখা (branch). x86 আর ARM-এ এই দুটি আলাদা instruction. প্রথমে cmp a, b — যা ভেতরে a − b হিসাব করে, ফল ফেলে দেয়, শুধু flag রাখে। তারপর একটি শর্তসাপেক্ষ লাফ, যা flag পড়ে সিদ্ধান্ত নেয়। শর্তগুলোকে বলে condition code:
| প্রশ্ন (cmp a, b-এর পর) | ARM | x86 | Flag-এর শর্ত (ARM ধাঁচে) |
|---|---|---|---|
| a = b | EQ | je | Z = 1 |
| a ≠ b | NE | jne | Z = 0 |
| a < b (signed) | LT | jl | N ≠ V (x86: SF ≠ OF) |
| a > b (signed) | GT | jg | Z = 0 এবং N = V |
| a < b (unsigned) | LO (= CC) | jb | ARM: C = 0; x86: CF = 1 |
| a > b (unsigned) | HI | ja | ARM: C = 1 এবং Z = 0; x86: CF = 0 এবং ZF = 0 |
লক্ষ করুন x86-এর নামকরণ: signed তুলনায় “less/greater” (jl, jg), unsigned-এ “below/above” (jb, ja). C কোডে int আর unsigned তুলনার জন্য compiler ঠিক এই দুই পরিবার থেকে instruction বাছে। নিচের simulator চারটি CMP চালিয়ে ছয়টি শর্ত হিসাব করে — দেখুন কোথায় signed আর unsigned উত্তর ভিন্ন হয়:
// CMP a, b = compute a - b, keep only the flags (ARM-style C = NOT borrow).
function cmp8(a, b) {
const raw = a + ((~b) & 0xFF) + 1; // a + ~b + 1
const r = raw & 0xFF, C = raw >> 8; // C = carry out = no borrow
const sa = a >> 7, sb = b >> 7, sr = r >> 7;
const V = +(sa !== sb && sr !== sa); // signs differ, result flips
return { r, Z: +(r === 0), N: sr, C, V };
}
const cond = { // ARM name (x86 jump)
"EQ (je)": f => f.Z === 1,
"NE (jne)": f => f.Z === 0,
"LT (jl)": f => f.N !== f.V, // signed less than
"LO (jb)": f => f.C === 0, // unsigned lower = borrow
"GT (jg)": f => f.Z === 0 && f.N === f.V,
"HI (ja)": f => f.C === 1 && f.Z === 0,
};
const hex = v => "0x" + v.toString(16).toUpperCase().padStart(2, "0");
const pairs = [[0xFF, 0x01], [0x01, 0xFF], [0x80, 0x7F], [0x05, 0x05]];
for (const [a, b] of pairs) {
const f = cmp8(a, b), sa = a > 127 ? a - 256 : a, sb = b > 127 ? b - 256 : b;
console.log(`CMP ${hex(a)}, ${hex(b)} (signed ${sa} vs ${sb}, unsigned ${a} vs ${b})`);
console.log(` a-b=${hex(f.r)} Z=${f.Z} N=${f.N} C=${f.C} V=${f.V} x86 CF=${1 - f.C}`);
console.log(" " + Object.entries(cond).map(([k, t]) => `${k}:${t(f) ? "T" : "F"}`).join(" "));
}আউটপুট:
CMP 0xFF, 0x01 (signed -1 vs 1, unsigned 255 vs 1)
a-b=0xFE Z=0 N=1 C=1 V=0 x86 CF=0
EQ (je):F NE (jne):T LT (jl):T LO (jb):F GT (jg):F HI (ja):T
CMP 0x01, 0xFF (signed 1 vs -1, unsigned 1 vs 255)
a-b=0x02 Z=0 N=0 C=0 V=0 x86 CF=1
EQ (je):F NE (jne):T LT (jl):F LO (jb):T GT (jg):T HI (ja):F
CMP 0x80, 0x7F (signed -128 vs 127, unsigned 128 vs 127)
a-b=0x01 Z=0 N=0 C=1 V=1 x86 CF=0
EQ (je):F NE (jne):T LT (jl):T LO (jb):F GT (jg):F HI (ja):T
CMP 0x05, 0x05 (signed 5 vs 5, unsigned 5 vs 5)
a-b=0x00 Z=1 N=0 C=1 V=0 x86 CF=0
EQ (je):T NE (jne):F LT (jl):F LO (jb):F GT (jg):F HI (ja):F
প্রথম সারিতেই মূল শিক্ষা: 0xFF আর 0x01-এর তুলনায় LT:T (signed −1 < 1) অথচ LO:F আর HI:T (unsigned 255 > 1) — একই বিট, একই একটি বিয়োগ, শুধু ভিন্ন flag পড়ে ভিন্ন উত্তর। তৃতীয় সারিতে V=1: −128 − 127 wrap করে 0x01 দিয়েছে, N=0; কিন্তু N ≠ V, তাই LT ঠিকই T — চিহ্ন-বিট একা দেখলে ভুল হতো। আর ARM-এর C আর x86-এর CF প্রতিটি সারিতে ঠিক উল্টো, যেমন অংশ ৬-এ বলা হয়েছে।
RISC-V — flag ছাড়াই শাখা
RISC-V (আর তার আগে MIPS) ইচ্ছা করেই কোনো flag register রাখেনি। তুলনা আর শাখা একটি instruction-এ: beq, bne, blt, bge, bltu, bgeu — দুটি register সরাসরি তুলনা করে লাফ দেয়। এটাই compare-and-branch. আর তুলনার ফল সংখ্যা হিসেবে দরকার হলে slt/sltu, যার ফল একটি সাধারণ register-এ যায়।
উদাহরণ (হাতে লেখা, তুলনার জন্য; a ও b ৩২-বিট int, x86-64-এ Intel syntax-এ edi/esi, ARM64-এ w0/w1, RISC-V-তে x10/x11):
# if (a < b) goto L;
# x86-64 # ARM64 # RISC-V (RV32I)
cmp edi, esi cmp w0, w1 blt x10, x11, L
jl L b.lt L
# unsigned a < b:
cmp edi, esi cmp w0, w1 bltu x10, x11, L
jb L b.lo L
কেন flag বাদ? এতে প্রতিটি instruction-এর ফল শুধু তার গন্তব্য register-এ যায়, লুকোনো কোনো পার্শ্ব-ফল থাকে না — pipeline আর out-of-order CPU-তে নির্ভরতা খোঁজা সহজ হয় (পর্ব ৩৬, ৪০)। খরচও আছে: signed overflow জানতে বা বহু-শব্দের (multi-word) যোগে carry বের করতে বাড়তি instruction লাগে, যেমন যোগের পর sltu দিয়ে carry বের করা। শাখা ও লুপ assembly-তে কেমন দেখায়, তা পর্ব ২৯-এ বিস্তারিত।
৮ · Shifter — logical, arithmetic, rotate আর barrel shifter
বিট সরানো (shift) CPU-র সবচেয়ে বেশি ব্যবহৃত কাজগুলোর একটি: ২-এর ঘাতে গুণ-ভাগ, bitmask বানানো, field বের করা, আর পর্ব ২১-এর shift-add গুণে। তিন ধরনের shift, সাথে rotate:
- Left shift (
sll): সব বিট বাঁয়ে, ডানে ০ ঢোকে; k ঘর মানে 2k দিয়ে গুণ (বাঁয়ের বিট হারানো পর্যন্ত)। - Logical right shift (
srl): ডানে সরে, বাঁয়ে ০ ঢোকে — unsigned সংখ্যার 2k দিয়ে ভাগ। - Arithmetic right shift (
sra): ডানে সরে, বাঁয়ে sign-বিটের কপি ঢোকে — signed সংখ্যার 2k দিয়ে ভাগ, তবে নিচের দিকে গোল করে (floor): −74 >> 3 = −10, −9 নয়। - Rotate: এক প্রান্ত দিয়ে বেরোনো বিট অন্য প্রান্তে ঢোকে, কিছু হারায় না। x86-এ
rol/ror, ARM-এror; RV32I-এ নেই, তবে ঐচ্ছিক bit-manipulation extension (Zbb)-এrol/rorআছে।
Barrel shifter — log2 n স্তরের mux
সবচেয়ে সরল shifter প্রতি clock-এ এক ঘর সরায় (পর্ব ১৬-এর shift register) — ২৯ ঘর সরাতে ২৯ cycle. দ্রুত CPU-তে দরকার এক ধাপেই যেকোনো দূরত্ব। একটি উপায়: প্রতিটি আউটপুট বিটের জন্য একটি n:1 mux — n বিটে n টি বড় mux, তারের জঙ্গল। চতুর উপায় হলো barrel shifter: shift-এর পরিমাণকে binary-তে ভাঙুন। ৫ = 1012 = 4 + 1. তাহলে “১ ঘর সরানো হবে কি না”, “২ ঘর সরানো হবে কি না”, “৪ ঘর সরানো হবে কি না” — তিনটি স্তর, প্রতিটি স্তরে n টি ২:১ mux, আর স্তর k-এর select হলো shift পরিমাণের বিট k.
Delay-ও মাত্র log2 n স্তরের mux — ৩২ বিটে ৫টি। RV32I-এর sll/srl/sra shift পরিমাণের নিচের ৫টি বিটই শুধু দেখে (rs2-এর বিট ৪…০) — ঠিক পাঁচটি স্তরের select; x86-ও ৩২-বিট shift-এ count-কে ৫ বিটে mask করে। Arithmetic আর logical-এর পার্থক্য শুধু বাঁয়ে ঢোকা বিটে (sign না ০), আর left shift-এর জন্য হয় আলাদা স্তর, নয়তো ইনপুট-আউটপুটের বিটক্রম উল্টে একই right shifter ব্যবহার — দুটি কৌশলই প্রচলিত। নিচে ৮-বিটের sra 0xB6 by 5 তিন স্তরে:
x = 1011 0110 (0xB6, signed -74) shamt = 5 = 101 (binary) stage 1 (by 1): select = bit0 = 1 stage 2 (by 2): select = bit1 = 0 stage 3 (by 4): select = bit2 = 1
in 1011 0110
out 1101 1011 (shift by 1, fill = sign 1)
in 1101 1011 out 1101 1011 (pass: every mux picks its own bit)
in 1101 1011
out 1111 1101 (shift by 4, four sign bits enter)
result 1111 1101 = 0xFD = -3
check -74 / 32 = -2.3125, floor = -3
srl 5 would give 0000 0101 = 5 (182 / 32 = 5.69 -> 5)
- Shift পরিমাণকে বিটে ভাঙা। 5 = 101 (binary). স্তর ১-এর select = বিট ০ = 1, স্তর ২-এর = বিট ১ = 0, স্তর ৩-এর = বিট ২ = 1. অর্থাৎ ১ ঘর + ৪ ঘর = ৫ ঘর।
- স্তর ১: ১ ঘর। প্রতিটি আউটপুট বিট i নেয় ইনপুটের বিট i + 1; সবচেয়ে বাঁয়ের বিটে ঢোকে sign-বিট ১ (sra). 1011 0110 → 1101 1011.
- স্তর ২: সোজা পার। Select 0, তাই প্রতিটি mux নিজের বিটটিই পাঠায়; মান বদলায় না — 1101 1011.
- স্তর ৩: ৪ ঘর। আউটপুট বিট i নেয় বিট i + 4; বাঁয়ের চার ঘরে sign-বিট ১. 1101 1011 → 1111 1101.
- ফল যাচাই। 1111 1101 = −3. −74 ÷ 32 = −2.3125, আর floor করলে −3 — arithmetic shift নিচের দিকে গোল করে। একই ইনপুটে srl 5 দিত 0000 0101 = 5 (182 ÷ 32-এর পূর্ণ অংশ)। যেকোনো পরিমাণেই মাত্র তিন স্তর পেরোতে হয়।
নিচের simulator তিন রকম shift (srl, sra, sll) ১, ৩ আর ৫ ঘরে চালায় এবং প্রতিটি স্তরের পরের মান ছাপে:
// 8-bit barrel shifter: 3 stages of 2:1 muxes (shift by 1, 2, 4).
// Stage k looks at bit k of the shift amount: 0 = pass, 1 = shift by 2^k.
const N = 8, b8 = v => v.toString(2).padStart(N, "0").replace(/(....)(....)/, "$1 $2");
function stage(x, dist, kind) { // one layer = 8 muxes
let out = 0;
for (let i = 0; i < N; i++) { // output bit i
let bit;
if (kind === "sll") bit = i - dist >= 0 ? (x >> (i - dist)) & 1 : 0;
else if (i + dist < N) bit = (x >> (i + dist)) & 1;
else bit = kind === "sra" ? (x >> (N - 1)) & 1 : 0; // fill: sign or 0
out |= bit << i;
}
return out;
}
function barrel(x, sh, kind) {
let v = x; const trace = [];
for (let k = 0; k < 3; k++) { // log2(8) = 3 stages
const on = (sh >> k) & 1;
if (on) v = stage(v, 1 << k, kind);
trace.push(`s${1 << k}:${on ? "shift" : "pass "} ${b8(v)}`);
}
return { v, trace };
}
const x = 0xB6; // 1011 0110 = 182 or -74
console.log(`input ${b8(x)} (unsigned ${x}, signed ${x - 256})`);
for (const kind of ["srl", "sra", "sll"])
for (const sh of [1, 3, 5]) {
const { v, trace } = barrel(x, sh, kind);
const sv = v & 0x80 ? v - 256 : v;
console.log(`${kind} ${sh} (${sh.toString(2).padStart(3, "0")}): ` +
`${trace.join(" | ")} -> ${v} / ${sv}`);
}আউটপুট (শেষে unsigned / signed মান):
input 1011 0110 (unsigned 182, signed -74)
srl 1 (001): s1:shift 0101 1011 | s2:pass 0101 1011 | s4:pass 0101 1011 -> 91 / 91
srl 3 (011): s1:shift 0101 1011 | s2:shift 0001 0110 | s4:pass 0001 0110 -> 22 / 22
srl 5 (101): s1:shift 0101 1011 | s2:pass 0101 1011 | s4:shift 0000 0101 -> 5 / 5
sra 1 (001): s1:shift 1101 1011 | s2:pass 1101 1011 | s4:pass 1101 1011 -> 219 / -37
sra 3 (011): s1:shift 1101 1011 | s2:shift 1111 0110 | s4:pass 1111 0110 -> 246 / -10
sra 5 (101): s1:shift 1101 1011 | s2:pass 1101 1011 | s4:shift 1111 1101 -> 253 / -3
sll 1 (001): s1:shift 0110 1100 | s2:pass 0110 1100 | s4:pass 0110 1100 -> 108 / 108
sll 3 (011): s1:shift 0110 1100 | s2:shift 1011 0000 | s4:pass 1011 0000 -> 176 / -80
sll 5 (101): s1:shift 0110 1100 | s2:pass 0110 1100 | s4:shift 1100 0000 -> 192 / -64
প্রতিটি সারিতে shift পরিমাণের binary রূপ (যেমন 101) ঠিক বলে দেয় কোন স্তর “shift” আর কোনটি “pass”. srl-এ 182 → 91, 22, 5 (182-কে 2, 8, 32 দিয়ে ভাগের পূর্ণ অংশ); sra-তে −74 → −37, −10, −3 (floor ভাগ)। sll-এ বাঁয়ের বিট হারিয়ে যায়: 182 × 8 = 1456, mod 256 = 176. পর্ব ২১-এ এই shifter-ই গুণের প্রতিটি ধাপে কাজে লাগবে।
৯ · তুলনা, যাচাই আর বাস্তবের ALU
সমান কি না — দুই পথ
A = B কি না জানার প্রথম পথ: A − B করে Z flag দেখা। এটি সহজ, কারণ ALU তো আছেই — কিন্তু পুরো carry chain পেরোতে হয়। দ্বিতীয় পথ দ্রুততর: প্রতিটি বিটে XNOR — (ai ⊕ bi)′, যা দুটি বিট সমান হলে ১ — তারপর সবগুলোর AND-এর গাছ। কোনো carry নেই, শুধু log2 n স্তর। অনেক pipeline CPU-তে beq/bne-এর জন্য এমন আলাদা equality comparator রাখা হয়, যাতে শাখার সিদ্ধান্ত আগেভাগে নেওয়া যায় (পর্ব ৩৬–৩৭)।
| প্রশ্ন | ALU থেকে কীভাবে | RISC-V |
|---|---|---|
| A = B | A − B-এর Z = 1, বা XNOR-AND গাছ | beq; সংখ্যা হিসেবে sub + seqz |
| A < B (signed) | A − B-এর N ⊕ V | slt, blt |
| A < B (unsigned) | A − B-এ borrow, অর্থাৎ CarryOut = 0 | sltu, bltu |
পুরো নকশাটি C দিয়ে যাচাই — ৬৫,৫৩৬টি জোড়া
একটি নকশা কয়েকটি উদাহরণে ঠিক কাজ করলেই নিশ্চিত হওয়া যায় না। ৮ বিটে মোট জোড়া মাত্র 256 × 256 = ৬৫,৫৩৬টি, তাই সবগুলো পরীক্ষা করা যায় — একে বলে exhaustive testing. নিচের C প্রোগ্রাম gate-স্তরের ripple মডেলটিকে C-র নিজস্ব গণিতের সাথে মেলায়: ফল, C, V, আর slt (N ⊕ V) ও sltu (NOT C).
#include <stdio.h>
/* gate-level 8-bit ripple ALU: add when bneg = 0, sub when bneg = 1 */
static unsigned alu8(unsigned a, unsigned b, unsigned bneg, unsigned *C, unsigned *V) {
unsigned c = bneg, r = 0, cin7 = 0;
for (int i = 0; i < 8; i++) {
unsigned x = (a >> i) & 1u, y = ((b >> i) & 1u) ^ bneg;
if (i == 7) cin7 = c;
r |= (x ^ y ^ c) << i;
c = (x & y) | (x & c) | (y & c);
}
*C = c; *V = cin7 ^ c; return r;
}
static int s8(unsigned p) { return (int)(p ^ 0x80u) - 128; } /* no UB */
static unsigned out8(int t) { return (unsigned)(t < -128 || t > 127); }
int main(void) {
long bad_add = 0, bad_sub = 0, bad_slt = 0, bad_sltu = 0, naive_wrong = 0;
for (unsigned a = 0; a < 256; a++) for (unsigned b = 0; b < 256; b++) {
unsigned C, V, r = alu8(a, b, 0, &C, &V);
if (r != ((a + b) & 0xFFu) || C != (unsigned)(a + b > 255u)
|| V != out8(s8(a) + s8(b))) bad_add++;
r = alu8(a, b, 1, &C, &V);
if (r != ((a - b) & 0xFFu) || C != (unsigned)(a >= b)
|| V != out8(s8(a) - s8(b))) bad_sub++;
unsigned lt = (unsigned)(s8(a) < s8(b));
if (((r >> 7) ^ V) != lt) bad_slt++; /* SLT = N xor V */
if ((r >> 7) != lt) naive_wrong++; /* SLT = N only */
if ((C ^ 1u) != (unsigned)(a < b)) bad_sltu++; /* SLTU = NOT C */
}
printf("pairs tested: %d\nadd mismatches: %ld, sub mismatches: %ld\n",
256 * 256, bad_add, bad_sub);
printf("slt (N xor V) mismatches: %ld\nsltu (NOT C) mismatches : %ld\n",
bad_slt, bad_sltu);
printf("naive slt (N only) wrong: %ld pairs\n", naive_wrong); return 0;
}আউটপুট:
pairs tested: 65536
add mismatches: 0, sub mismatches: 0
slt (N xor V) mismatches: 0
sltu (NOT C) mismatches : 0
naive slt (N only) wrong: 16384 pairs
সব ৬৫,৫৩৬টি জোড়ায় gate-স্তরের মডেল নিখুঁত: যোগ-বিয়োগের ফল, C (বিয়োগে ARM-এর “no borrow” অর্থে) আর V — কোথাও অমিল নেই, আর N ⊕ V নিয়মের slt ও NOT C নিয়মের sltu-ও শূন্য ভুল। শেষ লাইনটিই সতর্কবাণী: শুধু চিহ্ন-বিট দেখা “সরল” slt ১৬,৩৮৪টি জোড়ায় — মোটের ঠিক এক-চতুর্থাংশে — ভুল উত্তর দেয়; এগুলোই সেই জোড়া যেখানে A − B signed overflow করে। প্রোগ্রামে signed রূপান্তর (p ^ 0x80) - 128 দিয়ে করা হয়েছে, যাতে কোনো implementation-defined বা undefined আচরণ না থাকে।
বাস্তবের ALU — অনেকগুলো execution unit
পাঠ্যবইয়ের CPU-তে একটি ALU. আধুনিক বড় core-এ কিন্তু একাধিক execution unit থাকে: কয়েকটি সরল integer ALU (প্রায়ই ৪ বা তার বেশি, যারা যোগ, লজিক, shift এক cycle-এ শেষ করে), আলাদা branch unit, আলাদা multiplier আর divider (গুণে কয়েক cycle, ভাগে আরও বেশি লাগে), load/store-এর ঠিকানা হিসাবের unit, আর floating-point ও SIMD unit. এক cycle-এ কয়েকটি instruction একসাথে আলাদা unit-এ পাঠানোর এই কৌশলই superscalar নকশা — পর্ব ৩৯-এ। রেস্তোরাঁর রান্নাঘরের মতো: একজন বাবুর্চি সব না করে আলাদা চুলা — ভাজা, তন্দুর, চা — একসাথে চলে।
এক chip-এ ALU — 74181. Texas Instruments-এর 74181 সাধারণত প্রথম single-chip ALU হিসেবে পরিচিত, যা ১৯৭০ সালের দিকে বাজারে আসে। এটি ৪-বিটের: চারটি select লাইন আর একটি mode লাইন দিয়ে ১৬টি লজিক আর ১৬টি গাণিতিক কাজ বাছা যেত, ভেতরে carry-lookahead-এর ব্যবস্থাসহ। কয়েকটি 74181 পাশাপাশি বসিয়ে (আর 74182 lookahead carry generator দিয়ে জুড়ে) ১৬ বা ৩২ বিটের ALU বানানো হতো — ঠিক এই পর্বের slice-চিন্তা, শুধু ৪ বিটের টুকরোয়। বলা হয়, ১৯৭০-এর দশকের বেশ কিছু minicomputer, যেমন DEC-এর কিছু PDP-11 মডেল আর Xerox Alto, এর ওপর নির্ভর করত। পরে microprocessor পুরো ALU-কে CPU chip-এর ভেতরে নিয়ে নেয়।
আরও গভীরে: আধুনিক ৬৪-বিট core-এ integer যোগ সাধারণত এক cycle-এ শেষ হয়, তাই ALU-র adder আর ফল সরাসরি পরের instruction-এ ফেরত দেওয়ার (bypass/forwarding) পথ মিলে প্রায়ই chip-এর সবচেয়ে কঠিন timing-পথগুলোর একটি হয়; এজন্য Kogge–Stone-ধাঁচের parallel-prefix adder আর যত্নে সাজানো layout ব্যবহার হয়। x86-এর মতো flag-ওয়ালা ISA-তে out-of-order core flag-গুলোকেও register-এর মতো rename করে, আর কিছু instruction সব flag না বদলানোয় (যেমন inc CF-কে ছোঁয় না) বাড়তি নির্ভরতা সামলাতে হয়। গবেষণায় approximate adder আর কম-শক্তির ALU নিয়েও কাজ চলছে, বিশেষত AI accelerator-এর জন্য।
১০ · AI-এর যুগে ALU নকশা
AI টুল ALU-র ছবি আর সংজ্ঞা সুন্দরভাবে বলতে পারে, কিন্তু ঠিক সেই কোণগুলোতে ভুল করে যেখানে hardware বাগ লুকিয়ে থাকে। প্রচলিত কয়েকটি ভুল:
| AI-এর প্রচলিত ভুল | ঠিক কথা | কীভাবে যাচাই |
|---|---|---|
| slt = A − B-এর MSB | Overflow হলে ভুল; SLT = N ⊕ V | −128 slt 1 (৮ বিট) হাতে বা simulator-এ |
| “বিয়োগের পর C = 1 মানে borrow” — সব CPU-তে | x86-এ তাই, ARM-এ উল্টো (C = NOT borrow) | ISA manual-এর SUB/CMP অংশ |
| MIPS-এর ALU control কোড RISC-V-এর অংশ বলা | ALU control কোড implementation-এর পছন্দ; ISA শুধু instruction ঠিক করে | RISC-V spec-এ “ALU control” খুঁজে দেখুন — নেই |
“RISC-V-তে zero/carry flag আছে”, বা nor instruction | RV32I-এ কোনো flag register নেই, nor-ও নেই | RV32I instruction তালিকা |
| ৩২-বিট barrel shifter-এ “৩২ স্তর” বা “৪ স্তর” | log2 32 = ৫ স্তর, প্রতি স্তরে ৩২টি ২:১ mux | shift পরিমাণের বিট গুনে |
Verilog-এ a < b লিখে ধরে নেওয়া signed তুলনা | সাধারণ wire/reg unsigned; signed ঘোষণা বা $signed লাগে | simulator-এ −1 < 1 পরীক্ষা (পর্ব ১৪) |
AI-কে ALU নিয়ে প্রশ্ন করলে width, ISA আর প্রতিটি carry চেয়ে নিন, যাতে নিজে মেলাতে পারেন:
AI-এর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- একই adder দিয়ে বিয়োগ কীভাবে হয়? — A + B′ + 1: প্রতিটি B বিটে XOR (Binvert) আর বিট ০-এর CarryIn = 1.
- ALU control 0110 আর 0111 কী বোঝায় (MIPS-ধাঁচে)? — 0110 = subtract, 0111 = set-on-less-than; দুটিতেই Bnegate = 1.
- Zero flag-এর circuit? — সব ফল-বিটের NOR (OR-এর গাছ, তারপর NOT).
- শুধু চিহ্ন-বিট দিয়ে slt কেন ভুল? — A − B overflow করলে চিহ্ন উল্টে যায়; সঠিক Less = N ⊕ V.
- ৩২-বিট barrel shifter-এ কয়টি স্তর আর কয়টি ২:১ mux? — ৫ স্তর, 5 × 32 = ১৬০টি mux.
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১x86 আর ARM-এ condition flag আছে, MIPS আর RISC-V ইচ্ছা করে বাদ দিয়েছে। কোন নকশাটি ভালো?
Flag-এর ধারণা খুব পুরোনো — ১৯৬০-৭০-এর দশকের বহু যন্ত্রে আর 8080, 6502, 8086-এর মতো প্রথম দিকের microprocessor-এ ছিল। সুবিধা স্পষ্ট: একটি sub বা cmp-এর পর একসাথে চারটি তথ্য পাওয়া যায়, আর পরের যেকোনো শর্তসাপেক্ষ instruction সেগুলো পড়তে পারে। বহু-শব্দের যোগে (যেমন ৩২-বিট যন্ত্রে ১২৮-বিট সংখ্যা) adc (add with carry) এক instruction-এ আগের শব্দের carry নিয়ে নেয়, যা cryptography আর বড় সংখ্যার library-তে খুব কাজের।
অসুবিধা আসে pipeline আর out-of-order যুগে। Flag একটি লুকোনো, সবার ভাগের register: প্রায় প্রতিটি গাণিতিক instruction সেটি লেখে, তাই পাশাপাশি দুটি স্বাধীন instruction-ও flag-এর কারণে একে অপরের ওপর নির্ভরশীল দেখায়। x86-এ আরও জটিলতা — inc আর dec CF বদলায় না, ফলে কিছু instruction flag-এর এক অংশ লেখে আর বাকিটা আগের মান থেকে নেয় (partial flag update); কিছু Intel প্রজন্মে এ থেকে বাড়তি stall হতো বলে optimization guide-এ সতর্ক করা হয়েছে। আধুনিক x86 core তাই flag-কেও rename করে, যা বাড়তি hardware.
ARM মাঝামাঝি পথ নিয়েছে: সাধারণ add flag ছোঁয় না, শুধু adds/subs-এর মতো “S” লাগানো instruction লেখে — প্রোগ্রামার বা compiler ঠিক করে কখন flag দরকার। MIPS আর RISC-V সম্পূর্ণ বাদ দিয়েছে: তুলনার ফল হয় সাধারণ register-এ (slt), নয়তো শাখা নিজেই তুলনা করে (blt). দাম হলো overflow বা carry লাগলে বাড়তি instruction — যেমন ৬৪-বিট যোগ ৩২-বিট RISC-V-তে নিচের অর্ধেকে add, carry বের করতে sltu, তারপর ওপরের অর্ধেকে দুটি add. RISC-V-এর নকশাকারদের যুক্তি ছিল, এমন কাজ সাধারণ প্রোগ্রামে বিরল, আর সরল, flag-হীন pipeline সব প্রোগ্রামকে লাভ দেয়; সমালোচকেরা বড়-সংখ্যার গণিতে এর খরচ দেখান।
মূল উপলব্ধি: “ভালো” নির্ভর করে কোন কাজ বেশি চলে আর কোন যুগের hardware-এর ওপর। Flag ছোট, ক্রমিক CPU-তে সস্তা ছিল; গভীর pipeline আর out-of-order যুগে লুকোনো ভাগের অবস্থা দামি হয়ে উঠেছে।
প্র ০২74181 থেকে Am2901 — “bit-slice” যুগ কী ছিল, আর microprocessor এসে কেন তা শেষ হয়ে গেল?
১৯৭০-এর দশকে একটি পুরো ৩২-বিট CPU এক chip-এ ধরানোর মতো transistor ছিল না। তাই নকশাকারেরা CPU বানাতেন ছোট ছোট TTL chip জুড়ে, বোর্ডের পর বোর্ড। Texas Instruments-এর 74181 (প্রায় ১৯৭০) এই কাজ সহজ করে: একটি chip-ই ৪-বিটের একটি ALU slice, ৩২টি কাজসহ। কয়েকটি পাশাপাশি বসালে যেকোনো প্রস্থ, আর 74182 দিয়ে carry-lookahead জুড়লে গতিও ভালো। বলা হয়, DEC-এর কিছু PDP-11 আর VAX মডেল, Xerox Alto-সহ বহু যন্ত্র এর ওপর তৈরি — অর্থাৎ এই পর্বের “একই slice বারবার” চিন্তাটি বাস্তবে শিল্প চালিয়েছে।
এর পরের ধাপ ছিল AMD-এর Am2900 পরিবার (মাঝ-১৯৭০-এর দশক)। এর Am2901 chip-এ ৪-বিট ALU-র সাথে একটি ছোট register file আর shifter-ও ছিল — অর্থাৎ একটি পুরো datapath-এর ৪-বিট টুকরো। এর সাথে microprogrammed control (পর্ব ৩৪) জুড়ে কোম্পানিগুলো নিজের পছন্দমতো instruction set-এর যন্ত্র বানাত; বিভিন্ন minicomputer, graphics আর সামরিক যন্ত্রে এমন নকশা ব্যবহার হয়েছে বলে জানা যায়।
তাহলে শেষ হলো কেন? Moore-এর সূত্র (পর্ব ০২)। Transistor-এর সংখ্যা যত বেড়েছে, পুরো CPU এক chip-এ ঢুকেছে — 8086, 68000, তারপর 80386 আর RISC chip-গুলো। এক chip-এর ভেতরের তার অনেক ছোট আর দ্রুত, বোর্ডের তারের মতো delay আর শক্তি খরচ নেই; তৈরির খরচও কম। একসময় bit-slice বোর্ডের যন্ত্রের চেয়ে একটি microprocessor-ই দ্রুত হয়ে গেল। তবে চিন্তাটি হারায়নি: আজও chip নকশাকারেরা ALU বা datapath-কে একই রকম bit-slice সারি হিসেবে সাজান (datapath layout), কারণ নিয়মিত গঠন ছোট, দ্রুত আর যাচাই করা সহজ।
মূল উপলব্ধি: প্রযুক্তির সীমা নকশার আকার ঠিক করে। Slice-এর ধারণা স্থায়ী, কিন্তু তা কোথায় থাকে — আলাদা chip-এ নাকি এক chip-এর ভেতরে — তা নির্ভর করে transistor কত সস্তা তার ওপর।
প্র ০৩৩২-বিট ripple ALU এত ধীর হলে আধুনিক CPU কয়েক GHz-এ এক cycle-এ ৬৪-বিট যোগ কীভাবে করে?
Ripple ALU-তে carry প্রতিটি slice পেরিয়ে যায় — ৬৪ বিটে মোটামুটি ১২৮ স্তর gate-এর পথ। এক GHz-এর ঘড়িতে একটি cycle মাত্র এক ন্যানোসেকেন্ড, আর তার মধ্যে register থেকে পড়া, mux, ফল লেখা — সবকিছু ধরাতে হয়। Ripple adder দিয়ে তা অসম্ভব। সমাধান তিন দিক থেকে।
প্রথম, দ্রুত adder. Carry-lookahead (পর্ব ১১)-এর মূল চিন্তা হলো প্রতিটি বিট বলে দেয় সে carry “তৈরি করে” (generate, g = a·b) নাকি “পার করে দেয়” (propagate, p = a ⊕ b বা a + b). এই g আর p গুলো একটি গাছে জুড়ে সব carry প্রায় log2 n স্তরে পাওয়া যায়। এই গাছের বিভিন্ন রূপের নাম Kogge–Stone (১৯৭৩), Brent–Kung, Han–Carlson ইত্যাদি — প্রতিটিতে delay, তারের সংখ্যা আর শক্তির আলাদা ভারসাম্য। Carry-select adder আরেক কৌশল: উপরের অর্ধেকের যোগ carry ০ আর ১ দুই ধরে আগেই করে রাখা, তারপর আসল carry এলে mux দিয়ে বেছে নেওয়া — “সব করি, দরকারিটা রাখি” চিন্তা আবারও।
দ্বিতীয়, circuit আর layout. ALU-র মতো গুরুত্বপূর্ণ অংশ অনেক সময় হাতে সাজানো বা বিশেষ circuit কৌশলে বানানো হয়, transistor-এর আকার মেপে মেপে। তৃতীয়, কাজ ভাগ করা: Intel-এর Pentium 4 (২০০০-এর শুরুতে)-এর integer ALU core-এর ঘড়ির দ্বিগুণ হারে চলত বলে জানানো হয়, আর যোগ ১৬-বিটের টুকরোয় ধাপে ধাপে করা হতো — খুব উচ্চ ঘড়ির লক্ষ্য পূরণে চরম একটি পথ, যা পরের নকশাগুলোতে আর রাখা হয়নি।
সবশেষে মনে রাখার মতো: যোগ এক cycle-এ শেষ না হলে নির্ভরশীল প্রতিটি instruction-কে অপেক্ষা করতে হয়, কারণ i = i + 1-এর মতো লুপ প্রতি পাকে আগের ফল চায়। তাই সরল integer যোগের latency এক cycle রাখা প্রায় সব নকশার অগ্রাধিকার।
মূল উপলব্ধি: ALU-র যুক্তি পাঠ্যবইয়ের মতোই, কিন্তু গতি আসে carry-র পথ ছোট করার গণিত আর circuit-কৌশল থেকে। “কী হিসাব করে” আর “কত দ্রুত করে” — দুটি আলাদা নকশা-প্রশ্ন।
প্র ০৪Shifter এক ঘর করে সরালেও তো কাজ চলে — barrel shifter-এর বাড়তি খরচ কি সত্যিই দরকার?
ইতিহাস এর ভালো উত্তর দেয়। Intel 8086 (১৯৭৮)-এ CL register-এ রাখা সংখ্যার সমান ঘর সরাতে প্রতিটি বিটের জন্য আলাদা clock লাগত — বড় shift মানে অনেক cycle. পরের প্রজন্মের x86 chip-এ (বিশেষ করে 80386-এ) barrel shifter আসে বলে সাধারণত উল্লেখ করা হয়, আর তখন থেকে যেকোনো দূরত্বের shift প্রায় স্থির সময়ে হয়। কারণ shift সব জায়গায়: array-র সূচককে element-এর আকার দিয়ে গুণ (×4, ×8 মানে shift), bit-field বের করা, hash আর checksum, cryptography, আর floating-point-এ mantissa সাজানো (পর্ব ২৪)।
ARM-এর প্রথম chip ARM1 (১৯৮৫) এই চিন্তাকে আরও এগিয়ে নেয়: প্রায় প্রতিটি data-processing instruction-এর দ্বিতীয় operand ALU-তে ঢোকার আগে barrel shifter পেরোতে পারে। ফলে add r0, r1, r2, lsl #2 এক instruction-এই r0 = r1 + r2 × 4 করে। ছোট, শক্তি-সাশ্রয়ী chip-এ এটি অনেক instruction বাঁচিয়েছে। ARM64-এও shifted-register operand রয়ে গেছে।
খরচ কত? n বিটে n·log2 n টি ২:১ mux — ৬৪ বিটে ৩৮৪টি, প্রতিটি কয়েকটি transistor. আজকের কোটি-কোটি transistor-এর chip-এ এটি নগণ্য। আসল খরচ তারে: প্রতিটি স্তরে বিটগুলো ১, ২, ৪, ৮… ঘর দূরে যায়, তাই শেষের স্তরে লম্বা তার লাগে, যা layout আর delay-কে প্রভাবিত করে। এজন্য কিছু নকশা ৪:১ mux দিয়ে স্তর কমায়, বা left আর right একই hardware-এ বিটক্রম উল্টে সারে।
শেষে, এমবেডেড জগতে এখনো কিছু ছোট microcontroller-এ শুধু ১-বিট shift instruction থাকে (যেমন ৮-বিট AVR-এ lsl/lsr এক ঘর করে সরায়) — সেখানে compiler বড় shift-কে লুপ বা অন্য কৌশলে ভাঙে। চাহিদা আর খরচের হিসাবে প্রতিটি বাজার নিজের উত্তর বেছে নেয়।
মূল উপলব্ধি: যে কাজ খুব বেশি চলে, সেটিকে দ্রুত করাই আর্কিটেকচারের একটি মূল নীতি (“common case fast”). Shift এত বেশি চলে যে log2 n স্তরের hardware প্রায় সব বড় CPU-র জন্য লাভজনক।
প্র ০৫ISA-তে slt আর sltu দুটিই কেন লাগে? Software-এ signed-unsigned তুলনা কোথায় বাগ আর কোথায় কৌশল হয়ে ওঠে?
বিট-প্যাটার্ন নিজে signed না unsigned তা বলে না (পর্ব ০৫) — অর্থ ঠিক করে প্রোগ্রাম। তাই একই দুটি register-এর তুলনার দুই উত্তর হতে পারে, যেমন 0xFFFFFFFF বনাম 1: signed-এ −1 < 1, unsigned-এ বিশাল > 1. Hardware যদি শুধু একটি তুলনা দিত, অন্যটি বানাতে প্রতিবার বাড়তি instruction লাগত (যেমন দুটির MSB উল্টে তুলনা)। দুটি আলাদা instruction দামেও সস্তা: একটি দেখে N ⊕ V, অন্যটি borrow — একই বিয়োগ থেকে।
বাগের দিক: C-তে int আর unsigned মিশে গেলে compiler সাধারণত unsigned তুলনা বাছে (usual arithmetic conversions), তাই -1 < 1u মিথ্যা হয়ে যায়। Assembly-তে সেটি দেখায় jb/bltu হিসেবে, যেখানে প্রোগ্রামার ভেবেছিলেন jl/blt. Buffer-এর দৈর্ঘ্য যাচাইয়ে এমন ভুল বহুবার নিরাপত্তা-ত্রুটি তৈরি করেছে: ঋণাত্মক দৈর্ঘ্য signed তুলনায় “ছোট” বলে পাস করে, তারপর memcpy-তে unsigned হয়ে বিশাল হয়ে যায়।
কৌশলের দিক: array-র সীমা যাচাই 0 <= i && i < n-এ দুটি তুলনা লাগে। কিন্তু i-কে unsigned ধরে (unsigned)i < (unsigned)n একটিই তুলনা — ঋণাত্মক i unsigned-এ বিশাল হয়ে যায় বলে আপনা থেকেই বাদ পড়ে (n অঋণাত্মক হলে)। Compiler আর JIT (যেমন Java-র bounds check বাদ দেওয়ার optimization) এই কৌশল নিয়মিত ব্যবহার করে, আর RISC-V-তে এটি একটি bltu/bgeu. RISC-V-তে sltu rd, x0, rs দিয়ে “rs ≠ 0” পাওয়া যায় (pseudo-instruction snez), আর sltiu rd, rs, 1 দিয়ে “rs = 0” (seqz) — sltu-র আরেকটি চতুর ব্যবহার।
মূল উপলব্ধি: Signed আর unsigned তুলনা একই বিয়োগের দুটি আলাদা প্রশ্ন। কোন প্রশ্ন করছেন তা জানলে এটি কৌশল, না জানলে বাগ।
অনুশীলন
পরীক্ষার ধাঁচে লেখা — আগে খাতায় সমাধান করুন, তারপর উত্তর মিলিয়ে নিন।
-
Flag নির্ণয় (হাতে হিসাব): একটি ৮-বিট ALU-তে A =
0x9C, B =0x32. (ক) add (control 0010) আর sub (control 0110)-এর ফল hex-এ এবং Z, N, C, V বের করুন; sub-এর C ARM আর x86 দুই নিয়মে লিখুন। (খ) slt (0111) আর sltu-এর ফল কত? সরল slt (শুধু চিহ্ন-বিট) কী দিত?A =
1001 1100(unsigned 156, signed −100), B =0011 0010(50).কাজ ফল Z N C V ব্যাখ্যা add 0xCE(−50)0 1 0 0 −100 + 50 = −50 সীমার ভেতরে; unsigned 206 < 256 sub 0x6A(+106)0 0 ARM C = 1, x86 CF = 0 1 −100 − 50 = −150 সীমার বাইরে; unsigned 156 ≥ 50, borrow নেই Sub-এর carry: A + B′ + 1 =
1001 1100+1100 1101+ 1 = 1 0110 1010. MSB-তে ঢোকা carry c7 = 0 (বিট ৬-এ 0 + 1 + 0 = 1), বেরোনো c8 = 1, তাই V = 1.(খ) slt = N ⊕ V = 0 ⊕ 1 = 1 (−100 < 50, সঠিক)। সরল slt দিত N = 0 — ভুল। sltu = NOT C(ARM) = 0 (156 < 50 মিথ্যা)। একই জোড়ায় signed আর unsigned উত্তর ভিন্ন।
-
Barrel shifter trace: ৮-বিট barrel shifter-এ (স্তর ১, ২, ৪) x =
1001 1101(0x9D)-কে ৬ ঘর সরানো হবে। (ক) কোন স্তর shift করবে আর কোনটি pass? (খ) sra, srl আর sll-এ প্রতিটি স্তরের পরের মান আর চূড়ান্ত দশমিক মান লিখুন। (গ) ৬৪-বিট barrel shifter-এ কয়টি স্তর আর কয়টি ২:১ mux লাগবে?(ক) 6 = 110 (binary): স্তর ১ (১ ঘর) pass, স্তর ২ (২ ঘর) shift, স্তর ৩ (৪ ঘর) shift.
stage1(pass) stage2(by 2) stage3(by 4) result sra 1001 1101 1110 0111 1111 1110 -2 (x = -99) srl 1001 1101 0010 0111 0000 0010 2 (x = 157) sll 1001 1101 0111 0100 0100 0000 64যাচাই: −99 ÷ 64 = −1.55, floor = −2 ✓; 157 ÷ 64-এর পূর্ণ অংশ 2 ✓; 157 × 64 = 10048, mod 256 = 64 ✓.
(গ) log2 64 = ৬ স্তর, প্রতি স্তরে ৬৪টি mux — মোট 6 × 64 = ৩৮৪টি ২:১ mux.
-
আউটপুট অনুমান করুন: RV32I-এ নিচের instruction-গুলো চলার পর x7, x8, x9, x10 আর x11-এ কী থাকবে? (
liএকটি pseudo-instruction, যা register-এ ধ্রুবক বসায়।)li x5, -1 li x6, 1 slt x7, x5, x6 sltu x8, x5, x6 sub x9, x6, x5 sltu x10, x0, x9 srai x11, x5, 4x7 = 1 # signed: -1 < 1 x8 = 0 # unsigned: 0xFFFFFFFF < 1 is false x9 = 2 # 1 - (-1) x10 = 1 # 0 <u 2, i.e. "x9 != 0" (snez idiom) x11 = -1 (0xFFFFFFFF) # arithmetic shift keeps the signslt: ভেতরে −1 − 1 = −2, কোনো overflow নেই, N = 1 → 1.sltu: x5-এর প্যাটার্ন0xFFFFFFFFunsigned-এ 4,294,967,295, যা 1-এর চেয়ে বড় → 0.sltu x10, x0, x9: 0 < x9 (unsigned) মানে x9 ≠ 0; এটাইsnezpseudo-instruction.srai: সব বিট ১-এর সংখ্যা ডানে সরালে বাঁয়ে sign ১ ঢোকে — ফল বদলায় না, −1 থাকে।srliহলে আসত0x0FFFFFFF.
-
Circuit-এর নকশা: Patterson–Hennessy ধাঁচের ১-বিট slice (Ainvert, Binvert, CarryIn, ৪:১ mux: AND, OR, sum, Less) ধরে: (ক) NAND আর “A − B-এর বদলে B − A” — প্রতিটির জন্য Ainvert, Binvert, বিট ০-এর CarryIn আর Operation-এর মান লিখুন, আর বলুন MIPS-ধাঁচের ৪-বিট control (যেখানে CarryIn = Bnegate) দিয়ে তা সম্ভব কি না। (খ) XOR কি এই slice-এ সম্ভব? না হলে সবচেয়ে কম পরিবর্তনে কীভাবে যোগ করবেন? (গ) ৪-বিট ALU-র Zero flag-এর Boolean সূত্র লিখুন।
(ক) NAND: Ainvert = 1, Binvert = 1, Operation = 01 (OR) → a′ + b′ = (a·b)′. CarryIn অপ্রাসঙ্গিক, কারণ mux adder-এর ফল নেয় না; তাই ৪-বিট control
1101দিয়ে সম্ভব।B − A: Ainvert = 1, Binvert = 0, CarryIn = 1, Operation = 10 → a′ + b + 1 = B − A. কিন্তু MIPS-ধাঁচের নকশায় CarryIn আলাদা সংকেত নয়, Bnegate-এর সাথে বাঁধা (Bnegate = 1 মানে Binvert = 1 এবং CarryIn = 1). তাই সেই ৪-বিট control-এ B − A হয় না; করতে হলে বিট ০-এর CarryIn-এর জন্য আলাদা control তার লাগবে। Software-এর সহজ সমাধান হলো operand অদলবদল করে
sub.(খ) সম্ভব নয় — mux-এর চারটি ইনপুটের কোনোটিই a ⊕ b নয়, আর inverter বসিয়ে AND বা OR থেকে XOR পাওয়া যায় না। তবে full adder-এর ভেতরে a ⊕ b আগে থেকেই হিসাব হয় (sum = (a ⊕ b) ⊕ cin). সবচেয়ে কম পরিবর্তন: সেই অভ্যন্তরীণ a ⊕ b তারটি বের করে mux-এর পঞ্চম ইনপুটে দেওয়া — mux তখন ৮:১ হয় আর Operation ৩ বিটের। RISC-V-এর
xorসমর্থনের জন্য বাস্তব ALU-তে এমন বাড়তি ইনপুট থাকে।(গ) Z = (r3 + r2 + r1 + r0)′ = r3′ · r2′ · r1′ · r0′ (De Morgan). ২-ইনপুট gate দিয়ে: দুটি OR, তারপর একটি NOR — দুই স্তর।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রম১-বিট slice দিয়ে গড়া ripple ALU-তে A − B হওয়ার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Control unit পাঠায় Bnegate = 1 আর Operation = 10
- প্রতিটি slice-এর XOR gate B-এর বিট উল্টে দেয়, আর বিট ০-এর CarryIn হয় ১
- বিট ০ থেকে MSB পর্যন্ত carry এক slice থেকে পরের slice-এ ছড়ায়
- প্রতিটি slice-এর mux adder-এর sum বেছে নিয়ে ফলের বিট দেয়
- ফল আর MSB-র carry থেকে Z, N, C, V তৈরি হয়
Patterson–Hennessy ধাঁচের ALU-তে (control = Ainvert, Bnegate, Operation1, Operation0; Operation 00 AND, 01 OR, 10 add, 11 less) ৪-বিট control 0110 কোন কাজ বোঝায়?
- add
- subtract
- set-on-less-than
- NOR
একটি n-বিট adder দিয়ে A − B করতে B-এর প্রতিটি বিট উল্টানোর পাশাপাশি আর কী করতে হয়?
- ফলের সব বিট উল্টানো
- MSB slice-এর CarryIn = 1 করা
- বিট ০-এর CarryIn = 1 করা
- A-এর সব বিটও উল্টানো
৮-বিট ALU-তে A = −128, B = 1 নিয়ে slt চালানো হলো। ভেতরের A − B-এর ফল 0111 1111, N = 0, V = 1. সঠিক slt ফল আর “শুধু চিহ্ন-বিট” নিয়মের ফল কী?
- সঠিক 1, সরল নিয়মে 0
- সঠিক 0, সরল নিয়মে 1
- দুটিতেই 1
- দুটিতেই 0
৩২-বিট barrel shifter ২:১ multiplexer-এর স্তর দিয়ে বানালে কয়টি স্তর আর মোট কয়টি ২:১ mux লাগে?
- ৩২ স্তর, ১,০২৪টি mux
- ৪ স্তর, ১২৮টি mux
- ৮ স্তর, ২৫৬টি mux
- ৫ স্তর, ১৬০টি mux
৮-বিটে CMP 0xFF, 0x01 (অর্থাৎ 0xFF − 0x01) চালানোর পর কোন কথাটি সত্য?
- Signed less-than মিথ্যা, unsigned less-than সত্য
- Signed less-than সত্য, unsigned less-than মিথ্যা
- Z = 1, কারণ দুটির বিট একই রকম দেখায়
- Signed আর unsigned less-than দুটিই সত্য
৮-বিটে 5 − 9 বিয়োগের পর carry flag-এর মান x86 (CF) আর ARM (C)-এ কত হবে?
- দুটিতেই 1
- x86 CF = 0, ARM C = 1
- x86 CF = 1, ARM C = 0
- দুটিতেই 0
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- ALU = combinational ক্যালকুলেটর: A, B আর control ঢোকে; ফল আর Z, N, C, V বেরোয়। ভেতরে সব কাজ একসাথে হয়, mux একটি বেছে নেয়।
- ১-বিট slice: AND, OR, full adder, Less আর ৪:১ mux; Ainvert ও Binvert-এর XOR. ৩২টি slice জুড়ে ripple ALU (দ্রুত নকশায় carry-lookahead).
- বিয়োগ = A + B′ + 1 (Bnegate = Binvert + বিট ০-এর CarryIn); NOR = A′·B′. MIPS-ধাঁচের control: 0000 AND, 0001 OR, 0010 add, 0110 sub, 0111 slt, 1100 NOR.
- Flag: Z = সব বিটের NOR, N = MSB, V = cn ⊕ cn−1, C = CarryOut (বিয়োগে ARM: no-borrow, x86: borrow). Signed less = N ⊕ V, unsigned less = borrow.
- Barrel shifter: log2 n স্তরের ২:১ mux, স্তর k-এর select = shift পরিমাণের বিট k; srl-এ ০, sra-তে sign ঢোকে। RISC-V flag ছাড়াই
blt/bltu-তে তুলনা করে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৪ · Hardware description language — Verilog-এর পরিচয় আর simulationপরবর্তী পর্বALU-কে Verilog-এ লিখে simulation-এ চালানো।
- পর্ব ১২ · Multiplexer, decoder আর encoder — বেছে নেওয়া আর ঠিকানা খোঁজাআগের পর্বALU-র ফল বাছাইয়ের mux আর barrel shifter-এর ভিত্তি।
- পর্ব ১১ · Adder — half adder, full adder, ripple carry আর carry-lookaheadভিত্তিALU-র adder অংশ আর দ্রুত carry-র হিসাব।
- কম্পিউটার আর্কিটেকচার ও ডিজিটাল লজিক · কোর্স হোম৬২টি পর্বলজিক গেট থেকে pipeline আর cache — পুরো রোডম্যাপ।
- C প্রোগ্রামিং · Bitwise operationsপাশের ট্র্যাকAND, OR, XOR আর shift — C-তে ALU-র কাজগুলো সরাসরি।
- C প্রোগ্রামিং · C আর assemblyপাশের ট্র্যাকCompiler কীভাবে cmp আর শর্তসাপেক্ষ লাফ বানায়।