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

ক্যাপস্টোন — একটি RISC-V CPU simulator বানানো

Capstone: Building an RV32I Instruction-Set Simulator — Decoder, Execute Loop, a 5-Stage Pipeline Timing Model and a Cache Model, with Testing and Extensions

Taught in Bangla, this lesson is a capstone that builds an RV32I instruction-set simulator in four milestones: a decoder for fields and immediates, an execute loop with registers, memory and function calls, a 5-stage pipeline timing model with forwarding, stalls and branch prediction, and a cache model for AMAT and total time, plus testing with golden models, common bugs, extensions, a rubric and portfolio tips.

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

  • একটি simulator প্রজেক্টকে ছোট, আলাদাভাবে যাচাই করা যায় এমন milestone-এ ভাগ করা, আর প্রতিটির জন্য জানা উত্তর ঠিক করে রাখা
  • RV32I-এর ছয় format থেকে field আর sign-extended immediate বের করা, আর disassembly দিয়ে decoder যাচাই করা
  • Register file, little-endian মেমরি আর PC নিয়ে একটি fetch–decode–execute লুপ লেখা, যা function call আর stack-সহ পুরো প্রোগ্রাম চালায়
  • ISS-এর trace থেকে forwarding, load-use stall আর branch penalty ধরে cycle ও CPI হিসাব করা, তারপর cache model দিয়ে AMAT আর মোট সময়
  • সাধারণ bug চেনা, reference simulator ও compliance test দিয়ে পরীক্ষা, সম্প্রসারণের পথ, আর প্রজেক্টটি portfolio ও ইন্টারভিউতে উপস্থাপন

১ · ক্যাপস্টোনের মানচিত্র — কী বানাবেন, কোন পর্ব কোথায় লাগবে

এই কোর্সে আপনি একটি CPU-কে টুকরো টুকরো করে দেখেছেন: instruction কীভাবে bit-এ লেখা হয়, datapath-এ কীভাবে চলে, pipeline-এ কোথায় আটকায়, আর মেমরি কেন ধীর। এই ক্যাপস্টোনে সেই সব টুকরো জুড়ে একটি কাজ করা যন্ত্র বানাবেন — software-এ। জিনিসটির নাম instruction set simulator (ISS): একটি প্রোগ্রাম, যা RISC-V-এর machine code পড়ে ঠিক সেই কাজ করে, যা একটি আসল RV32I CPU করত। তারপর তার পাশে দুটি মাপার যন্ত্র বসাবেন — একটি pipeline-এর সময় মাপবে, আরেকটি cache-এর।

একটি গার্মেন্টস কারখানার কথা ভাবুন। এক কর্মকর্তা লাইনের পাশে দাঁড়িয়ে খাতায় লেখেন: কোন শার্ট কোন টেবিলে গেল, কোন টুকরো কোথায় সেলাই হলো — কী তৈরি হলো তার নির্ভুল হিসাব। আরেকজন সেই খাতা পরে পড়ে হিসাব করেন: প্রতিটি টেবিলে কত মিনিট লাগল, কোথায় সুতার অপেক্ষায় লাইন থেমেছিল। প্রথমজন আমাদের ISS (ফল ঠিক রাখে), খাতাটি trace, আর দ্বিতীয়জন timing model. দুই কাজ আলাদা রাখলে প্রত্যেকটি সহজ থাকে, আর একই খাতা দিয়ে অনেক রকম “যদি এমন হতো” হিসাব করা যায়।

  1. M1 · Decoderword → fieldsপর্ব ২৭
  2. M2 · ISSregs, mem, PCপর্ব ২৬, ৩০, ৩২
  3. TracePC, ঠিকানাখাতা
  4. M3 · Pipelinecycles, CPIপর্ব ৩৫–৩৭
  5. M4 · CacheAMAT, মোট সময়পর্ব ৪৫–৪৭

প্রতিটি milestone কোন পর্বের ওপর দাঁড়িয়ে, নিচের টেবিলে দেখুন। কোনো জায়গায় আটকালে সেই পর্বে ফিরে যান — এই পর্বে নতুন তত্ত্ব প্রায় নেই, আছে জোড়া লাগানো আর যাচাই।

Milestoneকী বানাবেনযে পর্বগুলো লাগবেজানা উত্তর (যাচাই)
M1 · Decoderopcode, rd, funct3, rs1, rs2, funct7; I/S/B/U/J immediateপর্ব ২৭ encoding, পর্ব ০৫ two's complementস্বাধীন encoder-এর বানানো hex-এর disassembly মূল assembly-র সাথে হুবহু
M2 · Execute লুপx0–x31, PC, ৮ KiB মেমরি, ১৮টি instructionপর্ব ২৬ instruction, ২৮ addressing, ২৯ লুপ, ৩০ stack, ৩২ datapath, ০৬ endiannessa0 = 55, c[] = 1 3 6 … 55, s0 অপরিবর্তিত, ৮৪টি instruction
M3 · Pipeline modelforwarding, load-use stall, branch penalty, ২-বিট predictorপর্ব ৩৫ pipelining, ৩৬ hazard, ৩৭ branch prediction, ০৩ CPIহাতে হিসাব: প্রতি লুপ-ঘুরতি ১০ cycle; মোট ১২০
M4 · Cache modeldirect-mapped বনাম ২-way LRU, AMAT, মোট সময়পর্ব ৪৫ cache-এর মৌলিক কথা, ৪৬ associativity, ৪৭ cache performanceহাতে আঁকা set-এর অবস্থা; miss-এর সংখ্যা
এই পর্বের কোড সম্পর্কে

চারটি JavaScript প্রোগ্রাম এই পাতাতেই চালানো যায়। প্রতিটি নিজে সম্পূর্ণ — আগের block-এর ওপর নির্ভর করে না। তাই প্রতিটির শুরুতে একই প্রোগ্রামের hex word আবার লেখা আছে, আর M3 ও M4-এ M2-র ছাপানো trace হুবহু data হিসেবে বসানো। নিজের প্রজেক্টে এগুলো হবে আলাদা ফাইল (decode.js, iss.js, pipeline.js, cache.js), আর trace যাবে একটি ফাইলে।

২ · নকশার সিদ্ধান্ত — data structure, পরীক্ষার প্রোগ্রাম আর golden model

ISS-এর ভেতরে কী থাকবে

একটি CPU-র architectural state — অর্থাৎ যা প্রোগ্রাম দেখতে পায় — RV32I-এ খুব ছোট: ৩২টি ৩২-বিটের register, একটি program counter (PC), আর মেমরি। তাই data structure-ও সরল:

  • Register: Int32Array(32) — প্রতিটি ঘর ঠিক ৩২ বিট, আর JavaScript নিজেই প্রতিটি লেখায় মানকে ৩২ বিটে কেটে নেয় (যোগে overflow হলে wrap-around, পর্ব ০৫-এর মতো)। x0 আলাদা করে রক্ষা করতে হয়: লেখার সময় rd = 0 হলে লেখাই বাদ।
  • মেমরি: Uint8Array(8192) — ৮ KiB, byte-ভিত্তিক, কারণ RISC-V-এর ঠিকানা byte-এর। একটি word পড়া মানে চারটি byte জোড়া লাগানো, ছোট ঠিকানার byte সবচেয়ে কম গুরুত্বের (little-endian, পর্ব ০৬)।
  • PC: একটি সাধারণ সংখ্যা। প্রতিটি instruction-এর শেষে pc = next, যেখানে next সাধারণত pc + 4, আর শাখা বা লাফে অন্য কিছু।
  • Trace: দুটি তালিকা — চলা প্রতিটি instruction-এর PC, আর প্রতিটি load/store-এর ঠিকানা। এগুলোই পরের milestone-গুলোর input.

একটি গুরুত্বপূর্ণ সিদ্ধান্ত: ISS-এ কোনো সময়ের ধারণা নেই। এটি একটি functional simulator — প্রতিটি instruction একবারে, পুরোপুরি চলে। Pipeline বা cache-এর হিসাব এর ভেতরে না ঢুকিয়ে আলাদা model-এ রাখা হয়, যা trace পড়ে। এই পদ্ধতিকে বলে trace-driven simulation. সুবিধা: ISS ছোট আর নির্ভুল রাখা সহজ, আর একই trace দিয়ে দশ রকম cache নকশা তুলনা করা যায়। সীমাবদ্ধতা: আসল CPU ভুল অনুমানের পথে (wrong path) কিছু instruction আংশিক চালায়, যা trace-এ নেই — এ নিয়ে ভাবনার প্রশ্ন ০২-এ।

পরীক্ষার প্রোগ্রাম — উত্তর আগে থেকে জানা

Simulator-এর প্রথম পরীক্ষা এমন একটি প্রোগ্রাম, যার উত্তর হাতে বের করা যায়। আমাদের প্রোগ্রাম C-তে এরকম:

int prefix(const int *a, int *c, int n) {   // n >= 1
    int s = 0;
    do { s += *a++; *c++ = s; } while (--n);   // c[i] = a[0] + ... + a[i]
    return s;
}
// main: a[i] = i + 1 (i = 0..9) at 0x100, c[] at 0x200;  a0 = prefix(a, c, 10) -> 55

a[] = 1, 2, …, 10 হলে c[] হবে 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, আর ফেরত মান ১ + ২ + … + ১০ = ১০ × ১১ ÷ ২ = ৫৫. প্রোগ্রামে আছে লুপ, load-store, একটি function call, stack pointer দিয়ে একটি callee-saved register (s0) রক্ষা, আর ra দিয়ে ফেরা — পর্ব ২৯ আর ৩০-এর প্রায় সবকিছু। Assembly রূপটি (যেভাবে একটি assembler পেলে লিখতেন):

00  lui   sp, 2            # sp = 0x2000, top of the 8 KiB memory
04  auipc a0, 0            # \  la a0, a    : a0 = pc + 0      = 0x4
08  addi  a0, a0, 252      # /               a0 = 0x4 + 252   = 0x100
0c  addi  a1, a0, 256      # a1 = &c = 0x200
10  addi  a2, zero, 10     # n = 10
14  jal   ra, prefix       # ra = 0x18, jump to 0x1c
18  ecall                  # our convention: stop here, result in a0
prefix:                    # a0 = a, a1 = c, a2 = n  ->  a0 = sum
1c  addi  sp, sp, -4
20  sw    s0, 0(sp)        # s0 is callee-saved: keep the caller's value
24  addi  s0, zero, 0      # s = 0
loop:
28  lw    t0, 0(a0)        # t0 = *a
2c  add   s0, s0, t0       # s += t0
30  sw    s0, 0(a1)        # *c = s
34  addi  a0, a0, 4        # a++
38  addi  a1, a1, 4        # c++
3c  addi  a2, a2, -1       # --n
40  bne   a2, zero, loop   # back to 0x28 while n != 0
44  addi  a0, s0, 0        # mv a0, s0 : return value
48  lw    s0, 0(sp)        # restore s0
4c  addi  sp, sp, 4
50  jalr  zero, 0(ra)      # ret

লক্ষ করুন la (load address) একটি pseudo-instruction: assembler এটিকে auipc + addi জোড়ায় ভাঙে — PC-relative ঠিকানা (পর্ব ২৮), যাতে প্রোগ্রাম যেকোনো ঠিকানায় বসানো যায়। হাতে যাচাই: dynamic instruction সংখ্যা = main-এর ৬ (jal পর্যন্ত) + prologue ৩ + লুপ ৭ × ১০ + epilogue ৪ + ecall ১ = ৮৪.

Golden model আর স্বাধীন যাচাই

পর্ব ১৯-এর প্রজেক্টে golden model দেখেছেন: একটি সরল, নির্ভরযোগ্য reference, যার সাথে বাকি সব মেলানো হয়। এখানে দুই স্তরে এর ব্যবহার:

  1. Encoding-এর জন্য: প্রোগ্রামের hex word এই পর্বের decoder দিয়ে বানানো হয়নি; একটি আলাদা, স্বাধীন encoder script (Python-এ লেখা, field বসিয়ে বসিয়ে) দিয়ে বানানো। দুই দিক থেকে একই উত্তর এলে দুটির একসাথে একই ভুল করার সম্ভাবনা কম। নিজের প্রজেক্টে এই কাজটি করবে একটি আসল RISC-V assembler — যেমন GNU toolchain-এর as (সাধারণত -march=rv32i দিয়ে), আর objdump -d-এর disassembly আপনার decoder-এর আউটপুটের সাথে মেলাবেন।
  2. আচরণের জন্য: আপনার ISS পরে নিজেই golden model হয় — পর্ব ১৪-এর Verilog-এ একটি CPU বানালে, প্রতিটি instruction-এর পরে Verilog CPU-র register আর ISS-এর register মেলানো যায়। এটিই co-simulation. আর আপনার ISS-কে যাচাই করার জন্য আছে বাইরের reference — অংশ ৭-এ।

৩ · Milestone ১ — decoder: bit কেটে field আর immediate

Decoder-এর কাজ পর্ব ২৭-এর encoding-এর ঠিক উল্টো। RISC-V-এর সুবিধা হলো register field-গুলো সব format-এ একই জায়গায়: opcode বিট ৬–০, rd ১১–৭, funct3 ১৪–১২, rs1 ১৯–১৫, rs2 ২৪–২০, funct7 ৩১–২৫. তাই decoder প্রথমেই সব field কেটে নিতে পারে — কোন format, সেটা না জেনেই। কোন field-এর অর্থ আছে, তা পরে opcode বলে দেয়। Bit কাটার কৌশল একটাই — ডানে সরানো, তারপর mask (C-তে একই কাজ: bitwise operation):

