ALU Design — Arithmetic Logic Unit-এর ভেতরের স্থাপত্য
ALU Design
ALU-তে adder/subtractor, bitwise logic unit, আর shifter — সবগুলো প্রতিটা চক্রে সমান্তরালে গণনা হয়; একটা op-select code driven output MUX ঠিক করে কোনটা 'জেতে'। এটাই curriculum-এর প্রথম প্রোগ্রামযোগ্য হার্ডওয়্যার।
আগে এটা বুঝি
এই মডিউলের driving question ছিল — কয়েক কোটি on/off switch দিয়ে “যোগ করা” কীভাবে সম্ভব হয়? গত কয়েকটা লেসনে আমরা সেই প্রশ্নের একটা সরাসরি উত্তর পেয়েছি: half adder, full adder, ripple-carry adder, আর তারপর একটা combined adder/subtractor যেটা একটা mode বিট দিয়ে ADD আর SUB-এর মধ্যে বেছে নেয়। গত লেসনে আমরা শিখেছি কীভাবে একটা MUX বহু উৎস থেকে একটাকে বেছে নেয় — এমনকি সেই বাছাই দিয়ে যেকোনো Boolean function বানানো যায়।
আজ আমরা এই দুইটা ধারণা একসাথে জোড়া লাগিয়ে সম্পূর্ণ নতুন কিছু বানাব — এমন একটা সার্কিট যেটা একই তারে, একই ইনপুটে, ভিন্ন ভিন্ন সময়ে ভিন্ন ভিন্ন কাজ করতে পারে — শুধু একটা ছোট control code বদলে দিয়ে। এর নাম ALU — Arithmetic Logic Unit।
এটা একটা ছোট শব্দগুচ্ছ শোনালেও, থামুন একটু — এইটাই এই পুরো curriculum-এর প্রথম মুহূর্ত যেখানে হার্ডওয়্যার সত্যিকার অর্থে, এমনকি ক্ষুদ্র পরিসরে হলেও, “প্রোগ্রামযোগ্য” হয়ে ওঠে। এতদিন আমরা যা বানিয়েছি তার প্রতিটাই একটা নির্দিষ্ট কাজের জন্য হার্ডওয়্যার্ড — একটা adder সবসময় যোগ করে, আর কিছু না। কিন্তু একটা ALU-তে একই সার্কিট, শুধু একটা কয়েক-বিটের সিগন্যাল বদলে, হয়ে যায় কখনো একটা adder, কখনো একটা AND গেট, কখনো একটা shifter। এই “একই হার্ডওয়্যার, বিভিন্ন সময়ে বিভিন্ন আচরণ” ধারণাটাই — আরও অনেক বড় পরিসরে — পুরো একটা CPU-র সংজ্ঞা। ALU আসলে সেই সম্পূর্ণ ধারণার প্রথম, ছোট্ট, সম্পূর্ণ-বোধগম্য নমুনা।
Level ৩-এ (CPU Architecture) আমরা দেখব একটা instruction-এর
opcode কীভাবে একটা “ALUControl” সিগন্যাল তৈরি করে — আর সেই
সিগন্যালটাই আজকের লেসনে আমরা যা বানাব তার input হয়ে যায়। আজকের
লেসন শেষে, a = b + c লেখার পর সিলিকনে ঠিক কী ঘটে তার একটা
বিশাল অংশ আপনার কাছে আর কোনো রহস্য থাকবে না।
মূল ধারণা
ALU-র সংজ্ঞা — এক নজরে
একটা n-bit ALU নেয়:
- দুইটা
n-bit অপারেন্ড —A,B - একটা
k-bit op-select কোড (একে অনেক জায়গায় ALUControl বা ALUOp বলা হয়)
আর দেয়:
- একটা
n-bit Result - কয়েকটা flag —
Zero,Carry,Overflow,Negative(গত লেসনগুলোতে adder/subtractor-এর প্রসঙ্গে এই flag-গুলোর প্রথম দুইটার সাথে পরিচয় হয়েছে)
আজকের লেসনে আমরা একটা n=8-bit ALU বানাব, ৩-বিট op-select
(ALUOp[2:0]) দিয়ে আটটা operation-এর মধ্যে বেছে নেওয়া যাবে:
| ALUOp | Operation | কী গণনা করে |
|---|---|---|
000 | AND | A AND B (bitwise) |
001 | OR | A OR B (bitwise) |
010 | ADD | A + B |
011 | XOR | A XOR B (bitwise) |
100 | NOT | NOT A (শুধু A-এর উপর, unary) |
101 | SHR | A কে ডানে shift, পরিমাণ আসে B-এর নিচের বিট থেকে |
110 | SUB | A - B |
111 | SLT | 1 যদি A \lt B (signed), নাহলে 0 |
Sub-unit ১ — Adder/Subtractor (পুনর্ব্যবহার, নতুন করে বানানো নয়)
আগের লেসনের adder/subtractor সার্কিটটাই এখানে হুবহু পুনর্ব্যবহার হচ্ছে — এটাই এই লেসনের প্রথম গুরুত্বপূর্ণ পাঠ: ভালো hardware design মানে বারবার নতুন circuit বানানো না, বরং প্রমাণিত building block পুনর্ব্যবহার করা।
সংক্ষেপে মনে করিয়ে দিই সেই সার্কিটের যুক্তি — n টা full adder
পাশাপাশি চেইন করা (ripple carry), আর প্রতিটা B বিটের সামনে
একটা XOR গেট, যার একটা ইনপুট mode বিট M:
M=0 হলে B_i' = B_i (অপরিবর্তিত), আর adder-এর initial
carry-in = M = 0 — ফল A + B। M=1 হলে B_i' বিট-ইনভার্টেড
(one’s complement), আর carry-in = M = 1 — one’s complement-এ
+1 যোগ করাটাই two’s complement বানানোর নিয়ম, তাই ফল হয় A
আর B-এর two’s complement-এর যোগফল, অর্থাৎ A - B।
এই ALU-তে adder-এর mode বিট M সরাসরি ALUOp-এর সবচেয়ে উঁচু
বিট (bit 2) থেকে আসে, যখন operation ADD বা SUB হয় — অন্য
cases-এ adder এখনো চলে (সবসময় চলে, নিচে ব্যাখ্যা করা হবে),
কিন্তু তার ফলাফল ব্যবহৃত হয় না।
Sub-unit ২ — Bitwise Logic Unit
এটা সবচেয়ে সহজ sub-unit — n টা প্যারালাল AND গেট, n টা
প্যারালাল OR গেট, n টা প্যারালাল XOR গেট, আর n টা inverter
(শুধু A-এর উপর, NOT operation-এর জন্য)। প্রতিটা বিট-পজিশন
i-এ:
এই চারটা group সম্পূর্ণ স্বতন্ত্র — একে অপরের উপর নির্ভর করে না, তাই সবগুলো একসাথে, একই সময়ে গণনা হতে পারে (parallel)। কোনো “sequencing” বা “priority” নেই এখানে — এটাই combinational logic-এর স্বাভাবিক চরিত্র।
Sub-unit ৩ — Barrel Shifter
Shift একটা বিশেষ challenge, কারণ naive পদ্ধতি (একটা shift register-এ
এক বিট করে ঘুরিয়ে shift করা, k বার shift করতে k clock cycle)
combinational ALU-তে চলবে না — ALU-র পুরো কাজটাই এক combinational
পাসেই শেষ হতে হবে, কোনো clock cycle অপেক্ষা ছাড়াই।
সমাধান: barrel shifter — যেকোনো shift amount (0 থেকে
n-1 পর্যন্ত) একটা মাত্র combinational পাসে সম্পন্ন করে, শুধু
\log_2 n টা ধাপে।
কৌশল — বাইনারি representation-এর সুবিধা নেওয়া। যেকোনো shift
amount s (0 \le s \le n-1) কে বাইনারিতে লেখা যায় — n=8-এ
৩ বিট (s₂s₁s₀)। আর s = s_2 \cdot 4 + s_1 \cdot 2 + s_0 \cdot 1। তাই “s বিট ডানে shift করা” আসলে সমান “৪ বিট shift করব
কি না (s₂ অনুযায়ী), তারপর ২ বিট shift করব কি না (s₁
অনুযায়ী), তারপর ১ বিট shift করব কি না (s₀ অনুযায়ী) — এই
তিনটা স্বাধীন সিদ্ধান্তের সমষ্টি।”
প্রতিটা “শিফট করব কি না” সিদ্ধান্তই একটা 2-to-1 MUX — গত লেসনের
সেই একই building block! n টা 2-to-1 MUX (প্রতি বিট-পজিশনে
একটা) একটা স্তর বানায়; তিনটা স্তর ধারাবাহিকভাবে বসালে যেকোনো
0-থেকে-7 shift amount বাস্তবায়িত হয়।
A[7:0] ──┐
▼
┌──────────────────┐
│ Stage 0 (shift 1)│ ◄── s0 (shift by 1 or 0)
│ n × 2-to-1 MUX │
└──────────────────┘
│
▼
┌──────────────────┐
│ Stage 1 (shift 2)│ ◄── s1 (shift by 2 or 0)
│ n × 2-to-1 MUX │
└──────────────────┘
│
▼
┌──────────────────┐
│ Stage 2 (shift 4)│ ◄── s2 (shift by 4 or 0)
│ n × 2-to-1 MUX │
└──────────────────┘
│
▼
Result[7:0] (A কে ঠিক s2s1s0 বিট ডানে shift করা)এই ALU-তে shift amount আলাদা কোনো ইনপুট হিসেবে নেই — বাস্তব
ISA-র মতো, B-এর নিচের \log_2 n বিটই shift amount (বাকি
উঁচু বিটগুলো shift operation-এ ব্যবহৃত হয় না)। এটা কাল্পনিক নয়
— বাস্তব RISC-V-এ SLL/SRL/SRA instruction-এ ঠিক এই
নিয়মেই দ্বিতীয় register-এর নিচের কয়েকটা বিট shift amount
নির্ধারণ করে।
Output MUX — যেখানে সবকিছু একত্রিত হয়
এখন পর্যন্ত আমাদের কাছে আছে পাঁচ-ছয়টা স্বাধীন ফলাফল, সবগুলো প্রতি চক্রে সমান্তরালে গণনা হচ্ছে:
(SLT এই তালিকায় নেই — নিচে “hood” অংশে দেখব সেটা SUB-এর
flag থেকেই বিনামূল্যে বেরিয়ে আসে, আলাদা কোনো circuit ছাড়াই।)
এখন গত লেসনের মূল কৌশলটাই আবার প্রয়োগ হয় — একটা n-বিট-প্রশস্ত
output multiplexer (কার্যত n টা সমান্তরাল 8-to-1 MUX,
একটা প্রতি বিট-পজিশনে, সবগুলো একই ALUOp[2:0] দিয়ে নিয়ন্ত্রিত)
এই সবগুলো candidate ফলাফলের মধ্যে ঠিক একটা বেছে নেয়, ALUOp
অনুযায়ী — ঠিক গত লেসনের “8-to-1 MUX দিয়ে একটা function বানানো”
উদাহরণের মতোই, শুধু এখানে data input-এ constant 0/1 নয়,
বরং সম্পূর্ণ sub-circuit-এর ফলাফল বসছে।
A[7:0] B[7:0]
│ │
┌──────────────┼───────────────┼──────────────┬─────────────┐
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌─────────┐ ┌───────────┐ ┌────────────┐ ┌──────────┐ ┌───────────┐
│ AND │ │ OR │ │ ADD/SUB │ │ XOR │ │ NOT A │
│ (n gate)│ │ (n gate) │ │ (n adder + │ │ (n gate) │ │ (n gate) │
│ │ │ │ │ XOR-invert│ │ │ │ │
│ │ │ │ │ on B, M= │ │ │ │ │
│ │ │ │ │ ALUOp[2]) │ │ │ │ │
└────┬────┘ └─────┬─────┘ └──────┬─────┘ └────┬─────┘ └─────┬─────┘
│ │ │ │ │ │
│ │ │ └─ Cout(MSB) │ │
│ │ │ Cin(MSB) │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │ Barrel │ │ │
│ │ │ Shifter │◄─────── B[2:0] (শিফট পরিমাণ)
│ │ │ (SHR) │ │ │
│ │ └──────┬──────┘ │ │
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌───────────────────────────────────────────────────────────────────┐
│ Output MUX (n সমান্তরাল 8-to-1 MUX, select = ALUOp[2:0]) │
└───────────────────────────────┬───────────────────────────────────┘
│
▼
Result[7:0]
│
┌───────────────┼───────────────┬──────────────┐
▼ ▼ ▼ ▼
Zero flag Carry flag Overflow flag Negative flag
(NOR-reduce (adder Cout, (Cin_MSB XOR (Result[7],
of Result) SUB/ADD-এ Cout_MSB) MSB)
বৈধ)একটা বিকল্প (এবং বাস্তব) দৃষ্টিভঙ্গি — 1-bit ALU slice
Fig 8.2-এ পুরো ALU-টা একটা “n-বিট প্রশস্ত ব্লক” হিসেবে আঁকা
হয়েছে — যেন AND, OR, ADD প্রতিটাই একটা মাত্র বড় n-বিট
circuit। এটা ধারণাগতভাবে ঠিক, কিন্তু বাস্তব চিপ ডিজাইনে (আর
Patterson & Hennessy-র বিখ্যাত textbook-এ) ALU প্রায়ই সম্পূর্ণ
উল্টো দিক থেকে বানানো হয়: প্রথমে একটা একক 1-bit ALU slice
ডিজাইন করা হয়, তারপর সেই একই ছোট circuit-টা n বার পাশাপাশি
বসিয়ে (প্রতিটা বিট-পজিশনের জন্য একটা) পুরো n-বিট ALU বানানো
হয়।
একটা 1-bit slice-এর ইনপুট: Aᵢ, Bᵢ (একটা বিট করে), আগের
slice থেকে আসা CarryIn, আর সেই একই ৩-বিট ALUOp (সব slice-এ
অভিন্ন — এটাই গুরুত্বপূর্ণ, কোনো slice-নির্দিষ্ট control
নেই)। আউটপুট: Resultᵢ (একটা বিট), আর পরের slice-এর জন্য
CarryOut।
Aᵢ Bᵢ
│ │
┌───────┼──────────┼────────┐
│ ▼ ▼ │
│ AND, OR, XOR, NOT, ও │
│ ১-বিট full adder │
│ (CarryIn থেকে, │
│ CarryOut পরের slice-এ) │
│ │ │ │
│ ▼ ▼ │
│ ছোট output MUX │ ◄── ALUOp[2:0] (সব slice-এ অভিন্ন)
│ (এই বিটের জন্য) │
└───────────┬────────────────┘
▼
Resultᵢ
CarryIn ──► [slice i] ──► CarryOut ──► CarryIn of [slice i+1]কেন এই দৃষ্টিভঙ্গি গুরুত্বপূর্ণ: এটা দেখায় যে n-বিট ALU
আসলে কোনো এক বিশাল, বিশেষভাবে ডিজাইন-করা n-বিট circuit নয় —
বরং একটা ছোট, সহজে-verify-করা 1-bit building block-এর n-গুণ
পুনরাবৃত্তি, ঠিক যেভাবে গত লেসনের full adder n বার চেইন করে
ripple-carry adder বানানো হয়েছিল। এই “ছোট block বানাও, verify
করো, তারপর n বার পুনরাবৃত্তি করো” প্যাটার্নটা এই পুরো module-এর
একটা কেন্দ্রীয়, বারবার-ফিরে-আসা থিম — transistor থেকে গেট,
গেট থেকে adder, adder থেকে ALU, প্রতিটা ধাপেই একই কৌশল।
ভেতরে কী ঘটছে
যেখানে ব্লক ডায়াগ্রাম আর বাস্তব সিলিকন আলাদা হয়ে যায়
Flag generation — কোথা থেকে আসে প্রতিটা flag
Zero flag। সংজ্ঞা: Result = 0 হলে Zero = 1। এটা একটাও
বাড়তি “তুলনা” গেট ছাড়াই বানানো যায় — Result-এর সব বিট একটা
বিশাল OR গেটে দিন, তারপর invert করুন:
যুক্তি সহজ: n বিটের OR তখনই 0 হবে যখন প্রতিটা বিট 0
— অর্থাৎ পুরো সংখ্যাটাই 0। Invert করলে “সব বিট শূন্য” হলে
Zero=1।
Carry flag। সরাসরি adder/subtractor-এর সবচেয়ে উঁচু বিটের
carry-out (Cout_7 আমাদের n=8 উদাহরণে)। কিন্তু একটা সতর্কতা
— এই flag শুধু ADD/SUB operation-এই অর্থবহ; AND-এর
সময় adder এখনো “চলছে” (সবসময় চলে, নিচে দেখুন), তার carry-out
থাকবে, কিন্তু সেই মান অর্থহীন — control unit-এর দায়িত্ব শুধু
প্রাসঙ্গিক সময়ে (যখন operation আসলেই ADD/SUB) এই flag পড়া বা
রেজিস্টারে লেখা।
Overflow flag। আগের লেসনেই প্রতিষ্ঠিত — সবচেয়ে উঁচু বিটে carry-in আর carry-out ভিন্ন হলে signed overflow ঘটেছে:
Negative flag। সবচেয়ে সহজ — Result-এর MSB (sign বিট)
সরাসরি কপি:
SLT — বিনামূল্যে signed comparison, subtract-এর flag থেকেই
এখানেই একটা সুন্দর, বাস্তব প্রকৌশল কৌশল — A \lt B (signed)
নির্ণয় করতে কোনো নতুন তুলনা-circuit লাগে না। SUB operation
(A - B) থেকে যে Negative আর Overflow flag পাওয়া যায়,
সেগুলো দিয়েই সরাসরি উত্তর পাওয়া যায়:
সবগুলো sub-unit কি সবসময় চলে, নাকি শুধু নির্বাচিতটা?
একটা স্বাভাবিক অনুমান হলো — “যেহেতু আমরা শুধু একটা operation-এর
ফলাফল ব্যবহার করছি, বাকিগুলো নিশ্চয়ই ‘বন্ধ’ থাকে, শক্তি বাঁচাতে।”
সবচেয়ে সরল combinational ডিজাইনে এটা সত্যি নয়। AND, OR,
XOR, adder, shifter — সবগুলো circuit প্রতিটা চক্রে, প্রতিবার
গণনা করে, ALUOp যাই হোক না কেন — শুধু output MUX বাকিদের ফলাফল
উপেক্ষা করে। এটা RAM-এর একটা decoder-এর ঠিক উল্টো নীতি — decoder-এ
শুধু নির্বাচিত আউটপুটই সক্রিয় হয়, কিন্তু এখানে সব sub-circuit-ই
সবসময় “সক্রিয়,” শুধু ফলাফল ব্যবহৃত হয় বা হয় না।
Critical path — কোন sub-unit ALU-র গতি নির্ধারণ করে
Logic gate গ্লোসারিতে দেখা গেছে, সবচেয়ে দীর্ঘ gate-chain (critical path) circuit-এর সর্বোচ্চ clock speed ঠিক করে —
একটা ALU-তে critical path মানে সবচেয়ে ধীর sub-unit-এর delay + output MUX-এর delay — কারণ output MUX-কে অপেক্ষা করতে হয় যতক্ষণ না তার সবচেয়ে ধীর সম্ভাব্য input স্থির হয় (এমনকি সেই input শেষ পর্যন্ত নির্বাচিত না হলেও, MUX নিজে জানে না আগে থেকে কোনটা নির্বাচিত হবে — physically সব input-ই তার গেটে পৌঁছায়)।
| Sub-unit (n=8) | আনুমানিক gate-depth |
|---|---|
| AND / OR (bitwise) | ১ |
| XOR (bitwise) | ২ |
| NOT | ১ |
| Barrel shifter (৩ MUX-স্তর) | ~৬ (স্তর প্রতি ~২) |
| Ripple-carry adder/subtractor | ~১৭ (carry ripple + B-invert XOR) |
| Output MUX (8-to-1, tree) | ~৬ |
স্পষ্টভাবে adder/subtractor সবচেয়ে ধীর — আর সেটাই পুরো
ALU-র critical path নির্ধারণ করে (~17 + 6 = ~23 gate-delay,
বাকি sub-unit যতই দ্রুত হোক না কেন)। এটাই বাস্তব প্রকৌশলে একটা
কেন্দ্রীয় সত্য: carry-lookahead adder (ripple carry-র বদলে,
যেটা O(\log n) গভীরতায় carry হিসাব করে, O(n) নয়) সরাসরি
পুরো ALU-র, তাই পুরো CPU-র, clock speed বাড়িয়ে দিতে পারে — কারণ
adder-ই bottleneck।
দুই-স্তরের control — ALUOp কেন সরাসরি ALU-তে যায় না
এই লেসনে আমরা ধরে নিয়েছি ALUOp[2:0] কোথাও থেকে “জাদুমন্ত্রে”
এসে যায়। বাস্তব CPU-তে এটা এত সরাসরি না — আর এই ফাঁকটুকু বোঝা
জরুরি, কারণ এটা Level ৩-এর control unit লেসনের ভিত্তি গড়ে দেয়।
সমস্যাটা এখানে: instruction-এর opcode নিজে থেকে সবসময়
সরাসরি বলে দেয় না কোন ALU operation চালাতে হবে। MIPS-এর একটা
বাস্তব উদাহরণ — সব R-type instruction (add, sub, and,
or, slt…) একই opcode শেয়ার করে (000000)! আসল
পার্থক্য থাকে instruction-এর একদম শেষের একটা আলাদা ফিল্ডে, যার
নাম funct (function code)।
তাই বাস্তবে control দুই স্তরে ভাগ হয়:
- প্রধান control unit শুধু opcode দেখে একটা মোটা-দাগের
ALUOpসংকেত পাঠায় (যেমন “এটা একটা R-type instruction, আসল operation পরে ঠিক হবে” বনাম “এটা একটা lw/sw, নিশ্চিতভাবে ADD চাই address গণনার জন্য”)। - একটা ছোট, আলাদা “ALU control unit” (নিজেই একটা ছোট
combinational circuit, অনেকটা এই লেসনের decoder-নির্ভর যুক্তির
মতো) সেই মোটা-দাগের
ALUOpআর instruction-এরfunctফিল্ড — দুটো একসাথে দেখে, চূড়ান্ত, নির্দিষ্টALUControlসংকেত বানায় — এটাই এই লেসনেরALUOp[2:0]-এর ভূমিকায় সত্যিকারের ইনপুট হিসেবে ALU-তে পৌঁছায়।
Instruction opcode ──► প্রধান control unit ──► ALUOp (মোটা-দাগের, যেমন 2 বিট)
│
Instruction funct ─────────────────────────────────┤
(শুধু R-type-এ অর্থবহ) ▼
ছোট "ALU control unit"
(এই লেসনের decoder-জাতীয় যুক্তি)
│
▼
ALUControl (নির্দিষ্ট, এই লেসনের ALUOp[2:0])
│
▼
ALU (আজকের লেসনের সার্কিট)কেন এই বাড়তি স্তর দরকার: যদি প্রধান control unit-কে সরাসরি প্রতিটা সম্ভাব্য ALU operation-এর জন্য আলাদা opcode ডিজাইন করতে হতো, ISA-তে instruction encoding space (উপলব্ধ opcode বিট) দ্রুত ফুরিয়ে যেত। funct ফিল্ড দিয়ে একই opcode-এর নিচে বহু variant “লুকিয়ে” রাখা encoding space সাশ্রয় করে — এটা একটা বাস্তব ট্রেড-অফ, ঠিক এই লেসনের output-MUX fan-in ট্রেড-অফেরই আরেকটা রূপ, শুধু hardware এরিয়ার বদলে এখানে instruction-encoding “এরিয়া” বাঁচানো হচ্ছে। Level ৩-এ যখন আমরা সম্পূর্ণ control unit বানাব, এই দুই-স্তরের গঠনটাই তার কেন্দ্রীয় স্থাপত্য হবে।
একটা বাস্তব কেস স্টাডি — 74181 চিপ
১৯৭০-এর দশকের SN74181 একটা বাস্তব, বাণিজ্যিকভাবে বহুল-ব্যবহৃত ৪-বিট ALU chip — এই লেসনে যা আমরা ধারণাগতভাবে বানালাম তার একটা বাস্তব, সিলিকনে বানানো পূর্বপুরুষ। এতে ৪-বিট function-select input (১৬টা arithmetic + ১৬টা logic function বেছে নিতে পারে, একটা mode বিট দিয়ে) — আমাদের ৩-বিট, ৮-function ALU-র চেয়ে বড়, কিন্তু নীতিগতভাবে অভিন্ন। DEC PDP-11/45, Xerox Alto-র মতো বহু ঐতিহাসিক minicomputer একাধিক 74181 চিপ পাশাপাশি জুড়ে (৪-বিট প্রতিটা, cascade করে ১৬/৩২-বিট ALU) তাদের CPU বানিয়েছিল — এটাই “বাস্তব ALU হাতে-কলমে কেমন দেখতে” তার একটা প্রামাণ্য উদাহরণ, আজও digital design ইতিহাসের একটা গুরুত্বপূর্ণ chip হিসেবে পরিচিত।
একটা ALU-ই বারবার — multi-cycle datapath-এ পুনর্ব্যবহার
এই লেসনে আমরা ধরে নিয়েছি ALU শুধু “প্রধান” গণনার (যেমন add $t0, $t1, $t2) জন্য ব্যবহৃত হয়। কিন্তু বাস্তব CPU ডিজাইনে
(বিশেষত সহজ, multi-cycle স্থাপত্যে) একই ALU একই instruction-এর
বিভিন্ন ধাপে সম্পূর্ণ ভিন্ন উদ্দেশ্যে বারবার ব্যবহৃত হয় — এটাই
এই লেসনের কেন্দ্রীয় থিমের (“একই hardware, control code দিয়ে
ভিন্ন আচরণ”) সবচেয়ে বড়, সবচেয়ে বাস্তব উদাহরণ।
একটা multi-cycle MIPS-জাতীয় ডিজাইনে, একটা মাত্র instruction সম্পন্ন করতে ALU কয়েকবার ভিন্ন কাজে ব্যবহৃত হতে পারে:
| ধাপ | ALU কী গণনা করে | ALUOp |
|---|---|---|
| Instruction fetch | PC + 4 (পরের instruction-এর ঠিকানা) | ADD |
Memory-reference address গণনা (lw/sw) | base register + offset | ADD |
| R-type execute | প্রকৃত operation (ADD/SUB/AND/…) | instruction-নির্ভর |
| Branch address গণনা | PC + 4 + (offset \times 4) | ADD |
লক্ষ্য করুন — প্রথম তিনটাই মূলত ADD, কিন্তু সম্পূর্ণ ভিন্ন ডেটার উপর, ভিন্ন ভিন্ন clock cycle-এ। এটাই দেখায় কেন “শুধু একটা ALU রাখা” আর্থিকভাবে এত লাভজনক — একটা সাধারণ, সস্তা CPU ডিজাইনে একটাই ALU circuit বারবার re-route (input MUX দিয়ে, যেটা কোন দুইটা মান এই মুহূর্তে ALU-তে যাচ্ছে তা নির্বাচন করে — এই লেসনের output MUX-এর মতোই নীতি, ALU-র প্রবেশপথে) করে গোটা instruction সম্পন্ন হয়। Level ৩-এ multi-cycle datapath বানানোর সময় এই পুরো প্যাটার্নটা বিস্তারিত দেখা যাবে।
উদাহরণ
সম্পূর্ণ ট্রেস করা উদাহরণ — একটা 8-bit ALU-র মধ্য দিয়ে
A = 00001010₂ (10 দশমিকে), B = 00000011₂ (3 দশমিকে)।
ALU-র প্রতিটা sub-unit একসাথে গণনা করবে — আমরা সবগুলো
হিসাব করব, তারপর দেখব ALUOp অনুযায়ী output MUX ঠিক কোনটা
বেছে নেয়।
| ALUOp | Operation | গণনা | Result (বাইনারি) | Result (দশমিক) |
|---|---|---|---|---|
000 | AND | 00001010 AND 00000011 | 00000010 | 2 |
001 | OR | 00001010 OR 00000011 | 00001011 | 11 |
010 | ADD | 00001010 + 00000011 | 00001101 | 13 |
011 | XOR | 00001010 XOR 00000011 | 00001001 | 9 |
100 | NOT | NOT 00001010 | 11110101 | −11 (signed) |
101 | SHR | shift amount = B[2:0] = 011₂ = 3; 00001010 \gg 3 | 00000001 | 1 |
110 | SUB | 00001010 - 00000011 | 00000111 | 7 |
111 | SLT | A \lt B? (10 \lt 3?) | 00000000 | 0 |
ধরুন ALUOp = 110 (SUB)। Output MUX ঠিক এই সারিটা বেছে
নেবে — বাকি সাতটা গণনা physically হয়ে গেছে, কিন্তু তাদের ফলাফল
এই চক্রে কোথাও ব্যবহৃত হবে না।
Flag গণনা (SUB নির্বাচিত অবস্থায়):
Bit-by-bit adder trace (B' = NOT(00000011) = 11111100,
Cin=1, two’s complement subtract):
bit: 7 6 5 4 3 2 1 0
A: 0 0 0 0 1 0 1 0
B': 1 1 1 1 1 1 0 0
Cin(in): 1 1 1 1 0 0 0 1
Sum: 0 0 0 0 0 1 1 1
Cout: 1 1 1 1 1 0 0 0Result = 00000111₂ = 7 — মিলে গেছে 10 - 3 = 7-এর সাথে।
- Zero =
NOR(সব Result বিট)=NOR(0,0,0,0,0,1,1,1) = 0(নিশ্চিতভাবেই —7 \ne 0) - Carry =
Coutat MSB (bit 7) =1(কোনো borrow হয়নি,10 \ge 3unsigned-ও সত্য) - Overflow =
Cinat MSB XORCoutat MSB =1 XOR 1 = 0(কোনো signed overflow নেই — স্বাভাবিক, উত্তর7সহজেই ৮-বিট signed range-এ ধরে) - Negative =
Result[7] = 0(positive result)
- A=00001010, B=00000011, ALUOp=110ইনপুট — তিনটাই একসাথে সব sub-unit-এ পাঠানো হয়
- ৮টা sub-unit সমান্তরালে গণনা করেAND=2, OR=11, ADD=13, XOR=9, NOT=−11, SHR=1, SUB=7, SLT=0 — সবগুলো একই চক্রে প্রস্তুত
- Output MUX ALUOp=110 পড়েঠিকানা 6 (বাইনারি 110) নির্বাচন করে — SUB-এর candidate result
- Result = 00000111 (7)বাকি ৭টা candidate ফলাফল এই চক্রে বাতিল হয়ে গেল
- Flag: Zero=0, Carry=1, Overflow=0, Negative=0সরাসরি adder-এর carry chain থেকে, কোনো বাড়তি তুলনা-circuit ছাড়াই
দ্বিতীয় উদাহরণ — SLT, উল্টো দিক থেকে A ও B বদলে
এবার A = 00000011₂ (3), B = 00001010₂ (10) — আগেরটার
উল্টো, আর ALUOp = 111 (SLT)।
SLT আসলে ভেতরে A - B গণনা করে (SUB-এর মতোই adder ব্যবহার
করে), শুধু চূড়ান্ত output MUX ভিন্ন জায়গা থেকে ফলাফল টানে — পুরো
Result নয়, বরং একটা মাত্র বিট, Negative \oplus Overflow।
B' = NOT(00001010) = 11110101, Cin=1:
bit: 7 6 5 4 3 2 1 0
A: 0 0 0 0 0 0 1 1
B': 1 1 1 1 0 1 0 1
Cin(in): 0 0 0 0 1 1 1 1
Sum: 1 1 1 1 1 0 0 1
Cout: 0 0 0 0 1 1 1 0Result(অভ্যন্তরীণ subtract) = 11111001₂ = -7 (3 - 10 = -7,
মিলে যাচ্ছে)।
- Negative =
Result[7] = 1 - Overflow =
Cinat MSB XORCoutat MSB =0 XOR 0 = 0 - SLT =
Negative XOR Overflow = 1 XOR 0 = 1
উত্তর: SLT = 1 — সঠিক, কারণ 3 \lt 10। লক্ষ্য করুন
output MUX-এর 111 ঠিকানায় পুরো ৮-বিট Result বসে না — শুধু
এই একটা bit-value 1 প্রতিটা bit-position-এ replicate (বা শুধু
LSB-তে বসিয়ে বাকি সব 0) করে দেওয়া হয়, যেহেতু SLT-এর উত্তর
সবসময় 0 বা 1 (একটা boolean, n-বিট সংখ্যা নয়)।
নিজে চালিয়ে দেখুন
Digital/Logisim-এ একটা 4-bit ALU বানিয়ে AND/OR/ADD/SUB যাচাই
১. চারটা AND গেট, চারটা OR গেট (৪-বিট প্রশস্ত, A[3:0],
B[3:0]-এর উপর) বসান।
2. আগের লেসনের adder/subtractor সার্কিট (৪-বিট, mode বিট M
সহ) পুনর্ব্যবহার করুন — নতুন করে বানানোর দরকার নেই যদি সেটা
ইতিমধ্যে সংরক্ষিত থাকে (Digital/Logisim-এ sub-circuit হিসেবে
import করা যায়)।
3. একটা 2-বিট op-select ইনপুট বসান (এই experiment-এ শুধু ৪টা
operation — AND, OR, ADD, SUB — তাই ৪-to-1 output MUX যথেষ্ট,
পূর্ণ ৩-বিট লাগবে না)।
4. চারটা sub-unit-এর ফলাফল output MUX-এর data input-এ জোড়া
লাগান, op-select-কে MUX-এর select লাইনে।
5. A=0101 (5), B=0011 (3) বসিয়ে চারটা op-select মান (00, 01, 10, 11) একে একে বসান, প্রতিবার Result পড়ুন।
প্রত্যাশিত ফলাফল:
op | operation | Result
00 | AND | 0001 (1)
01 | OR | 0111 (7)
10 | ADD | 1000 (8)
11 | SUB | 0010 (2)এবার A=0000, B=0000 বসিয়ে ADD নির্বাচন করুন — Zero flag
1 হওয়া উচিত। তারপর A=1000, B=1000 (দুইটা negative সংখ্যা,
৪-বিট signed-এ -8) নিয়ে ADD করুন — Overflow flag 1 হওয়া
উচিত (-8 + -8 = -16, যেটা ৪-বিট signed range -8..7-এর বাইরে)।
হাতে করা truth table আর simulated circuit-এর ফলাফল হুবহু মেলে — বিশেষভাবে adder-এর carry chain-এর সাথে output MUX-এর সংযোগ সঠিকভাবে কাজ করছে কি না।
Python দিয়ে reference model — হাতের trace বনাম কোড
def alu(a: int, b: int, op: int, bits: int = 8):
mask = (1 <\< bits) - 1
a &= mask
b &= mask
and_r = a & b
or_r = a | b
add_r = (a + b) & mask
xor_r = a ^ b
not_r = (~a) & mask
shamt = b & (bits - 1) # নিচের log2(bits) বিট
shr_r = (a >> shamt) & mask
# subtract — two's complement দিয়ে, carry/overflow বের করার জন্য
b_inv = (~b) & mask
sub_full = a + b_inv + 1
sub_r = sub_full & mask
carry = (sub_full >> bits) & 1
def sign(x):
return (x >> (bits - 1)) & 1
# overflow: A ও B_inv-এর sign সমান কিন্তু result-এর sign ভিন্ন হলে
overflow = 1 if (sign(a) == sign(b_inv) and sign(sub_r) != sign(a)) else 0
negative = sign(sub_r)
slt = negative ^ overflow
results = {
0b000: and_r, 0b001: or_r, 0b010: add_r, 0b011: xor_r,
0b100: not_r, 0b101: shr_r, 0b110: sub_r, 0b111: slt,
}
result = results[op]
zero = 1 if result == 0 else 0
return {
'result': result, 'zero': zero, 'carry': carry,
'overflow': overflow, 'negative': negative,
}
# এই লেসনের প্রথম উদাহরণ যাচাই — A=10, B=3, SUB
r = alu(10, 3, 0b110)
print(f"SUB: result={r['result']} (expect 7), "
f"Z={r['zero']} C={r['carry']} V={r['overflow']} N={r['negative']}")
# দ্বিতীয় উদাহরণ — A=3, B=10, SLT
r2 = alu(3, 10, 0b111)
print(f"SLT: result={r2['result']} (expect 1)")
# পুরো ৮-অপারেশন টেবিল, A=10, B=3
for op in range(8):
r = alu(10, 3, op)
print(f"op={op:03b} → result={r['result']}")প্রত্যাশিত output:
SUB: result=7 (expect 7), Z=0 C=1 V=0 N=0
SLT: result=1 (expect 1)
op=000 → result=2
op=001 → result=11
op=010 → result=13
op=011 → result=9
op=100 → result=245
op=101 → result=1
op=110 → result=7
op=111 → result=0(op=100-এ 245 দেখাচ্ছে কারণ Python অজানা unsigned আকারে
প্রিন্ট করছে — 245 = 11110101₂, যেটা signed interpretation-এ
-11, এই লেসনের টেবিলের সাথেই মেলে।) লক্ষ্য করুন op=111
(SLT)-এ এখানে A=10, B=3 দিয়ে চালানো হয়েছে টেবিলের সাথে মেলানোর
জন্য (10 \lt 3 মিথ্যা, তাই result=0), যেখানে দ্বিতীয় পরীক্ষায়
A=3, B=10 দিয়ে (3 \lt 10 সত্য, result=1) — দুই দিকই এই
লেসনের হাতে-করা উদাহরণের সাথে যাচাই হলো।
এই লেসনের হাতে-করা উদাহরণ (A=10,B=3,SUB=7, flags) একটা independent সফটওয়্যার মডেলের সাথে হুবহু মেলে — এটাই যেকোনো hardware design যাচাইয়ের প্রথম ধাপ: একটা software reference model বানিয়ে RTL-এর সাথে তুলনা করা।
নিজে বানান
একটা সম্পূর্ণ 8-bit, 8-operation ALU (AND/OR/ADD/XOR/NOT/SHR/SUB/SLT)
- পাঁচটা sub-unit আলাদাভাবে বানান ও পরীক্ষা করুন — AND array, OR array, XOR array, NOT array (এই চারটা মিলিয়ে "logic unit" নাম দিতে পারেন), আর আগের লেসনের adder/subtractor circuit পুনর্ব্যবহার করুন
- Fig 8.1 অনুযায়ী একটা ৩-স্তরের barrel shifter বানান (৮-বিট, শুধু logical right shift) — shift amount ইনপুট নিন B[2:0] থেকে সরাসরি
- একটা 8-to-1 output MUX (৮-বিট প্রশস্ত, মানে ৮টা সমান্তরাল 8-to-1 MUX) বানিয়ে সব sub-unit-এর ফলাফল data input-এ যুক্ত করুন, এই লেসনের টেবিল অনুযায়ী ক্রম মিলিয়ে
- Zero (NOR-reduce), Carry (adder Cout), Overflow (Cin_MSB XOR Cout_MSB), Negative (Result[7]) flag বানান
- এই লেসনের প্রথম উদাহরণের (A=10, B=3, প্রতিটা ALUOp) সবগুলো row নিজের সার্কিটে চালিয়ে হাতে-করা টেবিলের সাথে মেলান — একটাও অমিল থাকা উচিত না
- শেষে SLT (ALUOp=111) আলাদাভাবে পরীক্ষা করুন কয়েকটা signed A/B জোড়া দিয়ে, বিশেষভাবে একটা overflow-উৎপাদক জোড়া দিয়ে (যেমন সবচেয়ে বড় positive বনাম সবচেয়ে বড়-ম্যাগনিচিউড negative) — Negative flag একা ভুল উত্তর দেয় কি না, আর XOR-Overflow সংশোধন করে কি না, নিজে দেখুন
এই ALU সম্পূর্ণ হলে আপনার হাতে থাকবে curriculum-এর “8-bit ALU”
প্রজেক্টের মূল অংশ — Level ২-র module page-এ তালিকাভুক্ত সেই
প্রজেক্টেরই এটা গোড়ার কাজ। শেষ ধাপ (overflow-উৎপাদক SLT পরীক্ষা)
বিশেষভাবে গুরুত্বপূর্ণ — এটাই একমাত্র জায়গা যেখানে “hood” অংশের
Negative \oplus Overflow সূত্রের আসল প্রয়োজনীয়তা নিজের চোখে
দেখা যায়, নাহলে সূত্রটা অপ্রয়োজনীয় জটিলতা মনে হতে পারে।
নিজে বাড়ান (ঐচ্ছিক, advanced): left shift আর arithmetic right shift যোগ করুন (মোট ১০টা operation, ৪-বিট op-select লাগবে) — আর দেখুন output MUX-এর আকার বাড়ায় critical path কতটা বাড়ে, Digital/Logisim-এর timing analysis টুল দিয়ে মাপুন যদি পাওয়া যায়।
বাস্তব সিস্টেমে
ALU যেখানে প্রতিদিন কাজ করছে
MIPS-এর ALUControl সিগন্যাল। এই লেসনের সবচেয়ে সরাসরি বাস্তব
প্রতিফলন — classic MIPS single-cycle CPU ডিজাইনে (Patterson &
Hennessy textbook-এর কেন্দ্রীয় উদাহরণ) একটা ৪-বিট ALUControl
সিগন্যাল main control unit-এর ২-বিট ALUOp আর instruction-এর
funct field মিলিয়ে তৈরি হয়, আর সেটাই সরাসরি ALU-র output-MUX-এর
select লাইনে যায় — ঠিক এই লেসনের ALUOp[2:0]-এর মতোই ধারণা,
শুধু বাস্তব chip-এ আরও কয়েকটা operation বেশি।
RISC-V-এর R-type instruction decoding। ADD, SUB, AND,
OR, XOR, SLL, SRL, SRA, SLT, SLTU — প্রতিটাই
RISC-V-তে funct3/funct7 বিট দিয়ে এনকোড করা, আর control
unit সেগুলো ডিকোড করে ঠিক এই লেসনের মতো একটা ALU op-select
বানায়। Level ৩-এ (Assembly, CPU Architecture) আমরা এই encoding
বিস্তারিত দেখব।
x86-এর মাইক্রো-অপ ব্যাকএন্ড। আধুনিক x86 CPU (Intel/AMD) জটিল CISC instruction-কে ছোট ছোট “micro-op”-এ ভেঙে ফেলে, আর প্রতিটা micro-op শেষমেশ একটা শেয়ার্ড execution unit-এ যায় — সেই execution unit-এর কেন্দ্রেই এই লেসনের মতোই একটা ALU থাকে। আধুনিক superscalar core-এ একাধিক ALU port থাকে (Level ১১-এ বিস্তারিত), যাতে একই চক্রে একাধিক independent ALU operation সমান্তরালে চলতে পারে — এই লেসনের “একটা ALU, প্রতি চক্রে একটা operation” সীমাবদ্ধতারই একটা বাস্তব সমাধান।
74181 — ইতিহাসের প্রথম বাণিজ্যিক ALU chip। ইতিমধ্যে আলোচিত “hood” অংশে — বহু ঐতিহাসিক minicomputer-এর ভিত্তি।
GPU shader core-এর ALU। একটা GPU-তে হাজার হাজার ছোট, সরল ALU একসাথে (SIMT — Single Instruction, Multiple Thread মডেলে) একই operation ভিন্ন ভিন্ন ডেটার উপর চালায় — graphics rendering আর deep learning-এর matrix multiplication উভয়েরই ভিত্তি এই বিপুল-সংখ্যক সরল ALU-র সমান্তরাল ব্যবহার। Level ১১-এ GPU architecture নিয়ে আরও গভীরে যাওয়া হবে।
DSP-এর MAC unit। Digital Signal Processor (audio/video processing চিপ)-এ একটা বিশেষায়িত “Multiply-Accumulate (MAC)” unit থাকে — কার্যত একটা বর্ধিত ALU, যার একটা অতিরিক্ত অভ্যন্তরীণ accumulator register (এই লেসনের পরবর্তী বিষয়, sequential logic, ছাড়া যা সম্ভব নয়) থাকে ধারাবাহিক গুণ-যোগ দ্রুত করতে।
ক্যালকুলেটর চিপ। সাধারণ পকেট ক্যালকুলেটরের ভেতরের চিপেও
এই লেসনের মূল ধারণা — একটা shared arithmetic circuit, বোতাম
(+, -, ×, ÷) অনুযায়ী নির্বাচিত operation চালায়।
FPGA-তে soft ALU। গত লেসনের FPGA LUT আলোচনার সরাসরি সম্প্রসারণ — একটা FPGA-তে যখন কেউ একটা processor design (soft-core CPU) synthesize করেন, ALU-টাও এই লেসনের কাঠামো অনুসরণ করেই বহু LUT আর MUX দিয়ে বাস্তবায়িত হয়, কোনো dedicated ALU সিলিকন ছাড়াই।
যে ভুলগুলো সবাই করে
“ALU নিজেই সিদ্ধান্ত নেয় কোন operation চালাবে — এটা 'বুদ্ধিমান'।”
ALU সম্পূর্ণভাবে dumb — এর কোনো সিদ্ধান্ত-গ্রহণ ক্ষমতা নেই।
ALUOp সিগন্যাল সম্পূর্ণভাবে বাইরে থেকে আসে — Level ৩-এ যা আমরা
“control unit” হিসেবে বানাব, সেটাই instruction-এর opcode ডিকোড
করে ঠিক করে দেয় ALU কী করবে। ALU শুধু সেই সিগন্যাল অনুযায়ী,
একটা fixed, পূর্ব-তারযুক্ত MUX দিয়ে result বেছে দেয় — এখানে কোনো
“বোঝা,” কোনো “যুক্তি,” কোনো memory নেই (এই লেসনের নিজস্ব কোনো
state নেই — সম্পূর্ণ combinational)। এই ভুল ধারণাটা গুরুত্বপূর্ণ
সংশোধন করা দরকার, কারণ Level ৩-এ যখন পুরো CPU দেখব, বোঝা জরুরি
যে “বুদ্ধি” (instruction বোঝা, কী করতে হবে ঠিক করা) থাকে control
unit-এ, ALU শুধু কার্যকর করে।
“যেহেতু শুধু একটা operation-এর ফলাফল দরকার, ALU শুধু সেটাই গণনা করে — বাকিগুলো 'বন্ধ' থাকে।”
এই লেসনের সরলতম combinational ডিজাইনে সবগুলো sub-unit প্রতি
চক্রে সবসময় গণনা করে, ALUOp যাই হোক না কেন — শুধু output
MUX বাকি ফলাফল উপেক্ষা করে। এটা কোনো bug নয়, বরং সরলতম সঠিক
ডিজাইন — কোনো sub-unit-কে “conditionally বন্ধ” রাখতে হলে বাড়তি
control logic লাগত। বাস্তব উচ্চ-কর্মক্ষমতা চিপে power বাঁচাতে
“hood” অংশে আলোচিত operand/clock gating ব্যবহার হয়, কিন্তু এটা
একটা conscious optimization, ALU-র মৌলিক আচরণ নয়।
“Barrel shifter মানে 'শুধু তার নড়াচড়া করা' — এটা কার্যত instant, কোনো delay নেই।”
Shift amount নির্ভর করে কোন তারটা কোথায় সংযুক্ত হবে সেটা
নির্ধারণ করতে বাস্তব MUX গেট লাগে — প্রতিটা স্তরে (log₂n স্তর)
বাস্তব propagation delay আছে। “Hood” অংশের টেবিলে দেখা গেছে
n=8-এ barrel shifter-এর delay adder-এর তুলনায় কম হলেও শূন্য
নয় (~৬ গেট-delay) — বড় n-এ (যেমন ৬৪-বিট) এই delay উল্লেখযোগ্য
হয়ে ওঠে।
“ALU-তে যত বেশি operation যোগ করা যায়, ততই ভালো — 'বেশি ফিচার মানেই ভালো ডিজাইন'।”
প্রতিটা নতুন operation output MUX-এর আকার বাড়ায় (k-to-1
থেকে (k+1)-to-1) — গত লেসনের fan-in বিশ্লেষণ অনুযায়ী, বড় MUX-এর
delay বেশি। তাই বেশি operation যোগ করলে ALU-র প্রতিটা operation-ই
সামান্য ধীর হয়ে যায় — এমনকি যেগুলো এমনিতে খুব সাধারণ (যেমন
AND)। বাস্তব ISA ডিজাইনে তাই ALU-তে কোন operation রাখা হবে তা
একটা সচেতন trade-off — শুধু “ব্যবহারযোগ্যতা” নয়, “প্রতিটা
সাধারণ operation-এর গতি কতটা ক্ষতিগ্রস্ত হচ্ছে”-ও একটা বিবেচ্য
বিষয়।
বুঝেছেন কি না দেখুন
1A = 00000110₂ (6), B = 00000101₂ (5), ALUOp = 001 (OR)।
Result কী, আর Zero flag কী হবে?
প্রয়োগ
A = 00000110₂ (6), B = 00000101₂ (5), ALUOp = 001 (OR)।
Result কী, আর Zero flag কী হবে?Bitwise OR:
00000110
OR
00000101
= 00000111Result = 00000111₂ = 7।
Zero flag = NOR(সব Result বিট) — যেহেতু Result \ne 0
(অন্তত একটা বিট 1), Zero = 0।
উত্তর: Result = 7, Zero = 0।
2A = 00000101₂ (5), B = 00000010₂ (2), ALUOp = 101 (SHR)।
Result কী?
প্রয়োগ
A = 00000101₂ (5), B = 00000010₂ (2), ALUOp = 101 (SHR)।
Result কী?প্রথমে shift amount বের করি — B-এর নিচের ৩ বিট: B[2:0] = 010₂ = 2।
A কে ডানে ২ বিট logical shift করি:
00000101 >> 2 = 00000001(সহজে দেখতে: 00000101 থেকে ডানে দুইটা বিট সরিয়ে বাম দিকে
00 ভরাট করা — 101 এর নিচের ২ বিট 01 হারিয়ে যায়, বাকি
000001 থাকে)
উত্তর: Result = 00000001₂ = 1।
3প্রমাণ করুন কেন Zero = NOR(R_7, R_6, ..., R_0) সঠিকভাবে
“Result সম্পূর্ণ শূন্য” চেনায় — কোনো ভুল-পজিটিভ বা ভুল-নেগেটিভ
কেস আছে কি না যুক্তি দিয়ে দেখান।
যুক্তি
Zero = NOR(R_7, R_6, ..., R_0) সঠিকভাবে
“Result সম্পূর্ণ শূন্য” চেনায় — কোনো ভুল-পজিটিভ বা ভুল-নেগেটিভ
কেস আছে কি না যুক্তি দিয়ে দেখান।NOR গেটের সংজ্ঞা: আউটপুট 1 হয় শুধুমাত্র যখন সব ইনপুট 0;
অন্য যেকোনো ইনপুট-combination-এ আউটপুট 0।
এখানে n টা ইনপুট হলো Result-এর প্রতিটা বিট। তাই:
Zero = 1ঠিক তখনই যখনR_7 = R_6 = \cdots = R_0 = 0— অর্থাৎ প্রতিটা বিট0, যার একমাত্র মানেResultসংখ্যা হিসেবে ঠিক0।- যদি
Result-এর কমপক্ষে একটা বিট1হয় (Result \ne 0যেকোনো non-zero মানের জন্য), তাহলে সেই একটা1ইনপুট-ই OR গেটকে1বানিয়ে দেবে, ফলে NOR (invert)0দেবে — সঠিকভাবেZero = 0।
কোনো edge case বাকি নেই — n-বিট সংখ্যার জন্য “শূন্য” আর “সব
বিট শূন্য” ঠিক সমার্থক (এটাই বাইনারি positional notation-এর
সংজ্ঞা), তাই এই NOR-ভিত্তিক পদ্ধতি সব 2^n সম্ভাব্য Result
মানের জন্যই সঠিক — প্রমাণ সম্পূর্ণ (exhaustive না করেই, শুধু
NOR-এর সংজ্ঞা থেকে সরাসরি)।
4ধরুন আপনাকে এই ALU-তে একটা নতুন operation যোগ করতে বলা হলো —
SGT (Set if Greater Than, A \gt B হলে 1)। কীভাবে বানাবেন
— নতুন কোনো তুলনা-circuit লাগবে, নাকি বিদ্যমান sub-unit থেকেই
সম্ভব?
ডিজাইন
SGT (Set if Greater Than, A \gt B হলে 1)। কীভাবে বানাবেন
— নতুন কোনো তুলনা-circuit লাগবে, নাকি বিদ্যমান sub-unit থেকেই
সম্ভব?নতুন কোনো তুলনা-circuit লাগে না — এটা বিদ্যমান SLT এবং Zero
থেকেই বের করা যায়, শুধু ভিন্নভাবে সাজিয়ে।
লক্ষ্য করুন: A \gt B ঠিক তখনই সত্য যখন A \lt B মিথ্যা এবং
A = B মিথ্যা (অর্থাৎ B \lt A, বা সমতুল্যভাবে, A-B positive
এবং non-zero)।
দুইটা উপায় আছে:
উপায় ১ — SLT উল্টো ক্রমে চালানো। যদি ALU-তে ইনপুট হিসেবে
A আর B-এর জায়গা বদলে (B-কে প্রথম ইনপুটে, A-কে দ্বিতীয়
ইনপুটে দিয়ে) SLT চালানো যায়, তাহলে ফলাফল হবে B \lt A, যেটা
ঠিক A \gt B-এর সংজ্ঞা। এটা কোনো নতুন hardware লাগে না — শুধু
control unit-এর wiring-এ operand-দুটো অদল-বদল করে দিলেই হয়।
উপায় ২ — SUB-এর flag থেকে সরাসরি। SUB (A-B) চালিয়ে,
Result non-zero (Zero flag-এর পরিপূরক) আর positive (Negative XOR Overflow-এর পরিপূরক, অর্থাৎ SLT-এর উল্টো) — দুটো শর্ত
AND করলেই SGT:
এই দ্বিতীয় পদ্ধতিতে একটা বাড়তি AND গেট + একটা inverter লাগে (output MUX-এর বাইরে, একটা ছোট বাড়তি “post-processing” ধাপ হিসেবে) — কোনো নতুন adder বা তুলনা-সার্কিট নয়। এটাই এই লেসনের কেন্দ্রীয় থিমের আরেকটা উদাহরণ — বিদ্যমান flag-গুলো পুনর্বিন্যাস করেই নতুন উপযোগী তথ্য পাওয়া যায়, নতুন গণনা-সার্কিট ছাড়াই।
5কেন বাস্তব CPU ডিজাইনে (বিশেষত সাধারণ, ব্যয়-সচেতন প্রসেসরে) প্রতি
operation-এর জন্য আলাদা dedicated hardware না বানিয়ে একটাই
shared ALU বানানো হয়? এই সিদ্ধান্তের খরচ কী?
যুক্তি
সুবিধা — area ও power। যদি প্রতিটা operation-এর (AND, OR, ADD, SUB, XOR, SHR, …) জন্য আলাদা, সবসময়-সক্রিয় circuit থাকত, চিপের সিলিকন এরিয়া অনেক বেশি লাগত — প্রতিটা সার্কিট জায়গা নেয়, transistor খরচ করে, static leakage power খরচ করে (এমনকি ব্যবহৃত না হলেও)। একটা shared ALU-তে সবগুলো sub-unit মিলিয়েই এই এরিয়া, আর যেহেতু একই সময়ে মাত্র একটা ব্যবহৃত হয়, output MUX দিয়ে ফলাফল একত্রিত করার খরচ (কিছু বাড়তি গেট) সেই এরিয়া-সাশ্রয়ের তুলনায় নগণ্য।
খরচ — throughput। একটা shared ALU-তে প্রতি clock cycle-এ মাত্র একটা operation সম্পন্ন হতে পারে — যদি একটা প্রোগ্রামে দুইটা সম্পূর্ণ স্বাধীন ADD (একে অপরের ফলাফলের উপর নির্ভর করে না) থাকে, single-ALU ডিজাইনে তাদের ধারাবাহিকভাবে, ভিন্ন ভিন্ন চক্রে চালাতে হবে — একসাথে না।
Trade-off-এর সমাধান — বেশি ALU। যদি throughput-ই বেশি গুরুত্বপূর্ণ হয় (উচ্চ-কর্মক্ষমতা CPU-তে যেমন), ডিজাইনার একাধিক সম্পূর্ণ ALU রাখতে পারেন (superscalar স্থাপত্য, Level ১১-এ বিস্তারিত) — প্রতিটা তার নিজস্ব sub-unit-সহ, একসাথে একাধিক independent operation চালাতে সক্ষম। কিন্তু তখন এরিয়া আর power খরচ সরাসরি ALU-সংখ্যার সাথে বাড়ে — এটাই মূল trade-off: কম এরিয়া/power (shared, single ALU, কম throughput) বনাম বেশি throughput (একাধিক ALU, বেশি এরিয়া/power)। বাস্তব ডিজাইন এই স্পেকট্রামের কোথায় বসবে তা নির্ভর করে target market-এর উপর — মোবাইল/embedded চিপ প্রায়ই কম ALU (power-সচেতন), হাই-এন্ড সার্ভার/ডেস্কটপ CPU প্রায়ই একাধিক ALU port (throughput-সচেতন)।
6এই লেসনের “hood” অংশের critical path টেবিল অনুযায়ী, n=8
ALU-তে সবচেয়ে ধীর sub-unit কোনটা, আর কেন সেটাই পুরো ALU-র
সর্বোচ্চ clock speed নির্ধারণ করে (বাকি sub-unit দ্রুত হওয়া
সত্ত্বেও)?
প্রয়োগ
n=8
ALU-তে সবচেয়ে ধীর sub-unit কোনটা, আর কেন সেটাই পুরো ALU-র
সর্বোচ্চ clock speed নির্ধারণ করে (বাকি sub-unit দ্রুত হওয়া
সত্ত্বেও)?সবচেয়ে ধীর sub-unit হলো ripple-carry adder/subtractor (~১৭ গেট-delay), বাকি সব sub-unit (AND/OR/XOR/NOT ~১-২, barrel shifter ~৬)-এর চেয়ে উল্লেখযোগ্যভাবে বেশি।
কেন এটাই ঠিক করে দেয়: output MUX-কে physically তার সব
input-এর মধ্যে ঠিকানা অনুযায়ী নির্বাচন করতে হয় — কিন্তু MUX
নিজে জানে না আগে থেকে কোন input শেষমেশ নির্বাচিত হবে (সেটা
নির্ভর করে ALUOp-এর উপর, যেটা runtime-এ বদলায়)। তাই সার্কিট
ডিজাইনে (worst-case timing analysis-এ) ধরে নিতে হয় — MUX-এর
আউটপুট ততক্ষণ “স্থির” (নির্ভরযোগ্য) নয় যতক্ষণ না তার সবচেয়ে
ধীর সম্ভাব্য input স্থির হয়েছে। যদি clock খুব দ্রুত চলে
(adder-এর ফলাফল স্থির হওয়ার আগেই পরবর্তী clock edge চলে আসে),
আর ঘটনাক্রমে সেই চক্রে ALUOp = SUB (adder-এর ফলাফল আসলেই
দরকার), তাহলে ভুল, অস্থির ফলাফল ধরা পড়বে — একটা প্রকৃত timing
bug।
তাই পুরো circuit-এর সর্বোচ্চ clock frequency নির্ধারিত হয়
worst-case (সবচেয়ে ধীর) path দিয়ে, এমনকি যদি সেই path শুধু
কিছু নির্দিষ্ট ALUOp মানে “প্রাসঙ্গিক” হয় — কারণ hardware-কে
সব সম্ভাব্য ALUOp মানের জন্য সঠিকভাবে কাজ করতে হবে, শুধু
সাধারণ কেসের জন্য নয়। এটাই কেন carry-lookahead adder (যেটা
adder-এর delay O(n) থেকে O(\log n)-এ নামায়) সরাসরি পুরো
ALU-র, তাই পুরো CPU-র, সম্ভাব্য clock speed বাড়িয়ে দিতে পারে।
এরপর কী
এরপর কী — এই ফলাফলটা “মনে রাখবে” কে?
আজ আমরা একটা ALU বানিয়েছি যেটা প্রতি চক্রে একটা নির্দিষ্ট
operation-এর ফলাফল তৈরি করতে পারে — control code দিয়ে নির্বাচিত।
কিন্তু একটা গুরুত্বপূর্ণ প্রশ্ন এখনো অনুত্তরিত: এই ফলাফলটা
পরের চক্রে কোথায় যায়? যদি আমি একটা program চালাই যেখানে প্রথমে
x = a + b (একটা ADD), তারপর y = x * 2 (যেটা x-এর মান
ব্যবহার করে) — সেই x-এর মান দ্বিতীয় operation শুরু হওয়ার
আগ পর্যন্ত কোথাও ধরে রাখতে হবে।
এখন পর্যন্ত আমরা যা বানিয়েছি — গেট, adder, MUX, decoder, ALU — সবকিছুই combinational: আউটপুট সম্পূর্ণভাবে বর্তমান ইনপুটের উপর নির্ভর করে, কোনো “ইতিহাস” বা “স্মৃতি” নেই। ইনপুট সরিয়ে নিলে আউটপুটও মুহূর্তেই হারিয়ে যায়।
পরের লেসনে আমরা এই curriculum-এর সবচেয়ে গুরুত্বপূর্ণ ধারণাগত মোড়ে পৌঁছাব — কীভাবে একটা সার্কিট নিজের ফলাফল মনে রাখতে পারে, এমনকি ইনপুট বদলে গেলেও। এই একটা লাফেই আমরা combinational logic থেকে sequential logic-এ প্রবেশ করব — যেখান থেকে register, counter, state machine, আর শেষ পর্যন্ত পুরো CPU-র memory-সচেতন আচরণের জন্ম হয়।
আরও পড়ুন
- Computer Organization and Design: The Hardware/Software Interface, Chapter 3 ও Appendix — The Arithmetic Logic Unit — David A. Patterson, John L. Hennessy · MIPS ALU-র 1-bit slice ডিজাইন আর ALUControl encoding-এর প্রামাণ্য আলোচনা — এই লেসনের গঠন এর সরলীকৃত রূপ
- The RISC-V Instruction Set Manual, Volume I: Unprivileged ISA — Integer Computational Instructions — RISC-V International · বাস্তব ISA-তে ADD/SUB/AND/OR/XOR/SLT/SLL/SRL/SRA কীভাবে opcode/funct field দিয়ে এনকোড হয়
- The TTL SN74181 Arithmetic Logic Unit — Datasheet ও historical usage — Texas Instruments · ১৯৭০-এর দশকের বাস্তব ৪-বিট ALU chip — বহু minicomputer-এ ব্যবহৃত, এই লেসনের 'বাস্তব সিস্টেমে' অংশে আলোচিত