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

Pipeline hazard — structural, data hazard, forwarding আর stall

Pipeline Hazards — Structural Hazards, Data Hazards, Forwarding & Stalls

Taught in Bangla, this lesson explains why pipelines stall: structural hazards from a single memory port or slow units, RAW, WAR and WAW dependences and why only RAW hurts a five-stage pipeline, stalls and bubbles, the full forwarding-unit conditions with ForwardA and ForwardB, double data hazards, load-use hazards and the hazard detection unit, compiler scheduling, and exam numericals on cycles and CPI with stalls, with a runnable pipeline simulator.

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

  • Structural hazard কেন হয় — একটি memory port, register file-এর অর্ধ-cycle কৌশল আর pipeline-হীন ধীর unit
  • RAW, WAR, WAW dependence চেনা, দূরত্ব মাপা, আর কোনটি আসলে hazard তা প্রমাণসহ বলা
  • Stall আর bubble, forwarding unit-এর পূর্ণ শর্ত (x0-এর পরীক্ষা আর অগ্রাধিকারের নিয়মসহ) আর ForwardA/ForwardB-এর encoding
  • Load-use hazard, hazard detection unit কীভাবে bubble ঢোকায়, আর compiler কীভাবে ক্রম বদলে stall এড়ায়
  • Pipeline diagram এঁকে মোট cycle গোনা আর stall সহ CPI-এর পরীক্ষার ধাঁচের numerical

১ · Hazard কী — assembly line যখন থমকে যায়

একটি গার্মেন্টস কারখানার assembly line কল্পনা করুন। পাঁচটি টেবিল: কাপড় কাটা, সেলাই, বোতাম লাগানো, ইস্ত্রি, প্যাকিং। প্রতি মিনিটে প্রতিটি শার্ট এক টেবিল সামনে এগোয়, তাই একসাথে পাঁচটি শার্টে কাজ চলে, আর প্রতি মিনিটে একটি শার্ট বের হয়। পর্ব ৩৫-এ ঠিক এই ছবিটিই দেখেছেন — RISC-V-এর পাঁচটি stage: IF (instruction আনা), ID (decode আর register পড়া), EX (ALU-তে হিসাব), MEM (data memory পড়া বা লেখা), WB (ফল register-এ লেখা)। আদর্শ অবস্থায় প্রতি cycle-এ একটি instruction শেষ হয়, অর্থাৎ ideal CPI = ১.

কিন্তু কারখানায় মাঝে মাঝে লাইন থামে। হয়তো ইস্ত্রির টেবিল আর সেলাইয়ের টেবিল একই মেশিন ভাগ করে ব্যবহার করে। হয়তো বোতাম লাগানোর লোক এমন একটি শার্ট পেলেন, যার কাপড় এখনো রং-এর ঘর থেকে ফেরেনি। এমন যেকোনো পরিস্থিতি, যেখানে পরের instruction তার নির্ধারিত cycle-এ চলতে পারে না, তাকে বলে pipeline hazard. তিন রকম hazard আছে:

Structural hazard

একই cycle-এ দুটি instruction একই hardware অংশ চায়, কিন্তু অংশটি একটিই। যেমন একটিমাত্র memory, যেখান থেকে instruction আনা আর ডেটা পড়া — দুটোই একসাথে করতে হবে।

সম্পদের অভাব

Data hazard

একটি instruction-এর দরকার আগের instruction-এর ফল, কিন্তু সেই ফল এখনো register-এ লেখা হয়নি। এই পর্বের মূল বিষয়।

ডেটার অপেক্ষা

Control hazard

Branch-এর ফল জানার আগেই পরের instruction আনতে হয় — কোনটি আনব? এর পুরো আলোচনা আর branch prediction আছে পর্ব ৩৭-এ; এই পর্বে আমরা branch-হীন কোড নিয়ে কাজ করব।

পথ অজানা

Hazard সামলানোর তিনটি মূল উপায় এই পর্বে দেখবেন: (১) stall — নির্ভরশীল instruction-কে অপেক্ষা করানো; (২) forwarding — ফল register-এ পৌঁছানোর আগেই তাকে সরাসরি দরকারের জায়গায় পাঠানো; (৩) instruction scheduling — compiler-কে দিয়ে instruction-এর ক্রম এমনভাবে সাজানো, যাতে অপেক্ষা কম লাগে।

মূল ধারণা

Hazard সঠিকতার প্রশ্ন, শুধু গতির নয়। Hardware যদি hazard না সামলায়, প্রোগ্রাম ভুল ফল দেবে — একটি instruction পুরোনো (stale) মান পড়ে ফেলবে। Stall গতি কমায় কিন্তু সঠিকতা রক্ষা করে; forwarding আর scheduling সঠিকতা রেখে গতি ফিরিয়ে আনে।

এই পর্বজুড়ে আমরা Patterson–Hennessy-র RISC-V ৫-stage pipeline-এর নিয়ম মেনে চলব। দুটি নিয়ম মনে রাখুন: register file cycle-এর প্রথম অর্ধেকে লেখে আর দ্বিতীয় অর্ধেকে পড়ে; আর forwarding আসে EX/MEM ও MEM/WB pipeline register থেকে EX stage-এর ALU-তে। Single-cycle datapath (পর্ব ৩২) আর control signal-এর টেবিল (পর্ব ৩৩) মনে থাকলে বুঝতে আরও সহজ হবে।

২ · Structural hazard — একটি সম্পদ, দুই দাবিদার

Structural hazard হয় যখন hardware-এ যথেষ্ট সম্পদ নেই। Pipeline-এর নকশাকারীর কাজ হলো সম্পদ এমনভাবে বসানো, যাতে প্রতিটি stage প্রতি cycle-এ নিজের অংশ নিজেই ব্যবহার করতে পারে। তিনটি ক্লাসিক উদাহরণ দেখি।

উদাহরণ ১: একটিমাত্র memory port

ধরুন প্রসেসরের কাছে একটিই memory, আর প্রতি cycle-এ সেখানে একটিই কাজ করা যায় (একটি port). এখন নিচের ক্রমে cycle ৪-এ কী হয় দেখুন:

cycle            1    2    3    4    5    6    7    8
lw  x5, 0(x10)   IF   ID   EX   MEM  WB
add x6, x7, x8        IF   ID   EX   MEM  WB
sub x9, x7, x8             IF   ID   EX   MEM  WB
or  x11, x7, x8                 IF   ID   EX   MEM  WB

Cycle ৪-এ lw data memory থেকে পড়তে চায়, আর একই cycle-এ or-কে memory থেকে আনতে (fetch) হবে। Port একটি হলে একজনকে অপেক্ষা করতেই হবে — সাধারণত fetch এক cycle পিছিয়ে যায়। প্রতিটি load আর store-এ এমন একটি stall হলে CPI অনেক বেড়ে যাবে।

সমাধান: instruction আর ডেটার জন্য আলাদা memory, বাস্তবে আলাদা L1 cache — split cache: একটি L1 instruction cache (I-cache), একটি L1 data cache (D-cache). এটি Harvard architecture-এর ধারণা, তবে শুধু সবচেয়ে কাছের স্তরে; L2 আর main memory সাধারণত দুইয়ের জন্য একটিই (unified). প্রায় সব আধুনিক x86-64, ARM আর RISC-V core-এ L1 এভাবে ভাগ করা থাকে। মেমরির পুরো hierarchy দেখবেন পর্ব ৪৪-এ।

একটি রেস্তোরাঁর রান্নাঘরে ভাঁড়ার ঘরের দরজা একটি। একজন সহকারী সেখান থেকে নতুন অর্ডারের কাগজ আনেন, আরেকজন একই দরজা দিয়ে চাল-ডাল আনেন। দুজন একসাথে এলে একজনকে দাঁড়িয়ে থাকতে হয়। সমাধান — দুটি দরজা: অর্ডারের জন্য একটি জানালা, মালপত্রের জন্য আলাদা দরজা। Split I-cache/D-cache ঠিক এটাই।

উদাহরণ ২: register file — একই cycle-এ লেখা আর পড়া

উপরের ছবিতে cycle ৫-এ lw WB-তে register-এ লেখে, আর একই cycle-এ or ID-তে register পড়ে। এটিও একটি সম্পদ-দ্বন্দ্ব হতে পারত। দুটি কৌশলে এটি মেটানো হয়:

  • আলাদা port: register file-এ দুটি read port (rs1, rs2 একসাথে পড়ার জন্য) আর একটি write port থাকে, তাই ID-এর দুটি পড়া আর WB-এর একটি লেখা একই cycle-এ চলতে পারে।
  • অর্ধ-cycle কৌশল: লেখা হয় cycle-এর প্রথম অর্ধেকে, পড়া দ্বিতীয় অর্ধেকে। ফলে একই register যদি একই cycle-এ লেখা ও পড়া হয়, পাঠক নতুন মানটিই পায়। এই নিয়মের কারণেই পরের অংশে stall-এর হিসাব একটি cycle কম আসবে।

বোর্ডের উদাহরণে ভাবুন: ক্লাসের প্রথম অর্ধেকে শিক্ষক বোর্ডে নতুন সংখ্যা লেখেন, দ্বিতীয় অর্ধেকে ছাত্ররা বোর্ড থেকে টুকে নেন — একই ক্লাসে লেখা আর পড়া দুটোই হলো, আর ছাত্ররা নতুন সংখ্যাটিই পেলেন।

উদাহরণ ৩: pipeline-হীন ধীর unit

ধরুন একটি integer divider ২০ cycle নেয়, আর তার ভেতরে pipeline নেই — একটি ভাগ শেষ না হওয়া পর্যন্ত আরেকটি শুরু করা যায় না। তাহলে পরপর দুটি div এলে দ্বিতীয়টি কয়েক cycle অপেক্ষা করবে, যদিও দুটির মধ্যে কোনো ডেটা-সম্পর্ক নেই। এটিও structural hazard. সমাধান দুই রকম: divider-কে pipelined করা (বেশি transistor), অথবা মেনে নেওয়া — কারণ ভাগ খুব কম আসে, তাই গড় CPI-তে প্রভাব সামান্য। অংশ ৮-এর হিসাবে এর একটি সংখ্যাগত উদাহরণ পাবেন।

নকশার শিক্ষা: RISC-V-এর মতো ISA এমনভাবে বানানো, যাতে প্রতিটি instruction প্রতিটি সম্পদ সর্বোচ্চ একবার, একটি নির্দিষ্ট stage-এ ব্যবহার করে (যেমন memory শুধু MEM-এ, register লেখা শুধু WB-তে)। এজন্যই সরল ৫-stage pipeline-এ split cache আর ঠিকমতো port থাকলে কোনো structural hazard থাকে না। x86-এর মতো ISA-তে, যেখানে একটি instruction memory পড়ে, হিসাব করে, আবার memory-তে লেখে, সেখানে hardware সেটিকে আগে কয়েকটি ছোট micro-op-এ ভাঙে।

৩ · Data dependence — RAW, WAR, WAW আর কেন শুধু RAW বিপদ

দুটি instruction একই register ছুঁলে তাদের মধ্যে একটি data dependence তৈরি হয়। আগের instruction-কে বলি i, পরেরটিকে j (প্রোগ্রামের ক্রমে i আগে)। একই register-এ “পড়া” আর “লেখা”-র তিনটি সমন্বয়:

নামকী ঘটেউদাহরণঅন্য নাম
RAW (read after write)i লেখে, তারপর j সেটি পড়ে — j-এর i-এর ফল লাগবেইadd x1, x2, x3
sub x4, x1, x5
true dependence
WAR (write after read)i পড়ে, তারপর j একই register-এ লেখে — j যেন আগেভাগে লিখে না ফেলেsub x4, x1, x5
add x1, x2, x3
anti-dependence
WAW (write after write)i আর j দুজনেই একই register-এ লেখে — শেষে j-এর মানই থাকতে হবেadd x1, x2, x3
lw x1, 0(x6)
output dependence

RAW হলো আসল ডেটা-প্রবাহ — মানটি সত্যিই i থেকে j-তে যায়। WAR আর WAW-কে বলা হয় “name dependence”: কোনো মান আদান-প্রদান হয় না, শুধু দুজনে একই register-এর নাম ব্যবহার করছে। আলাদা register নিলেই এরা মিলিয়ে যায় — এই ধারণাই পর্ব ৪০-এর register renaming-এর ভিত্তি।