bits(w, hi, lo) = (w >>> lo) & (2hi − lo + 1 − 1)

কঠিন অংশ immediate. পাঁচ রকম format-এ immediate-এর বিট পাঁচ রকমভাবে ছড়ানো, যাতে hardware-এ rs1, rs2, rd-এর তার কখনো সরাতে না হয় আর sign bit সবসময় বিট ৩১-এ থাকে:

FormatImmediate কোথা থেকে জোড়া হয়মাপউদাহরণ
Iimm[11:0] = w[31:20]১২ বিট, sign-extendaddi, lw, jalr
Simm[11:5] = w[31:25], imm[4:0] = w[11:7]১২ বিটsw
Bimm[12] = w[31], imm[11] = w[7], imm[10:5] = w[30:25], imm[4:1] = w[11:8], imm[0] = 0১৩ বিট (জোড় সংখ্যা)bne, beq, blt
Uimm[31:12] = w[31:12], নিচের ১২ বিট ০উপরের ২০ বিটlui, auipc
Jimm[20] = w[31], imm[19:12] = w[19:12], imm[11] = w[20], imm[10:1] = w[30:21], imm[0] = 0২১ বিট (জোড় সংখ্যা)jal

জোড়া লাগানোর পরে n বিটের মানটিকে ৩২ বিটে sign-extend করতে হয় (পর্ব ০৫): সবচেয়ে উঁচু বিট ১ হলে বাকি সব উঁচু বিট ১. JavaScript-এ এক লাইনের কৌশল: (v << (32 − n)) >> (32 − n) — বাঁয়ে সরিয়ে sign bit-কে বিট ৩১-এ তোলা, তারপর arithmetic ডান-সরানো (>>), যা sign bit কপি করে ভরে। >>> ব্যবহার করলে ০ ভরত — এটিই সবচেয়ে প্রচলিত bug-গুলোর একটি।

নিচের decoder প্রোগ্রামের ২১টি word নেয়, সব field কাটে, opcode থেকে format ঠিক করে সেই format-এর immediate বের করে, আর disassembly ছাপায়। Branch আর jal-এর লক্ষ্য ছাপা হয় পুরো ঠিকানা হিসেবে (pc + imm), যাতে assembly listing-এর সাথে সরাসরি মেলানো যায়:

// Milestone 1: RV32I decoder + disassembler. Words = the capstone program (hex from an independent encoder).
const words = `00002137 00000517 0fc50513 10050593 00a00613 008000ef 00000073 ffc10113 00812023 00000413 00052283
  00540433 0085a023 00450513 00458593 fff60613 fe0614e3 00040513 00012403 00410113 00008067`.trim().split(/\s+/).map(h => parseInt(h, 16) >>> 0);
const ABI = "zero ra sp gp tp t0 t1 t2 s0 s1 a0 a1 a2 a3 a4 a5 a6 a7".split(" "), R = n => ABI[n] || "x" + n, hex = v => "0x" + (v >>> 0).toString(16);
const bits = (w, hi, lo) => (w >>> lo) & ((1 << (hi - lo + 1)) - 1), sext = (v, n) => (v << (32 - n)) >> (32 - n); // n-bit sign-extend
const TYPE = { 0x33: "R", 0x13: "I", 0x03: "I", 0x67: "I", 0x73: "I", 0x23: "S", 0x63: "B", 0x37: "U", 0x17: "U", 0x6f: "J" };
function decode(w) {
  const f = { op: bits(w, 6, 0), rd: bits(w, 11, 7), f3: bits(w, 14, 12),
              rs1: bits(w, 19, 15), rs2: bits(w, 24, 20), f7: bits(w, 31, 25) };
  f.type = TYPE[f.op] || "?";
  f.imm = { R: 0, "?": 0,
    I: sext(bits(w, 31, 20), 12),
    S: sext((f.f7 << 5) | f.rd, 12),
    B: sext((bits(w, 31, 31) << 12) | (bits(w, 7, 7) << 11) | (bits(w, 30, 25) << 5) | (bits(w, 11, 8) << 1), 13),
    U: (w & 0xfffff000) | 0,                                     // upper 20 bits, low 12 bits = 0
    J: sext((bits(w, 31, 31) << 20) | (bits(w, 19, 12) << 12) | (bits(w, 20, 20) << 11) | (bits(w, 30, 21) << 1), 21),
  }[f.type];
  return f;
}
function disasm(w, pc) {
  const { op, rd, rs1, rs2, f3, f7, imm } = decode(w);
  if (op === 0x33) return [f7 === 0x20 ? "sub" : "add", "sll", "slt", "sltu", "xor", f7 === 0x20 ? "sra" : "srl", "or", "and"][f3] + ` ${R(rd)}, ${R(rs1)}, ${R(rs2)}`;
  if (op === 0x13) return ["addi", "slli", "slti", "sltiu", "xori", f7 === 0x20 ? "srai" : "srli", "ori", "andi"][f3] + ` ${R(rd)}, ${R(rs1)}, ${f3 % 4 === 1 ? rs2 : imm}`;
  if (op === 0x03) return ["lb", "lh", "lw", "?", "lbu", "lhu"][f3] + ` ${R(rd)}, ${imm}(${R(rs1)})`;
  if (op === 0x23) return ["sb", "sh", "sw"][f3] + ` ${R(rs2)}, ${imm}(${R(rs1)})`;
  if (op === 0x63) return ["beq", "bne", "?", "?", "blt", "bge", "bltu", "bgeu"][f3] + ` ${R(rs1)}, ${R(rs2)}, ${hex(pc + imm)}`;
  if (op === 0x37 || op === 0x17) return `${op === 0x37 ? "lui" : "auipc"} ${R(rd)}, ${hex(imm >>> 12)}`;
  if (op === 0x6f) return `jal ${R(rd)}, ${hex(pc + imm)}`;
  if (op === 0x67) return `jalr ${R(rd)}, ${imm}(${R(rs1)})`;
  return w === 0x73 ? "ecall" : "illegal";
}
console.log("pc  word      op type rd f3 rs1 rs2 f7    imm   disassembly");
words.forEach((w, k) => { const f = decode(w), p = (v, n) => String(v).padStart(n);
  console.log(`${(4 * k).toString(16).padStart(2, "0")}  ${w.toString(16).padStart(8, "0")}  ${f.op.toString(16).padStart(2, "0")}` +
    `  ${f.type} ${p(f.rd, 4)}${p(f.f3, 3)}${p(f.rs1, 4)}${p(f.rs2, 4)}${p(f.f7, 4)}${p(f.imm, 7)}   ${disasm(w, 4 * k)}`); });

আউটপুট (নির্ধারিত — প্রতিবার একই):

pc  word      op type rd f3 rs1 rs2 f7    imm   disassembly
00  00002137  37  U    2  2   0   0   0   8192   lui sp, 0x2
04  00000517  17  U   10  0   0   0   0      0   auipc a0, 0x0
08  0fc50513  13  I   10  0  10  28   7    252   addi a0, a0, 252
0c  10050593  13  I   11  0  10   0   8    256   addi a1, a0, 256
10  00a00613  13  I   12  0   0  10   0     10   addi a2, zero, 10
14  008000ef  6f  J    1  0   0   8   0      8   jal ra, 0x1c
18  00000073  73  I    0  0   0   0   0      0   ecall
1c  ffc10113  13  I    2  0   2  28 127     -4   addi sp, sp, -4
20  00812023  23  S    0  2   2   8   0      0   sw s0, 0(sp)
24  00000413  13  I    8  0   0   0   0      0   addi s0, zero, 0
28  00052283  03  I    5  2  10   0   0      0   lw t0, 0(a0)
2c  00540433  33  R    8  0   8   5   0      0   add s0, s0, t0
30  0085a023  23  S    0  2  11   8   0      0   sw s0, 0(a1)
34  00450513  13  I   10  0  10   4   0      4   addi a0, a0, 4
38  00458593  13  I   11  0  11   4   0      4   addi a1, a1, 4
3c  fff60613  13  I   12  0  12  31 127     -1   addi a2, a2, -1
40  fe0614e3  63  B    9  1  12   0 127    -24   bne a2, zero, 0x28
44  00040513  13  I   10  0   8   0   0      0   addi a0, s0, 0
48  00012403  03  I    8  2   2   0   0      0   lw s0, 0(sp)
4c  00410113  13  I    2  0   2   4   0      4   addi sp, sp, 4
50  00008067  67  I    0  0   1   0   0      0   jalr zero, 0(ra)

ডানের disassembly কলাম অংশ ২-এর assembly listing-এর সাথে ২১টি লাইনেই মেলে — অথচ hex word এসেছে একটি আলাদা encoder থেকে; এটাই প্রথম স্বাধীন যাচাই। কয়েকটি সারি খেয়াল করুন। ffc10113-এ বিট ৩১ = ১, তাই I-immediate -4; একই সারির f7 কলামে 127 — I-type-এ ওই বিটগুলো আসলে immediate-এর অংশ, “funct7” নয়। Decoder সব field কাটে, কিন্তু অর্থ ঠিক করে format. fe0614e3 (B-type)-এর imm -24, তাই লক্ষ্য 0x40 − 24 = 0x28 — লুপের শুরু। খেয়াল করুন লক্ষ্য গোনা হয় শাখা instruction-এর নিজের PC থেকে, PC + 4 থেকে নয় (MIPS-এ PC + 4 থেকে — দুই ISA গুলিয়ে ফেলা একটি সাধারণ ভুল)। 008000ef (J)-এর imm 8: 0x14 + 8 = 0x1c. lui sp, 0x2-এর imm কলামে 8192 = 0x2000 — U-type-এর মান সরাসরি register-এ যায়। আর B-type সারিতে rd কলামে 9 দেখাচ্ছে, যা আসলে imm[4:1] আর imm[11]-এর বিট — B-type-এ rd নেই।

৪ · Milestone ২ — execute লুপ: register, মেমরি আর একটি function call

ISS-এর হৃৎপিণ্ড একটি অসীম লুপ, যা পর্ব ৩২-এর single-cycle datapath-এর software রূপ: PC থেকে word আনা (fetch), field আর immediate কাটা (decode), ALU-র কাজ বা ঠিকানা হিসাব (execute), দরকার হলে মেমরি পড়া-লেখা (memory), ফল register-এ লেখা (write-back), তারপর PC বদলানো। প্রতিটি instruction-এর জন্য মূল প্রশ্ন দুটি: rd-তে কী লিখব (v), আর পরের PC কী (next). নিচে একটি instruction — লুপের প্রথম lw t0, 0(a0) — ISS-এর ভেতর দিয়ে ধাপে ধাপে:

  1. Fetchld(pc)৪ byte
  2. Decodefieldsopcode → I
  3. Executea + iIঠিকানা
  4. Memoryld(0x100)little-endian
  5. Write-backt0 ← 1PC ← 0x2c
ISS-এর অবস্থা (লুপে প্রথমবার ঢোকার মুহূর্ত)
pc
w
op · rd · f3 · rs1
a = x[rs1] (a0) · iI
ঠিকানা a + iI
mem[0x100..0x103]
v (rd-তে যাবে)
t0 (x5)
count · dataTrace-এর শেষ ঘর
  1. Fetch. pc = 0x28. ISS মেমরির 0x28–0x2b-এর চারটি byte জোড়া দিয়ে word পড়ে: 0x00052283. এর আগে ৯টি instruction চলেছে (main-এর ৬টি আর prologue-এর ৩টি), আর data trace-এ একটি ঘর আছে — prologue-এর sw s0 লিখেছে 0x1ffc-তে।
  2. Decode. বিট ৬–০ = 0000011 = 0x03 → load, I-type. rd = বিট ১১–৭ = 5 (t0), funct3 = 2 → lw (word), rs1 = 10 (a0). I-immediate = বিট ৩১–২০ = 0, sign-extend করেও 0. ISS একই সাথে register file থেকে a = x[10] = 0x100 পড়ে রাখে — datapath-এ যেমন decode-এর সময়েই register পড়া হয়।
  3. Execute. ঠিকানা = a + iI = 0x100 + 0 = 0x100 — base + offset addressing (পর্ব ২৮)। কোনো ALU ফল register-এ যাবে না; ALU এখানে শুধু ঠিকানা বানাল।
  4. Memory. ld(0x100) চারটি byte পড়ে: 01 00 00 00. Little-endian মানে সবচেয়ে ছোট ঠিকানার byte সবচেয়ে কম গুরুত্বের: 0x01 | 0x00 << 8 | 0x00 << 16 | 0x00 << 24 = 1 — a[0]. একই সাথে data trace-এ যোগ হলো R100; M4 এই ঘরটি পড়বে।
  5. Write-back আর পরের PC. rd = 5 ≠ 0, তাই x[5] = v = 1. Load শাখা নয়, তাই next = pc + 4 = 0x2c. এটি ছিল চলা দশম instruction (count = 10).
  6. পরের instruction শুরু. pc = 0x2c — add s0, s0, t0, যে এইমাত্র লেখা t0 = 1 পড়বে। ISS-এ এতে কোনো অপেক্ষা নেই — instruction একটির পর একটি পুরোপুরি চলে। কিন্তু pipeline-এ ঠিক এই জোড়াটিই একটি load-use stall ঘটাবে (অংশ ৫)।

