Foundationপ্রথম নীতি থেকে
LEVEL 2লেসন ৮/১৬অ্যাডভান্সড১ ঘণ্টা ১৫ মিনিট

ALU Design — Arithmetic Logic Unit-এর ভেতরের স্থাপত্য

ALU Design

ALU-তে adder/subtractor, bitwise logic unit, আর shifter — সবগুলো প্রতিটা চক্রে সমান্তরালে গণনা হয়; একটা op-select code driven output MUX ঠিক করে কোনটা 'জেতে'। এটাই curriculum-এর প্রথম প্রোগ্রামযোগ্য হার্ডওয়্যার।

এই লেসন শেষে আপনি পারবেন

  • ALU-কে 'একটা control code দিয়ে নির্বাচিত multi-function circuit' হিসেবে সংজ্ঞায়িত করে এটা কেন এই curriculum-এর প্রথম, ক্ষুদ্র পরিসরে হলেও, 'programmable' হার্ডওয়্যার তা ব্যাখ্যা করতে পারবেন
  • একটা n-bit ALU-র প্রতিটা sub-unit — adder/subtractor, bitwise logic unit, barrel shifter — আলাদাভাবে ডিজাইন করে একটা বড় output-selection MUX দিয়ে জোড়া লাগাতে পারবেন
  • একটা নির্দিষ্ট op-select code আর A/B ইনপুট দিয়ে সম্পূর্ণ ALU-র মধ্য দিয়ে হাতে trace করে প্রতিটা sub-unit-এর আউটপুট আর চূড়ান্ত ফলাফল বের করতে পারবেন
  • Zero, Carry, Overflow, Negative flag ALU-র ভেতর থেকেই কীভাবে উৎপন্ন হয় তা ব্যাখ্যা ও হাতে গণনা করতে পারবেন, আর সেই flag দিয়েই কীভাবে বিনামূল্যে signed comparison (SLT) পাওয়া যায় তা দেখাতে পারবেন
  • Barrel shifter-এর MUX-ভিত্তিক নীতি ব্যাখ্যা করতে পারবেন — shift amount-এর প্রতিটা বিট কীভাবে একটা আলাদা shift-stage নিয়ন্ত্রণ করে, log₂n ধাপে যেকোনো shift সম্পন্ন করে
  • একটা single, shared ALU ডিজাইনের area/power সুবিধা আর throughput খরচ (প্রতি cycle-এ একটাই operation) ব্যাখ্যা করে superscalar/multiple-ALU ট্রেড-অফের সাথে যুক্ত করতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

এই মডিউলের 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
  • কয়েকটা flagZero, Carry, Overflow, Negative (গত লেসনগুলোতে adder/subtractor-এর প্রসঙ্গে এই flag-গুলোর প্রথম দুইটার সাথে পরিচয় হয়েছে)

Result,flags=ALU(A,B,op-select)\text{Result}, \text{flags} = \text{ALU}(A, B, \text{op-select})

আজকের লেসনে আমরা একটা n=8-bit ALU বানাব, ৩-বিট op-select (ALUOp[2:0]) দিয়ে আটটা operation-এর মধ্যে বেছে নেওয়া যাবে:

ALUOpOperationকী গণনা করে
000ANDA AND B (bitwise)
001ORA OR B (bitwise)
010ADDA + B
011XORA XOR B (bitwise)
100NOTNOT A (শুধু A-এর উপর, unary)
101SHRA কে ডানে shift, পরিমাণ আসে B-এর নিচের বিট থেকে
110SUBA - B
111SLT1 যদি 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:

Bi=BiMB_i' = B_i \oplus M

M=0 হলে B_i' = B_i (অপরিবর্তিত), আর adder-এর initial carry-in = M = 0 — ফল A + BM=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-এ:

ANDi=AiBiORi=Ai+BiXORi=AiBiNOTi=Ai\text{AND}_i = A_i \cdot B_i \qquad \text{OR}_i = A_i + B_i \qquad \text{XOR}_i = A_i \oplus B_i \qquad \text{NOT}_i = A_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 করা)
Fig 8.1৩-স্তরের barrel shifter (n=8, ডানে logical shift) — প্রতিটা স্তর shift amount-এর একটা বিট দিয়ে নিয়ন্ত্রিত। মোট delay মাত্র log₂n = 3 MUX-layer, ৭টা ভিন্ন shift-amount আলাদা সার্কিট ছাড়াই।

এই ALU-তে shift amount আলাদা কোনো ইনপুট হিসেবে নেই — বাস্তব ISA-র মতো, B-এর নিচের \log_2 n বিটই shift amount (বাকি উঁচু বিটগুলো shift operation-এ ব্যবহৃত হয় না)। এটা কাল্পনিক নয় — বাস্তব RISC-V-এ SLL/SRL/SRA instruction-এ ঠিক এই নিয়মেই দ্বিতীয় register-এর নিচের কয়েকটা বিট shift amount নির্ধারণ করে।