Dependence হলো প্রোগ্রামের ধর্ম; hazard হলো pipeline-এর ধর্ম। একটি dependence তখনই hazard, যখন pipeline-এর কাঠামোর কারণে পড়া-লেখার ক্রম উল্টে যেতে পারে। এখন প্রমাণ করি কেন ক্লাসিক in-order ৫-stage pipeline-এ শুধু RAW hazard হয়:

  1. সব instruction register পড়ে একই stage-এ (ID, stage ২) আর লেখে একই stage-এ (WB, stage ৫)। Instruction-গুলো প্রোগ্রামের ক্রমে, প্রতি cycle-এ সর্বোচ্চ একটি করে ঢোকে, আর কেউ কাউকে টপকে যায় না (in-order).
  2. WAW অসম্ভব: i আগে ঢুকেছে, তাই i-এর WB আগে হবে, j-এর WB পরে। সবার WB একই stage-এ বলে লেখার ক্রম প্রোগ্রামের ক্রমের সমান — শেষ মান j-এরই থাকে।
  3. WAR অসম্ভব: i পড়ে নিজের stage ২-এ, j লেখে নিজের stage ৫-এ। j অন্তত এক cycle পরে ঢুকেছে, তাই j-এর লেখা i-এর পড়ার অন্তত ৩ + ১ = ৪ cycle পরে। পুরোনো মান পড়ার আগে মুছে যাওয়ার কোনো সুযোগ নেই।
  4. RAW সম্ভব: i লেখে stage ৫-এ, অথচ j পড়ে stage ২-এ। j যদি i-এর মাত্র ১ বা ২ cycle পরে ঢোকে, j-এর পড়া i-এর লেখার আগেই ঘটে যাবে — j পুরোনো মান পাবে। এখানেই বিপদ।

সতর্কতা: “শুধু RAW” কথাটি সেই ক্লাসিক in-order pipeline-এর জন্য সত্য, যেখানে সব instruction একই সংখ্যক stage পেরোয়। যখন কিছু instruction বেশি stage নেয় (যেমন floating point unit), বা instruction ক্রম ভেঙে চলে (out-of-order), তখন WAW আর WAR-ও hazard হয়ে ওঠে — অংশ ৯-এ দেখবেন।

পরীক্ষার ক্লাসিক: সব dependence চিহ্নিত করা

প্রশ্নে সাধারণত instruction-এর একটি ক্রম দিয়ে বলা হয়: সব dependence চিহ্নিত করুন, কোনগুলো hazard, forwarding থাকলে আর না থাকলে। পদ্ধতিটি যান্ত্রিক — প্রতিটি জোড়া (i, j)-এর জন্য দেখুন i-এর destination register (rd) j-এর source (rs1/rs2)-এ আছে কি না (RAW), i-এর source j-এর rd কি না (WAR), দুজনের rd এক কি না (WAW). একটি সূক্ষ্ম বিষয়: মাঝখানে অন্য কেউ একই register-এ লিখলে দূরের জোড়াটি আর dependence নয় — j তখন মাঝের লেখকের মান পড়ে। আর x0-কে বাদ দিন — সে সবসময় ০, তার সাথে কোনো dependence হয় না।

নিচের analyser ঠিক এই কাজটি করে। দূরত্ব d = j − i (পরপর হলে d = ১)। Stall-এর সংখ্যা প্রতিটি জোড়ার জন্য আলাদাভাবে হিসাব করা (কেন ৩ − d, তা পরের অংশে প্রমাণ করব)। Run চাপুন, তারপর নিজের ক্রম বসিয়ে দেখুন:

// Dependence analyser: RAW / WAR / WAW between instruction pairs, 5-stage RISC-V pipeline
const prog = ["lw  x5, 0(x10)", "add x6, x5, x7", "sub x5, x8, x9",
              "and x7, x6, x5", "sw  x7, 4(x10)"];
function parse(s) {                                   // -> {op, rd, src[]}, x0 ignored
  const [op, ...a] = s.replace(/[(),]/g, " ").trim().split(/\s+/);
  const r = a.map(t => t.startsWith("x") ? +t.slice(1) : null);
  let rd = null, src = [];
  if (op === "sw") src = [r[2], r[0]];                // sw rs2, imm(rs1)
  else if (op === "lw") { rd = r[0]; src = [r[2]]; }  // lw rd, imm(rs1)
  else if (op === "addi") { rd = r[0]; src = [r[1]]; }
  else { rd = r[0]; src = [r[1], r[2]]; }             // R-type
  return { op, rd: rd || null, src: src.filter(x => x) };
}
const I = prog.map(parse);
const writes = (k, r) => I[k].rd === r;
const clean = (i, j, r) => { for (let k = i + 1; k < j; k++) if (writes(k, r)) return false; return true; };
console.log("pair    type  reg  dist  no forwarding       with forwarding");
for (let j = 1; j < I.length; j++) for (let i = 0; i < j; i++) {
  const d = j - i, out = [];
  const rd = I[i].rd;
  if (rd && I[j].src.includes(rd) && clean(i, j, rd)) {
    const nf = d <= 2 ? `HAZARD, ${3 - d} stall(s)` : "ok (reg file)";
    const fw = I[i].op === "lw" && d === 1 ? "HAZARD, 1 stall" : d <= 2 ? "ok (forwarded)" : "ok";
    out.push(["RAW", rd, nf, fw]);
  }
  for (const r of new Set(I[i].src)) if (writes(j, r) && clean(i, j, r)) out.push(["WAR", r, "no hazard", "no hazard"]);
  if (rd && writes(j, rd) && clean(i, j, rd)) out.push(["WAW", rd, "no hazard", "no hazard"]);
  for (const [t, r, nf, fw] of out)
    console.log(`I${i + 1}->I${j + 1}  ${t}   x${String(r).padEnd(3)}  ${d}    ${nf.padEnd(20)}${fw}`);
}

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

pair    type  reg  dist  no forwarding       with forwarding
I1->I2  RAW   x5    1    HAZARD, 2 stall(s)  HAZARD, 1 stall
I1->I3  WAW   x5    2    no hazard           no hazard
I2->I3  WAR   x5    1    no hazard           no hazard
I2->I4  RAW   x6    2    HAZARD, 1 stall(s)  ok (forwarded)
I2->I4  WAR   x7    2    no hazard           no hazard
I3->I4  RAW   x5    1    HAZARD, 2 stall(s)  ok (forwarded)
I4->I5  RAW   x7    1    HAZARD, 2 stall(s)  ok (forwarded)

পাঁচটি instruction-এ সাতটি dependence. RAW চারটি: I1→I2 (x5, load-এর ঠিক পরেই — forwarding থাকলেও ১টি stall), I2→I4 (x6, দূরত্ব ২), I3→I4 (x5), I4→I5 (x7). লক্ষ করুন I1→I4 তালিকায় নেই — I4 যে x5 পড়ে তা I3-এর লেখা, কারণ I3 মাঝখানে x5-এ আবার লিখেছে। WAW (I1→I3, x5) আর দুটি WAR (I2→I3 x5, I2→I4 x7) প্রোগ্রামে আছে, কিন্তু এই pipeline-এ hazard নয় — অংশ ৩-এর প্রমাণ অনুযায়ী। আর forwarding থাকলে একমাত্র বাকি hazard load-use. একটি সাবধানতা: প্রতিটি জোড়ার stall আলাদা হিসাব — একটি stall একসাথে কয়েকটি hazard-এর অপেক্ষা মিটিয়ে দিতে পারে, তাই মোট cycle জানতে অংশ ৬-এর পূর্ণ simulator লাগবে।

৪ · Stall — hardware যখন bubble ঢোকায়

RAW hazard সামলানোর সবচেয়ে সরল উপায়: নির্ভরশীল instruction-কে ID stage-এ আটকে রাখা, যতক্ষণ না তার দরকারি মান register file-এ পৌঁছায়। আটকে থাকার প্রতিটি cycle হলো একটি stall. যে cycle-গুলোতে পেছনের instruction সামনে এগোয় না, সেখানে সামনের stage-গুলোতে একটি “ফাঁকা” instruction চলে যায় — একে বলে bubble. Bubble দেখতে একটি nop-এর মতো: সে কিছু লেখে না, memory ছোঁয় না, শুধু pipeline-এর একটি ঘর দখল করে এগিয়ে যায়।

গুরুত্বপূর্ণ: এই bubble hardware নিজে ঢোকায়, প্রোগ্রামে কোনো nop লেখা থাকে না। যে hardware dependence চিনে stall করায়, তাকে বলে hazard detection unit; এই আটকে রাখার ব্যবস্থাকে interlock-ও বলা হয়। প্রথম দিকের কিছু প্রসেসরে এই দায়িত্ব দেওয়া হয়েছিল compiler-কে — সে নিজেই nop বসাত (ভাবনার প্রশ্ন ১ দেখুন)।

কয়টি stall? — প্রমাণ

ধরুন producer instruction i cycle ১-এ fetch হয়। তাহলে i-এর WB cycle ৫-এ। Consumer j = i + d, stall না থাকলে fetch হয় cycle ১ + d-তে, আর register পড়ে (ID) cycle ২ + d-তে। s টি stall দিলে j-এর ID হয় cycle ২ + d + s-এ।

  1. Register file প্রথম অর্ধেকে লেখে, দ্বিতীয় অর্ধেকে পড়ে — তাই j-এর পড়া i-এর লেখার cycle-এ হলেও চলবে: শর্ত 2 + d + s ≥ 5.
  2. অর্থাৎ s ≥ 3 − d, আর s ঋণাত্মক হতে পারে না: s = max(0, 3 − d).
  3. d = ১ (ঠিক পরের instruction) → ২ stall; d = ২ → ১ stall; d ≥ ৩ → কোনো stall নেই।
  4. অর্ধ-cycle কৌশল না থাকলে পড়া লেখার পরের cycle-এ হতে হতো: 2 + d + s ≥ 6, তাই s = max(০, ৪ − d) — পরপর instruction-এ ৩টি stall.
Forwarding ছাড়া: stall = max(0, 3 − d)
মোট cycle = n + (k − 1) + মোট stall = n + 4 + stall (k = ৫ stage, n = instruction সংখ্যা)

শেষ সূত্রটি পর্ব ৩৫-এর: প্রথম instruction শেষ হতে ৫ cycle, তারপর প্রতিটি নতুন instruction এক cycle করে যোগ করে, আর প্রতিটি stall আরও একটি। উদাহরণ: add x1, x2, x3 আর ঠিক পরেই sub x4, x1, x5 — forwarding ছাড়া ২ + ৪ + ২ = ৮ cycle; forwarding থাকলে ৬ cycle.

ঢাকার মেট্রোরেলের স্টেশনে একটি ট্রেন প্ল্যাটফর্মে দাঁড়িয়ে থাকলে পেছনের ট্রেন সিগন্যালে থামে, তার পেছনেরটিও থামে — কিন্তু সামনের ট্রেনগুলো চলতেই থাকে, আর দুটি ট্রেনের মাঝে একটি ফাঁকা জায়গা তৈরি হয়। Stall ঠিক তাই: আটকে থাকা instruction আর তার পেছনের সবাই থামে, সামনের সবাই এগোয়, আর মাঝের ফাঁকটিই bubble.

৫ · Forwarding (bypassing) — ফল register-এ পৌঁছানোর আগেই পাঠিয়ে দেওয়া

একটু ভাবুন: add x1, x2, x3-এর ফল কখন তৈরি হয়? EX stage-এর শেষে, cycle ৩-এ। অথচ register file-এ লেখা হয় cycle ৫-এ। আর sub x4, x1, x5-এর মানটি আসলে লাগে কখন? তার নিজের EX-এর শুরুতে, cycle ৪-এ। অর্থাৎ ফলটি দরকারের আগেই তৈরি — শুধু ভুল জায়গায় বসে আছে (EX/MEM pipeline register-এ)। তাহলে সেখান থেকে একটি তার টেনে সরাসরি ALU-র ইনপুটে দিলেই হয়! এটাই forwarding, অন্য নাম bypassing.

রেস্তোরাঁর রান্নাঘরে একজন বাবুর্চি ঝোল রান্না শেষ করলেন। নিয়ম হলো সব রান্না প্রথমে ফ্রিজে (register file) রাখতে হবে, তারপর পরের জন সেখান থেকে নেবেন। কিন্তু পরের জন পাশেই দাঁড়িয়ে আছেন — হাতে হাতে দিয়ে দিলেই সময় বাঁচে। Forwarding মানে এই হাতে হাতে দেওয়া; ফ্রিজে রাখার কাজটি (WB) পরে ঠিকই হয়।

RISC-V-এর ৫-stage pipeline-এ দুটি forwarding পথ যথেষ্ট:

  • EX/MEM → EX: ঠিক আগের instruction-এর ALU ফল (d = ১)।
  • MEM/WB → EX: দুই আগের instruction-এর ফল, অথবা ঠিক আগের load-এর memory থেকে পড়া মান (d = ২, বা load-এর ক্ষেত্রে stall-এর পরে)।
  • d = ৩ হলে কোনো forwarding লাগে না — অর্ধ-cycle কৌশলে register file থেকেই নতুন মান পাওয়া যায়।

নিচের stepper-এ তিনটি instruction দেখুন। শুরুর মান: x1 = ৯৯ (পুরোনো), x2 = ১০, x3 = ২০, x5 = ৭, x7 = ১২. প্রতিটি সারি একটি stage, প্রতিটি ধাপ একটি cycle:

add x1, x2, x3 → sub x4, x1, x5 → and x6, x1, x7
Cycle
IF
ID (register পড়া)
EX (ALU)
MEM
WB
ForwardA (EX-এর instruction)
ফল
  1. Cycle ১. add fetch হলো। Pipeline-এ এখনো আর কেউ নেই।
  2. Cycle ২. add ID-তে register পড়ল: x2 = ১০, x3 = ২০. পেছনে sub fetch হলো।
  3. Cycle ৩. add-এর ALU হিসাব করল ১০ + ২০ = ৩০. একই cycle-এ sub ID-তে x1 পড়ল — কিন্তু register file-এ এখনো পুরোনো মান ৯৯, কারণ add লিখবে cycle ৫-এ। এই ৯৯ ভুল; সমস্যাটি ধরা পড়বে পরের cycle-এ। add-এর নিজের operand register থেকেই এসেছে, তাই তার ForwardA = 00.
  4. Cycle ৪. sub এখন EX-এ, add MEM-এ — তার ফল ৩০ বসে আছে EX/MEM register-এ। Forwarding unit তুলনা করে: EX/MEM.RegWrite = ১, EX/MEM.rd = x1 ≠ x0, আর EX/MEM.rd = ID/EX.rs1 (x1). তিনটি শর্তই সত্য, তাই ForwardA = 10: ALU-র A ইনপুটে ৯৯-এর বদলে ৩০ যায়। ফল ৩০ − ৭ = ২৩, সঠিক। Stall লাগল না।
  5. Cycle ৫. and EX-এ। এবার EX/MEM-এ আছে sub (rd = x4), যা x1-এর সাথে মেলে না। কিন্তু MEM/WB-এ আছে add (rd = x1) — মিলে গেল, তাই ForwardA = 01: A = ৩০, ফল ৩০ AND ১২ = ১২. লক্ষ করুন, এই cycle-এই add x1-এ ৩০ লিখছে, কিন্তু and register পড়েছিল cycle ৪-এ, তাই তারও forwarding দরকার ছিল।
  6. সারাংশ. তিনটি instruction ৩ + ৪ = ৭ cycle-এ শেষ, একটিও stall নেই। Forwarding ছাড়া sub-কে ২ cycle আটকাতে হতো, মোট ৯ cycle. প্রথম forwarding (d = ১) এসেছে EX/MEM থেকে, দ্বিতীয়টি (d = ২) MEM/WB থেকে।

Forwarding unit-এর শর্ত — ধাপে ধাপে গড়ে তোলা

EX stage-এ ALU-র প্রতিটি ইনপুটের আগে একটি তিন-ইনপুটের multiplexer বসানো হয়। যে hardware ঠিক করে mux কোন ইনপুট নেবে, তাকে বলে forwarding unit. Encoding (Patterson–Hennessy-র RISC-V বইয়ের রীতি):

Mux controlউৎসমানে
ForwardA = 00ID/EXALU-র প্রথম operand আসে register file থেকে (ID-তে পড়া মান) — কোনো forwarding নেই
ForwardA = 10EX/MEMপ্রথম operand আসে ঠিক আগের instruction-এর ALU ফল থেকে
ForwardA = 01MEM/WBপ্রথম operand আসে data memory-র মান বা আরও আগের ALU ফল থেকে
ForwardB = 00 / 10 / 01একই তিন উৎসএকই নিয়ম, দ্বিতীয় operand (rs2)-এর জন্য

এখন শর্তটি নিজে গড়ে তুলি। “EX/MEM থেকে forward করব” কখন সঠিক?

  1. আগের instruction আসলেই register-এ লিখবে: EX/MEM.RegWrite = 1. sw বা branch-এর rd ঘরে কিছু bit থাকে, কিন্তু তারা register লেখে না — এই শর্ত ছাড়া তাদের “ফল” ভুল করে forward হতো।
  2. Destination x0 নয়: EX/MEM.rd ≠ 0. RISC-V-এ addi x0, x1, 5 বৈধ instruction (ফল ফেলে দেওয়া হয়) — x0 সবসময় ০ থাকবে। এই শর্ত না থাকলে পরের add x2, x0, x0 forward করা মান পেয়ে x2 = ০-এর বদলে (x1 + ৫)-এর দ্বিগুণ পেত।
  3. Register মিলে যায়: EX/MEM.rd = ID/EX.rs1.
// EX hazard (Patterson–Hennessy, RISC-V সংস্করণের রীতি)
if (EX/MEM.RegWrite and (EX/MEM.RegisterRd ≠ 0)
    and (EX/MEM.RegisterRd = ID/EX.RegisterRs1))  ForwardA = 10
if (EX/MEM.RegWrite and (EX/MEM.RegisterRd ≠ 0)
    and (EX/MEM.RegisterRd = ID/EX.RegisterRs2))  ForwardB = 10

// MEM hazard — শুধু তখনই, যখন EX hazard নেই
if (MEM/WB.RegWrite and (MEM/WB.RegisterRd ≠ 0)
    and not (EX/MEM.RegWrite and (EX/MEM.RegisterRd ≠ 0)
             and (EX/MEM.RegisterRd = ID/EX.RegisterRs1))
    and (MEM/WB.RegisterRd = ID/EX.RegisterRs1))  ForwardA = 01
// ForwardB-এর জন্য একই, Rs1-এর জায়গায় Rs2

অগ্রাধিকারের নিয়ম — double data hazard

MEM hazard-এর শর্তে ওই লম্বা “not (…)” অংশটি কেন? ধরুন একই register পরপর কয়েকবার বদলানো হচ্ছে — যেমন একটি loop-এ যোগফল জমানো: add x1, x1, x2, add x1, x1, x3, add x1, x1, x4. তৃতীয় instruction যখন EX-এ, তখন EX/MEM আর MEM/WB — দুটিতেই rd = x1! কোনটি নেব? প্রোগ্রামের অর্থ অনুযায়ী সবচেয়ে সাম্প্রতিক মানটি, অর্থাৎ EX/MEM-এরটি। নিচের stepper-এ forwarding unit-এর সিদ্ধান্তটি ধাপে ধাপে দেখুন (শুরুতে x1 = ৫, x2 = ১, x3 = ২, x4 = ৩):

Cycle ৫: তৃতীয় add x1, x1, x4 এখন EX-এ
এখন কী দেখছি
EX/MEM (২য় add)
MEM/WB (১ম add)
ID/EX (৩য় add)
ForwardA
ALU ফল
  1. অবস্থা. প্রথম add ৫ + ১ = ৬ করেছে, সে এখন MEM/WB-এ। দ্বিতীয় add ৬ + ২ = ৮ করেছে (নিজেও forwarding পেয়ে), সে EX/MEM-এ। তৃতীয় add ID-তে x1 পড়েছিল cycle ৪-এ, তখন register file-এ ছিল পুরোনো ৫ — সেটি ID/EX-এ বসে আছে।
  2. EX hazard পরীক্ষা. EX/MEM.RegWrite = ১, EX/MEM.rd = x1 ≠ x0, আর x1 = ID/EX.rs1 — তিনটিই সত্য। তাই ForwardA = 10 হওয়ার দাবি আছে, মান ৮.
  3. MEM hazard পরীক্ষা (নিয়ম ছাড়া). MEM/WB.RegWrite = ১, rd = x1 ≠ x0, x1 = rs1 — এটিও সত্য! একই operand-এর জন্য দুটি প্রার্থী: ৮ (EX/MEM) আর ৬ (MEM/WB).
  4. ভুল অগ্রাধিকার. যদি hardware MEM/WB-কে বেছে নেয়, ALU পাবে ৬, ফল ৬ + ৩ = ৯. কিন্তু প্রোগ্রামের হিসাবে x1 = ৫ + ১ + ২ + ৩ = ১১. দ্বিতীয় add-এর অবদান (+২) হারিয়ে গেল — নীরব, ভয়ংকর ভুল।
  5. সঠিক নিয়ম. MEM hazard-এর শর্তে “EX hazard নেই” অংশটি থাকায় এখানে MEM শর্ত মিথ্যা হয়, তাই ForwardA = 10, A = ৮, ফল ৮ + ৩ = ১১ ✓. নিয়মের কারণ: EX/MEM-এর instruction প্রোগ্রামে MEM/WB-এরটির পরে, তাই তার লেখাই সর্বশেষ।
  6. ForwardB. দ্বিতীয় operand x4: EX/MEM.rd আর MEM/WB.rd দুটিই x1, কোনোটিই x4 নয় — তাই ForwardB = 00, মান register file থেকে পড়া ৩. চূড়ান্ত x1 = ১১, আর পুরো ক্রমে একটিও stall নেই।

Hardware-এ এই অগ্রাধিকার আসলে একটি সরল if–else শৃঙ্খল। নিচে SystemVerilog-এ একটি forwarding unit (উদাহরণ, synthesizable combinational লজিক — Verilog-এর পরিচয় আছে পর্ব ১৪-এ):