এবার পুরো ISS. সাবসেটে আছে ১৮টি instruction: add sub and or xor slt addi slli slti lw sw beq bne blt jal jalr lui auipc, আর ecall-কে আমরা থামার সংকেত ধরেছি। প্রতিটি অজানা encoding-এ ISS চুপচাপ এগিয়ে না গিয়ে error দেয় — golden model-এর প্রথম শর্ত: যা জানি না, সেটা জানি না বলা। শেষে ছাপায় শূন্য নয় এমন register, c[], stack-এর word, আর দুটি trace:

// Milestone 2: RV32I instruction-set simulator (subset). 8 KiB little-endian memory, x0 hard-wired to 0.
const prog = `00002137 00000517 0fc50513 10050593 00a00613 008000ef 00000073 ffc10113 00812023 00000413 00052283
  00540433 0085a023 00450513 00458593 fff60613 fe0614e3 00040513 00012403 00410113 00008067`.trim().split(/\s+/);
const mem = new Uint8Array(8192), x = new Int32Array(32);
const ld = a => (mem[a] | mem[a + 1] << 8 | mem[a + 2] << 16 | mem[a + 3] << 24);       // little-endian load
const st = (a, v) => { for (let k = 0; k < 4; k++) mem[a + k] = (v >>> (8 * k)) & 255; };
prog.forEach((h, k) => st(4 * k, parseInt(h, 16)));                // program at address 0
for (let i = 0; i < 10; i++) st(0x100 + 4 * i, i + 1);              // data: a[i] = i + 1 at 0x100
const B = (w, hi, lo) => (w >>> lo) & ((1 << (hi - lo + 1)) - 1), sx = (v, n) => (v << (32 - n)) >> (32 - n);
let pc = 0, count = 0, pcTrace = "", dataTrace = []; x[8] = 7;     // x8 = s0 = 7: the caller's value must survive
for (;;) {
  const w = ld(pc), op = B(w, 6, 0), rd = B(w, 11, 7), f3 = B(w, 14, 12), f7 = B(w, 31, 25);
  const a = x[B(w, 19, 15)], b = x[B(w, 24, 20)], iI = sx(w >>> 20, 12), iS = sx((f7 << 5) | rd, 12), iU = w & 0xfffff000;
  const iB = sx(B(w, 31, 31) << 12 | B(w, 7, 7) << 11 | B(w, 30, 25) << 5 | B(w, 11, 8) << 1, 13);
  const iJ = sx(B(w, 31, 31) << 20 | B(w, 19, 12) << 12 | B(w, 20, 20) << 11 | B(w, 30, 21) << 1, 21);
  let next = pc + 4, v = null;                                       // v = value to write to rd
  count++; pcTrace += (pc / 4).toString(36);
  if (op === 0x33) v = [f7 === 0x20 ? a - b : a + b, NaN, +(a < b), NaN, a ^ b, NaN, a | b, a & b][f3];  // add sub slt xor or and
  else if (op === 0x13) v = [a + iI, a << (iI & 31), +(a < iI)][f3] ?? NaN;                          // addi slli slti
  else if (op === 0x03) { v = ld(a + iI); dataTrace.push("R" + (a + iI).toString(16)); }   // lw
  else if (op === 0x23) { st(a + iS, b); dataTrace.push("W" + (a + iS).toString(16)); }     // sw
  else if (op === 0x63) { const t = [a === b, a !== b, , , a < b][f3]; if (t === undefined) v = NaN; else if (t) next = pc + iB; }
  else if (op === 0x37) v = iU;                                       // lui
  else if (op === 0x17) v = pc + iU;                                  // auipc
  else if (op === 0x6f) { v = pc + 4; next = pc + iJ; }               // jal
  else if (op === 0x67) { v = pc + 4; next = (a + iI) & ~1; }         // jalr: clear bit 0
  else if (w === 0x73) break; else v = NaN;                          // ecall = halt; anything else = unsupported
  if (Number.isNaN(v)) throw new Error(`unsupported instruction at pc ${pc}`);
  if (v !== null && rd !== 0) x[rd] = v;                              // writes to x0 are dropped
  pc = next;
}
const names = "zero ra sp gp tp t0 t1 t2 s0 s1 a0 a1 a2".split(" "); console.log(`halted at pc 0x${pc.toString(16)} after ${count} instructions`);
console.log(x.reduce((s, v, r) => v ? s + `${names[r]}=${v < 0 || v > 255 ? "0x" + (v >>> 0).toString(16) : v} ` : s, "regs: "));
console.log("c[] =", [...Array(10)].map((_, i) => ld(0x200 + 4 * i)).join(" "), "| stack word", ld(0x1ffc));
console.log("pc trace (pc/4, base 36):", pcTrace, "\ndata trace:", dataTrace.join(" "));

আউটপুট (নির্ধারিত):

halted at pc 0x18 after 84 instructions
regs: ra=24 sp=0x2000 t0=10 s0=7 a0=55 a1=0x228
c[] = 1 3 6 10 15 21 28 36 45 55 | stack word 7
pc trace (pc/4, base 36): 012345789abcdefgabcdefgabcdefgabcdefgabcdefgabcdefgabcdefgabcdefgabcdefgabcdefghijk6
data trace: W1ffc R100 W200 R104 W204 R108 W208 R10c W20c R110 W210 R114 W214 R118 W218 R11c W21c R120 W220 R124 W224 R1ffc

প্রতিটি সংখ্যা অংশ ২-এর হাতে-হিসাবের সাথে মেলে: ৮৪টি instruction, a0=55, আর c[] ঠিক prefix sum-এর তালিকা। Function call-এর নিয়মগুলোও যাচাই হলো: sp=0x2000 — function ফেরার পরে stack pointer আগের জায়গায়; s0=7 — caller-এর রাখা মান অক্ষত, কারণ prologue এটি stack-এ রেখেছিল (stack word 7) আর epilogue ফেরত এনেছে; ra=24 = 0x18, jal-এর পরের instruction-এর ঠিকানা, আর jalr ঠিক সেখানেই ফিরেছে (halt-এর pc 0x18). t0=10 শেষ পড়া a[9], a1=0x228 = 0x200 + ১০ × ৪. PC trace-এ ০–৯, a–k অক্ষরগুলো pc/4 (base 36): abcdefg লুপের সাতটি instruction, দশবার; শেষে hijk epilogue আর 6 হলো ecall. Data trace-এ ২২টি ঘর: শুরুতে stack-এ লেখা, তারপর প্রতি ঘুরতিতে একটি R (a[i]) আর একটি W (c[i]), শেষে stack থেকে পড়া।

দুটি ছোট লাইন সবচেয়ে বেশি গুরুত্বপূর্ণ। if (v !== null && rd !== 0) x[rd] = v; — x0-তে লেখা বাদ; এটি না থাকলে jalr zero, 0(ra) (যা ret) x0-তে 0x54 লিখে দিত, আর এর পরে x0 পড়ে এমন প্রতিটি instruction (যেমন addi a2, zero, 10) ভুল ফল দিত। আর (a + iI) & ~1 — spec অনুযায়ী jalr-এর লক্ষ্য ঠিকানার বিট ০ মুছে দিতে হয়। ভুলে গেলে সাধারণ প্রোগ্রামে কিছুই হয় না (ঠিকানা এমনিতেই জোড়), তাই bug-টি দীর্ঘদিন লুকিয়ে থাকে — compliance test ঠিক এমন জিনিসই ধরে।

৫ · Milestone ৩ — trace থেকে pipeline-এর সময়

ISS বলেছে কী চলেছে: ৮৪টি instruction, কোন ক্রমে। এবার প্রশ্ন: পর্ব ৩৫-এর পাঁচ ধাপের pipeline-এ (IF, ID, EX, MEM, WB) এতে কত cycle লাগবে? আদর্শ pipeline-এ n টি instruction-এ n + ৪ cycle. বাস্তবে দুই কারণে বাড়ে, আর নিয়মগুলো পর্ব ৩৬ ও ৩৭-এর হুবহু:

  • Data hazard (পর্ব ৩৬): Forwarding থাকলে ALU-র ফলের জন্য অপেক্ষা নেই; load-use হলে ১ cycle stall. Forwarding না থাকলে consumer-কে producer-এর WB-এর cycle পর্যন্ত ID-তে থাকতে হয় (অর্ধ-cycle কৌশলে সেই cycle-এই পড়া যায়)।
  • Control hazard (পর্ব ৩৭): শাখার ফল জানা যায় EX-এ। ভুল অনুমান হলে পেছনের দুটি instruction (IF আর ID-তে) মুছে ফেলতে হয় — penalty ২ cycle. Predict-not-taken-এ প্রতিটি নেওয়া শাখা আর প্রতিটি লাফ (jal, jalr) ২ cycle দেয়।
  • ২-বিট predictor: প্রতিটি শাখার নিজের 2-bit saturating counter, শুরু 01 (weakly not-taken). Taken অনুমান করতে লক্ষ্য ঠিকানাও লাগে — সেটি দেয় একটি BTB, যেখানে একটি শাখা প্রথমবার নেওয়ার পরেই তার লক্ষ্য জমা হয়। তাই প্রথম দেখায় যেকোনো নেওয়া শাখা বা লাফ ২ cycle দেয়।

মডেলটি পর্ব ৩৬-এর simulator-এর একই কৌশল ব্যবহার করে: প্রতিটি instruction-এর জন্য D[i] = যে শেষ cycle-এ সে ID-তে থাকে। স্বাভাবিকভাবে D[i] = D[i−1] + 1; একটি producer p-এর ফল লাগলে D[i] ≥ D[p] + ১ (ALU, forwarding), D[p] + ২ (load, forwarding), বা D[p] + ৩ (forwarding নেই); আগের instruction-টি ভুল অনুমানের শাখা হলে +২. শেষ instruction-এর WB = D + ৩, সেটিই মোট cycle. নিচে লুপের প্রথম ঘুরতি, forwarding আর predict-not-taken সহ:

Cycle-এ cycle-এ পাঁচটি stage (prologue-এর পরে, a0 = 0x100, a2 = 10)
Cycle
IF
ID
EX
MEM
WB
ঘটনা
  1. Cycle ১৩. lw t0, 0(a0) ID-তে, তার পেছনে add IF-এ। সামনে prologue-এর শেষ তিনটি instruction. এই মডেলে lw-এর D = ১৩.
  2. Cycle ১৪ — hazard ধরা পড়ল. lw EX-এ, add ID-তে, আর add-এর rs2 = t0 = lw-এর rd. পর্ব ৩৬-এর hazard detection unit-এর শর্ত মিলেছে (ID/EX.MemRead = ১), তাই PC আর IF/ID আটকে রাখা হলো, আর পরের cycle-এ EX-এ যাবে একটি bubble.
  3. Cycle ১৫ — stall. add আবার ID-তে, sw আবার IF-এ, EX-এ bubble. lw MEM-এ মেমরি থেকে 1 পড়ল। একই cycle-এ addi s0 WB-তে s0 = 0 লিখল — add এটি register file থেকে পাবে।
  4. Cycle ১৬ — forwarding. add EX-এ; t0 = 1 আসে MEM/WB → EX পথে (একই cycle-এ lw WB-তে লিখছে)। ফল s0 = 0 + 1 = 1. Load-use-এর দাম মোট ১ cycle — forwarding ছাড়া হতো ২.
  5. Cycle ১৭. sw s0, 0(a1) EX-এ, যার store data s0 আসে EX/MEM → EX পথে (add এখন MEM-এ)। কোনো stall নেই। Cycle ১৮ আর ১৯ স্বাভাবিক: sw c[0]-এ 1 লেখে, addi a0, addi a1 এগোয়।
  6. Cycle ২০ — অনুমান. bne ID-তে। Predict-not-taken-এ fetch চলছে সোজা: IF-এ এসেছে 0x44-এর addi a0, s0, 0 — লুপের পরের instruction, যা এখন চলার কথা নয়।
  7. Cycle ২১ — ভুল ধরা পড়ল. bne EX-এ: a2 = 9 (EX/MEM থেকে forward করা, addi a2 এখন MEM-এ), 9 ≠ 0, তাই taken. অনুমান ভুল — IF-এর 0x48 আর ID-র 0x44 দুটিকেই মুছে ফেলা হলো, PC ← 0x28.
  8. Cycle ২২ — দুটি bubble. ঠিক পথের lw t0 এখন IF-এ। মুছে ফেলা দুটি instruction bubble হয়ে এগোবে, কিছুই লিখবে না। এই ২ cycle-ই branch penalty.
  9. Cycle ২৪ — ঘুরতি পূর্ণ. দ্বিতীয় lw EX-এ (ঠিকানা 0x104), আর আবার একই load-use. প্রথম lw-এর EX ছিল cycle ১৪, দ্বিতীয়টির ২৪ — এক ঘুরতি = ১০ cycle = ৭টি instruction + ১ stall + ২ flush. দশ ঘুরতির শেষটিতে শাখা নেওয়া হয় না, তাই সেখানে penalty নেই।

এবার হাতে পুরো হিসাব, forwarding + predict-not-taken: মোট cycle = n + ৪ + data stall + control stall. Load-use প্রতি ঘুরতিতে ১টি → ১০; নেওয়া শাখা ৯টি, আর jal ও jalr ১টি করে → ১১টি redirect × ২ = ২২. তাই ৮৪ + ৪ + ১০ + ২২ = ১২০ cycle. ২-বিট predictor-এ bne-এর counter 01 থেকে শুরু: প্রথম taken ভুল (01 → 10), পরের আটটি ঠিক, শেষের not-taken ভুল — ২টি ভুল; সাথে jal আর jalr প্রথম দেখায় BTB-তে নেই → মোট ৪টি redirect × ২ = ৮. প্রোগ্রাম:

// Milestone 3: trace-driven timing model of a 5-stage pipeline (IF ID EX MEM WB), branches resolved in EX.
// Input = the program words + the pc trace printed by Milestone 2 (pasted here as data).
const prog = `00002137 00000517 0fc50513 10050593 00a00613 008000ef 00000073 ffc10113 00812023 00000413 00052283
  00540433 0085a023 00450513 00458593 fff60613 fe0614e3 00040513 00012403 00410113 00008067`.trim().split(/\s+/);
const trace = "012345789" + "abcdefg".repeat(10) + "hijk6";       // = the 84-character pc trace of Milestone 2
const info = prog.map(h => { const w = parseInt(h, 16), op = w & 127, f = (lo, n) => (w >>> lo) & ((1 << n) - 1);
  const src = [0x37, 0x17, 0x6f].includes(op) ? [] : [0x33, 0x23, 0x63].includes(op) ? [f(15, 5), f(20, 5)] : [f(15, 5)];
  return { rd: [0x23, 0x63, 0x73].includes(op) ? 0 : f(7, 5), src: src.filter(r => r), load: op === 3,
           branch: op === 0x63, ctrl: [0x63, 0x6f, 0x67].includes(op) }; });
const dyn = [...trace].map(c => parseInt(c, 36));                    // dynamic list of static indices
function run(fwd, predictor) {
  const D = [], ctr = {}, btb = new Set(), last = {};                // D[i] = last cycle i spends in ID
  let dataSt = 0, ctrlSt = 0, redirect = false;                      // last[r] = latest producer of r
  dyn.forEach((s, i) => {
    const I = info[s], taken = I.ctrl && dyn[i + 1] !== s + 1 && i + 1 < dyn.length;
    let d = i === 0 ? 2 : D[i - 1] + 1;
    if (redirect) { d += 2; ctrlSt += 2; redirect = false; }        // 2 wrong-path instructions flushed
    const early = d;
    for (const r of I.src) if (r in last)                            // RAW: wait for the producer's value
      d = Math.max(d, D[last[r]] + (!fwd ? 3 : info[dyn[last[r]]].load ? 2 : 1));
    dataSt += d - early; D.push(d);
    if (I.rd) last[I.rd] = i;
    if (!I.ctrl) return;
    const predT = predictor === "2-bit" && btb.has(s) && (ctr[s] ?? 1) >= 2;   // counters start at 01
    if (predT !== taken) redirect = true;                            // discovered in EX: wrong path
    if (I.branch) ctr[s] = Math.max(0, Math.min(3, (ctr[s] ?? 1) + (taken ? 1 : -1)));
    else ctr[s] = 3;                                                 // jal/jalr: always taken
    if (taken) btb.add(s);                                           // BTB learns the target
  });
  const cycles = D[D.length - 1] + 3, n = dyn.length;
  console.log(`${(fwd ? "forwarding" : "no forwarding").padEnd(14)} ${predictor.padEnd(11)} cycles ${cycles}` +
    ` = ${n} + 4 + ${dataSt} data + ${ctrlSt} control stalls   CPI ${(cycles / n).toFixed(3)}`);
}
const n = dyn.length; console.log(`instructions ${n}, no hazards: ${n + 4} cycles, CPI ${((n + 4) / n).toFixed(3)}`);
run(false, "predict-NT"); run(true, "predict-NT"); run(true, "2-bit");

আউটপুট (নির্ধারিত):

instructions 84, no hazards: 88 cycles, CPI 1.048
no forwarding  predict-NT  cycles 176 = 84 + 4 + 66 data + 22 control stalls   CPI 2.095
forwarding     predict-NT  cycles 120 = 84 + 4 + 10 data + 22 control stalls   CPI 1.429
forwarding     2-bit       cycles 106 = 84 + 4 + 10 data + 8 control stalls   CPI 1.262

দ্বিতীয় আর তৃতীয় সারি হাতের হিসাবের সাথে হুবহু মেলে: ১২০ cycle, CPI ১.৪২৯ আর ১০৬ cycle, CPI ১.২৬২. Predictor ২২টি control stall-কে ৮-এ নামিয়েছে — ১২০ / ১০৬ ≈ ১.১৩ গুণ দ্রুত। Forwarding না থাকলে ৬৬টি data stall: প্রতি ঘুরতিতে তিনটি নির্ভরশীল জোড়া (lw → add, add → sw, addi a2 → bne), প্রতিটিতে ২ cycle, মানে ৬ × ১০ = ৬০, আর main ও prologue-এ আরও তিনটি জোড়া (auipc → addi a0, addi a0 → addi a1, addi sp → sw) × ২ = ৬. প্রথম সারির ৮৮ cycle হলো hazard ছাড়া আদর্শ, যেখানে CPI ১.০৪৮ — ১-এর একটু ওপরে, কারণ pipeline ভরতে ৪ cycle লাগে, আর ৮৪টি instruction-এ তা পুরোপুরি ঢাকা পড়ে না। পর্ব ০৩-এর iron law-এ সময় = IC × CPI × Tc: 1 GHz clock-এ ১২০ ns বনাম ১০৬ ns.

মডেলের সীমা সৎভাবে লিখুন। এটি একটি trace-driven মডেল, cycle-accurate simulator নয়। এখানে ধরে নেওয়া হয়েছে instruction cache সবসময় hit, data মেমরি ১ cycle-এ সাড়া দেয় (M4 এই শর্ত ভাঙবে), ভুল পথে আনা instruction-এর কোনো প্রভাব নেই, আর ecall WB-তে পৌঁছালে প্রোগ্রাম শেষ। রিপোর্টে এই ধারণাগুলো লেখাই পেশাদার অভ্যাস — পাঠক তাহলে জানেন কোন সংখ্যা কতটা বিশ্বাস করবেন।

৬ · Milestone ৪ — cache model, AMAT আর মোট সময়

M3-এ data মেমরি ছিল এক cycle-এর। এবার তার জায়গায় বসাই একটি ছোট data cache: মোট ৬৪ বাইট, ব্লক ১৬ বাইট, write-allocate (store miss হলেও ব্লক আনা হয়)। দুটি নকশা তুলনা করব: direct-mapped (৪টি line) আর ২-way set-associative (২টি set × ২ way, LRU). ঠিকানা ভাগ হয় পর্ব ৪৫-এর নিয়মে: ১৬ বাইট ব্লক → offset ৪ বিট; direct-mapped-এ ৪টি line → index ২ বিট, বাকিটা tag; ২-way-এ ২টি set → index ১ বিট।

0x104 → offset 4, index (0x104 >> 4) mod 4 = 0, tag 0x104 >> 6 = 4
0x204 → offset 4, index 0, tag 8  — একই line, ভিন্ন tag

সমস্যাটি এখানেই দেখা যাচ্ছে: a[] শুরু 0x100-তে, c[] 0x200-তে — ঠিক ২৫৬ বাইট দূরে, আর ২৫৬ cache-এর মাপ ৬৪-এর গুণিতক। তাই a[i] আর c[i] সবসময় একই direct-mapped line-এ পড়ে, আর লুপ তাদের পালা করে ছোঁয়: পড়ি a[i], লিখি c[i], পড়ি a[i+1] … প্রতিবার একজন আরেকজনকে বের করে দেয় — পর্ব ৪৬-এর conflict miss. ২-way cache-এ একই set-এ দুটি ব্লক একসাথে থাকতে পারে। নিচের stepper-এ ২-way LRU-র প্রতিটি set-এর অবস্থা, পাশে direct-mapped-এর মোট miss:

৬৪ B, ১৬ B ব্লক, ২ set × ২ way; set-এর ভেতরে বাঁয়েরটি LRU
Access
set · tag
ফল
set 0
set 1
মোট miss: ২-way · direct-mapped
  1. W1ffc — stack-এ লেখা. ব্লক 0x1ff0 (0x1ff0–0x1fff), set = (0x1ffc >> 4) mod 2 = 1, tag = 0x1ffc >> 5 = 255. Cache খালি, তাই compulsory (ঠান্ডা) miss; write-allocate বলে ব্লকটি আনা হলো।
  2. R100 — a[0]. ব্লক 0x100, set 0, tag 8. Miss; set 0-এ এখন একটি ব্লক।
  3. W200 — c[0]. ব্লক 0x200, set 0, tag 16. Miss, কিন্তু set 0-এর দ্বিতীয় way খালি — a[0]-এর ব্লক থেকে যায়। Direct-mapped-এ এখানেই 0x100-এর ব্লক বের হয়ে যেত (একই line 0).
  4. R104 থেকে W20c — ছয়টি hit. a[1..3] আর c[1..3] একই দুটি ব্লকে (১৬ বাইটে চারটি int) — spatial locality. Direct-mapped-এ এই ছয়টিই miss: প্রতিটি access আগেরটিকে বের করে দেয়, মোট miss ৯.
  5. R110 — a[4]. ব্লক 0x110, set 1, tag 8. Miss; set 1-এ stack-এর ব্লকের পাশে জায়গা পেল।
  6. W210 — c[4]. ব্লক 0x210, set 1. Set ভর্তি, তাই LRU — stack-এর ব্লক 0x1ff0 — বের হলো। শেষে stack থেকে পড়ার সময় এর দাম দিতে হবে।
  7. R114 থেকে W21c — আবার ছয়টি hit. ২-way-এ মোট miss এখনো ৫; direct-mapped-এ ১৭.
  8. R120 — a[8]. ব্লক 0x120, set 0, tag 9. Set 0-এ LRU হলো 0x100-এর ব্লক (শেষ ছোঁয়া হয়েছিল R10c-এ, তারপর W20c) — সেটি বের হলো।
  9. W220 — c[8]. ব্লক 0x220, set 0. এবার LRU 0x200-এর ব্লক, সেটি বের হলো। এগুলো conflict নয়, নতুন ব্লকের প্রথম ছোঁয়া — compulsory miss.
  10. শেষ তিনটি. R124, W224 hit; R1ffc (epilogue-এর lw s0) miss, কারণ stack-এর ব্লক access ১১-তে বের হয়েছিল। ২-way-এ মোট ৮টি miss. Direct-mapped-এ উল্টো: R1ffc-ই একমাত্র hit (line 3-এ আর কেউ আসেনি), মোট ২১টি miss.

এবার সময়। পর্ব ৪৭-এর সূত্রগুলো হুবহু: AMAT = hit time + miss rate × miss penalty; memory stall cycle = miss-এর সংখ্যা × miss penalty; আর CPI = CPIbase + (মেমরি access / instruction) × miss rate × miss penalty. এখানে CPIbase হলো M3-এর ১২০ / ৮৪ (forwarding, predict-not-taken), কারণ M3 ইতিমধ্যে প্রতিটি data access-কে ১ cycle-এর hit ধরে নিয়েছে। Hit time ১ cycle আর miss penalty ২০ cycle — শেখার জন্য ধরে নেওয়া মান, কোনো নির্দিষ্ট chip-এর নয়:

// Milestone 4: data-cache model driven by the data trace of Milestone 2 (pasted here as data).
// Cache: 64 bytes of data, 16-byte blocks, write-allocate (a store miss also fetches the block).
const trace = `W1ffc R100 W200 R104 W204 R108 W208 R10c W20c R110 W210 R114 W214 R118 W218 R11c W21c
  R120 W220 R124 W224 R1ffc`.trim().split(/\s+/).map(t => parseInt(t.slice(1), 16));
const HIT = 1, PENALTY = 20, CYCLES = 120, IC = 84;     // assumed latencies; 120 cycles = Milestone 3 (forwarding, predict-NT)
function simulate(ways, size = 64, block = 16) {
  const sets = size / block / ways, cache = [...Array(sets)].map(() => []);   // each set: tags, LRU first
  let misses = 0, seq = "";
  for (const addr of trace) {
    const blk = Math.floor(addr / block), set = blk % sets, tag = Math.floor(blk / sets), s = cache[set];
    const k = s.indexOf(tag);
    if (k >= 0) { s.splice(k, 1); seq += "H"; }                     // hit: move to the MRU end
    else { misses++; seq += "M"; if (s.length === ways) s.shift(); }   // miss: evict the LRU tag
    s.push(tag);
  }
  const mr = misses / trace.length, amat = HIT + mr * PENALTY, stall = misses * PENALTY;
  console.log(`${size} B ${ways === 1 ? "direct-mapped" : ways + "-way LRU"} (${sets} sets x ${ways} way): ${seq}`);
  console.log(`  ${trace.length} accesses, ${misses} misses, hit rate ${(100 * (1 - mr)).toFixed(1)}%,` +
    ` AMAT = ${HIT} + ${mr.toFixed(4)} x ${PENALTY} = ${amat.toFixed(2)} cycles`);
  console.log(`  total = ${CYCLES} + ${misses} x ${PENALTY} = ${CYCLES + stall} cycles,` +
    ` CPI = ${(CYCLES / IC).toFixed(3)} + ${(trace.length / IC).toFixed(4)} x ${mr.toFixed(4)} x ${PENALTY} = ${((CYCLES + stall) / IC).toFixed(3)}`);
}
for (const a of [0x104, 0x204, 0x1ffc])                     // direct-mapped 64 B: 4 offset bits, 2 index bits
  console.log(`0x${a.toString(16)}: tag ${a >> 6}, index ${(a >> 4) & 3}, offset ${a & 15}`);
simulate(1); simulate(1, 128); simulate(2);

আউটপুট (নির্ধারিত):