Output MUX — যেখানে সবকিছু একত্রিত হয়

এখন পর্যন্ত আমাদের কাছে আছে পাঁচ-ছয়টা স্বাধীন ফলাফল, সবগুলো প্রতি চক্রে সমান্তরালে গণনা হচ্ছে:

AND_result, OR_result, ADD/SUB_result, XOR_result, NOT_result, SHR_result\text{AND\_result},\ \text{OR\_result},\ \text{ADD/SUB\_result},\ \text{XOR\_result},\ \text{NOT\_result},\ \text{SHR\_result}

(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)
                                 বৈধ)
Fig 8.2সম্পূর্ণ ALU-র top-level block diagram — সব sub-unit সমান্তরালে চলে, output MUX ALUOp অনুযায়ী একটা বেছে নেয়, flag logic Result আর adder-এর carry থেকে বেরোয়।

একটা বিকল্প (এবং বাস্তব) দৃষ্টিভঙ্গি — 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]
Fig 8.3একটা 1-bit ALU slice — এই একই ছোট circuit n বার পাশাপাশি বসিয়ে (CarryOut একটার থেকে পরেরটার CarryIn-এ জুড়ে) পুরো n-বিট ALU তৈরি হয়। এটাই বাস্তব চিপ ডিজাইনে সবচেয়ে সাধারণ পদ্ধতি।

কেন এই দৃষ্টিভঙ্গি গুরুত্বপূর্ণ: এটা দেখায় যে 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 করুন:

Zero=R7+R6++R1+R0\text{Zero} = \overline{R_7 + R_6 + \cdots + R_1 + R_0}

যুক্তি সহজ: 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 ঘটেছে:

Overflow=Cin, MSBCout, MSB\text{Overflow} = C_{\text{in, MSB}} \oplus C_{\text{out, MSB}}

Negative flag। সবচেয়ে সহজ — Result-এর MSB (sign বিট) সরাসরি কপি:

Negative=R7\text{Negative} = R_7

SLT — বিনামূল্যে signed comparison, subtract-এর flag থেকেই

এখানেই একটা সুন্দর, বাস্তব প্রকৌশল কৌশল — A \lt B (signed) নির্ণয় করতে কোনো নতুন তুলনা-circuit লাগে নাSUB operation (A - B) থেকে যে Negative আর Overflow flag পাওয়া যায়, সেগুলো দিয়েই সরাসরি উত্তর পাওয়া যায়:

SLT=NegativeOverflow\text{SLT} = \text{Negative} \oplus \text{Overflow}

সবগুলো 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 ঠিক করে —

fmax=1tcritical+tsetupf_{\max} = \frac{1}{t_{\text{critical}} + t_{\text{setup}}}

একটা 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 দুই স্তরে ভাগ হয়:

  1. প্রধান control unit শুধু opcode দেখে একটা মোটা-দাগের ALUOp সংকেত পাঠায় (যেমন “এটা একটা R-type instruction, আসল operation পরে ঠিক হবে” বনাম “এটা একটা lw/sw, নিশ্চিতভাবে ADD চাই address গণনার জন্য”)।
  2. একটা ছোট, আলাদা “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 (আজকের লেসনের সার্কিট)
Fig 8.4দুই-স্তরের control — কেন একটা R-type instruction-এর জন্য 'ALUOp' যথেষ্ট নয়, funct ফিল্ডও লাগে।

কেন এই বাড়তি স্তর দরকার: যদি প্রধান 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 fetchPC + 4 (পরের instruction-এর ঠিকানা)ADD
Memory-reference address গণনা (lw/sw)base register + offsetADD
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 ঠিক কোনটা বেছে নেয়।

ALUOpOperationগণনাResult (বাইনারি)Result (দশমিক)
000AND00001010 AND 00000011000000102
001OR00001010 OR 000000110000101111
010ADD00001010 + 000000110000110113
011XOR00001010 XOR 00000011000010019
100NOTNOT 0000101011110101−11 (signed)
101SHRshift amount = B[2:0] = 011₂ = 3; 00001010 \gg 3000000011
110SUB00001010 - 00000011000001117
111SLTA \lt B? (10 \lt 3?)000000000

ধরুন 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   0

Result = 00000111₂ = 7 — মিলে গেছে 10 - 3 = 7-এর সাথে।

  • Zero = NOR(সব Result বিট) = NOR(0,0,0,0,0,1,1,1) = 0 (নিশ্চিতভাবেই — 7 \ne 0)
  • Carry = Cout at MSB (bit 7) = 1 (কোনো borrow হয়নি, 10 \ge 3 unsigned-ও সত্য)
  • Overflow = Cin at MSB XOR Cout at MSB = 1 XOR 1 = 0 (কোনো signed overflow নেই — স্বাভাবিক, উত্তর 7 সহজেই ৮-বিট signed range-এ ধরে)
  • Negative = Result[7] = 0 (positive result)