// উদাহরণ — forwarding unit (SystemVerilog, combinational)
module forwarding_unit (
    input  logic [4:0] id_ex_rs1, id_ex_rs2, ex_mem_rd, mem_wb_rd,
    input  logic       ex_mem_regwrite, mem_wb_regwrite,
    output logic [1:0] forward_a, forward_b
);
    always_comb begin
        forward_a = 2'b00;                                   // register file
        if (ex_mem_regwrite && ex_mem_rd != 5'd0 && ex_mem_rd == id_ex_rs1)
            forward_a = 2'b10;                               // EX/MEM wins
        else if (mem_wb_regwrite && mem_wb_rd != 5'd0 && mem_wb_rd == id_ex_rs1)
            forward_a = 2'b01;                               // MEM/WB

        forward_b = 2'b00;
        if (ex_mem_regwrite && ex_mem_rd != 5'd0 && ex_mem_rd == id_ex_rs2)
            forward_b = 2'b10;
        else if (mem_wb_regwrite && mem_wb_rd != 5'd0 && mem_wb_rd == id_ex_rs2)
            forward_b = 2'b01;
    end
endmodule

else if-ই সেই “not already forwarded from EX/MEM” শর্ত — প্রথম শর্ত সত্য হলে দ্বিতীয়টি দেখাই হয় না। প্রতিটি ইনপুটে মাত্র দুটি ৫-bit comparator আর কয়েকটি gate — খুব ছোট hardware, কিন্তু CPI-তে বিশাল প্রভাব।

সূক্ষ্ম বিষয়: Store-এর data (sw-এর rs2) কোথায় লাগে? MEM stage-এ, কিন্তু পাঠ্যবইয়ের নকশায় সেটিও EX-এর ForwardB mux দিয়ে যায়, তারপর EX/MEM-এ বসে। তাই add x7, …-এর ঠিক পরে sw x7, 8(x10) কোনো stall ছাড়াই চলে। কিছু বাস্তব নকশায় MEM/WB থেকে MEM-এ আলাদা একটি পথও থাকে (যেমন lw-এর ঠিক পরে একই মান sw করার জন্য); এই পর্বের হিসাবে আমরা শুধু পাঠ্যবইয়ের দুটি পথ ধরব।

৬ · Load-use hazard — forwarding-ও যেখানে হার মানে

এবার producer যদি একটি load হয়? lw x5, 0(x10)-এর মান memory থেকে আসে MEM stage-এর শেষে। ঠিক পরের add x6, x5, x7-এর মান লাগে তার EX-এর শুরুতে — যা একই cycle! সময়ে পেছনে তো কিছু পাঠানো যায় না। তাই forwarding থাকলেও একটি stall লাগবেই। একে বলে load-use hazard.

প্রমাণ আগের মতো: lw cycle ১-এ fetch হলে তার মান তৈরি হয় cycle ৪-এর শেষে (MEM). Consumer j = i + d-এর EX (s stall সহ) cycle ৩ + d + s-এ, আর তাকে অন্তত cycle ৫ হতে হবে (MEM/WB → EX forwarding): 3 + d + s ≥ 5, তাই s = max(0, 2 − d) — d = ১-এ ১টি stall, d ≥ ২-এ শূন্য। একই হিসাবে ALU producer-এর জন্য: মান তৈরি cycle ৩-এর শেষে, শর্ত 3 + d + s ≥ 4, তাই s = max(০, ১ − d) = ০ সবসময়।

Producer → consumer দূরত্ব d১২৩ বা বেশি
Forwarding নেই (ALU বা load)২ stall১ stall০
Forwarding আছে, producer ALU০ (EX/MEM → EX)০ (MEM/WB → EX)০ (register file)
Forwarding আছে, producer lw১ stall, তারপর MEM/WB → EX০ (MEM/WB → EX)০ (register file)

Hazard detection unit — কখন, আর কীভাবে bubble ঢোকায়

Load-use চেনার সবচেয়ে ভালো সময় হলো consumer যখন ID-তে আর load যখন EX-এ। Patterson–Hennessy-র শর্ত:

if (ID/EX.MemRead and
    ((ID/EX.RegisterRd = IF/ID.RegisterRs1) or
     (ID/EX.RegisterRd = IF/ID.RegisterRs2)))
    stall the pipeline (১ cycle)

ID/EX.MemRead একমাত্র load-এর জন্যই ১ — তাই এটিই বলে “EX-এ যে আছে সে একটি load”. বাকি অংশ দেখে, ID-তে থাকা instruction তার rd পড়বে কি না। Stall হলে তিনটি কাজ একসাথে হয়:

  1. PC আটকে রাখা (PCWrite = ০): একই instruction আবার fetch হয়, নতুন কিছু ঢোকে না।
  2. IF/ID register আটকে রাখা (IF/IDWrite = ০): ID-তে থাকা consumer পরের cycle-এ আবার decode হয়।
  3. ID/EX-এ সব control signal ০ (একটি mux দিয়ে): EX-এ যা ঢোকে তা RegWrite = ০, MemRead = ০, MemWrite = ০ — কিছুই বদলায় না। এটিই bubble.

পাঠ্যবইয়ের এই সরল শর্ত দেখে না consumer আসলেই rs2 ব্যবহার করে কি না (যেমন addi-এর rs2 ঘরে immediate-এর bit থাকে), তাই কখনো কখনো অকারণ stall হয় — নিরাপদ দিকে ভুল। বাস্তব নকশা decoder থেকে জেনে নেয় কোন source আসলে ব্যবহৃত হচ্ছে; এই পর্বের simulator-ও তাই করে। নিচের stepper-এ পুরো ঘটনাটি দেখুন (Mem[x10] = ৪০, x7 = ২, x9 = ০):

lw x5, 0(x10) → add x6, x5, x7 → addi x9, x9, 1
Cycle
IF
ID
EX
MEM
WB
Hazard unit-এর পরীক্ষা
PCWrite / IF/IDWrite
ForwardA (EX-এর instruction)
  1. Cycle ১. lw fetch হলো।
  2. Cycle ২. lw ID-তে, add fetch হলো। ID/EX এখনো ফাঁকা, তাই hazard unit-এর তুলনার কিছু নেই।
  3. Cycle ৩. lw EX-এ (ঠিকানা হিসাব), add ID-তে। Hazard detection unit দেখে: ID/EX.MemRead = ১ (EX-এ load), ID/EX.rd = x5, আর IF/ID.rs1 = x5 — মিলে গেল। তাই stall: PCWrite = ০, IF/IDWrite = ০, আর ID/EX-এ যাওয়ার control signal ০ করা হলো।
  4. Cycle ৪. EX-এ ঢুকেছে একটি bubble — সব control ০, তাই সে কিছুই করবে না। add ID-তে আবার decode হচ্ছে, addi আবার fetch (PC বদলায়নি)। lw MEM-এ memory থেকে ৪০ পড়ল। এবার ID/EX-এ bubble, তার MemRead = ০, তাই আর stall নয়।
  5. Cycle ৫. add EX-এ। EX/MEM-এ bubble (RegWrite = ০), তাই EX hazard মিথ্যা; MEM/WB-এ lw (rd = x5) — ForwardA = 01, A = ৪০, ফল ৪০ + ২ = ৪২ ✓. একই cycle-এ lw WB-তে x5-এ ৪০ লিখছে।
  6. Cycle ৬. Bubble WB-তে পৌঁছাল এবং কিছুই লিখল না। addi EX-এ ০ + ১ = ১ হিসাব করছে — তার কোনো dependence নেই।
  7. সারাংশ. তিনটি instruction: ৩ + ৪ + ১ stall = ৮ cycle. Hazard detection unit না থাকলে add cycle ৪-এ EX-এ যেত, যখন ৪০ তখনো memory থেকে আসেনি — ফল হতো পুরোনো x5 + ২, ভুল। Forwarding ছাড়া এখানে ২টি stall লাগত (৯ cycle).

পুরো simulator: forwarding ছাড়া বনাম forwarding সহ

নিচের simulator একটি পাঁচ-instruction প্রোগ্রামের cycle-by-cycle diagram আঁকে। নিয়মগুলো ঠিক উপরের প্রমাণের: forwarding ছাড়া consumer-কে producer-এর WB-এর cycle পর্যন্ত ID-তে অপেক্ষা করতে হয়; forwarding থাকলে ALU-র ফলের জন্য অপেক্ষা নেই, load-এর ঠিক পরে ১ cycle. ** মানে instruction আগের stage-এই আটকে আছে (stall), আর ডান পাশে লেখা থাকে EX-এর সময় প্রতিটি operand কোথা থেকে এল:

// 5-stage pipeline timing with hazard detection; forwarding EX/MEM->EX and MEM/WB->EX
// each entry: [text, rd (0 = none), source registers, isLoad]
const prog = [["lw  x5, 0(x10)", 5, [10], 1], ["add x6, x5, x7", 6, [5, 7]],
  ["sub x8, x6, x5", 8, [6, 5]], ["and x9, x8, x6", 9, [8, 6]], ["sw  x9, 8(x10)", 0, [10, 9]]];
const writer = (r, i) => prog.map(q => q[1]).lastIndexOf(r, i - 1);  // nearest earlier writer
function simulate(fwd) {
  const D = [], notes = [];                // D[i] = last cycle instruction i spends in ID
  prog.forEach(([, , src], i) => {
    let d = i === 0 ? 2 : D[i - 1] + 1;    // earliest: one cycle after the previous one
    for (const r of src) {
      const p = writer(r, i); if (p < 0 || r === 0) continue;
      d = Math.max(d, D[p] + (!fwd ? 3 : prog[p][3] ? 2 : 1));    // WB / load-use / ALU
    }
    const why = src.map(r => {             // where the operand comes from when i is in EX
      const p = writer(r, i), gap = p < 0 ? 9 : d - D[p];
      return gap === 1 ? `x${r} EX/MEM->EX` : gap === 2 ? `x${r} MEM/WB->EX` :
             gap === 3 ? `x${r} regfile(WB same cycle)` : "";
    }).filter(s => s);
    D.push(d); notes.push(why.join(", "));
  });
  const total = D[D.length - 1] + 3, cell = s => s.padEnd(4);
  console.log(`${fwd ? "WITH" : "NO"} forwarding: ${total} cycles, ` +
              `${total - prog.length - 4} stall cycles\n${"cycle".padEnd(16)}` +
              [...Array(total)].map((_, c) => cell(String(c + 1))).join(""));
  prog.forEach(([txt], i) => {
    const row = Array(total).fill(cell("")), first = i === 0 ? 2 : D[i - 1] + 1;
    const F = i === 0 ? 1 : (i === 1 ? 2 : D[i - 2] + 1);         // fetched when i-1 enters ID
    for (let c = F; c <= D[i] + 3; c++) row[c - 1] = cell("**");   // ** = held (stall)
    row[F - 1] = cell("IF"); row[first - 1] = cell("ID");
    ["EX", "MEM", "WB"].forEach((s, k) => row[D[i] + k] = cell(s));
    console.log(txt.padEnd(16) + row.join("") + (notes[i] ? " " + notes[i] : ""));
  });
}
simulate(false); console.log(); simulate(true);

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

NO forwarding: 17 cycles, 8 stall cycles
cycle           1   2   3   4   5   6   7   8   9   10  11  12  13  14  15  16  17
lw  x5, 0(x10)  IF  ID  EX  MEM WB
add x6, x5, x7      IF  ID  **  **  EX  MEM WB                                       x5 regfile(WB same cycle)
sub x8, x6, x5          IF  **  **  ID  **  **  EX  MEM WB                           x6 regfile(WB same cycle)
and x9, x8, x6                      IF  **  **  ID  **  **  EX  MEM WB               x8 regfile(WB same cycle)
sw  x9, 8(x10)                                  IF  **  **  ID  **  **  EX  MEM WB   x9 regfile(WB same cycle)

WITH forwarding: 10 cycles, 1 stall cycles
cycle           1   2   3   4   5   6   7   8   9   10
lw  x5, 0(x10)  IF  ID  EX  MEM WB
add x6, x5, x7      IF  ID  **  EX  MEM WB               x5 MEM/WB->EX
sub x8, x6, x5          IF  **  ID  EX  MEM WB           x6 EX/MEM->EX, x5 regfile(WB same cycle)
and x9, x8, x6                  IF  ID  EX  MEM WB       x8 EX/MEM->EX, x6 MEM/WB->EX
sw  x9, 8(x10)                      IF  ID  EX  MEM WB   x9 EX/MEM->EX

Forwarding ছাড়া ১৭ cycle, যার ৮টি stall: প্রতিটি instruction আগেরটির ফলের ওপর নির্ভরশীল (d = ১), তাই প্রত্যেকে ২ cycle ID-তে আটকে থাকে (৪ × ২ = ৮)। প্রতিটি operand এসেছে register file থেকে, ঠিক producer-এর WB-এর cycle-এ — অর্ধ-cycle কৌশলের সুবিধা। খেয়াল করুন একটি instruction আটকালে পেছনেরটি IF-এ আটকে থাকে (**) — এটাই PC আর IF/ID আটকে রাখা। Forwarding সহ ১০ cycle, মাত্র ১টি stall — lw-এর ঠিক পরের add, যে x5 পায় MEM/WB → EX পথে। sub x6 পায় EX/MEM থেকে আর x5 register file থেকে (lw ঠিক সেই cycle-এ লিখছে); and দুটি পথই একসাথে ব্যবহার করে (x8 EX/MEM, x6 MEM/WB); আর sw-এর store data x9 আসে EX/MEM থেকে। Speedup = ১৭ / ১০ = ১.৭. প্রোগ্রামের ক্রম বদলে নিজে পরীক্ষা করুন — যেমন add-কে lw থেকে এক ঘর দূরে সরালে কী হয়।

৭ · Compiler scheduling — ক্রম বদলে stall এড়ানো

Load-use stall hardware দিয়ে সরানো যায় না, কিন্তু software দিয়ে লুকানো যায়। কৌশল সহজ: load আর তার ব্যবহারকারীর মাঝে এমন একটি instruction বসান, যার ওই load-এর সাথে কোনো সম্পর্ক নেই। একে বলে instruction scheduling (বা code scheduling); compiler এটি স্বয়ংক্রিয়ভাবে করে, যদি নতুন ক্রমে প্রোগ্রামের অর্থ না বদলায়।

Patterson–Hennessy-র বইয়ে একটি বিখ্যাত উদাহরণ আছে (a = b + e; c = b + f;), যেখানে ক্রম বদলে ১৩ cycle থেকে ১১ cycle হয়। আমরা একটু বড় নিজস্ব উদাহরণ নিই:

d = a + b;
e = a - c;
f = d + e;      // a, b, c, d, e, f মেমরিতে, ঠিকানা x10 + 0, 4, 8, 12, 16, 20

সরল অনুবাদে (প্রতিটি statement নিজের ক্রমে) ৯টি instruction, যেখানে দুটি load-use জোড়া আছে: lw x6 → add x7, আর lw x28 → sub x29. তাই forwarding সহ ৯ + ৪ + ২ = ১৫ cycle. সমাধান: তৃতীয় load (lw x28, যা c আনে) উপরে তুলে আনা — সে কারও ওপর নির্ভর করে না, আর যে দুটি instruction-কে সে টপকে যাবে (add x7, sw x7), তারা কেউ x28 পড়ে বা লেখে না।

কিন্তু “উপরে তোলা” কখন বৈধ? Compiler একটি dependence graph বানায় — প্রতিটি RAW, WAR, WAW, আর মেমরির ক্ষেত্রে একই ঠিকানার store-load, load-store বা store-store জোড়া — তারপর শুধু সেই ক্রমগুলো বদলায় যাদের মধ্যে কোনো তীর নেই। এই উদাহরণে সব ঠিকানা একই base register (x10) থেকে ভিন্ন offset-এ, আর x10 কোথাও বদলায় না, তাই lw x28, 8(x10) নিশ্চিতভাবে sw x7, 12(x10)-এর সাথে ভিন্ন ঠিকানা ছোঁয়। (ভিন্ন base register হলে compiler সাধারণত ধরে নেয় একই ঠিকানা হতে পারে — aliasing — আর ক্রম বদলায় না।)

নিচের প্রোগ্রাম একটি সরল greedy list scheduling চালায়: প্রতিবার “প্রস্তুত” (সব পূর্বসূরি বসে গেছে এমন) instruction-গুলোর মধ্যে প্রথমটি নেয়, যেটি ঠিক আগের load-এর ফল ব্যবহার করে না; এমন কেউ না থাকলে প্রথম প্রস্তুতটিকেই নেয়:

// Greedy list scheduling to remove load-use stalls. C: d = a + b; e = a - c; f = d + e;
// a,b,c,d,e,f live at 0,4,8,12,16,20(x10). Entry: [text, op, rd, [sources], memory offset]
const P = [["lw  x5, 0(x10)", "lw", 5, [10], 0], ["lw  x6, 4(x10)", "lw", 6, [10], 4],
  ["add x7, x5, x6", "add", 7, [5, 6]], ["sw  x7, 12(x10)", "sw", 0, [10, 7], 12],
  ["lw  x28, 8(x10)", "lw", 28, [10], 8], ["sub x29, x5, x28", "sub", 29, [5, 28]],
  ["sw  x29, 16(x10)", "sw", 0, [10, 29], 16], ["add x30, x7, x29", "add", 30, [7, 29]],
  ["sw  x30, 20(x10)", "sw", 0, [10, 30], 20]];
const isMem = q => q[1] === "lw" || q[1] === "sw";
function mustStayAfter(j, i) {           // must P[j] stay after P[i] (i < j)?
  const a = P[i], b = P[j];
  if (a[2] && b[3].includes(a[2])) return true;                 // RAW
  if (b[2] && a[3].includes(b[2])) return true;                 // WAR
  if (a[2] && a[2] === b[2]) return true;                       // WAW
  return isMem(a) && isMem(b) && (a[1] === "sw" || b[1] === "sw") && a[4] === b[4]; // same address
}
const loadUse = (prev, q) => prev && prev[1] === "lw" && q[3].includes(prev[2]);
const cycles = seq => seq.length + 4 + seq.filter((q, k) => loadUse(seq[k - 1], q)).length;
function schedule() {
  const done = new Set(), out = [];
  while (out.length < P.length) {
    const ready = P.map((_, j) => j).filter(j => !done.has(j) &&
      P.every((_, i) => i >= j || done.has(i) || !mustStayAfter(j, i)));
    const pick = ready.find(j => !loadUse(out[out.length - 1], P[j])) ?? ready[0];
    done.add(pick); out.push(P[pick]);
  }
  return out;
}
const S = schedule();
for (const [name, seq] of [["original", P], ["scheduled", S]]) {
  console.log(`${name}: ${cycles(seq)} cycles (with forwarding)`);
  seq.forEach((q, k) => console.log("   " + q[0].padEnd(18) + (loadUse(seq[k - 1], q) ? "<- 1 stall (load-use)" : "")));
}

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

original: 15 cycles (with forwarding)
   lw  x5, 0(x10)
   lw  x6, 4(x10)
   add x7, x5, x6    <- 1 stall (load-use)
   sw  x7, 12(x10)
   lw  x28, 8(x10)
   sub x29, x5, x28  <- 1 stall (load-use)
   sw  x29, 16(x10)
   add x30, x7, x29
   sw  x30, 20(x10)
scheduled: 13 cycles (with forwarding)
   lw  x5, 0(x10)
   lw  x6, 4(x10)
   lw  x28, 8(x10)
   add x7, x5, x6
   sw  x7, 12(x10)
   sub x29, x5, x28
   sw  x29, 16(x10)
   add x30, x7, x29
   sw  x30, 20(x10)

মূল ক্রমে দুটি load-use stall — মোট ১৫ cycle. Scheduler তৃতীয় step-এ দেখল add x7 প্রস্তুত, কিন্তু সে ঠিক আগের lw x6-এর ফল চায়; তাই পরের প্রস্তুত instruction lw x28 বসাল। এক ঢিলে দুই পাখি: add x7 এখন lw x6 থেকে দুই ঘর দূরে, আর sub x29 lw x28 থেকে তিন ঘর দূরে (মাঝে add x7 আর sw x7). নতুন ক্রমে কোনো stall নেই — ১৩ cycle, অর্থাৎ ৯ + ৪, ৫-stage pipeline-এ ৯টি instruction-এর সর্বনিম্ন সম্ভব সময়। Speedup ১৫ / ১৩ ≈ ১.১৫. Forwarding ছাড়া একই দুটি ক্রমে লাগত ২৩ আর ২০ cycle (অংশ ৬-এর simulator দিয়ে যাচাই করা) — scheduling সেখানেও সাহায্য করে, কিন্তু forwarding-এর বিকল্প নয়।

বাস্তবে compiler-এর scheduler অনেক বেশি তথ্য ব্যবহার করে: প্রতিটি instruction-এর আসল latency (একটি গুণ হয়তো ৩ cycle, L1 cache থেকে load হয়তো ৪–৫ cycle), কতগুলো functional unit আছে, আর register কম পড়ে যাচ্ছে কি না — কারণ load আগে তুলে আনলে একসাথে বেশি মান “জীবিত” থাকে, ফলে বেশি register লাগে (register allocation-এর সাথে টানাটানি)। Loop unrolling scheduling-কে আরও বেশি স্বাধীন instruction দেয়। GCC-তে -O2 থেকে scheduling চালু থাকে, আর -mtune= দিয়ে নির্দিষ্ট CPU-র latency টেবিল বেছে নেওয়া যায়।

৮ · Stall সহ CPI — হিসাব আর পরীক্ষার numerical

বড় প্রোগ্রামে প্রতিটি cycle গোনা সম্ভব নয়; তখন আমরা গড় হিসাব করি। ৫-stage pipeline-এ প্রতিটি instruction আদর্শভাবে ১ cycle নেয়, আর প্রতিটি stall একটি বাড়তি cycle যোগ করে। তাই পর্ব ০৩-এর CPI-র সংজ্ঞা থেকে সরাসরি:

CPI = Ideal CPI + Σ (প্রতি instruction-এ ঘটনার হার × প্রতিবারের stall)
= 1 + fload-use × 1 + fstructural × sstructural + … (+ branch-এর অংশ, পর্ব ৩৭)

কেন ঠিক? মোট cycle = n + ৪ + মোট stall. n বড় হলে ৪-এর ভাগ (৪ / n) শূন্যের দিকে যায়, তাই CPI = মোট cycle / n ≈ ১ + (মোট stall / n). আর মোট stall / n = Σ (ঘটনার সংখ্যা / n) × প্রতিবারের stall = Σ f × s. শর্ত একটিই: ঘটনাগুলোর stall যেন একে অপরের সাথে না মেশে (overlap না করে) — সাধারণ পরীক্ষার প্রশ্নে এটি ধরে নেওয়া হয়।

ক্লাসিক উদাহরণ: forwarding আছে; ২৫% instruction load; তাদের ৪০%-এর ঠিক পরের instruction ফলটি ব্যবহার করে। Load-use ঘটনার হার = ০.২৫ × ০.৪০ = ০.১০ (প্রতি ১০টি instruction-এ একটি), তাই CPI = ১ + ০.১০ × ১ = ১.১০. ভগ্নাংশের ভগ্নাংশ গুণ করতে ভুলবেন না — “৪০% load-use” মানে সব instruction-এর ৪০% নয়, load-গুলোর ৪০%.

এখন forwarding-এর দাম। Forwarding mux ALU-র সামনে বসে, তাই EX stage-এর পথ একটু লম্বা হয় — clock cycle সামান্য বাড়তে পারে (critical path, পর্ব ১৮)। তাই সঠিক তুলনা CPI দিয়ে নয়, CPI × clock cycle time দিয়ে। নিচের calculator চারটি পরিস্থিতি হিসাব করে:

// CPI with stalls: CPI = ideal CPI (1) + sum over causes of (frequency x stall cycles)
function cpi(name, causes, clockPs) {
  let c = 1;
  const parts = causes.map(([what, freq, stalls]) => {
    c += freq * stalls;
    return `${freq.toFixed(3)} x ${stalls} [${what}]`;
  });
  const tPerInstr = c * clockPs;                       // average time per instruction
  console.log(`${name}\n   CPI = 1 + ${parts.join(" + ")} = ${c.toFixed(3)}` +
              `  -> ${clockPs} ps clock, ${tPerInstr.toFixed(1)} ps per instruction`);
  return tPerInstr;
}
// A) forwarding: 25% loads, 40% of them followed immediately by a user of the result
const A = cpi("A) forwarding, load-use only", [["load-use", 0.25 * 0.40, 1]], 250);
// B) same program after compiler scheduling removes 3/4 of the load-use pairs
const B = cpi("B) forwarding + scheduling", [["load-use", 0.25 * 0.40 * 0.25, 1]], 250);
// C) no forwarding: 30% of instructions have a user at distance 1 (2 stalls),
//    another 10% only at distance 2 (1 stall); no forwarding muxes -> 240 ps clock
const C = cpi("C) no forwarding", [["dist-1", 0.30, 2], ["dist-2", 0.10, 1]], 240);
// D) A plus a non-pipelined divider: 0.5% of instructions are a divide that
//    finds the divider still busy and waits 12 cycles on average
const D = cpi("D) A + busy divider", [["load-use", 0.10, 1], ["div-busy", 0.005, 12]], 250);
console.log(`\nA vs C: forwarding is ${(C / A).toFixed(3)} times faster (even with a slower clock)`);
console.log(`B vs A: scheduling gives ${(A / B).toFixed(3)} times;  D vs A: ${(D / A).toFixed(3)} times slower`);

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

A) forwarding, load-use only
   CPI = 1 + 0.100 x 1 [load-use] = 1.100  -> 250 ps clock, 275.0 ps per instruction
B) forwarding + scheduling
   CPI = 1 + 0.025 x 1 [load-use] = 1.025  -> 250 ps clock, 256.3 ps per instruction
C) no forwarding
   CPI = 1 + 0.300 x 2 [dist-1] + 0.100 x 1 [dist-2] = 1.700  -> 240 ps clock, 408.0 ps per instruction
D) A + busy divider
   CPI = 1 + 0.100 x 1 [load-use] + 0.005 x 12 [div-busy] = 1.160  -> 250 ps clock, 290.0 ps per instruction