0x104: tag 4, index 0, offset 4
0x204: tag 8, index 0, offset 4
0x1ffc: tag 127, index 3, offset 12
64 B direct-mapped (4 sets x 1 way): MMMMMMMMMMMMMMMMMMMMMH
  22 accesses, 21 misses, hit rate 4.5%, AMAT = 1 + 0.9545 x 20 = 20.09 cycles
  total = 120 + 21 x 20 = 540 cycles, CPI = 1.429 + 0.2619 x 0.9545 x 20 = 6.429
128 B direct-mapped (8 sets x 1 way): MMMMMMMMMMMMMMMMMMMMMH
  22 accesses, 21 misses, hit rate 4.5%, AMAT = 1 + 0.9545 x 20 = 20.09 cycles
  total = 120 + 21 x 20 = 540 cycles, CPI = 1.429 + 0.2619 x 0.9545 x 20 = 6.429
64 B 2-way LRU (2 sets x 2 way): MMMHHHHHHMMHHHHHHMMHHM
  22 accesses, 8 misses, hit rate 63.6%, AMAT = 1 + 0.3636 x 20 = 8.27 cycles
  total = 120 + 8 x 20 = 280 cycles, CPI = 1.429 + 0.2619 x 0.3636 x 20 = 3.333

H/M-এর সারি stepper-এর সাথে মেলে: direct-mapped-এ শুধু শেষের stack-এর পড়াটি hit (…MH), ২-way-এ MMMHHHHHHMMHHHHHHMMHHM. Direct-mapped-এ AMAT ২০.০৯ cycle — প্রায় প্রতিটি access মেমরিতে যাচ্ছে, cache থাকা আর না থাকা প্রায় সমান; মোট সময় ১২০ + ২১ × ২০ = ৫৪০ cycle, CPI ৬.৪২৯. ২-way-এ AMAT ৮.২৭, মোট ২৮০ cycle, CPI ৩.৩৩৩ — একই মাপের cache, প্রায় দ্বিগুণ দ্রুত। দ্বিতীয় নকশাটি সবচেয়ে শিক্ষণীয়: direct-mapped cache দ্বিগুণ (১২৮ B) করেও ২১টিই miss, কারণ 0x100 আর 0x200 এখনো একই line-এ (২৫৬ হলো ১২৮-এরও গুণিতক)। সমস্যাটি ধারণক্ষমতার নয়, ঠিকানার সংঘর্ষের। CPI-র দুই রূপ মিলিয়ে দেখুন: ১.৪২৯ + (২২ / ৮৪) × ০.৯৫৪৫ × ২০ = ৬.৪২৯ = ৫৪০ / ৮৪ — পর্ব ৪৭-এর প্রমাণের মতোই, প্রতি instruction-এর stall যোগ করা আর মোট stall যোগ করে ভাগ করা একই জিনিস।

Software দিয়েও সমাধান: c[]-কে এক ব্লক সরিয়ে 0x210-তে রাখলে (array-র মাঝে ১৬ বাইট padding) a[i] আর c[i] আর একই line-এ পড়ে না। একই trace-এর ঠিকানা বদলে scratch script-এ চালালে direct-mapped ৬৪ B cache-এ miss ২১ থেকে নেমে ৮ — ২-way-এর সমান। পর্ব ৪৮-এর cache-বান্ধব প্রোগ্রাম-এ এই কৌশলের আরও উদাহরণ আছে। মডেলের ধারণাগুলোও লিখে রাখুন: instruction cache নিখুঁত, প্রতিটি miss-এ CPU পুরো ২০ cycle দাঁড়ায় (store miss-ও, কোনো write buffer নেই), আর dirty ব্লক ফেরত লেখার খরচ ধরা হয়নি।

৭ · সাধারণ bug, পরীক্ষার কৌশল আর reference simulator

Simulator লেখা শেষ হলে আসল কাজ শুরু — প্রমাণ করা যে এটি ঠিক। প্রায় সব ISS-এ প্রথম দিকে একই পাঁচ-ছয় রকম bug দেখা যায়। প্রতিটির একটি “আঙুলের ছাপ” আছে, যা চিনলে খোঁজা সহজ:

Bugলক্ষণ (আঙুলের ছাপ)ঠিক নিয়ম
Immediate sign-extend না করালাফ বা ঠিকানা ঠিক ২১২ = 4096 বা ২১৩ = 8192 বেশি; ঋণাত্মক addi-তে বিশাল সংখ্যা; লুপ কখনো শেষ হয় নাজোড়ার পরে n বিটের মানকে sign-extend; >>, >>> নয়
x0-তে লেখাret-এর পরে হঠাৎ zero শূন্য নয়; li/mv ধরনের pseudo-instruction ভুল ফল দেয়লেখার সময় rd = 0 হলে বাদ (বা প্রতিটি instruction-এর পরে x[0] = 0)
PC-relative offset ভুল ভিত্তি থেকেপ্রতিটি শাখা ৪ বাইট দূরে পড়েলক্ষ্য = শাখা বা jal-এর নিজের PC + imm; auipc-ও নিজের PC যোগ করে
B/J immediate-এর বিট ভুল ক্রমেছোট লাফ ঠিক, বড় লাফ ভুল (বিট ১১ বা ১২-এর কারণে)অংশ ৩-এর টেবিল; imm[0] সবসময় ০
Byte order উল্টো1 পড়লে আসে 16777216 (= 0x01000000)Little-endian: ছোট ঠিকানার byte কম গুরুত্বের (পর্ব ০৬)
lb/lh-এ sign-extend না করা, বা lbu-তে করা0xFF byte পড়ে 255 বনাম −1 গোলমালlb, lh sign-extend; lbu, lhu zero-extend (পর্ব ২৬)
jalr-এর বিট ০ না মোছা; rd আর rs1 একই হলে ক্রম ভুলসাধারণ প্রোগ্রামে কিছু হয় না; jalr ra, 0(ra)-তে ভুল জায়গায় লাফলক্ষ্য = (rs1 + imm) & ~1, আর rs1 পড়তে হবে rd লেখার আগে
Signed বনাম unsigned তুলনাbltu/sltu-তে ঋণাত্মক সংখ্যা ভুল দিকেJavaScript-এ unsigned তুলনার জন্য (a >>> 0) < (b >>> 0)

শেষ সারির jalr-এর ক্রম-সমস্যা আমাদের ISS-এ আপনাআপনি এড়ানো হয়েছে: a (rs1-এর মান) instruction-এর শুরুতেই পড়ে রাখা, আর rd-তে লেখা হয় সবার শেষে। এই ছোট নকশা-সিদ্ধান্ত — “আগে সব source পড়া, পরে সব destination-এ লেখা” — hardware-এর মতোই, আর একটি পুরো শ্রেণির bug মুছে দেয়।

পরীক্ষার কৌশল — স্তরে স্তরে

  1. প্রতিটি instruction-এর unit test: একটি instruction, জানা register মান, প্রত্যাশিত ফল। বিশেষ মানগুলো ভুলবেন না: 0, −1, সবচেয়ে বড় ধনাত্মক (0x7fffffff), সবচেয়ে ছোট ঋণাত্মক (0x80000000), আর immediate-এর দুই প্রান্ত (−2048, 2047).
  2. জানা উত্তরের প্রোগ্রাম: এই পর্বের prefix sum, একটি recursive factorial (পর্ব ৩০), একটি bubble sort. প্রতিটি নিজেকে যাচাই করে — শেষে ফল মিলিয়ে একটি নির্দিষ্ট register-এ ১ বা ০ রাখে।
  3. Differential testing: একই ELF ফাইল আপনার ISS আর একটি reference simulator-এ চালিয়ে প্রতিটি instruction-এর পরের register মিলিয়ে দেখা। RISC-V-এর প্রচলিত reference হলো Spike — RISC-V-এর “golden” ISA simulator হিসেবে পরিচিত, যা মূলত UC Berkeley থেকে এসেছে, আর এর একটি instruction-by-instruction log দেওয়ার option আছে। দ্রুত, পূর্ণ সিস্টেম চালানোর জন্য QEMU (একটি emulator, যা অনুবাদ করে চালায় — ভাবনার প্রশ্ন ০৪)। দুটির সঠিক command-line option তাদের নিজস্ব documentation দেখে নিন।
  4. Official test suite: RISC-V project-এর riscv-tests সংগ্রহে প্রতিটি instruction-এর ছোট assembly পরীক্ষা আছে, যেগুলো নিজেরাই pass/fail জানায়। আরও আনুষ্ঠানিক compliance test-এর জন্য আছে architecture test suite (riscv-arch-test) আর তা চালানোর framework RISCOF: প্রতিটি পরীক্ষা মেমরির একটি অংশে একটি “signature” লেখে, আর framework সেটি একটি reference model-এর signature-এর সাথে মেলায়। এই tool-গুলোর নাম আর গঠন সময়ের সাথে বদলায় — শুরু করার আগে RISC-V International-এর বর্তমান documentation দেখে নিন।
  5. Regression: একটি bug ঠিক করার পরে সেই bug-এর জন্য একটি পরীক্ষা যোগ করুন, আর প্রতিটি পরিবর্তনের পরে সব পরীক্ষা এক কমান্ডে চালান।

৮ · সম্প্রসারণ, রুব্রিক আর portfolio

যেখান থেকে প্রজেক্ট আলাদা হয়ে ওঠে

➗ M extension

mul, mulh, div, rem … opcode 0x33, funct7 = 1. Spec-এর বিশেষ নিয়মগুলো পরীক্ষা করুন: শূন্য দিয়ে ভাগে trap নেই — quotient-এর সব বিট ১, remainder = ভাজ্য; −2৩১ ÷ −1-এ quotient = −2৩১, remainder ০. mulh-এ ৬৪ বিটের গুণফল লাগে — JavaScript-এ BigInt.

পর্ব ২১–২২

🚨 CSR আর trap

ecall-কে থামার সংকেত না ধরে আসল trap: mepc ← PC, mcause ← 11 (M-mode থেকে ecall), PC ← mtvec; অবৈধ instruction-এ mcause 2; mret-এ ফেরা। তারপর একটি timer interrupt. পর্ব ৩৮-এর exception আর interrupt সরাসরি কাজে লাগবে।

পর্ব ৩৮

🧾 পূর্ণ disassembler আর ELF loader

M1-কে সব RV32I instruction আর pseudo-instruction-এর নামে (ret, mv, li) বাড়ান; GNU toolchain-এর ELF ফাইল থেকে program আর data অংশ মেমরিতে বসান — তখন C প্রোগ্রাম সরাসরি চলবে।

পর্ব ২৭, ৩১

⏱️ Cycle-by-cycle pipeline

Trace-এর বদলে প্রতিটি cycle-এ IF/ID, ID/EX, EX/MEM, MEM/WB register আপডেট করা একটি মডেল; ভুল পথের instruction সত্যিই fetch হয় আর মুছে যায়। ফল M3-এর সংখ্যার সাথে মেলানো প্রথম পরীক্ষা।

পর্ব ৩৫–৩৭, ৪০

🔌 Verilog-এ CPU

পর্ব ১৪-এর Verilog দিয়ে single-cycle, পরে pipelined RV32I core; testbench প্রতিটি retire হওয়া instruction-এর PC আর rd-এর মান ছাপায়, আর আপনার ISS-এর trace-এর সাথে লাইন ধরে মেলায় (co-simulation). পরে একটি FPGA বোর্ডে।

পর্ব ১৪, ৩২–৩৩

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

আরও গভীরে: RISC-V-এর একটি আনুষ্ঠানিক, যন্ত্রে পড়া যায় এমন specification আছে Sail নামের একটি ভাষায় — সেখান থেকে একটি simulator বানানো যায়, যাকে অনেক যাচাই-প্রবাহে reference হিসেবে ব্যবহার করা হয়। Chip শিল্পে CPU যাচাইয়ের একটি প্রচলিত পদ্ধতি হলো এলোমেলো কিন্তু বৈধ instruction-ধারা তৈরি করা (random instruction generator), RTL আর ISS-এ একসাথে চালানো, আর প্রতিটি retire-এ তুলনা করা; এর সাথে formal tool দিয়ে নির্দিষ্ট শর্ত প্রমাণ। আর গবেষণায় ব্যবহৃত বড় simulator (যেমন gem5) একই কাঠামো মানে — একটি functional অংশ, যা ঠিক ফল দেয়, আর তার ওপর বসানো বদলযোগ্য timing model.

নিজেকে মূল্যায়নের রুব্রিক

পুরো প্রজেক্ট ১০০ নম্বরে। ৭০-এর নিচে হলে আগে “ঠিকতা” আর “পরীক্ষা” সারি পূর্ণ করুন — নতুন ফিচার পরে।

মাপকাঠিনম্বরপূর্ণ নম্বরের শর্ত
ঠিকতা৩০সব RV32I base instruction (fence আর CSR বাদে চলে); riscv-tests-এর প্রাসঙ্গিক পরীক্ষা পাস; নিজের তিনটি জানা-উত্তরের প্রোগ্রাম পাস
পরীক্ষা আর পুনরাবৃত্তি২০এক কমান্ডে সব পরীক্ষা; প্রতিটি পুরোনো bug-এর জন্য একটি regression test; reference simulator-এর সাথে অন্তত একটি প্রোগ্রামের instruction-ধরে তুলনা
Timing model২০Pipeline আর cache model trace থেকে চলে; হাতে হিসাব করা অন্তত দুটি উদাহরণের সাথে হুবহু মেলে; ধারণাগুলো লেখা
নকশা আর পড়ার সহজতা১৫decode, execute, memory, timing আলাদা module; অজানা encoding-এ স্পষ্ট error; magic number-এর বদলে নামওয়ালা ধ্রুবক
মাপজোখ ও বিশ্লেষণ১০অন্তত একটি প্রশ্নের উত্তর সংখ্যা দিয়ে (যেমন “associativity না padding?”), দুই নকশার তুলনা-টেবিলসহ
ডকুমেন্টেশন৫README: কী, কীভাবে চালাবেন, নকশার সিদ্ধান্ত, সীমাবদ্ধতা, নমুনা আউটপুট