ALUOp=110 (SUB), A=10, B=3 — সব sub-unit থেকে চূড়ান্ত ফলাফল পর্যন্ত
  1. A=00001010, B=00000011, ALUOp=110ইনপুট — তিনটাই একসাথে সব sub-unit-এ পাঠানো হয়
  2. ৮টা sub-unit সমান্তরালে গণনা করেAND=2, OR=11, ADD=13, XOR=9, NOT=−11, SHR=1, SUB=7, SLT=0 — সবগুলো একই চক্রে প্রস্তুত
  3. Output MUX ALUOp=110 পড়েঠিকানা 6 (বাইনারি 110) নির্বাচন করে — SUB-এর candidate result
  4. Result = 00000111 (7)বাকি ৭টা candidate ফলাফল এই চক্রে বাতিল হয়ে গেল
  5. 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   0

Result(অভ্যন্তরীণ subtract) = 11111001₂ = -7 (3 - 10 = -7, মিলে যাচ্ছে)।

  • Negative = Result[7] = 1
  • Overflow = Cin at MSB XOR Cout at 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-বিট সংখ্যা নয়)।

নিজে চালিয়ে দেখুন

EXPERIMENT

Digital/Logisim-এ একটা 4-bit ALU বানিয়ে AND/OR/ADD/SUB যাচাই

Digital (hneemann) বা Logisim Evolution· ২৫ মিনিট

১. চারটা 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-এর সংযোগ সঠিকভাবে কাজ করছে কি না।

EXPERIMENT

Python দিয়ে reference model — হাতের trace বনাম কোড

Python 3· ১৫ মিনিট
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-এর সাথে তুলনা করা।

নিজে বানান

BUILD IT

একটা সম্পূর্ণ 8-bit, 8-operation ALU (AND/OR/ADD/XOR/NOT/SHR/SUB/SLT)

Digital / Logisim schematic · ●●●●○
  1. পাঁচটা sub-unit আলাদাভাবে বানান ও পরীক্ষা করুন — AND array, OR array, XOR array, NOT array (এই চারটা মিলিয়ে "logic unit" নাম দিতে পারেন), আর আগের লেসনের adder/subtractor circuit পুনর্ব্যবহার করুন
  2. Fig 8.1 অনুযায়ী একটা ৩-স্তরের barrel shifter বানান (৮-বিট, শুধু logical right shift) — shift amount ইনপুট নিন B[2:0] থেকে সরাসরি
  3. একটা 8-to-1 output MUX (৮-বিট প্রশস্ত, মানে ৮টা সমান্তরাল 8-to-1 MUX) বানিয়ে সব sub-unit-এর ফলাফল data input-এ যুক্ত করুন, এই লেসনের টেবিল অনুযায়ী ক্রম মিলিয়ে
  4. Zero (NOR-reduce), Carry (adder Cout), Overflow (Cin_MSB XOR Cout_MSB), Negative (Result[7]) flag বানান
  5. এই লেসনের প্রথম উদাহরণের (A=10, B=3, প্রতিটা ALUOp) সবগুলো row নিজের সার্কিটে চালিয়ে হাতে-করা টেবিলের সাথে মেলান — একটাও অমিল থাকা উচিত না
  6. শেষে 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-এর গতি কতটা ক্ষতিগ্রস্ত হচ্ছে”-ও একটা বিবেচ্য বিষয়।

বুঝেছেন কি না দেখুন

1

A = 00000110₂ (6), B = 00000101₂ (5), ALUOp = 001 (OR)। Result কী, আর Zero flag কী হবে?

প্রয়োগ

Bitwise OR:

  00000110
OR
  00000101
= 00000111

Result = 00000111₂ = 7

Zero flag = NOR(সব Result বিট) — যেহেতু Result \ne 0 (অন্তত একটা বিট 1), Zero = 0

উত্তর: Result = 7, Zero = 0

2

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 সম্পূর্ণ শূন্য” চেনায় — কোনো ভুল-পজিটিভ বা ভুল-নেগেটিভ কেস আছে কি না যুক্তি দিয়ে দেখান।

যুক্তি

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 থেকেই সম্ভব?

ডিজাইন

নতুন কোনো তুলনা-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:

SGT=ZeroSLT\text{SGT} = \overline{\text{Zero}} \cdot \overline{\text{SLT}}

এই দ্বিতীয় পদ্ধতিতে একটা বাড়তি 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 দ্রুত হওয়া সত্ত্বেও)?

প্রয়োগ

সবচেয়ে ধীর 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-এ ব্যবহৃত, এই লেসনের 'বাস্তব সিস্টেমে' অংশে আলোচিত