A vs C: forwarding is 1.484 times faster (even with a slower clock)
B vs A: scheduling gives 1.073 times;  D vs A: 1.055 times slower

A হলো ক্লাসিক উত্তর, CPI ১.১. B-তে compiler তিন-চতুর্থাংশ load-use জোড়া সরিয়েছে: হার ০.১ × ০.২৫ = ০.০২৫, CPI ১.০২৫. C-তে forwarding নেই: ৩০% instruction-এর ঠিক পরেরটি নির্ভরশীল (২ stall), আরও ১০%-এর নির্ভরশীল instruction আছে শুধু দূরত্ব ২-এ (১ stall) — CPI ১.৭, যদিও clock একটু দ্রুত (২৪০ ps). তবুও প্রতি instruction-এ ৪০৮ ps বনাম ২৭৫ ps — forwarding ১.৪৮৪ গুণ দ্রুত। D দেখায় structural hazard-এর প্রভাব: মাত্র ০.৫% instruction ব্যস্ত divider-এর জন্য গড়ে ১২ cycle অপেক্ষা করলেও CPI ১.১০ থেকে ১.১৬-তে যায়, প্রায় ৫.৫% ধীর। ঘটনা বিরল হলেও stall লম্বা হলে গুণফল চোখে পড়ার মতো।

পরীক্ষার numerical: pipeline বনাম single-cycle

একটি single-cycle প্রসেসরের clock ৮০০ ps (পর্ব ৩২-এর ধাঁচে, সবচেয়ে ধীর instruction-এর পথ ধরে)। একই ISA-র ৫-stage pipelined সংস্করণে clock ২৫০ ps (সবচেয়ে ধীর stage আর pipeline register-এর overhead মিলিয়ে), forwarding আছে, আর load-use পরিস্থিতি উপরের A-র মতো। Speedup কত?

  1. Single-cycle: CPI = ১, প্রতি instruction-এ ৮০০ ps.
  2. Pipeline: CPI = ১.১, প্রতি instruction-এ ১.১ × ২৫০ = ২৭৫ ps.
  3. Speedup = ৮০০ / ২৭৫ ≈ ২.৯১ — আদর্শ ৫ নয়, কারণ (ক) stage-গুলো সমান ভাগ হয়নি (৮০০ / ৫ = ১৬০ ps-এর বদলে ২৫০ ps), আর (খ) stall-এর কারণে CPI ১-এর বেশি।

এই দুটি কারণ মনে রাখুন — pipeline-এর speedup নিয়ে যেকোনো প্রশ্নে উত্তর এখান থেকেই আসে।

৯ · লম্বা pipeline, multi-cycle unit আর বাস্তব core

আমাদের ৫-stage pipeline-এ সব instruction একই দৈর্ঘ্যের। বাস্তবে floating-point যোগ, গুণ বা ভাগ এক cycle-এ শেষ হয় না। Hennessy–Patterson-এর একটি পাঠ্যবই-উদাহরণে FP যোগের unit ৪ stage-এর, গুণের unit ৭ stage-এর, আর ভাগের unit pipeline-হীন, প্রায় দুই ডজন cycle নেয়। এমন multi-cycle functional unit এলে তিনটি নতুন সমস্যা দেখা দেয়:

  • RAW stall লম্বা হয়: একটি গুণের ফল ৭ cycle পরে আসে, তাই তার ব্যবহারকারী forwarding সহও কয়েক cycle অপেক্ষা করে। Compiler scheduling তখন আরও জরুরি।
  • WAW hazard ফিরে আসে: fmul.d f0, f2, f4-এর পরে fadd.d f0, f6, f8 এলে যোগটি আগে শেষ হতে পারে — তারপর ধীর গুণটি f0-এ লিখে ঠিক মানটি মুছে দেবে। Hardware-কে তখন দ্বিতীয় লেখাটি আটকাতে বা প্রথমটি বাতিল করতে হয়।
  • Register write port-এর structural hazard আর ক্রমভাঙা শেষ: ভিন্ন দৈর্ঘ্যের instruction একই cycle-এ WB-তে পৌঁছাতে পারে, আর পরের instruction আগে শেষ হতে পারে — তখন কোনো exception হলে “কোন পর্যন্ত কাজ হয়েছে” বলা কঠিন (precise exception, পর্ব ৩৮)।