Portfolio আর ইন্টারভিউতে উপস্থাপন

একটি ছোট CPU simulator চাকরির আবেদনে বা উচ্চশিক্ষার আবেদনে খুব কাজের একটি নমুনা, কারণ এতে একসাথে দেখা যায় bit-স্তরের সতর্কতা, নকশার সিদ্ধান্ত আর যাচাইয়ের অভ্যাস। Embedded, firmware, verification বা systems ধরনের পদে, আর বিশ্ববিদ্যালয়ের থিসিস বা ল্যাব প্রজেক্টে, প্রায়ই প্রশ্ন আসে “আপনি কী বানিয়েছেন, আর কীভাবে জানলেন যে এটি ঠিক?” README-তে রাখুন: কোন ISA সাবসেট, কীভাবে চালাবেন, পরীক্ষার ফলের একটি টেবিল, আর একটি “নকশার সিদ্ধান্ত” অংশ।

ইন্টারভিউতে trade-off ব্যাখ্যা করতে প্রস্তুত থাকুন — যেমন: কেন trace-driven? (সরল, দ্রুত, একই trace-এ অনেক নকশা; কিন্তু ভুল পথ আর মেমরির সমান্তরাল অনুরোধ ধরা পড়ে না)। কেন cache দ্বিগুণ করায় লাভ হলো না? (miss-গুলো conflict, capacity নয় — ২৫৬ বাইট ব্যবধান)। ২-বিট predictor কেন ৯টি taken-এর লুপে ২টি ভুল করে? (counter 01 থেকে শুরু, আর শেষের not-taken). নিজের সংখ্যা দিয়ে উত্তর দিতে পারা — “১২০ থেকে ১০৬ cycle” — মুখস্থ সংজ্ঞার চেয়ে অনেক বেশি বিশ্বাসযোগ্য।

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

  • RISC-V-এ immediate-এর বিট এত ছড়ানো কেন? — rs1, rs2, rd সব format-এ একই জায়গায় রাখতে, আর sign bit সবসময় বিট ৩১-এ রাখতে; decoder সহজ হয়, register পড়া আর decode একসাথে শুরু করা যায়।
  • x0 কীভাবে বাস্তবায়ন করবেন? — register file-এ লেখার সময় rd = 0 হলে write enable বন্ধ (hardware-এ x0-র ঘর আসলে নেই বা সবসময় ০)।
  • Functional আর cycle-accurate simulator-এর পার্থক্য? — প্রথমটি ফল দেয়, দ্বিতীয়টি সময়ও; দ্বিতীয়টি অনেক ধীর, তাই বড় workload-এ নমুনা অংশ বেছে চালানো হয়।
  • n = ৮৪, load-use stall ১০টি, ১১টি redirect × ২ cycle, ৫ ধাপ — CPI কত? — (৮৪ + ৪ + ১০ + ২২) / ৮৪ = ১২০ / ৮৪ ≈ ১.৪৩.
  • দুটি array ঠিক cache-মাপের গুণিতক দূরে থাকলে কী হয়? — direct-mapped-এ প্রতিটি জোড়া একই line-এ, পালা করে ছুঁলে প্রায় সব miss; সমাধান associativity বা padding.

৯ · AI-এর যুগে — simulator-এ AI সহকারী, যাচাইকারী আপনি

একটি AI চ্যাটবট কয়েক সেকেন্ডে একটি “RISC-V emulator in JavaScript” লিখে দেবে, আর সেটি সম্ভবত একটি সরল প্রোগ্রাম চালাবেও। বিপদ ঠিক সেখানেই — ভুলগুলো থাকে কিনারায়, যা সরল প্রোগ্রাম ছোঁয় না। এই বিষয়ে AI-এর সাধারণ ভুল:

AI প্রায়ই যা ভুল করেকীভাবে ধরবেন
B বা J immediate-এর বিট ভুল ক্রমে জোড়ে, বা sign-extend ভুলে যায়ঋণাত্মক আর বড় offset-এর শাখা-পরীক্ষা; একটি assembler-এর hex-এর সাথে মেলান
MIPS আর RISC-V গুলিয়ে ফেলে: branch লক্ষ্য PC + 4 থেকে, delay slot, $t0 ধাঁচের নাম, ভুল opcodeRISC-V-এর unprivileged spec-এর instruction টেবিল দেখুন
x0-তে লেখা আটকায় না; jalr-এর বিট ০ মোছে না; srai আর srli আলাদা করে নাঅংশ ৮-এর চেকলিস্টের পরীক্ষাগুলো
Pipeline cycle গোনায় ভুল: load-use-এ ২ stall ধরে (forwarding থাকা সত্ত্বেও), বা branch penalty কোন stage-এ, তা গুলিয়ে ফেলেছোট প্রোগ্রামের pipeline diagram হাতে আঁকুন; সূত্র n + ৪ + stall দিয়ে মেলান
Cache-এর ঠিকানা-ভাগ (tag, index, offset) ভুল; LRU-র বদলে FIFO; AMAT-এ ns আর cycle মিশিয়ে ফেলাএকটি trace-এর set হাতে লিখুন; এককগুলো একটি একটি করে মিলিয়ে দেখুন
Spike, QEMU বা test suite-এর command-line option বানিয়ে বলেপ্রতিটি tool-এর নিজস্ব documentation আর --help

AI-কে কোড লেখার চেয়ে পরীক্ষা ভাবার কাজে লাগান — সেখানে সে বেশি কাজের, আর ফল আপনি নিজে চালিয়ে যাচাই করতে পারেন:

আমি JavaScript-এ একটি RV32I instruction set simulator লিখেছি (decoder আর execute লুপ নিচে দিলাম)। সাবসেট: add sub and or xor slt addi slli slti lw sw beq bne blt jal jalr lui auipc, আর ecall মানে থামা। ১. প্রতিটি instruction-এর জন্য অন্তত দুটি কিনারার পরীক্ষা দাও (ঋণাত্মক immediate, 0x80000000, x0 গন্তব্য, rd = rs1). প্রতিটির জন্য hex word আর প্রত্যাশিত register মান হিসাব করে দেখাও — অনুমান নয়। ২. আমার B আর J immediate জোড়ার লাইন spec-এর বিট-বিন্যাসের সাথে বিট ধরে ধরে মেলাও। ৩. MIPS-এর কোনো নিয়ম (যেমন PC + 4 থেকে branch) ভুল করে ঢুকেছে কি না দেখো। ৪. কোথাও আমার কোড অজানা encoding চুপচাপ মেনে নেয় কি না বলো। নিশ্চিত না হলে বলো; কোনো tool-এর option বানিয়ে লিখো না।

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

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

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

প্র ০১Simulator কি শুধু পড়ার যন্ত্র? SPIM, SimpleScalar, QEMU, Spike — একেকটি simulator কোন প্রয়োজন থেকে এসেছে, আর আমাদের ছোট ISS তাদের কোনটির কাছাকাছি?

Simulator-এর ইতিহাস CPU-র ইতিহাসের মতোই পুরোনো, কারণ নতুন একটি machine তৈরির আগেই তার জন্য software লেখা আর পরীক্ষা করা দরকার হয়। শিক্ষায় সবচেয়ে পরিচিত উদাহরণগুলোর একটি SPIM — James Larus-এর লেখা একটি MIPS simulator, যা Patterson–Hennessy-র বইয়ের MIPS সংস্করণের সাথে বহু বছর ধরে ব্যবহৃত হয়েছে। নামটিই মজার: MIPS উল্টো করে লেখা। পরে MARS-এর মতো আরও সহজ, graphical MIPS simulator জনপ্রিয় হয়, আর RISC-V আসার পরে তার একটি RISC-V রূপও (RARS নামে) শিক্ষায় ব্যবহৃত হতে দেখা যায়। এগুলোর লক্ষ্য একটাই — assembly শেখা, আর প্রতিটি register চোখে দেখা।

গবেষণায় প্রয়োজন ছিল ভিন্ন: একটি নতুন cache বা predictor-এর ধারণা কতটা ভালো, তা chip বানানোর আগে মাপা। ১৯৯০-এর দশকে SimpleScalar নামের একটি simulator কাঠামো (Todd Austin প্রমুখের কাজ বলে পরিচিত) বহু বছর ধরে কম্পিউটার আর্কিটেকচার গবেষণায় বহুল ব্যবহৃত ছিল। এর গঠন আমাদের পর্বের মতোই: একটি functional অংশ, তার ওপরে বিভিন্ন মাপের timing model — শুধু cache, বা পুরো out-of-order core. পরে gem5 (দুটি পুরোনো প্রজেক্ট, M5 আর GEMS, মিলে) এই ভূমিকা অনেকটা নিয়েছে।

তৃতীয় প্রয়োজন হলো গতি — অন্য ISA-র পুরো operating system চালানো। Fabrice Bellard-এর লেখা QEMU (২০০০-এর দশকের শুরুতে প্রকাশিত) এর সবচেয়ে পরিচিত উদাহরণ; এটি অতিথি machine code-কে চলার সময় host-এর code-এ অনুবাদ করে চালায়। আর চতুর্থ প্রয়োজন হলো সত্যের একটি মাপকাঠি — RISC-V-এর ক্ষেত্রে Spike, যা spec-এর কাছাকাছি থাকা একটি reference হিসেবে পরিচিত, গতির চেয়ে নির্ভুলতাকে যেখানে বেশি গুরুত্ব দেওয়া হয়।

আমাদের M2 এই চার গোত্রের মধ্যে Spike আর SPIM-এর কাছাকাছি: একটি interpreter, যা প্রতিটি instruction decode করে চালায়, গতির কোনো কৌশল ছাড়া। আর M3 ও M4 SimpleScalar-ধাঁচের — functional অংশের trace-এর ওপর timing.

মূল উপলব্ধি: “Simulator” একটি শব্দ, কিন্তু প্রয়োজন চারটি — শেখা, মাপা, দ্রুত চালানো আর সত্য যাচাই। কোন প্রয়োজনে বানাচ্ছেন, তা আগে ঠিক করলে নকশার প্রতিটি সিদ্ধান্ত (interpreter না অনুবাদ, trace না cycle-by-cycle) সহজ হয়ে যায়।

প্র ০২আমাদের pipeline model trace-driven. কোন কোন বাস্তব ঘটনা এটি ধরতে পারে না, আর গবেষকেরা যখন cycle-accurate simulator ব্যবহার করেন, তখন তার ধীরগতির সমস্যা কীভাবে সামলান?

Trace-driven model-এর মূল ধারণা হলো: যা ঘটেছে তার তালিকা আগে, সময়ের হিসাব পরে। এটি তখনই নির্ভুল, যখন সময়ের হিসাব তালিকাটিকে বদলায় না। সরল in-order pipeline-এ এটি প্রায় সত্য — instruction-এর ক্রম একই, শুধু মাঝে মাঝে bubble. কিন্তু তিনটি জায়গায় এই ধারণা ভাঙে।

প্রথমত, ভুল পথ. আসল CPU ভুল অনুমানের পরে কয়েকটি instruction fetch করে, আর গভীর বা out-of-order core-এ (পর্ব ৪০) সেগুলো আংশিক চলেও — load পাঠায়, cache-এ ব্লক আনে, predictor-এর অবস্থা বদলায়। ISS-এর trace-এ এগুলো নেই, কারণ ISS কখনো ভুল পথে যায় না। পর্ব ৪১-এর Spectre-ধরনের সমস্যা ঠিক এই অদৃশ্য কাজ থেকেই আসে — trace-driven model তা দেখতে পায় না। দ্বিতীয়ত, সময়-নির্ভর আচরণ: একাধিক core-এ কোন thread আগে lock পাবে, বা একটি interrupt কোন instruction-এর পরে আসবে, তা সময়ের ওপর নির্ভর করে — সময় বদলালে trace-ই বদলে যায়। তৃতীয়ত, মেমরির সমান্তরালতা: out-of-order core একসাথে কয়েকটি miss চালাতে পারে, ফলে “প্রতিটি miss-এ পুরো penalty” ধারণা খুব হতাশাবাদী হয়ে যায় (পর্ব ৪৭-এর memory-level parallelism).

এসব ধরতে লাগে execution-driven, cycle-accurate simulator, যেখানে timing আর functional অংশ একসাথে চলে। দাম গতিতে — এমন simulator আসল hardware-এর চেয়ে প্রায়ই হাজার থেকে লক্ষ গুণ ধীর বলে ধরা হয় (নকশা আর বিস্তারিততার ওপর নির্ভর করে)। এক সেকেন্ডের প্রোগ্রাম simulate করতে দিন লেগে যেতে পারে।

সমাধানের একটি বিখ্যাত ধারা হলো নমুনা নেওয়া। ২০০২ সালের দিকে প্রকাশিত SimPoint কৌশল প্রোগ্রামকে টুকরো করে প্রতিটি টুকরোর “আচরণের ছাপ” (কোন basic block কতবার চলে) মেলায়, আর একই রকম টুকরোগুলোর একটি প্রতিনিধিই বিস্তারিত simulate করে। SMARTS-এর মতো পদ্ধতি পরিসংখ্যানের নমুনা-তত্ত্ব ব্যবহার করে বলে দেয় ফলের ভুল কত শতাংশের মধ্যে। বাকি অংশ দ্রুত, functional মোডে পার হয় — আমাদের ISS-এর মতো।