লম্বা (গভীর) pipeline-এ — যেখানে memory পড়া কয়েকটি stage নেয় — load-use-এর দূরত্বও বাড়ে। আধুনিক অনেক x86-64 আর ARM core-এ L1 data cache থেকে load-to-use latency প্রায় ৪–৫ cycle বলে প্রকাশিত তথ্যে দেখা যায়, তাই ঠিক পরের instruction-কে সেই ক’টি cycle অপেক্ষা করতে হয় (যদি না অন্য স্বাধীন কাজ পাওয়া যায়)।

ইতিহাস: scoreboard থেকে renaming

১৯৬৪ সালে Control Data Corporation-এর CDC 6600 (নকশায় Seymour Cray আর James Thornton) কয়েকটি স্বাধীন functional unit-এ instruction পাঠাত আর একটি কেন্দ্রীয় টেবিল — scoreboard — দিয়ে হিসাব রাখত কোন unit ব্যস্ত, কোন register-এর মান কে লিখবে। RAW-এ সে অপেক্ষা করাত, WAW-এ নতুন instruction পাঠানো থামাত, আর WAR-এ লেখা পিছিয়ে দিত। এর কয়েক বছর পর IBM System/360 Model 91-এ Robert Tomasulo-র পদ্ধতি register-এর নাম বদলে (register renaming) WAR আর WAW-কে পুরোপুরি মুছে দেয় — আজকের out-of-order core-এর ভিত্তি এটিই, বিস্তারিত পর্ব ৪০-এ।

আরও গভীরে: আধুনিক superscalar core প্রতি cycle-এ কয়েকটি instruction চালায় (পর্ব ৩৯), তাই প্রতিটি ফলকে অনেকগুলো ALU-র অনেকগুলো ইনপুটে পৌঁছাতে হয় — এই তারের জালকে বলে bypass network, আর issue width বাড়লে এর আকার মোটামুটি বর্গের হারে বাড়ে। এজন্য অনেক নকশায় execution unit-গুলো কয়েকটি “cluster”-এ ভাগ করা হয়, যেখানে cluster-এর ভেতরে forwarding তাৎক্ষণিক কিন্তু এক cluster থেকে আরেকটিতে এক cycle বেশি লাগে — DEC Alpha 21264 এমন নকশার একটি পরিচিত উদাহরণ। Bypass network-এর তার প্রায়ই chip-এর critical path আর power-এর একটি বড় অংশ নির্ধারণ করে।

পরীক্ষা ও ইন্টারভিউতে — কয়েকটি প্রচলিত ধরনের প্রশ্ন আর এক লাইনের উত্তর:

  • তিন রকম hazard কী কী, একটি করে উদাহরণ দিন। Structural (একটি memory port), data (add-এর পরে তার ফল ব্যবহার), control (branch-এর পরের instruction অজানা)।
  • ৫-stage in-order pipeline-এ WAR আর WAW কেন hazard নয়? সবাই একই stage-এ (ID) পড়ে আর একই stage-এ (WB) ক্রমানুসারে লেখে, তাই পড়া-লেখার ক্রম উল্টায় না।
  • Forwarding থাকলেও কোন ক্ষেত্রে stall লাগে? Load-use: lw-এর ঠিক পরের instruction তার ফল চাইলে ১ cycle, কারণ মান MEM-এর শেষে আসে।
  • Forwarding unit-এ rd ≠ 0 শর্ত কেন? x0-তে লেখা ফেলে দেওয়া হয়, তাই সেই “ফল” forward করলে x0-কে অশূন্য দেখাবে — ভুল।
  • Double data hazard-এ EX/MEM-কে অগ্রাধিকার কেন? EX/MEM-এর instruction প্রোগ্রামে পরে, তাই তার মানটিই সর্বশেষ।

১০ · AI-এর যুগে — pipeline-এর হিসাব AI দিয়ে, যাচাই নিজে

Pipeline diagram আঁকা আর cycle গোনা ঠিক সেই ধরনের কাজ, যেখানে AI আত্মবিশ্বাসের সাথে ভুল করে। এই বিষয়ে AI-এর সাধারণ ভুলগুলো:

  • MIPS আর RISC-V-এর রীতি মিশিয়ে ফেলা — যেমন পুরোনো MIPS-এর load delay slot বা $t0-ধাঁচের register, অথবা ForwardA-র encoding উল্টে বলা (01 আর 10 অদলবদল)।
  • Register file-এর অর্ধ-cycle কৌশল ভুলে গিয়ে forwarding ছাড়া পরপর instruction-এ ৩টি stall বলা — অথবা উল্টোটা, কোন রীতি ধরছে তা না বলেই।
  • Forwarding থাকলেও load-use stall বাদ দেওয়া, বা ALU-র ফলের জন্যও stall বসানো।
  • WAR/WAW-কে in-order ৫-stage pipeline-এ hazard বলা, বা মাঝখানে অন্য লেখক থাকলেও দূরের জোড়াকে RAW গোনা।
  • মোট cycle-এ pipeline ভরার ৪ cycle যোগ না করা, বা “৪০% load-use” কে সব instruction-এর ৪০% ধরে CPI হিসাব করা।

AI-কে শিক্ষক হিসেবে ব্যবহার করুন — রীতি আগে ঠিক করে দিন, আর প্রতিটি cycle দেখাতে বলুন, যাতে মেলানো যায়:

আমি কম্পিউটার আর্কিটেকচার শিখছি। নিচের RISC-V (RV32I) instruction-এর ক্রমটি বিশ্লেষণ করো। রীতি: Patterson–Hennessy-র ৫-stage pipeline (IF, ID, EX, MEM, WB); register file প্রথম অর্ধেকে লেখে, দ্বিতীয় অর্ধেকে পড়ে; forwarding শুধু EX/MEM→EX আর MEM/WB→EX; branch নেই। ১. প্রতিটি জোড়ার RAW, WAR, WAW dependence আর দূরত্ব একটি টেবিলে দাও; মাঝখানে অন্য কেউ একই register-এ লিখলে সেই জোড়া বাদ দাও। ২. Forwarding ছাড়া আর forwarding সহ — দুটির cycle-by-cycle pipeline diagram আঁকো, stall চিহ্নিত করো। ৩. প্রতিটি forwarding-এর ক্ষেত্রে ForwardA/ForwardB-এর মান (00/10/01) আর কোন pipeline register থেকে এল তা লেখো। ৪. মোট cycle = n + 4 + stall দিয়ে মিলিয়ে দেখাও। ৫. শেষে একটি ছোট JavaScript স্ক্রিপ্ট দাও, যা দিয়ে আমি নিজে cycle সংখ্যা যাচাই করতে পারি। Instruction-এর ক্রম: [এখানে আপনার কোড লিখুন]

AI-এর দেওয়া pipeline-উত্তর যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Hazard সামলানোর দায়িত্ব hardware-এর না compiler-এর? “MIPS” নামের ইতিহাস আর load delay slot থেকে কী শেখার আছে?

১৯৮০-এর দশকের শুরুতে Stanford বিশ্ববিদ্যালয়ে John Hennessy-র দল যে গবেষণা প্রসেসর বানায়, তার নাম MIPS — প্রচলিত ব্যাখ্যায় “Microprocessor without Interlocked Pipeline Stages”. নামেই দর্শনটি লুকিয়ে আছে: pipeline-এর stage-গুলোর মধ্যে hardware interlock (stall করানোর যন্ত্র) থাকবে না, hazard এড়ানোর দায়িত্ব compiler-এর। সে সময় প্রতিটি transistor দামি, আর hazard detection-এর লজিক clock cycle-কেও লম্বা করতে পারত। যুক্তি ছিল — compiler পুরো প্রোগ্রাম দেখে, তাই সে hardware-এর চেয়ে ভালোভাবে instruction সাজাতে পারবে।

এর একটি দৃশ্যমান ফল ছিল load delay slot: প্রথম প্রজন্মের বাণিজ্যিক MIPS (MIPS I) ISA-তে load-এর ঠিক পরের instruction load-এর ফল দেখবে না — এটি ISA-র নিয়মেই লেখা ছিল। Compiler সেখানে একটি স্বাধীন instruction বসাত, না পেলে একটি nop. এই পর্বের scheduling-এর কৌশলটিই, কিন্তু বাধ্যতামূলক। Branch-এর ক্ষেত্রেও একই রকম “branch delay slot” ছিল (পর্ব ৩৭)।

সমস্যা দেখা দিল পরের প্রজন্মে। Delay slot একটি নির্দিষ্ট pipeline-এর গঠন ISA-তে স্থায়ী করে দেয়। পরের chip-এ pipeline লম্বা হলে load-use latency দুই বা তিন cycle হয় — কিন্তু পুরোনো binary তো এক slot ধরেই লেখা! তাই পরের প্রজন্মের MIPS-এ (MIPS II থেকে) load delay slot বাদ দিয়ে hardware interlock যোগ করা হয় বলে জানা যায়। শিক্ষাটি গভীর: ISA একটি দীর্ঘমেয়াদি চুক্তি (পর্ব ২৫), আর microarchitecture-এর সাময়িক বৈশিষ্ট্য ISA-তে ঢোকালে পরে তার বোঝা টানতে হয়। RISC-V নকশার সময় এই অভিজ্ঞতা মাথায় রেখেই কোনো delay slot রাখা হয়নি।

তবে compiler-এর ভূমিকা হারায়নি — বদলেছে। আজ hardware সঠিকতা নিশ্চিত করে (interlock), আর compiler গতি বাড়ায় (scheduling). Itanium-এর মতো VLIW নকশা (পর্ব ৩৯) আবার compiler-কে বেশি দায়িত্ব দিতে চেয়েছিল; সাধারণ-উদ্দেশ্যের প্রসেসরের বাজারে সেটি তেমন সফল হয়নি, যদিও DSP আর কিছু বিশেষ chip-এ এই পদ্ধতি আজও চলে।

মূল উপলব্ধি: সঠিকতা hardware-এর দায়িত্ব হলে পুরোনো প্রোগ্রাম নতুন chip-এও ঠিক চলে; গতির জন্য compiler-এর সাহায্য তখন ঐচ্ছিক বোনাস। ISA-তে pipeline-এর খুঁটিনাটি ঢোকানো স্বল্পমেয়াদে লাভ, দীর্ঘমেয়াদে ঋণ।

প্র ০২১৯৬৪ সালের CDC 6600-এর scoreboard কীভাবে hazard সামলাত, আর তার সীমা কোথায় ছিল?

CDC 6600 সাধারণত প্রথম সফল “সুপারকম্পিউটার” হিসেবে পরিচিত। Seymour Cray আর James Thornton-এর দল এতে দশটি স্বাধীন functional unit রাখেন — যোগ, গুণ, ভাগ, shift, boolean ইত্যাদির জন্য আলাদা। ধারণা ছিল, একটি ধীর ভাগ চলার সময় অন্য unit-গুলো স্বাধীন instruction চালিয়ে যাবে। কিন্তু তখন instruction-গুলো ক্রম ভেঙে শেষ হবে, আর এই পর্বে যে “শুধু RAW” নিশ্চয়তা পেয়েছি, তা আর থাকে না — WAR আর WAW-ও বিপদ হয়ে ওঠে।

সমাধান ছিল scoreboard: একটি কেন্দ্রীয় হিসাবের খাতা, যেখানে লেখা থাকে প্রতিটি functional unit ব্যস্ত কি না, সে কোন register-এ লিখবে, তার source register-গুলো প্রস্তুত কি না, আর প্রতিটি register-এর মান কোন unit তৈরি করছে। প্রতিটি instruction চারটি ধাপে যায়: issue (unit খালি আর WAW নেই হলে পাঠানো), read operands (RAW মিটলে), execute, write result (WAR মিটলে)। অর্থাৎ WAW-এ issue থামে, RAW-এ operand পড়া থামে, আর WAR-এ লেখা থামে — তিন dependence-এর জন্য তিন জায়গায় অপেক্ষা।

সীমাও ছিল স্পষ্ট। Scoreboard dependence দূর করে না, শুধু অপেক্ষা করায়। WAR আর WAW আসলে নামের সংঘাত — আলাদা register পেলেই মিলিয়ে যেত — অথচ scoreboard সেখানেও সময় নষ্ট করত। আরও ছিল forwarding-এর অভাব: ফল register-এ লেখার পরেই পরের unit পড়তে পারত। এই দুর্বলতাগুলোই কয়েক বছর পর IBM System/360 Model 91-এর জন্য Robert Tomasulo-র পদ্ধতিতে সমাধান হয় — reservation station আর register-এর নাম বদলে WAR-WAW মুছে ফেলা, আর একটি common data bus দিয়ে ফল সরাসরি অপেক্ষমাণ instruction-এ পৌঁছে দেওয়া (এটিও এক ধরনের forwarding). পর্ব ৪০-এ এর বিস্তারিত আছে।