মূল উপলব্ধি: প্রতিটি simulator একটি চুক্তি — কোন ঘটনা মডেল করব, কোনটা বাদ দেব। ভালো প্রকৌশলী সেই চুক্তি লিখে রাখেন, আর ফল ব্যাখ্যা করার সময় মনে রাখেন কোন বাদ দেওয়া ঘটনা সংখ্যাটিকে কোন দিকে সরাতে পারে।

প্র ০৩১৯৯৪ সালের Pentium FDIV bug-এর মতো ঘটনা দেখায় যে একটি CPU-র ভুল কত দামি। আজ CPU নকশাকারীরা “golden model” আর compliance test দিয়ে কীভাবে নিশ্চিত হন যে hardware spec মানছে?

১৯৯৪ সালে একজন গণিতের অধ্যাপক, Thomas Nicely, লক্ষ করেন যে Intel-এর Pentium processor কিছু নির্দিষ্ট সংখ্যার floating-point ভাগে সামান্য ভুল ফল দিচ্ছে। কারণটি পরে জানা যায়: ভাগের hardware যে lookup table ব্যবহার করত (পর্ব ২২-এর SRT division), তার কয়েকটি ঘর ফাঁকা ছিল। ভুলটি বিরল ছিল, তবু জনমতের চাপে Intel chip বদলে দেওয়ার প্রস্তাব দেয়, আর প্রায়ই উদ্ধৃত হিসাবে এর জন্য প্রায় ৪৭৫ মিলিয়ন ডলারের খরচ দেখানো হয়েছিল। শিক্ষাটি শিল্পে গভীর ছাপ ফেলে: chip-এ ভুল software-এর মতো একটি আপডেটে সারানো যায় না।

আজকের যাচাইয়ের কেন্দ্রে থাকে একটি golden model — প্রায়ই একটি ISS — যাকে spec-এর সবচেয়ে নির্ভরযোগ্য রূপ ধরা হয়। নকশার RTL আর golden model একই প্রোগ্রাম চালায়, আর প্রতিটি instruction “retire” হওয়ার সাথে সাথে PC, লেখা register আর মেমরির পরিবর্তন মেলানো হয়। প্রথম অমিলেই simulation থামে — হাজার লাইনের log খুঁজতে হয় না। এই পর্বের co-simulation ঠিক এটাই, ছোট মাপে।

কিন্তু golden model নিজে কি ঠিক? এখানে আসে তিনটি স্তর। এক, compliance বা architecture test: ISA-র মালিক সংগঠন (RISC-V-এর ক্ষেত্রে RISC-V International) একটি পরীক্ষা-সংগ্রহ রাখে, যা দিয়ে যেকোনো বাস্তবায়ন — simulator বা chip — নির্দিষ্ট extension ঠিকভাবে মানে কি না দেখানো যায়। দুই, আনুষ্ঠানিক spec: RISC-V-এর Sail model-এর মতো যন্ত্রে পড়া যায় এমন বর্ণনা থেকে reference simulator বানানো, যাতে “spec বলছে” আর “simulator করছে”-এর মাঝে মানুষের ভুল-ব্যাখ্যার সুযোগ কমে। তিন, এলোমেলো পরীক্ষা: বৈধ কিন্তু অপ্রত্যাশিত instruction-ধারা তৈরি করে লাখ লাখ প্রোগ্রাম চালানো, কারণ মানুষ যে কিনারা ভাবেন না, এলোমেলো generator সেখানে পৌঁছে যায়।

FDIV-এর মতো ভুল ধরতে শেষ পর্যন্ত গণিতও লাগে — floating-point unit-এর মতো অংশের জন্য formal verification দিয়ে প্রমাণ করা হয় যে সব সম্ভাব্য input-এ ফল ঠিক। এক টেবিলের পাঁচটি ফাঁকা ঘর এলোমেলো পরীক্ষায় ধরা কঠিন, প্রমাণে নয়।

মূল উপলব্ধি: যাচাই কোনো একক পদ্ধতি নয়, একটি স্তরবিন্যাস — জানা-উত্তরের পরীক্ষা, golden model-এর সাথে প্রতিটি instruction-এ তুলনা, আনুষ্ঠানিক test suite, এলোমেলো পরীক্ষা আর গাণিতিক প্রমাণ। আপনার ক্যাপস্টোন প্রথম তিনটি স্তর ছোট মাপে করতে পারে।

প্র ০৪আমাদের ISS প্রতিটি instruction আলাদা করে decode করে চালায়। QEMU বা Apple-এর Rosetta 2-এর মতো ব্যবস্থা অন্য ISA-র প্রোগ্রাম প্রায় স্বাভাবিক গতিতে কীভাবে চালায়?

আমাদের M2-র লুপে প্রতিটি অতিথি instruction-এর জন্য host CPU কয়েক ডজন কাজ করে: word পড়া, অনেকগুলো shift আর mask, কয়েকটি তুলনা, তারপর আসল কাজ। এই ধরনের interpreter স্বভাবতই আসল hardware-এর চেয়ে অনেক গুণ ধীর। শেখার জন্য সমস্যা নেই, কিন্তু একটি পুরো operating system বা একটি গেম এভাবে চালানো অসম্ভব রকম ধীর।

প্রথম কৌশল সহজ: একই instruction বারবার decode না করা। লুপের সাতটি instruction আমাদের ISS দশবার decode করেছে; decode-এর ফল একটি টেবিলে রেখে দিলে পরের নয়বার শুধু চালানো। দ্বিতীয় আর বড় কৌশল হলো dynamic binary translation: অতিথি code-এর একটি টুকরো (সাধারণত একটি basic block, শাখা পর্যন্ত) প্রথমবার দেখলে পুরোটা host CPU-র machine code-এ অনুবাদ করে একটি cache-এ রাখা, আর পরের বার সরাসরি সেই অনুবাদ চালানো। QEMU-র অনুবাদ-ব্যবস্থার নাম TCG (Tiny Code Generator). অনুবাদ করা টুকরোগুলোকে পরস্পরের সাথে সরাসরি জুড়ে দেওয়া যায় (block chaining), ফলে লুপ পুরোটাই অনুবাদিত code-এ ঘোরে।

২০২০ সালে Apple যখন Mac-এ Intel-এর x86-64 থেকে নিজেদের ARM-ভিত্তিক chip-এ যায়, পুরোনো প্রোগ্রাম চালাতে Rosetta 2 ব্যবহার করে। এটি সাধারণত প্রোগ্রাম install বা প্রথমবার চালানোর সময় অনেকটা আগেভাগে অনুবাদ করে রাখে, আর দরকার হলে চলার সময়ও অনুবাদ করে। দুই ISA-র মেমরি-ক্রমের নিয়ম আলাদা (x86-এর নিয়ম কড়া, ARM-এর শিথিল — পর্ব ৫৩-এর memory consistency) — Apple-এর chip-এ x86-ধাঁচের কড়া ক্রম চালু করার একটি hardware ব্যবস্থা আছে বলে ব্যাপকভাবে জানা যায়, যা অনুবাদকে সহজ আর দ্রুত করে। অর্থাৎ ভালো emulation-এর জন্য কখনো hardware নিজেই সাহায্য করে।

একই ধারণা আরও জায়গায়: Android-এর emulator, ভিন্ন ISA-র Linux প্রোগ্রাম চালানো (QEMU-র user-mode), পুরোনো গেম-কনসোলের emulator, আর operating system-এর virtualization (অপারেটিং সিস্টেম কোর্সের virtual machine পর্ব)। RISC-V-এর জন্য লেখা xv6-এর মতো শিক্ষামূলক OS-ও সাধারণত QEMU-তেই চালানো হয় (xv6 পরিচিতি)।

মূল উপলব্ধি: Interpreter আর translator-এর ফল এক, পার্থক্য কাজের সময়ে — প্রতিবার নতুন করে বোঝা, নাকি একবার বুঝে লিখে রাখা। Simulator-এর গতি মূলত এই সিদ্ধান্ত থেকে আসে, আর আপনার ISS-কে ধাপে ধাপে দ্রুত করা নিজেই একটি চমৎকার দ্বিতীয় প্রজেক্ট।

প্র ০৫RISC-V-এর decoder লেখা x86-এর তুলনায় এত সহজ কেন? ISA নকশাকারীদের কোন সিদ্ধান্ত আমাদের M1-কে ৩৫ লাইনে নামিয়ে আনল, আর তার দাম কোথায় দিতে হয়েছে?

RISC-V-এর কাজ শুরু হয় ২০১০ সালে UC Berkeley-তে, Krste Asanović-এর দলে (Andrew Waterman, Yunsup Lee প্রমুখ) আর David Patterson-এর সহযোগিতায় — প্রথমে গবেষণা আর পড়ানোর জন্য একটি পরিচ্ছন্ন, উন্মুক্ত ISA হিসেবে। নকশাকারীরা আগের কয়েক দশকের ISA-র অভিজ্ঞতা থেকে শিখেছিলেন, আর তাঁদের কয়েকটি সিদ্ধান্ত সরাসরি decoder-কে সরল করে।

এক, base ISA-তে সব instruction ৩২ বিট — কোথায় একটি instruction শেষ, খুঁজতে হয় না। দুই, rs1, rs2 আর rd সব format-এ একই বিটে, তাই decode শেষ হওয়ার আগেই register file পড়া শুরু করা যায়। তিন, sign bit সবসময় বিট ৩১-এ, তাই sign extension-এর তার format-নির্ভর নয়। immediate-এর বিট ছড়ানো দেখে মানুষ বিভ্রান্ত হয় (অংশ ৩-এর টেবিল), কিন্তু hardware-এর কাছে এটি সস্তা — প্রতিটি immediate বিট মাত্র কয়েকটি সম্ভাব্য উৎস থেকে আসে। চার, MIPS-এর মতো branch delay slot নেই (পর্ব ৩৭)।

x86-এর অবস্থা ভিন্ন, ইতিহাসের কারণে। ১৯৭৮ সালের 8086 থেকে শুরু করে প্রতিটি প্রজন্ম পুরোনো encoding অক্ষত রেখে নতুন জিনিস যোগ করেছে — prefix byte, REX, VEX, EVEX, ModRM, SIB (পর্ব ২৭)। ফলে একটি instruction ১ থেকে ১৫ byte-এর যেকোনো দৈর্ঘ্যের, আর দৈর্ঘ্য জানতেই কয়েকটি byte পড়তে হয়। আধুনিক x86 core-এ তাই decode একটি বড়, শক্তিখেকো অংশ, আর অনেক নকশায় একবার decode করা micro-op একটি বিশেষ cache-এ রাখা হয়, যাতে বারবার decode না করতে হয়।

দাম কোথায়? প্রথমত code density: ৩২ বিটের নির্দিষ্ট দৈর্ঘ্যে প্রোগ্রাম বড় হয়। RISC-V এটি সামলায় ঐচ্ছিক C extension-এ (১৬ বিটের compressed instruction) — তখন ISS-কে ১৬ আর ৩২ বিট দুটিই বুঝতে হয়, আর ৩২ বিটের instruction ৪-এর গুণিতক ঠিকানায় না-ও থাকতে পারে। দ্বিতীয়ত, কিছু কাজে বেশি instruction লাগে — যেমন একটি ৩২-বিট ধ্রুবকে lui + addi, যেখানে x86-এ একটি। নকশাকারীদের যুক্তি ছিল, সরল instruction দ্রুত আর কম শক্তিতে চালানো যায়, আর জনপ্রিয় জোড়াগুলো hardware চাইলে জুড়ে (macro-op fusion) একটি হিসেবে চালাতে পারে।

মূল উপলব্ধি: ISA হলো hardware আর software-এর চুক্তি (পর্ব ২৫), আর চুক্তির ছোট ছোট সিদ্ধান্ত — বিট কোথায় বসবে — প্রজন্মের পর প্রজন্ম decoder-এর খরচ ঠিক করে দেয়। আপনার ৩৫ লাইনের decoder সেই পরিচ্ছন্ন চুক্তির সরাসরি ফল।

অনুশীলন