মজার বিষয়, “scoreboard” শব্দটি আজও ব্যবহৃত হয় — অনেক সরল in-order core-এ প্রতিটি register-এর জন্য একটি “pending” bit রাখা হয়, যা বলে কোনো দীর্ঘ instruction (যেমন cache miss হওয়া load) এখনো তার মান লিখছে কি না। এই পর্বের hazard detection unit তারই একটি ছোট রূপ।

মূল উপলব্ধি: Instruction-কে ক্রম ভেঙে চালাতে চাইলে hardware-কে প্রতিটি dependence-এর হিসাব রাখতে হয়। শুধু অপেক্ষা করানো (scoreboard) সঠিকতা দেয়; নাম বদলানো আর সরাসরি ফল পৌঁছানো (Tomasulo) গতিও দেয়।

প্র ০৩Forwarding কি বিনামূল্যে? বাস্তব নকশাকারী কখন একটি forwarding পথ বাদ দিতে পারেন?

পাঠ্যবইয়ের ৫-stage pipeline-এ forwarding প্রায় বিনামূল্যে মনে হয় — দুটি তিন-ইনপুটের mux, চারটি ৫-bit comparator. কিন্তু প্রতিটি mux ALU-র ঠিক সামনে বসে, অর্থাৎ EX stage-এর critical path-এ। ALU-র ফল EX/MEM register থেকে বের হয়ে তার বেয়ে mux পেরিয়ে আবার ALU-তে ঢোকে — এই পুরো চক্র এক cycle-এ শেষ হতে হবে। এজন্যই অংশ ৮-এর হিসাবে forwarding-ওয়ালা নকশার clock সামান্য ধীর ধরা হয়েছে। লাভ (CPI ১.৭ থেকে ১.১) এত বড় যে দাম মেনে নেওয়াই যুক্তিসংগত।

কিন্তু প্রসেসর প্রশস্ত হলে হিসাব বদলায়। ধরুন প্রতি cycle-এ ৪টি instruction চলে, প্রতিটির ২টি operand, আর ফল আসতে পারে ৪টি unit-এর ২–৩টি ভিন্ন stage থেকে। তাহলে প্রতিটি operand mux-এর ইনপুট ১০টির বেশি, আর তারের সংখ্যা মোটামুটি (unit সংখ্যা)²-এর হারে বাড়ে। ছোট transistor-এর যুগে তারের দেরি transistor-এর দেরির চেয়ে কম হারে কমেছে, তাই লম্বা bypass তার একসময় পুরো chip-এর গতি ঠিক করে দিতে পারে। Power-ও বাড়ে — প্রতি cycle-এ লম্বা তারে সংকেত চালাতে energy লাগে।

নকশাকারীদের হাতে কয়েকটি আপস আছে। একটি হলো clustering: unit-গুলোকে দলে ভাগ করা, দলের ভেতরে পূর্ণ bypass, দলের বাইরে এক cycle বেশি — DEC Alpha 21264-এর integer অংশ এভাবে দুটি cluster-এ ভাগ ছিল বলে বর্ণিত আছে। আরেকটি হলো কিছু বিরল পথ বাদ দেওয়া: ধরুন floating-point unit থেকে integer unit-এ সরাসরি পথ রাখার বদলে register file দিয়ে যেতে দেওয়া, কারণ এমন ঘটনা কম। তৃতীয়টি হলো কিছু ফল একটু দেরিতে পাঠানো, আর scheduler-কে (hardware বা compiler) সেই দেরি জানিয়ে রাখা।

এখানে Amdahl-এর সূত্রের (পর্ব ০৩) ভাবনাই খাটে: একটি forwarding পথ রাখার মূল্য = সে কত ঘন ঘন ব্যবহৃত হয় × প্রতিবার কত cycle বাঁচায়, আর দাম = clock-এর ওপর তার প্রভাব সব instruction-এর ওপর। নকশাকারীরা বাস্তব প্রোগ্রামের trace চালিয়ে মাপেন কোন পথ কতবার ব্যবহৃত হয়, তারপর সিদ্ধান্ত নেন।

মূল উপলব্ধি: Forwarding CPI কমায় কিন্তু clock আর power-এর দাম নেয়; ছোট নকশায় দাম নগণ্য, প্রশস্ত নকশায় এটি একটি প্রধান সীমা। প্রতিটি পথের সিদ্ধান্ত মাপা ব্যবহারের হার দিয়ে নিতে হয়।

প্র ০৪প্রসেসর L1-এ instruction আর ডেটা আলাদা রাখে, অথচ প্রোগ্রাম ও ডেটা একই memory-তে থাকে। এতে self-modifying কোড বা JIT compiler-এর কী সমস্যা হয়?

Structural hazard এড়াতে L1 instruction cache আর L1 data cache আলাদা — এটি “ভেতরে Harvard, বাইরে von Neumann” নকশা। Main memory একটিই, তাই প্রোগ্রাম ডেটার মতোই লেখা যায় — von Neumann-এর stored-program ধারণার মূল শক্তি (পর্ব ০১)। কিন্তু দুটি L1 আলাদা হলে একটি প্রশ্ন ওঠে: কোনো প্রোগ্রাম যদি store দিয়ে memory-তে নতুন instruction লেখে, সেই লেখা যায় D-cache-এ। I-cache-এ হয়তো তখনো সেই ঠিকানার পুরোনো instruction বসে আছে, আর pipeline-এ হয়তো পুরোনো instruction আগেই fetch হয়ে গেছে!

এমন কোড আজ খুব প্রচলিত — JavaScript বা Java-র JIT compiler চলার সময় machine code বানিয়ে memory-তে লেখে আর সেটি চালায়; debugger breakpoint বসাতে instruction বদলায়; OS নতুন প্রোগ্রাম load করে। তাই ISA-কে বলে দিতে হয়, এমন অবস্থায় কী নিশ্চয়তা আছে।

ISA-ভেদে উত্তর আলাদা। x86 ঐতিহাসিকভাবে খুব শক্ত নিশ্চয়তা দেয় — hardware নিজেই store-এর ঠিকানা আর fetch হওয়া instruction মিলিয়ে দেখে, প্রয়োজনে pipeline খালি করে (যদিও বিশেষ ক্ষেত্রে serializing instruction-এর দরকার নিয়ে Intel-এর manual-এ নিয়ম আছে)। ARM আর RISC-V সফটওয়্যারকে দায়িত্ব দেয়: RISC-V-এ fence.i instruction (Zifencei extension) বলে “এখন থেকে instruction fetch আমার আগের store-গুলো দেখবে”। ARM-এ cache পরিষ্কার করার আর instruction synchronization barrier-এর একটি নির্দিষ্ট ক্রম মানতে হয়। Multicore-এ আরও জটিল, কারণ অন্য core-এর I-cache-ও পুরোনো থাকতে পারে, তাই সাধারণত OS-এর সাহায্য লাগে।

এই ভিন্নতা একটি নকশা-আপস: hardware-এ স্বয়ংক্রিয় পরীক্ষা সফটওয়্যারের জন্য সহজ কিন্তু chip-এ লজিক ও power খরচ করে, আর বিরল ঘটনার জন্য সাধারণ পথকে জটিল করে। সফটওয়্যারের দায়িত্বে দিলে hardware সরল হয়, কিন্তু JIT লেখকদের সাবধান থাকতে হয় — একটি fence ভুলে গেলে মাঝে মাঝে, ধরা-কঠিন ভাবে ভুল চলে।

মূল উপলব্ধি: একটি structural hazard-এর সমাধান (split cache) নতুন একটি সঠিকতার প্রশ্ন তৈরি করে (instruction আর ডেটার দুই কপি মেলানো)। Architecture-এ প্রায় প্রতিটি সমাধানের সাথে একটি নতুন চুক্তি আসে, যা ISA-তে স্পষ্ট লিখতে হয়।

প্র ০৫Pipeline যত গভীর, clock তত দ্রুত — তাহলে ২০ বা ৩০ stage-এর pipeline কেন সবসময় ভালো নয়? Hazard এখানে কী ভূমিকা রাখে?

Pipeline গভীর করলে প্রতিটি stage-এর কাজ ছোট হয়, তাই clock বাড়ানো যায় — পর্ব ৩৫-এর হিসাব। কিন্তু hazard-এর stall cycle-এ মাপা হয়, আর গভীর pipeline-এ একই ঘটনার stall বেশি cycle নেয়। আমাদের ৫-stage-এ load-use মানে ১ cycle; memory পড়া যদি তিনটি stage-এ ভাগ হয়, তাহলে load-use ৩ cycle. Branch ভুল অনুমানের দাম (পর্ব ৩৭) আরও বাড়ে, কারণ branch-এর ফল জানতে বেশি stage লাগে, আর ততগুলো ভুল instruction ফেলে দিতে হয়।

এর সাথে আছে pipeline register-এর overhead: প্রতিটি stage-এর শেষে flip-flop-এর setup time আর clock-to-q delay (পর্ব ১৮) যোগ হয়, যা stage ছোট করলে কমে না। ধরুন মোট লজিক ২০০০ ps আর প্রতিটি register-এ ৫০ ps overhead. ১০ stage-এ cycle = ২০০ + ৫০ = ২৫০ ps; ৪০ stage-এ ৫০ + ৫০ = ১০০ ps — stage চার গুণ হলো, অথচ clock মাত্র আড়াই গুণ দ্রুত। আর CPI-তে hazard-এর অংশ stage-এর প্রায় সমানুপাতিক হারে বাড়ে।

এই দুই বিপরীত টানের মধ্যে একটি সর্বোত্তম গভীরতা থাকে। ২০০২ সালের দিকে কয়েকটি গবেষণা (যেমন IBM-এর Hartstein ও Puzak-এর কাজ) গাণিতিক মডেল আর simulation দিয়ে দেখায় যে পারফরম্যান্সের জন্য একটি সর্বোত্তম pipeline গভীরতা আছে — তার চেয়ে গভীর করলে hazard-এর দাম আর register-এর overhead লাভকে ছাড়িয়ে যায়; সংখ্যাটি নির্ভর করে প্রোগ্রামের ধরন আর transistor প্রযুক্তির ওপর। পরে power-ও হিসাবে ধরে করা গবেষণায় সর্বোত্তম বিন্দু আরও কম গভীরতার দিকে সরে বলে দেখা যায়, কারণ প্রতিটি নতুন pipeline register আর বেশি clock — দুটোই energy খায়।

বাস্তব ইতিহাসেও এটি দেখা গেছে: Intel Pentium 4-এর NetBurst নকশা খুব গভীর pipeline দিয়ে উঁচু clock-এ পৌঁছেছিল (পরের সংস্করণে ৩০-এর কাছাকাছি stage বলে প্রকাশিত), কিন্তু power আর প্রতি cycle-এ কম কাজের কারণে Intel পরে কম গভীর Core নকশায় ফিরে আসে (পর্ব ০৩-এর ভাবনার প্রশ্নও দেখুন)। আজকের উচ্চ-পারফরম্যান্স core-এর pipeline সাধারণত মাঝারি গভীরতার, আর বাকি গতি আসে প্রস্থ (superscalar) আর out-of-order execution থেকে।

মূল উপলব্ধি: Pipeline-এর গভীরতা clock আর CPI-এর মধ্যে একটি আপস — hazard-এর দাম cycle-এ মাপা হয় বলে গভীর pipeline-এ সেটি বাড়ে। সঠিক উত্তর লৌহসূত্রের গুণফল থেকে আসে, কোনো একটি গুণক থেকে নয়।

অনুশীলন