প্রতিটি অনুশীলন আপনার simulator-এর একটি সম্প্রসারণ বা পরীক্ষা। আগে নিজে সমাধান করুন, তারপর উত্তর মেলান; উপরের প্রোগ্রামগুলোতে সংখ্যা বা লাইন বদলে নিজের উত্তর চালিয়েও দেখতে পারেন।

  1. Decoder-এ নতুন instruction (কোড লিখুন): M extension-এর mul t2, t0, t1-এর word হলো 026283b3 (opcode 0x33, funct3 0, funct7 1). (ক) এই word M1-এর decoder-এ দিলে কী ছাপা হবে, আর কেন সেটি বিপজ্জনক? (খ) Decoder বাড়িয়ে লিখুন যাতে এটি mul t2, t0, t1 ছাপে, M extension-এর বাকি সাতটিও চেনে, আর opcode 0x33-এ অজানা funct7-এ illegal বলে।

    (ক) ছাপা হবে add t2, t0, t1. Decoder শুধু funct7 = 0x20 কি না দেখে (sub/sra) — 1 হলে সে ধরে নেয় add. কোনো error নেই, তাই ISS-এ mul নীরবে যোগ হয়ে যেত। এটিই সবচেয়ে বিপজ্জনক bug — ভুল ফল, কোনো সতর্কবার্তা ছাড়া।

    (খ) disasm-এ R-type লাইনের আগে দুটি লাইন:

    if (op === 0x33 && f7 === 1) return ["mul", "mulh", "mulhsu", "mulhu", "div", "divu", "rem", "remu"][f3] + ` ${R(rd)}, ${R(rs1)}, ${R(rs2)}`;
    if (op === 0x33 && f7 !== 0 && f7 !== 0x20) return "illegal";

    যাচাই (scratch-এ চালানো): 026283b3 → mul t2, t0, t1, 0262c3b3 → div t2, t0, t1, 406283b3 → এখনো sub t2, t0, t1, আর funct7 = 5-এর 0a6283b3 → illegal. (আরও কড়া হলে funct7 = 0x20 শুধু funct3 0 আর 5-এ বৈধ — বাকিগুলোতেও illegal.) ISS-এ চালাতে mul-এর জন্য Math.imul(a, b) ব্যবহার করুন — সাধারণ a * b বড় সংখ্যায় ৫৩ বিটের double-এর নির্ভুলতা হারায়।

  2. Output অনুমান করুন (trace by hand): M2-র প্রোগ্রামে শুধু পঞ্চম word 00a00613 (addi a2, zero, 10) বদলে 00400613 (addi a2, zero, 4) করা হলো। চালানোর আগে লিখুন: কতটি instruction চলবে, শূন্য নয় এমন register-গুলোর মান, c[]-এর দশটি ঘর, data trace-এ কতটি ঘর। তারপর M3-র forwarding + predict-not-taken মডেলে মোট cycle আর CPI কত?

    Instruction: ৬ + ৩ + ৭ × ৪ + ৪ + ১ = ৪২. Register: ra = 24 (0x18, বদলায়নি), sp = 0x2000, t0 = 4 (শেষ পড়া a[3]), s0 = 7 (ফিরিয়ে আনা), a0 = 1 + 2 + 3 + 4 = 10, a1 = 0x200 + ৪ × ৪ = 0x210. c[] = 1 3 6 10 0 0 0 0 0 0. Data trace: ১ + ২ × ৪ + ১ = ১০টি ঘর।

    halted at pc 0x18 after 42 instructions
    regs: ra=24 sp=0x2000 t0=4 s0=7 a0=10 a1=0x210
    c[] = 1 3 6 10 0 0 0 0 0 0 | stack word 7

    (পরিবর্তিত প্রোগ্রাম চালিয়ে পাওয়া আসল আউটপুট — অনুমানের সাথে হুবহু।) Pipeline: load-use ৪টি; redirect = jal ১ + নেওয়া শাখা ৩ + jalr ১ = ৫টি × ২ = ১০. মোট = ৪২ + ৪ + ৪ + ১০ = ৬০ cycle, CPI = ৬০ / ৪২ ≈ ১.৪২৯ — M3-এর trace-এ repeat(4) দিয়ে চালালেও তাই আসে। মজার ব্যাপার, n = ১০-এর CPI-ও ১.৪২৯ (১২০ / ৮৪): দুটিই ঠিক ১০ / ৭. কারণ k ঘুরতির জন্য মোট cycle = ১০k + ২০ আর instruction = ৭k + ১৪ (k = ১০-এ ১২০ ও ৮৪, k = ৪-এ ৬০ ও ৪২), আর (১০k + ২০) / (৭k + ১৪) সবসময় ১০ / ৭.

  3. ভিন্ন branch penalty-তে CPI (numerical): ধরুন একটি ভিন্ন নকশায় শাখা ও লাফের ফল জানা যায় MEM stage-এ, তাই প্রতিটি redirect-এর দাম ৩ cycle (বাকি সব নিয়ম M3-এর মতো, forwarding আছে)। ৮৪টি instruction-এর প্রোগ্রামে (১০টি load-use; predict-not-taken-এ ১১টি redirect, ২-বিট predictor-এ ৪টি) দুই কৌশলে মোট cycle আর CPI বের করুন। Predictor-এর লাভ penalty ২-এর তুলনায় বাড়ল না কমল?

    Predict-not-taken: ৮৪ + ৪ + ১০ + ১১ × ৩ = ৮৪ + ৪ + ১০ + ৩৩ = ১৩১ cycle, CPI = ১৩১ / ৮৪ ≈ ১.৫৬০. ২-বিট: ৮৪ + ৪ + ১০ + ৪ × ৩ = ১১০ cycle, CPI ≈ ১.৩১০.

    M3-এর মডেলে d += 2; ctrlSt += 2;-কে 3 করে চালালে ঠিক এই সংখ্যাই আসে (forwarding নেই এমন সারিতে ১৮৭). পর্ব ৩৭-এর সূত্রেও মেলে: CPI = ১ + (৪ + ১০ + redirect × P) / ৮৪. লাভ: penalty ২-এ ১২০ / ১০৬ ≈ ১.১৩২ গুণ, penalty ৩-এ ১৩১ / ১১০ ≈ ১.১৯১ গুণ — বাড়ল। Penalty যত বড়, প্রতিটি এড়ানো ভুলের মূল্য তত বেশি; গভীর pipeline-এ ভালো predictor এজন্যই অপরিহার্য।

  4. Bug খুঁজুন (debug): একজন শিক্ষার্থী M2-র ISS নিজে লিখেছেন। তাঁর B-immediate-এর লাইন: const iB = B(w, 31, 31) << 12 | B(w, 7, 7) << 11 | B(w, 30, 25) << 5 | B(w, 11, 8) << 1; বাকি সব হুবহু M2-র মতো। চালালে আসে: Error: unsupported instruction at pc 8232, আর তখন পর্যন্ত গোনা ১৭টি instruction. (ক) 8232 সংখ্যাটি থেকে bug-টি কীভাবে চিনবেন? (খ) ১৭ কেন? (গ) ঠিক করুন, আর এমন bug যাতে ভবিষ্যতে আরও আগে ধরা পড়ে, তার জন্য ISS-এ একটি পরিবর্তন প্রস্তাব করুন।

    (ক) 8232 = 0x2028. প্রত্যাশিত লক্ষ্য 0x28 (লুপের শুরু); পার্থক্য 0x2000 = ৮১৯২ = ২১৩ — ঠিক B-immediate-এর ১৩ বিটের মাপ। এটি sign-extension ভুলে যাওয়ার আঙুলের ছাপ: bne-এর imm −24-এর ১৩-বিট রূপ 0x1fe8 = 8168, sign-extend না করলে লক্ষ্য 0x40 + 8168 = 8232. লাইনে sx(…, 13) নেই।

    (খ) main-এর ৬টি + prologue ৩টি + লুপের ৭টি = ১৬টি instruction ঠিকভাবে চলে; প্রথম bne (১৬তম) ভুল জায়গায় লাফায়। ১৭তম গোনা হয় 8232-এ fetch-এর সময়, আর সেখানে আটকে যায়।

    (গ) ঠিক লাইন: const iB = sx(B(w, 31, 31) << 12 | … | B(w, 11, 8) << 1, 13);. খেয়াল করুন, error এসেছে প্রায় ভাগ্যক্রমে: 8232 মেমরির (৮১৯২ byte) বাইরে, আর JavaScript-এর Uint8Array সীমার বাইরে পড়লে undefined দেয়, যা bit-অপারেশনে 0 হয়ে যায় — word 0, তাই “unsupported”. সীমার বাইরে লেখা আরও খারাপ: চুপচাপ উপেক্ষিত হয়। প্রস্তাব: ld আর st-এ ঠিকানা পরীক্ষা (a < 0 || a + 3 >= mem.length হলে error), আর word-এর জন্য ঠিকানা ৪-এর গুণিতক কি না পরীক্ষা (misaligned). আসল CPU-তে এগুলোই access fault আর misaligned exception (পর্ব ৩৮)। তাহলে bug-টি “pc 8232-এ fetch fault” হিসেবে প্রথম ভুল লাফেই স্পষ্ট ধরা পড়ত।

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

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

ক্রমএকটি RISC-V simulator প্রজেক্টের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. জানা উত্তরের পরীক্ষা-প্রোগ্রাম লেখা আর স্বাধীন encoder বা assembler দিয়ে তার hex বানানো
  2. Decoder লিখে disassembly মূল assembly-র সাথে মেলানো
  3. Execute লুপ লিখে ফল যাচাই করা আর PC ও ঠিকানার trace ছাপানো
  4. Trace থেকে pipeline-এর cycle আর CPI হিসাব করা
  5. Data trace থেকে cache-এর miss, AMAT আর মোট সময় বের করা

ঠিকানা 0x40-এ একটি RV32I B-type শাখা আছে, যার জোড়া লাগানো ১৩-বিট immediate হলো 0x1fe8 (sign bit ১)। শাখা নেওয়া হলে পরের PC কত?

  • 0x28
  • 0x2c
  • 0x2028
  • 0x58

RV32I-এ jalr x0, 0(x1) চলছে, যেখানে x1 = 0x1d আর instruction-টির নিজের ঠিকানা 0x50. এর পরে PC আর x0-এর মান কী?

  • PC = 0x1d, x0 = 0
  • PC = 0x1c, x0 = 0x54
  • PC = 0x1c, x0 = 0
  • PC = 0x54, x0 = 0

একটি little-endian RISC-V মেমরিতে ঠিকানা 0x100, 0x101, 0x102, 0x103-এ যথাক্রমে byte fc, ff, ff, ff আছে। lw t0, 0(a0) (a0 = 0x100) চালালে t0-তে signed দশমিক মান কত?

  • −50331649
  • −4
  • 252
  • 4294967292

একটি ৫ ধাপের pipeline-এ (forwarding আছে, load-use-এ ১ cycle stall, শাখা EX-এ, প্রতিটি ভুল অনুমানে ২ cycle) একটি প্রোগ্রামে ৮৪টি instruction চলে, যার মধ্যে ১০টি load-use জোড়া আর ১১টি ভুল অনুমান। মোট cycle আর CPI কত?

  • ৮৮ cycle, CPI ≈ ১.০৫
  • ১১৬ cycle, CPI ≈ ১.৩৮
  • ১০৯ cycle, CPI ≈ ১.৩০
  • ১২০ cycle, CPI ≈ ১.৪৩

৬৪ বাইটের direct-mapped data cache (১৬ বাইট ব্লক)। একটি লুপ পালা করে a[i] (a শুরু 0x100) পড়ে আর c[i] (c শুরু 0x200) লেখে, আর প্রায় সব access miss হয়। কোন পরিবর্তন miss সবচেয়ে বেশি কমাবে?

  • Cache দ্বিগুণ করে ১২৮ বাইট direct-mapped
  • একই ৬৪ বাইটকে ২-way set-associative (LRU) করা
  • ব্লক ৮ বাইটে নামানো
  • Write-back-এর বদলে write-through

একটি প্রোগ্রামে ৮৪টি instruction আর ২২টি data access; cache ছাড়া (সব access ১ cycle-এর ধরে) pipeline-এ লাগে ১২০ cycle. Cache-এ ৮টি miss, hit time ১ cycle, miss penalty ২০ cycle, প্রতিটি miss-এ CPU পুরো penalty দাঁড়ায়। AMAT আর মোট cycle কত?

  • AMAT ≈ ৮.২৭ cycle, মোট ২৮০ cycle
  • AMAT ≈ ৭.২৭ cycle, মোট ২৮০ cycle
  • AMAT ≈ ৮.২৭ cycle, মোট ≈ ৩০২ cycle
  • AMAT ≈ ২.৯০ cycle, মোট ১৬০ cycle

শব্দকোষ

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

মনে রাখুন

  1. গঠন: functional ISS (কী হলো) + trace + আলাদা timing model (কত সময়)। ISS-এর অবস্থা: ৩২টি register (x0 সবসময় ০), PC, byte-ভিত্তিক little-endian মেমরি।
  2. Decoder: register field সব format-এ একই জায়গায়; immediate জোড়া লাগিয়ে n বিট থেকে sign-extend — (v << (32 − n)) >> (32 − n); branch/jal-এর লক্ষ্য = নিজের PC + imm.
  3. নমুনার সংখ্যা: ৮৪টি instruction, a0 = 55; pipeline-এ n + ৪ + data stall + control stall = ৮৪ + ৪ + ১০ + ২২ = ১২০ (CPI ১.৪২৯), ২-বিট predictor-এ ১০৬.
  4. Cache: AMAT = hit time + miss rate × miss penalty; মোট = pipeline cycle + miss × penalty. ২৫৬ বাইট দূরের দুটি array direct-mapped-এ thrash করে (৫৪০ cycle); ২-way বা padding-এ ৮টি miss (২৮০ cycle).
  5. যাচাই: জানা-উত্তরের প্রোগ্রাম, স্বাধীন encoder, golden model-এর সাথে co-simulation, riscv-tests ও compliance test; সবচেয়ে প্রচলিত bug — sign extension, x0, PC-relative ভিত্তি, byte order, jalr-এর বিট ০.

পরের পর্বে: নতুন মডিউল — পরীক্ষা ও ইন্টারভিউ প্রস্তুতি। প্রথমেই numerical সমস্যার সংগ্রহ: সংখ্যা পদ্ধতি, K-map, CPI, pipeline আর cache — এই ক্যাপস্টোনে আপনি যে হিসাবগুলো simulator দিয়ে করেছেন, সেগুলো এবার কাগজে-কলমে, পরীক্ষার ধাঁচে।

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

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