পরীক্ষার ধাঁচের সমস্যা — রীতি: RISC-V ৫-stage, register file প্রথম অর্ধেকে লেখে আর দ্বিতীয় অর্ধেকে পড়ে, forwarding EX/MEM→EX আর MEM/WB→EX, branch নেই। আগে নিজে করুন, তারপর পূর্ণ সমাধান মিলিয়ে নিন।

  1. সব dependence আর hazard চিহ্নিত করুন:
    I1: add  x1, x2, x3
    I2: lw   x4, 0(x1)
    I3: sub  x5, x4, x1
    I4: sw   x5, 8(x1)
    I5: addi x1, x1, 4
    (ক) প্রতিটি RAW, WAR, WAW জোড়া register আর দূরত্বসহ লিখুন। (খ) কোনগুলো forwarding ছাড়া hazard, আর কোনগুলো forwarding সহ? (গ) দুই ক্ষেত্রে মোট cycle কত?

    (ক) RAW (৬টি): I1→I2 (x1, d = ১), I1→I3 (x1, d = ২), I1→I4 (x1, d = ৩), I1→I5 (x1, d = ৪), I2→I3 (x4, d = ১), I3→I4 (x5, d = ১)। WAR (৩টি): I2→I5, I3→I5, I4→I5 — তিনজনেই x1 পড়ে, তারপর I5 x1-এ লেখে। WAW (১টি): I1→I5 (x1).

    (খ) Forwarding ছাড়া hazard: d = ১ বা ২-এর RAW — I1→I2 (২ stall), I1→I3 (১ stall, কিন্তু I1→I2-এর stall-এ এমনিতেই মিটে যায়), I2→I3 (২), I3→I4 (২)। I1→I4 আর I1→I5 দূরত্ব ৩ ও ৪, তাই register file-ই যথেষ্ট। WAR ও WAW কোনোটিই hazard নয় (in-order, একই stage-এ পড়া ও লেখা)। Forwarding সহ hazard: শুধু I2→I3 — load-use, ১ stall. Forward হয় তিনটি: I1→I2 EX/MEM থেকে, I3→I4 (store data) EX/MEM থেকে, আর I2→I3-এর x4 stall-এর পরে MEM/WB থেকে। I1→I3-এর x1 আসে register file থেকে (stall-এর ফলে I3-এর শেষ ID ঠিক I1-এর WB-এর cycle-এ); I1→I4 আর I1→I5-ও register file থেকে।

    (গ) Forwarding ছাড়া মোট stall ৬ (I2-এ ২, I3-এ ২, I4-এ ২; I5-এর x1 দূরত্ব ৪, অপেক্ষা নেই) — মোট ৫ + ৪ + ৬ = ১৫ cycle. Forwarding সহ ৫ + ৪ + ১ = ১০ cycle. (অংশ ৬-এর simulator-এ এই ক্রম বসিয়ে যাচাই করা।)

  2. Pipeline diagram আঁকুন আর cycle গুনুন: lw x2, 0(x1) · lw x3, 4(x1) · add x4, x2, x3 · sw x4, 8(x1). (ক) forwarding ছাড়া আর (খ) forwarding সহ cycle-by-cycle diagram আঁকুন। (গ) Forwarding সহ add-এর দুটি operand কোথা থেকে আসে, ForwardA আর ForwardB-এর মান কত?

    (ক) Forwarding ছাড়া — ১২ cycle (** = stall):

    cycle           1   2   3   4   5   6   7   8   9   10  11  12
    lw  x2, 0(x1)   IF  ID  EX  MEM WB
    lw  x3, 4(x1)       IF  ID  EX  MEM WB
    add x4, x2, x3          IF  ID  **  **  EX  MEM WB
    sw  x4, 8(x1)               IF  **  **  ID  **  **  EX  MEM WB

    add-কে x3-এর জন্য cycle ৬ পর্যন্ত (দ্বিতীয় lw-এর WB) অপেক্ষা করতে হয় — ২ stall; sw-কে x4-এর জন্য cycle ৯ পর্যন্ত — আরও ২. মোট ৪ + ৪ + ৪ = ১২.

    (খ) Forwarding সহ — ৯ cycle:

    cycle           1   2   3   4   5   6   7   8   9
    lw  x2, 0(x1)   IF  ID  EX  MEM WB
    lw  x3, 4(x1)       IF  ID  EX  MEM WB
    add x4, x2, x3          IF  ID  **  EX  MEM WB
    sw  x4, 8(x1)               IF  **  ID  EX  MEM WB

    (গ) add-এর EX cycle ৬-এ। x3 দ্বিতীয় lw থেকে, যা তখন WB-তে — MEM/WB থেকে, তাই ForwardB = 01. x2 প্রথম lw থেকে, যে cycle ৫-এ WB-তে লিখেছে, আর add-এর শেষ ID-ও cycle ৫-এ — অর্ধ-cycle কৌশলে register file থেকেই সঠিক মান, তাই ForwardA = 00. sw-এর store data x4 EX/MEM থেকে (ForwardB = 10). একমাত্র stall হলো দ্বিতীয় lw-এর load-use.

  3. কোড লিখুন — ক্রম বদলে stall সরান: নিচের কোড x = p + q; y = r + s;-এর সরল অনুবাদ (p, q, x, r, s, y যথাক্রমে x10 + 0, 4, 8, 12, 16, 20 ঠিকানায়)। Forwarding সহ মোট cycle কত? অর্থ না বদলে instruction-গুলো এমনভাবে সাজান, যাতে কোনো stall না থাকে, আর নতুন cycle সংখ্যা বলুন।
    lw  x5, 0(x10)
    lw  x6, 4(x10)
    add x7, x5, x6
    sw  x7, 8(x10)
    lw  x8, 12(x10)
    lw  x9, 16(x10)
    add x11, x8, x9
    sw  x11, 20(x10)

    মূল কোড: দুটি load-use — lw x6 → add x7 আর lw x9 → add x11. মোট ৮ + ৪ + ২ = ১৪ cycle.

    একটি সঠিক নতুন ক্রম (stall শূন্য):

    lw  x5, 0(x10)
    lw  x6, 4(x10)
    lw  x8, 12(x10)     # উপরে তোলা: add x7 আর lw x6-এর মাঝে
    add x7, x5, x6      # x6 দুই ঘর দূরে -> MEM/WB থেকে forward
    lw  x9, 16(x10)     # উপরে তোলা: add x11 আর lw x9-এর মাঝে sw x7
    sw  x7, 8(x10)
    add x11, x8, x9     # x9 দুই ঘর দূরে
    sw  x11, 20(x10)

    নতুন মোট ৮ + ৪ = ১২ cycle (speedup ১৪ / ১২ ≈ ১.১৭)। বৈধতার যুক্তি: (১) প্রতিটি RAW-এর ক্রম রক্ষা হয়েছে — প্রতিটি add তার দুটি load-এর পরে, প্রতিটি sw তার add-এর পরে; (২) কোনো register দুই জায়গায় লেখা হয় না, তাই WAR/WAW-এর ঝামেলা নেই; (৩) lw x8, 12(x10) আর lw x9, 16(x10) sw x7, 8(x10)-এর আগে গেছে — একই base x10, ভিন্ন offset, আর x10 বদলায় না, তাই একই ঠিকানা নয়। অন্য ক্রমও সঠিক হতে পারে, যেমন চারটি load আগে, তারপর দুটি add আর দুটি sw. তবে তখন একসাথে বেশি register জীবিত থাকে।

  4. CPI ও সময়ের numerical: একটি প্রোগ্রামে ৩০% load. (ক) Forwarding আছে, আর load-গুলোর ৫০%-এর ঠিক পরের instruction ফলটি ব্যবহার করে। CPI কত? (খ) Compiler scheduling সেই জোড়াগুলোর দুই-তৃতীয়াংশ সরিয়ে দিলে নতুন CPI? (গ) Forwarding ছাড়া একই প্রসেসরে ৩৫% instruction-এর ঠিক পরেরটি নির্ভরশীল, আর আরও ১৫%-এর নির্ভরশীল instruction আছে শুধু দূরত্ব ২-এ; এর clock ২০০ ps, আর forwarding-ওয়ালা সংস্করণের ২১০ ps. (ক)-এর তুলনায় কোনটি কতগুণ দ্রুত? (ঘ) ৮০০ ps-এর single-cycle নকশার তুলনায় (ক)-এর speedup কত?

    (ক) Load-use হার = ০.৩০ × ০.৫০ = ০.১৫; CPI = ১ + ০.১৫ × ১ = ১.১৫.

    (খ) বাকি থাকে এক-তৃতীয়াংশ: ০.১৫ / ৩ = ০.০৫; CPI = ১.০৫.

    (গ) Forwarding ছাড়া CPI = ১ + ০.৩৫ × ২ + ০.১৫ × ১ = ১ + ০.৭০ + ০.১৫ = ১.৮৫. প্রতি instruction-এ সময়: forwarding ছাড়া ১.৮৫ × ২০০ = ৩৭০ ps; forwarding সহ ১.১৫ × ২১০ = ২৪১.৫ ps. Forwarding-ওয়ালা নকশা ৩৭০ / ২৪১.৫ ≈ ১.৫৩২ গুণ দ্রুত, যদিও তার clock ৫% ধীর।

    (ঘ) ৮০০ / ২৪১.৫ ≈ ৩.৩১. আদর্শ ৫ গুণ হয়নি দুই কারণে: stage-গুলো সমান নয় (৮০০ / ৫ = ১৬০ ps হওয়ার কথা, হয়েছে ২১০ ps), আর CPI ১.১৫, ১ নয়। (সব সংখ্যা অংশ ৮-এর calculator-এর সূত্রে স্ক্রিপ্ট দিয়ে যাচাই করা।)

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

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

ক্রমInstruction-এর একটি ক্রম দিয়ে “মোট cycle কত” প্রশ্নের সমাধানের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. প্রতিটি instruction-এর destination আর source register লেখা
  2. সব RAW জোড়া আর তাদের দূরত্ব d খুঁজে বের করা
  3. রীতি অনুযায়ী প্রতিটি জোড়ার stall ঠিক করা (forwarding ছাড়া max(০, ৩ − d), load-use-এ ১)
  4. Cycle-by-cycle diagram এঁকে stall-গুলো বসানো, একসাথে মিটে যাওয়া অপেক্ষা একবারই গোনা
  5. মোট cycle = n + ৪ + stall দিয়ে diagram-এর শেষ WB মিলিয়ে নেওয়া

একটি ক্লাসিক in-order ৫-stage RISC-V pipeline-এ (IF, ID, EX, MEM, WB; সব instruction ID-তে register পড়ে আর WB-তে লেখে) কোন ধরনের data dependence hazard তৈরি করতে পারে?

  • RAW, WAR আর WAW — তিনটিই
  • RAW আর WAW
  • শুধু RAW
  • কোনোটিই নয়

Forwarding নেই, কিন্তু register file cycle-এর প্রথম অর্ধেকে লেখে আর দ্বিতীয় অর্ধেকে পড়ে। add x1, x2, x3-এর ঠিক পরে sub x4, x1, x5 এলে কয়টি stall লাগবে, আর দুটি instruction মোট কত cycle-এ শেষ হবে?

  • ১ stall, ৭ cycle
  • ২ stall, ৮ cycle
  • ৩ stall, ৯ cycle
  • ০ stall, ৬ cycle

RISC-V-এর ক্লাসিক ৫-stage pipeline-এ forwarding আছে। lw x5, 0(x6)-এর ঠিক পরে add x7, x5, x8. কোনটি সঠিক?

  • EX/MEM → EX forwarding দিয়ে কোনো stall ছাড়াই চলবে
  • ২টি stall লাগবে, তারপর register file থেকে পড়বে
  • Compiler nop না বসালে প্রোগ্রাম ভুল ফল দেবে
  • ১টি stall লাগবে, তারপর MEM/WB → EX forwarding

EX stage-এ আছে add x1, x1, x4. EX/MEM-এ আছে একটি instruction যার RegWrite = 1, rd = x1; MEM/WB-এ আছে আরেকটি যার RegWrite = 1, rd = x1. Patterson–Hennessy-র forwarding unit-এ ForwardA কত হবে?

  • 10 (EX/MEM থেকে)
  • 01 (MEM/WB থেকে)
  • 00 (register file থেকে)
  • Stall করে অপেক্ষা

একটি pipelined প্রসেসরে forwarding আছে। প্রোগ্রামে ২০% instruction load, আর তাদের ৩০%-এর ঠিক পরের instruction ফলটি ব্যবহার করে। অন্য কোনো stall না থাকলে CPI কত?

  • ১.৩০
  • ১.০৬
  • ১.২০
  • ১.১২

৫-stage pipeline-এ L1 instruction cache আর L1 data cache আলাদা রাখার প্রধান কারণ কী?

  • Data hazard পুরোপুরি দূর করা
  • Main memory-তে প্রোগ্রাম আর ডেটা আলাদা রাখা বাধ্যতামূলক বলে
  • একই cycle-এ instruction fetch আর ডেটা পড়া-লেখার structural hazard এড়ানো
  • Control hazard এড়ানো

শব্দকোষ

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

মনে রাখুন

  1. তিন hazard: structural (সম্পদ একটি — সমাধান split I/D cache, আলাদা port, অর্ধ-cycle register file), data (ফল এখনো তৈরি বা লেখা হয়নি), control (পর্ব ৩৭)।
  2. RAW, WAR, WAW: in-order ৫-stage pipeline-এ শুধু RAW hazard, কারণ সবাই ID-তে পড়ে আর WB-তে ক্রমানুসারে লেখে।
  3. Forwarding ছাড়া stall = max(০, ৩ − d); forwarding সহ ALU-র ফলে ০, load-use-এ ১. মোট cycle = n + ৪ + stall.
  4. Forwarding unit: RegWrite, rd ≠ 0 আর rd = rs — EX/MEM হলে 10, নইলে MEM/WB হলে 01, নইলে 00; EX/MEM-এর অগ্রাধিকার। Hazard detection: ID/EX.MemRead আর rd = IF/ID.rs1/rs2 হলে PC ও IF/ID আটকে control ০ করে bubble.
  5. CPI = ১ + Σ হার × stall; compiler scheduling load-use লুকায়, আর তুলনা সবসময় CPI × clock দিয়ে।

পরের পর্বে: এতক্ষণ branch সরিয়ে রেখেছিলাম। কিন্তু সাধারণ প্রোগ্রামে প্রায় প্রতি পাঁচ-ছয়টি instruction-এ একটি branch আসে, আর তার ফল জানার আগেই পরের instruction আনতে হয়। পরের পর্বে control hazard, আর static থেকে ২-bit ও আধুনিক branch predictor.

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

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