Foundationপ্রথম নীতি থেকে
LEVEL 3লেসন ১০/১৭অ্যাডভান্সড১ ঘণ্টা ১০ মিনিট

Pipelining ও Hazard — Assembly Line নীতি সিলিকনে

Pipelining and Hazards

Single-cycle CPU-তে instruction একটার পর একটা সম্পূর্ণ শেষ হয়; pipelining-এ সেগুলো ওভারল্যাপ করে চলে — কিন্তু সেই ওভারল্যাপই structural, data, আর control hazard নামের তিনটা নতুন সমস্যা তৈরি করে।

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

  • পাঁচ-স্তরের (IF/ID/EX/MEM/WB) instruction pipeline ব্যাখ্যা করে cycle-by-cycle instruction flow ডায়াগ্রামে trace করতে পারবেন
  • Non-pipelined (single-cycle) বনাম pipelined execution-এর throughput পার্থক্য N+K-1 সূত্র আর ক্লক পিরিয়ডের হিসাব দিয়ে গাণিতিকভাবে দেখাতে পারবেন
  • কেন বাস্তব speedup আদর্শ pipeline depth-এর সমান হয় না তা ব্যাখ্যা করতে পারবেন — stage imbalance, fill/drain overhead, আর hazard-এর প্রভাব আলাদা করে চিহ্নিত করে
  • তিন ধরনের hazard — structural, data, control — প্রতিটাকে সংজ্ঞায়িত করে একটা concrete instruction sequence-এ ঠিক কোন cycle-এ সমস্যাটা ঘটে তা হাতে trace করে দেখাতে পারবেন
  • কেন একটা ভাগ করা instruction/data memory একটা structural hazard তৈরি করে, আর split I-cache/D-cache (Harvard-style access) কীভাবে সেটা দূর করে তা ব্যাখ্যা করতে পারবেন

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

আগে এটা বুঝি

Level ২-এ আপনি নিজের হাতে একটা single-cycle CPU বানিয়েছেন। সেই CPU-র নিয়ম ছিল সহজ, প্রায় নিষ্ঠুরভাবে সহজ: একটা instruction আসে, সেটা fetch হয়, decode হয়, ALU-তে execute হয়, দরকার হলে memory ছোঁয়, রেজিস্টারে লেখে — তারপরই পরের instruction শুরু হয়। পুরো clock cycle-টা এতটাই লম্বা রাখা হয় যাতে সবচেয়ে ধীর instruction-ও (সাধারণত LOAD) পুরোপুরি শেষ হয়ে যেতে পারে একটা মাত্র cycle-এর ভেতর।

এবার একটা রান্নাঘরের কথা ভাবুন। চারজন রাঁধুনি একসাথে কাজ করছেন একটা লম্বা প্রসেসে — সবজি কাটা, রান্না করা, প্লেটে সাজানো, পরিবেশন করা। যদি আপনি নিয়ম করেন “একটা থালা সম্পূর্ণ পরিবেশন না হওয়া পর্যন্ত পরের থালার সবজি কাটা শুরু করা যাবে না” — তাহলে তিনজন রাঁধুনি প্রতি মুহূর্তে অলস বসে থাকবেন। এটাই single-cycle CPU-র অবস্থা: প্রতিটা মুহূর্তে datapath-এর বেশিরভাগ অংশ — instruction memory, ALU, data memory, register file — কিছুই করছে না, শুধু সেই একটা instruction-এর নিজের পালার অপেক্ষায় বসে আছে।

স্বাভাবিক সমাধানটা রান্নাঘরেও যা, সিলিকনেও তা-ই: যেই মুহূর্তে প্রথম থালার সবজি কাটা শেষ, সেটা রান্নার চুলায় পাঠিয়ে দ্বিতীয় থালার সবজি কাটা শুরু করে দাও। চারটা স্টেশন, একই সাথে চারটা আলাদা থালা — কেউ অলস বসে নেই। এই ধারণাটার নাম pipelining, আর এটাই আজকের লেসনের বিষয়।

কিন্তু রান্নাঘরের উপমাটা এখানেই ফুরোয় না। যদি দ্বিতীয় থালার রেসিপিতে লেখা থাকে “প্রথম থালার সসটা একটু ধার নাও” — তাহলে সমস্যা। দ্বিতীয় রাঁধুনি সেই সসের জন্য অপেক্ষা করতে বাধ্য। CPU pipeline-এও ঠিক এই সমস্যাটাই ঘটে, তিনটা ভিন্ন রূপে — আর এই লেসনের দ্বিতীয়ার্ধ পুরোটাই সেই তিনটা রূপ চেনার জন্য।

আজকের প্রশ্ন দুইটা: (১) overlap করলে ঠিক কতটা লাভ হয়, আর সেই লাভ কখন বাস্তবে পুরোপুরি পাওয়া যায় না? (২) overlap করার ফলে ঠিক কী কী নতুন সমস্যা তৈরি হয়?

মূল ধারণা

পাঁচটা স্টেশন

Level ২-এর datapath-কে পাঁচটা ধাপে ভাগ করলে দাঁড়ায়:

স্টেজসংক্ষেপকাজ
Instruction FetchIFPC ব্যবহার করে instruction memory থেকে instruction আনা, PC += 4
Instruction DecodeIDInstruction-এর বিট পড়ে opcode বোঝা, register file থেকে operand পড়া
ExecuteEXALU-তে গণনা — arithmetic, address calculation, branch condition
Memory AccessMEMদরকার হলে data memory-তে read/write (LOAD/STORE)
Write BackWBফলাফল register file-এ লেখা

Level ২-এর single-cycle CPU-তে এই পাঁচটা ধাপই একই clock cycle-এর ভেতর, একটার পর একটা, combinational logic দিয়ে propagate হয়ে যেত — কোনো ধাপের মাঝখানে কোনো latch ছিল না। আজ আমরা প্রতিটা ধাপের মাঝে একটা pipeline register বসাব — একটা সাধারণ D flip-flop-ভিত্তিক রেজিস্টার যেটা এক স্টেজের আউটপুট পরের cycle-এ পরের স্টেজের কাছে ধরে রাখে। এটাই পুরো কৌশলের হার্ডওয়্যার ভিত্তি: IF/ID, ID/EX, EX/MEM, MEM/WB — চারটা pipeline register, পাঁচটা স্টেজের মাঝে।

Pipeline diagram — মূল ভাষা

এই পুরো module-এ আমরা বারবার একটা নির্দিষ্ট ধরনের ছবি আঁকব: সারি করে instruction, কলাম করে cycle, প্রতিটা ঘরে instruction-টা কোন স্টেজে আছে।

Cycle:      1    2    3    4    5    6    7    8    9
Instr 1:    IF   ID   EX   MEM  WB
Instr 2:         IF   ID   EX   MEM  WB
Instr 3:              IF   ID   EX   MEM  WB
Instr 4:                   IF   ID   EX   MEM  WB
Instr 5:                        IF   ID   EX   MEM  WB

এটা পড়ার নিয়ম: প্রতি সারি একটা instruction, প্রতি column একটা clock cycle। প্রতিটা instruction ঠিক এক cycle পর পর শুরু হচ্ছে — instruction 2 fetch হচ্ছে cycle 2-তে, ঠিক যখন instruction 1 decode হচ্ছে। cycle 5-এ লক্ষ্য করুন: instruction 1 write-back করছে (একদম শেষ ধাপ), অথচ instruction 5 তখনও fetch হচ্ছে (একদম প্রথম ধাপ) — পাঁচটা ভিন্ন instruction পাঁচটা ভিন্ন ধাপে, একই মুহূর্তে, একই হার্ডওয়্যারে (কিন্তু পাঁচটা ভিন্ন sub-circuit-এ)।

এই diagram-টাই এই পুরো ব্যাচের (lesson 10-13) কেন্দ্রীয় ভাষা — প্রতিটা hazard, প্রতিটা fix, প্রতিটা misprediction এই একই grid-এ আঁকা হবে।

থ্রুপুট গণিত — কতটা লাভ?

N টা instruction, K-স্টেজ pipeline। উপরের diagram থেকে প্যাটার্নটা স্পষ্ট: শেষ instruction-টা fetch হয় cycle N-এ, আর তারপর তাকে বাকি K−1 টা স্টেজ পার হতে হয়। মোট সময়:

Cyclespipelined=N+K1\text{Cycles}_{\text{pipelined}} = N + K - 1

N=5, K=5 হলে: 5+5−1 = 9 cycle — উপরের diagram-এ ঠিক তাই দেখা যাচ্ছে (instruction 5-এর WB cycle 9-এ)।

তুলনায়, non-pipelined (single-cycle) পদ্ধতিতে প্রতিটা instruction সম্পূর্ণ একা একা K cycle সমান সময় নেয় (একটা লম্বা cycle, যার ভেতর সব স্টেজ sequentially চলে):

Cyclesnon-pipelined=N×K\text{Cycles}_{\text{non-pipelined}} = N \times K

Speedup=N×KN+K1NK\text{Speedup} = \frac{N \times K}{N + K - 1} \xrightarrow{N \to \infty} K

যত N বড় হয়, speedup তত K-এর কাছাকাছি যায় — এটাই “speedup ≈ pipeline depth” দাবিটার সঠিক, সীমাবদ্ধ রূপ। এটা ঠিক mathematics module-এর asymptotic notation লেসনের ভাষায় বলা যায়: Speedup(N) = O(K) as N → ∞, কিন্তু ছোট N-এ এই সীমা অনেক দূরে।

একটা বাস্তব সংখ্যার উদাহরণ

Level ২-এর single-cycle CPU-তে ক্লক পিরিয়ড ঠিক করতে হতো সবচেয়ে ধীর instruction-টার জন্য যথেষ্ট লম্বা করে। ধরা যাক প্রতিটা স্টেজের নিজস্ব combinational delay এরকম (এগুলো বাস্তবসম্মত আনুপাতিক সংখ্যা, real silicon-এর কাছাকাছি):

স্টেজDelay
IF (instruction memory read)200 ps
ID (register read + decode)100 ps
EX (ALU)200 ps
MEM (data memory access)200 ps
WB (register write)100 ps
মোট (single-cycle clock period)800 ps

Single-cycle CPU-তে প্রতিটা instruction, তার ধরন যা-ই হোক (এমনকি ADD, যার MEM স্টেজে কিছুই করার নেই), পুরো ৮০০ ps অপেক্ষা করে — কারণ ক্লক পিরিয়ড একটাই, সবার জন্য সমান, আর সেটা সবচেয়ে ধীর instruction (LOAD) দিয়ে ঠিক হয়েছে।

Pipeline-এ প্রতিটা স্টেজ আলাদা hardware, তাই ক্লক পিরিয়ড ঠিক করা যায় সবচেয়ে ধীর একটা স্টেজ দিয়ে — গোটা instruction দিয়ে না:

Clock periodpipelined=max(200,100,200,200,100)=200 ps\text{Clock period}_{\text{pipelined}} = \max(200, 100, 200, 200, 100) = 200 \text{ ps}

এবার N = 1000 instruction চালিয়ে দেখি সময় কতটা বাঁচে:

সূত্রসময়
Non-pipelined1000 × 800 ps800,000 ps = 800 ns
Pipelined(1000+5−1) × 200 ps200,800 ps ≈ 200.8 ns
Speedup800,000 / 200,800≈ ৩.৯৮×

N বড় হওয়ায় 3.98× প্রায় আদর্শ -এর সমান — fill/drain overhead (K−1 = 4 অতিরিক্ত cycle) মোট 1004 cycle-এর তুলনায় নগণ্য।

কিন্তু N = 5 হলে (ঠিক উপরের diagram-এর মতো ছোট প্রোগ্রাম):

সূত্রসময়
Non-pipelined5 × 800 ps4000 ps
Pipelined(5+5−1) × 200 ps1800 ps
Speedup4000 / 1800≈ ২.২২×

একই hardware, একই clock period, শুধু N ছোট হওয়ায় speedup থেকে নেমে 2.22×-এ। এটা ঠিক mathematics module-এর asymptotic-notation লেসনের সেই শিক্ষার প্রতিধ্বনি: একটা সীমা (এখানে Speedup → K) শুধু N → ∞-এ সত্য; ছোট N-এ “fill” আর “drain” (pipeline ভরা আর খালি হওয়ার সময়) উপেক্ষা করা যায় না।

Cycle:      1    2    3    4    5    6    7    8    9
Instr 1:    IF   ID   EX   MEM  WB
Instr 2:         IF   ID   EX   MEM  WB
Instr 3:              IF   ID   EX   MEM  WB
Instr 4:                   IF   ID   EX   MEM  WB
Instr 5:                        IF   ID   EX   MEM  WB
            └──── fill ────┘              └── drain ──┘

Cycle ১-৪: pipeline এখনো পুরো ভরেনি (সব স্টেজ ব্যস্ত নয়)। Cycle ৬-৯: আর নতুন instruction ঢুকছে না, pipeline খালি হচ্ছে। শুধু cycle ৫-এই সব পাঁচটা স্টেজ একসাথে ব্যস্ত — একটাই “পূর্ণ থ্রুপুট” cycle, বাকি ৮টা আংশিক। N যত বড় হয়, “পূর্ণ থ্রুপুট” cycle-এর অনুপাত তত বাড়ে।

Fill আর drain — pipeline-এর শুরু আর শেষে কিছু স্টেজ খালি থাকে, এটাই ছোট N-এ speedup কমার আসল কারণ।

CPI — hazard-এর খরচ মাপার সাধারণ একক

Pipeline performance আলোচনা করার জন্য computer architecture-এর সবচেয়ে সাধারণ একক হলো CPI — Cycles Per Instruction (প্রতি instruction-এ গড়ে কত cycle লাগছে)। এই সংখ্যাটা পরের দুইটা লেসনেও বারবার ফিরে আসবে, তাই এখনই সংজ্ঞায়িত করা ভালো।

CPI=মোট cycleমোট instruction\text{CPI} = \frac{\text{মোট cycle}}{\text{মোট instruction}}

একটা ideal pipeline-এ (কোনো hazard নেই, প্রতি cycle-এ ঠিক একটা নতুন instruction ঢোকে) স্টেডি-স্টেটে CPI = 1 — বড় N-এ fill/drain-এর প্রভাব নগণ্য হয়ে যায়, তাই Cycles ≈ N, ফলে CPI ≈ 1

যখনই কোনো hazard-এর কারণে একটা instruction-কে stall করতে হয় (পরের লেসনের বিষয়), সেই instruction-এর পেছনে অতিরিক্ত cycle যোগ হয়, আর CPI 1-এর বেশি হয়ে যায়:

CPIactual=CPIideal+গড় stall cycle প্রতি instruction=1+stalls per instruction\text{CPI}_{\text{actual}} = \text{CPI}_{\text{ideal}} + \text{গড় stall cycle প্রতি instruction} = 1 + \text{stalls per instruction}

উদাহরণ: যদি একটা প্রোগ্রামে প্রতি ৪টা instruction-এ গড়ে ১টা data hazard থাকে, আর প্রতিটা hazard গড়ে ২ cycle stall লাগায়, তাহলে:

stalls per instruction=14×2=0.5CPIactual=1.5\text{stalls per instruction} = \frac{1}{4} \times 2 = 0.5 \quad\Rightarrow\quad \text{CPI}_{\text{actual}} = 1.5

বাস্তব speedup তখন CPI_ideal / CPI_actual অনুপাতে কমে যায় — এই লেসনের ideal speedup আসলে 4 / CPI_actual-এ নেমে আসে। CPI যত ১-এর কাছে, pipeline তত তার আদর্শ থ্রুপুটের কাছাকাছি। পরের লেসনে আমরা ঠিক এই CPI সংখ্যাটা দিয়েই forwarding কতটা throughput বাঁচায় তা পরিমাপ করব।

ভেতরে কী ঘটছে

Hazard — যখন overlap করাটাই সমস্যা তৈরি করে

উপরের সবটা ধরে নিয়েছে instruction-গুলো একে অপরের থেকে সম্পূর্ণ স্বাধীন — কেউ কারো পথে বাধা দেয় না। বাস্তব প্রোগ্রামে এই ধারণা প্রায়ই ভুল প্রমাণিত হয়। যখনই overlap করা দুইটা instruction-এর মধ্যে কোনো না কোনো নির্ভরতা থাকে, তাকে বলে hazard — pipeline-এর সঠিকতা বা কার্যকারিতা নষ্ট করার সম্ভাবনা।

তিন ধরনের hazard আছে, আর প্রতিটার কারণ সম্পূর্ণ আলাদা।

১. Structural hazard — একই hardware, দুইটা দাবিদার

সংজ্ঞা: দুইটা instruction একই cycle-এ একই hardware resource ব্যবহার করতে চায়, কিন্তু সেই resource-এর মাত্র একটাই কপি আছে।

সবচেয়ে ক্লাসিক উদাহরণ: instruction memory আর data memory যদি একই physical memory হয় (একটা মাত্র memory port), তাহলে IF স্টেজ (instruction পড়া) আর MEM স্টেজ (data পড়া/লেখা) একই সময়ে সেই একই memory port চাইতে পারে।

উপরের diagram-এই এটা ঘটে। লক্ষ্য করুন cycle ৪:

Cycle:      1    2    3    4    5    6    7    8    9
Instr 1:    IF   ID   EX   MEM  WB
Instr 4:                   IF   ID   EX   MEM  WB

                      cycle 4: Instr 1 এর MEM স্টেজ
                      আর Instr 4 এর IF স্টেজ — দুটোই
                      একই সাথে ঘটছে!

Cycle ৪-এ Instruction 1 তার MEM স্টেজে আছে (হয়তো একটা LOAD — data memory থেকে পড়ছে), আর ঠিক একই cycle-এ Instruction 4 তার IF স্টেজে আছে (instruction memory থেকে পরের instruction আনছে)। যদি instruction memory আর data memory একই hardware port শেয়ার করে, তাহলে দুটো একসাথে সম্ভব না — একজনকে অপেক্ষা করতে হবে।

Structural hazard শুধু memory port-এই সীমাবদ্ধ না। আরও দুইটা সাধারণ উৎস:

  • Register file port সংখ্যা — এক cycle-এ যদি দুইটা instruction একসাথে register লিখতে চায়, কিন্তু register file-এর মাত্র একটা write port থাকে
  • একটা মাত্র shared functional unit — যেমন Level ২-র ALU-design লেসনে দেখা “একটা shared ALU” ডিজাইন যদি pipeline-এর একাধিক স্টেজে (যেমন address গণনা আর মূল ALU অপারেশন) একসাথে দরকার হয়

সমাধানের সাধারণ নীতি সবসময় একই: হয় hardware-এর কপি বাড়াও (দ্বিতীয় memory port, দ্বিতীয় ALU — খরচ বাড়ে), অথবা কাউকে এক cycle অপেক্ষা করাও (stall — throughput কমে)। এটাই একটা ক্লাসিক area-বনাম-throughput trade-off, যা লেসন ১৩-এ superscalar ডিজাইনে আবার ফিরে আসবে অনেক বড় আকারে।

২. Data hazard — ফলাফল এখনো তৈরি হয়নি

সংজ্ঞা: একটা instruction-এর দরকার এমন একটা ফলাফল, যেটা এখনো pipeline-এ থাকা একটা আগের instruction তৈরি করেনি (বা register file-এ লেখেনি)।

এই instruction sequence-টা দেখুন:

ADD  R1, R2, R3     ; R1 ← R2 + R3
SUB  R4, R1, R5     ; R4 ← R1 − R5   (R1 এইমাত্র তৈরি হওয়া মান দরকার!)

SUB তার operand R1 register file থেকে পড়ে তার ID স্টেজে। ADD তার ফলাফল register file-এ লেখে তার WB স্টেজে। সমস্যাটা: এই দুই স্টেজ একই cycle-এ ঘটে না।

Cycle:        1    2    3    4    5    6
ADD R1,R2,R3: IF   ID   EX   MEM  WB
SUB R4,R1,R5:      IF   ID   EX   MEM  WB
                    ↑              ↑
              SUB তার ID-তে   ADD তার WB-তে
              R1 পড়ছে          R1 লিখছে
              (cycle 3)         (cycle 5)

SUB-এর ID স্টেজ ঘটছে cycle ৩-এ — কিন্তু ADD তখনো তার EX স্টেজেই আছে (এমনকি ফলাফলও পুরোপুরি গণনা হয়নি!)। ADD-এর নতুন R1 মান register file-এ পৌঁছাবে cycle ৫-এর WB-তে, তারও পরে (এই লেসনের timing model অনুযায়ী — write সম্পূর্ণ হয় WB cycle-এর শেষে, মান পড়ার জন্য উপলব্ধ হয় তার পরের cycle থেকে)।

ফলাফল: SUB cycle ৩-এ R1-এর যে মান পড়বে, সেটা ADD লেখার আগের, পুরনো (stale) মান — ভুল উত্তর। এই গ্যাপটা ২ cycle-এর (cycle ৩ বনাম cycle ৬, যখন মানটা প্রথম নির্ভরযোগ্যভাবে পড়া যেত)।

এই নির্দিষ্ট প্যাটার্ন — একটা instruction যেটা লিখছে, তার ঠিক পরের instruction-গুলো সেই একই register পড়তে চায় — তাকে বলে Read-After-Write (RAW) hazard, বা true dependency। এটাকে “true” বলার কারণ: এটা প্রোগ্রামের আসল অর্থের অংশ। SUB সত্যিই ADD-এর ফলাফল দরকার — এটা এড়ানো যায় না, শুধু সঠিকভাবে হ্যান্ডেল করতে হয়।

৩. Control hazard — পরের instruction কোনটা, জানি না

সংজ্ঞা: একটা branch instruction-এর ফলাফল (taken নাকি not-taken, আর taken হলে target address কী) pipeline-এর গভীরে না গেলে জানা যায় না — কিন্তু IF স্টেজের প্রতি cycle-েই কোনো-না-কোনো instruction fetch করতে হয়।

Cycle:        1    2    3    4    5
BEQ R1,R0,L:  IF   ID   EX   MEM  WB
     ???:          IF   ??   ??   ??   ??

BEQ R1, R0, L (যদি R1 == R0 হয়, লেবেল L-এ jump করো) cycle ১-এ fetch হলো। কিন্তু cycle ২-এই CPU-কে পরের instruction fetch করতে হবে — অথচ BEQ কী সিদ্ধান্ত নেবে (taken/not-taken) তখনো জানা যায়নি, কারণ সেই সিদ্ধান্তটা নির্ভর করে R1 আর R0-এর তুলনার উপর, যেটা এখনো EX স্টেজেই পৌঁছায়নি।

CPU-র সামনে দুইটা রাস্তা: sequential পরের instruction (PC+4), অথবা branch target (L)। দুটোর কোনটা সঠিক, সেটা না জেনেই কিছু একটা fetch করতে হবে — কারণ pipeline-এর প্রতিটা cycle-এ একটা IF স্টেজ খালি রাখা মানেই একটা সরাসরি throughput ক্ষতি।

এই নির্দিষ্ট সমস্যাটাই সবচেয়ে জটিল, কারণ এখানে “একটু অপেক্ষা করো” সমাধান (data hazard-এর মতো) সবচেয়ে বেশি খরচ করে — প্রতিটা branch-এই এই অপেক্ষা লাগবে, যদি না CPU একটা অনুমান (prediction) করে। এই লেসনে আমরা শুধু সমস্যাটা সংজ্ঞায়িত করছি; এর প্রকৃত সমাধান — branch prediction — পুরো লেসন ১২-এর বিষয়।

তিনটা hazard-এর মূল কারণ — এক নজরে
  1. Structural hazardহার্ডওয়্যার resource একটাই, দাবিদার দুইটা
  2. Data hazardফলাফল এখনো তৈরি হয়নি, পরের instruction-এর দরকার এখনই
  3. Control hazardকোন instruction fetch করব তা-ই জানি না

উদাহরণ

একসাথে সব — একটা পূর্ণ trace

চারটা instruction, বাস্তব কোডে যেমন দেখা যায়:

1: LOAD  R1, 0(R2)     ; R1 ← Memory[R2]
2: ADD   R3, R1, R4    ; R3 ← R1 + R4        (R1-এর উপর নির্ভরশীল — data hazard)
3: SUB   R5, R3, R6    ; R5 ← R3 − R6        (R3-এর উপর নির্ভরশীল — data hazard)
4: BEQ   R5, R0, LABEL ; যদি R5 == 0, LABEL-এ jump          (control hazard)

কোনো fix ছাড়া (এই লেসনের কাজ শুধু hazard চেনা, ঠিক করা না), naive overlap করলে diagram এরকম দাঁড়ায়:

Cycle:              1    2    3    4    5    6    7    8
1 LOAD  R1,0(R2):   IF   ID   EX   MEM  WB
2 ADD   R3,R1,R4:        IF   ID   EX   MEM  WB
3 SUB   R5,R3,R6:             IF   ID   EX   MEM  WB
4 BEQ   R5,R0,L:                   IF   ID   EX   MEM  WB
                          ↑    ↑    ↑    ↑
                       hazard flags (নিচে দেখুন)

Cycle ৩ — Instruction 2-এর ID: R1 পড়তে চায়, কিন্তু Instruction 1 (LOAD) তখনো EX-এ — R1-এর মান memory থেকে এখনো আসেইনি (সেটা আসবে MEM স্টেজে, cycle ৪-এ)। Data hazard।

Cycle ৪ — Instruction 3-এর ID: R3 পড়তে চায়, কিন্তু Instruction 2 (ADD) তখনো EX-এ, ফলাফল এখনো তৈরি হয়নি। আরেকটা data hazard।

Cycle ৫ — Instruction 4-এর ID: R5 পড়তে চায়, কিন্তু Instruction 3 (SUB) তখনো EX-এ। তৃতীয় data hazard — লক্ষ্য করুন এই তিনটা instruction একে অপরের উপর সরাসরি নির্ভর করছে (R1 → R3 → R5), তাই প্রতিটা পরপর instruction পেয়ার-ই hazard তৈরি করছে।

Instruction 4 নিজেই একটা branch — তার ফলাফল জানা যাবে EX স্টেজে, cycle ৬-এ। কিন্তু cycle ৫-৬-এ CPU ইতিমধ্যে পরের instruction fetch করে ফেলবে, না জেনেই branch taken হবে নাকি না। Control hazard।

এই একটা মাত্র চার-instruction sequence-এই তিন ধরনের hazard-ই হাজির — বাস্তব কোডে এটা ব্যতিক্রম না, নিয়ম। পরের দুইটা লেসন এই ঠিক এই sequence-টার data hazard অংশ সমাধান করবে (forwarding ও stalling দিয়ে); লেসন ১২ সমাধান করবে control hazard অংশ।

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

EXPERIMENT

আপনার নিজের CPU-তে stall মাপুন — perf দিয়ে

Linux (perf প্রয়োজন)· ১৫ মিনিট

দুইটা ছোট C প্রোগ্রাম — একটাতে প্রতিটা loop iteration আগেরটার উপর সরাসরি নির্ভরশীল (একটা true data dependency chain), আরেকটাতে iteration-গুলো একে অপরের থেকে স্বাধীন।

// dependent.c — প্রতিটা ধাপ আগেরটার ফলাফলের উপর নির্ভরশীল
#include <stdio.h>

int main(void) {
    volatile long x = 1;
    long n = 500 * 1000 * 1000L;
    for (long i = 0; i \< n; i++) {
        x = x * 1103515245 + 12345;   // প্রতিটা ধাপ আগেরটার x দরকার
    }
    printf("%ld\n", x);
    return 0;
}
// independent.c — চারটা আলাদা chain, একে অপরের উপর নির্ভর করে না
#include <stdio.h>

int main(void) {
    volatile long a = 1, b = 2, c = 3, d = 4;
    long n = 125 * 1000 * 1000L;
    for (long i = 0; i \< n; i++) {
        a = a * 1103515245 + 12345;
        b = b * 1103515245 + 12345;
        c = c * 1103515245 + 12345;
        d = d * 1103515245 + 12345;   // চারটা independent chain — CPU একসাথে চালাতে পারে
    }
    printf("%ld %ld %ld %ld\n", a, b, c, d);
    return 0;
}
gcc -O1 -o dependent dependent.c
gcc -O1 -o independent independent.c
# -O1 ব্যবহার করুন — -O2/-O3 পুরো loop-টাই optimize করে ফেলতে পারে

perf stat -e cycles,instructions,stalled-cycles-backend ./dependent
perf stat -e cycles,instructions,stalled-cycles-backend ./independent

সাধারণ ফলাফলের প্যাটার্ন (সংখ্যা machine-ভেদে বদলাবে, প্যাটার্নটা লক্ষ্য করুন):

dependent.c:
    ~2,050,000,000  cycles
    ~2,000,000,000  instructions   # IPC ≈ 0.98 — প্রায় ১ instruction/cycle
    stalled-cycles-backend: বেশি %

independent.c:
    ~550,000,000    cycles
    ~1,500,000,000  instructions   # IPC ≈ 2.7 — cycle প্রতি ১-এর বেশি instruction!
    stalled-cycles-backend: কম %

dependent.c-তে IPC প্রায় ১-এর কাছাকাছি — প্রতিটা গুণ ও যোগ আগেরটার ফলাফলের অপেক্ষায় থাকে, তাই CPU (আধুনিক superscalar হওয়া সত্ত্বেও, লেসন ১৩ দেখুন) এখানে একসাথে বেশি কাজ করতে পারে না। independent.c-তে IPC ১-এর বেশি — চারটা independent chain একসাথে এগোতে পারে, কারণ কোনোটাই অন্যটার ফলাফলের অপেক্ষায় নেই।

এটাই এই আর পরের কয়েকটা লেসনের কেন্দ্রীয় বার্তার সরাসরি প্রমাণ: data hazard (dependency chain) সত্যিকারের, মাপযোগ্য throughput ক্ষতি করে — এমনকি আজকের সবচেয়ে আধুনিক CPU-তেও।

এটা কী প্রমাণ করে

একটা নির্ভরশীল instruction chain (প্রতিটা পরের instruction আগেরটার ফলাফলের অপেক্ষায়) বাস্তবেই কম IPC (instructions per cycle) দেয়, যেখানে independent instruction-এর chain প্রায় ideal-এর কাছাকাছি IPC পায় — hazard শুধু তত্ত্ব না, সরাসরি মাপা যায়।

নিজে বানান

BUILD IT

Hazard Detector — একটা instruction sequence স্ক্যান করে সব hazard খুঁজুন

Python · ●●○○○
  1. প্রতিটা instruction-কে (opcode, destination, sources) হিসেবে পার্স করুন
  2. পাঁচ-স্টেজ pipeline model করুন — প্রতিটা instruction-এর IF/ID/EX/MEM/WB cycle নম্বর হিসাব করুন
  3. প্রতিটা instruction-এর ID cycle-এ, আগের কোন instruction-এর destination register তার source-এর সাথে মেলে তা খুঁজুন
  4. সেই আগের instruction তখনও WB পার হয়নি কি না চেক করে RAW data hazard রিপোর্ট করুন
  5. একটা branch instruction পেলে control hazard রিপোর্ট করুন

লক্ষ্য: instruction sequence পড়ে স্বয়ংক্রিয়ভাবে বলে দেওয়া কোথায় কোথায় data hazard আছে — ঠিক যেভাবে উপরের example section-এ আমরা হাতে করলাম।

from dataclasses import dataclass, field

@dataclass
class Instr:
    text: str
    op: str
    dest: str | None
    srcs: list[str] = field(default_factory=list)
    is_branch: bool = False

def parse(line: str) -> Instr:
    """'ADD R3, R1, R4' জাতীয় লাইন পার্স করে"""
    parts = line.replace(',', '').split()
    op = parts[0]
    if op == 'BEQ':
        return Instr(line, op, None, parts[1:3], is_branch=True)
    if op == 'STORE':
        return Instr(line, op, None, parts[1:])
    dest = parts[1]
    srcs = parts[2:]
    if op == 'LOAD':
        srcs = [s.split('(')[-1].rstrip(')') for s in srcs]
    return Instr(line, op, dest, srcs)


STAGES = ['IF', 'ID', 'EX', 'MEM', 'WB']
K = len(STAGES)

def stage_cycle(instr_index: int, stage: str) -> int:
    """i-তম instruction (0-indexed), stage-এর cycle নম্বর (1-indexed)"""
    return instr_index + STAGES.index(stage) + 1


def find_hazards(program: list[str]):
    instrs = [parse(l) for l in program]
    print(f"{'#':>2}  {'Instruction':<20} {'IF':>3} {'ID':>3} {'EX':>3} {'MEM':>3} {'WB':>3}")
    for i, ins in enumerate(instrs):
        cycles = [stage_cycle(i, s) for s in STAGES]
        print(f"{i+1:>2}  {ins.text:<20} " + " ".join(f"{c:>3}" for c in cycles))

    print("\n— Hazard রিপোর্ট —")
    found = False
    for i, ins in enumerate(instrs):
        id_cycle = stage_cycle(i, 'ID')
        for j in range(i):
            prev = instrs[j]
            if prev.dest and prev.dest in ins.srcs:
                wb_cycle = stage_cycle(j, 'WB')
                # register file-এ মান পড়ার জন্য উপলব্ধ হয় WB cycle-এর *পরের* cycle থেকে
                available_from = wb_cycle + 1
                if id_cycle \< available_from:
                    found = True
                    gap = available_from - id_cycle
                    print(f"  DATA HAZARD: instr {j+1} লেখে {prev.dest!r}, "
                          f"instr {i+1} পড়ে cycle {id_cycle}-এ, "
                          f"মান উপলব্ধ হয় cycle {available_from}-এ "
                          f"({gap} cycle আগেভাগে পড়া হচ্ছে)")
        if ins.is_branch:
            found = True
            print(f"  CONTROL HAZARD: instr {i+1} ({ins.text}) — outcome cycle "
                  f"{stage_cycle(i,'EX')}-তে জানা যাবে, কিন্তু IF প্রতি cycle-এই চলে")
    if not found:
        print("  কোনো hazard পাওয়া যায়নি — সব instruction independent।")


program = [
    "LOAD  R1, 0(R2)",
    "ADD   R3, R1, R4",
    "SUB   R5, R3, R6",
    "BEQ   R5, R0, LABEL",
]
find_hazards(program)

প্রত্যাশিত output (এই লেসনের example section-এর সাথে মিলিয়ে দেখুন):

 #  Instruction           IF  ID  EX MEM  WB
 1  LOAD  R1, 0(R2)        1   2   3   4   5
 2  ADD   R3, R1, R4       2   3   4   5   6
 3  SUB   R5, R3, R6       3   4   5   6   7
 4  BEQ   R5, R0, LABEL    4   5   6   7   8

— Hazard রিপোর্ট —
  DATA HAZARD: instr 1 লেখে 'R1', instr 2 পড়ে cycle 3-এ, মান উপলব্ধ হয় cycle 6-এ (3 cycle আগেভাগে পড়া হচ্ছে)
  DATA HAZARD: instr 2 লেখে 'R3', instr 3 পড়ে cycle 4-এ, মান উপলব্ধ হয় cycle 7-এ (3 cycle আগেভাগে পড়া হচ্ছে)
  DATA HAZARD: instr 3 লেখে 'R5', instr 4 পড়ে cycle 5-এ, মান উপলব্ধ হয় cycle 8-এ (3 cycle আগেভাগে পড়া হচ্ছে)
  CONTROL HAZARD: instr 4 (BEQ R5, R0, LABEL) — outcome cycle 6-তে জানা যাবে, কিন্তু IF প্রতি cycle-এই চলে

নিজে বাড়ান:

  1. এই detector-এ একটা --fix stall মোড যোগ করুন যেটা প্রতিটা hazard-এর জন্য দরকারি stall cycle সংখ্যা হিসাব করে পরের instruction-গুলোকে পিছিয়ে দেয় (এটাই পরের লেসনের বিষয়ের একটা preview)
  2. একটা এলোমেলো instruction generator বানান আর দেখুন hazard density (কত % pair-এ hazard আছে) সাধারণ প্রোগ্রামে কেমন হয়
  3. Structural hazard যোগ করুন — একটা shared_memory=True ফ্ল্যাগ দিয়ে দেখান IF আর MEM কখন একই cycle-এ একই memory চায়

বাস্তব সিস্টেমে

Pipelining যেখানে বাস্তবে দেখা যায়

IBM 7030 Stretch (1961)। ইতিহাসের প্রথম বাণিজ্যিক pipelined কম্পিউটার — যদিও এটা তার নিজের performance target পূরণ করতে ব্যর্থ হয়েছিল (IBM নিজেই দাম কমিয়ে দিতে বাধ্য হয়েছিল), এর ডিজাইনে pipelining আর এমনকি speculative execution-এর প্রাথমিক ধারণাও ছিল। এই একটা “ব্যর্থ” প্রজেক্টই পরবর্তী কয়েক দশকের CPU ডিজাইনের ভিত্তি স্থাপন করে গেছে।

CDC 6600 (1964, Seymour Cray)। একাধিক pipelined functional unit সহ — এই লেসনের ভিত্তি (pipelining) আর লেসন ১৩-এর ভিত্তি (scoreboard, out-of-order-এর পূর্বসূরি) দুটোই এই একটা মেশিনে প্রথম বাস্তবায়িত হয়েছিল।

Classic MIPS R2000/R3000। পাঁচ-স্টেজ IF/ID/EX/MEM/WB pipeline-এর “পাঠ্যপুস্তক” উদাহরণ — Patterson & Hennessy-র বইয়ের মূল কেস স্টাডি, যা আজকের এই লেসনের কাঠামোরও ভিত্তি।

ARM Cortex-M0। আজকের বহু microcontroller-এ ব্যবহৃত একটা অত্যন্ত সরল ৩-স্টেজ (Fetch, Decode, Execute) in-order pipeline — প্রমাণ করে pipelining শুধু high-end CPU-র জন্য না, ক্ষুদ্রতম embedded chip-এও একই মূলনীতি কাজ করে, শুধু গভীরতা কম।

Split L1 I-cache/D-cache। প্রায় প্রতিটা আধুনিক CPU-তে আলাদা instruction cache আর data cache — সরাসরি এই লেসনের structural hazard সমস্যার সমাধান হিসেবে ডিজাইন করা।

Pentium (P5, 1993)। এই লেসনেরই ধারণা নিয়ে, কিন্তু একধাপ এগিয়ে — দুইটা সমান্তরাল ৫-স্টেজ pipeline (u-pipe, v-pipe), যা আসলে লেসন ১৩-এর superscalar-এর একটা প্রাথমিক রূপ।

RISC-V শিক্ষামূলক কোর — PicoRV32, VexRiscv। ওপেন-সোর্স, পড়ার জন্য উপযুক্ত real RTL কোড যেখানে ঠিক এই ৫-স্টেজ pipeline-এর hazard-হ্যান্ডলিং লজিক Verilog/Scala-তে দেখা যায় — Level ২-র HDL লেসনের সরাসরি এক্সটেনশন হিসেবে ঘেঁটে দেখার মতো।

Intel 80486 (1989)। প্রথম x86 CPU যেখানে instruction pipelining যোগ হয় (৫-স্টেজ, এই লেসনের ঠিক মডেল অনুসরণ করে) — আগের 80386-এর তুলনায় একই clock frequency-তে প্রায় দ্বিগুণ throughput এনেছিল, শুধু pipelining-এর কারণে, কোনো নতুন transistor logic ছাড়াই মূল instruction set-এ। এটা দেখায় pipelining কতটা “বিনামূল্যের” পারফরম্যান্স লাভ হতে পারে — একবার ডিজাইন করে ফেললে, প্রতিটা পরবর্তী প্রজন্মেই এর সুফল বহাল থাকে।

Pipeline Visualizer প্রজেক্ট (এই module-এর project track)। এই আর পরের দুইটা লেসনের সব ধারণা — hazard detection, stall bubble, forwarding path — একসাথে জড়ো হয়ে একটা animate করা ৫-স্টেজ pipeline visualizer বানানোর ভিত্তি তৈরি করে।

যে ভুলগুলো সবাই করে

“Pipelining প্রতিটা instruction-কে দ্রুত করে।”

ঠিক উল্টো। একটা একক instruction-এর latency (শুরু থেকে শেষ পর্যন্ত সময়) pipelining-এ কমে না, বরং সামান্য বাড়ে — কারণ প্রতিটা pipeline register নিজেই কিছুটা সময় (setup/hold time) খরচ করে।

উদাহরণে single-cycle-এ একটা instruction ৮০০ ps-এ শেষ হতো। Pipeline-এ সেই একই instruction পাঁচটা আলাদা ২০০ ps cycle পার হয়ে শেষ হয় — মোট ১০০০ ps, single-cycle-এর চেয়ে বেশি!

Pipelining latency কমায় না, throughput বাড়ায়। একটা instruction-এর জন্য pipeline আসলে সামান্য ধীর; কিন্তু একসাথে বহু instruction চালালে সামগ্রিক গতি অনেক বেশি — ঠিক যেমন একটা ফ্যাক্টরির assembly line-এ একটা মাত্র গাড়ি বানাতে হয়তো বেশি সময় লাগে (প্রতিটা স্টেশনে যাওয়া-আসার overhead), কিন্তু হাজার গাড়ি বানাতে অনেক কম সময় লাগে।

“৫-স্টেজ pipeline মানেই ৫× speedup।”

এই লেসনেই দেখানো হয়েছে — বাস্তব সংখ্যায় speedup দাঁড়ায় , না, কারণ স্টেজগুলো সমান দৈর্ঘ্যের না। আরও গভীর pipeline (যেমন ২০+ স্টেজের Pentium 4, লেসন ১২-এ বিস্তারিত) মানে আরও বেশি স্টেজ, কিন্তু আরও বেশি hazard penalty-ও — speedup কখনোই বিনামূল্যে আসে না।

উপরন্তু, এই সংখ্যাগুলো hazard আগে থেকে ধরেই নেয়নি — hazard-এর কারণে stall লাগলে বাস্তব speedup আরও কমে যায় (পরের লেসন দেখুন)।

“Structural hazard মানে ডিজাইনাররা ভুল করেছেন।”

না — এটা একটা সচেতন trade-off, ভুল না। একটা মাত্র shared memory port ব্যবহার করা মানে কম hardware, কম cost, কম power। প্রতিটা resource duplicate করলে (দুইটা memory port, দুইটা ALU) হার্ডওয়্যারের খরচ আর জটিলতা বাড়ে।

আধুনিক CPU split I-cache/D-cache ব্যবহার করে ঠিক এই নির্দিষ্ট hazard-টা এড়াতে — কারণ এটা এত সাধারণভাবে ঘটে (প্রতিটা instruction fetch, বহু instruction memory access) যে বাড়তি hardware-এর খরচ স্পষ্টভাবে লাভজনক। কিন্তু অন্য, কম ঘন ঘন ঘটা structural hazard-এর জন্য (যেমন একটা বিরল multiply unit শেয়ার করা) মাঝে মাঝে stall করাটাই সস্তা সমাধান — সব জায়গায় duplicate করার দরকার নেই।

“'Hazard' মানে শুধু data hazard — লোকে অনেক সময় এভাবেই বলে।”

কথ্য ভাষায় (এমনকি কিছু পুরনো লেখাতেও) “hazard” বললে প্রায়ই শুধু data hazard বোঝানো হয়, কারণ এটাই সবচেয়ে ঘন ঘন ঘটে আর forwarding/stalling-এর আলোচনায় সবচেয়ে বেশি জায়গা পায়।

কিন্তু আনুষ্ঠানিক সংজ্ঞায় structural আর control hazard-ও সমানভাবে “hazard” — তিনটাই একই মৌলিক সমস্যার ভিন্ন রূপ: “overlap করা দুইটা instruction-এর মধ্যে এমন একটা সম্পর্ক আছে যা ধরে না রাখলে ভুল ফলাফল বা throughput ক্ষতি হবে।”

পরীক্ষা বা সাক্ষাৎকারে যদি প্রশ্ন হয় “কয় ধরনের pipeline hazard আছে,” উত্তর সবসময় তিন প্রকার — শুধু data hazard বললে অসম্পূর্ণ উত্তর।

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

1

একটা ৪-স্টেজ pipeline-এ (ধরুন IF, EX, MEM, WB — ID নেই) ৮টা instruction চালানো হচ্ছে। মোট কত cycle লাগবে? Non-pipelined হলে কত লাগত (প্রতিটা স্টেজ ১ cycle ধরে)?

যুক্তি

N=8, K=4

Pipelined: N + K − 1 = 8 + 4 − 1 = 11 cycle।

Non-pipelined: N × K = 8 × 4 = 32 cycle।

Speedup: 32/11 ≈ 2.9× — আদর্শ -এর কাছাকাছি কিন্তু কম, কারণ N=8 এখনও তুলনামূলক ছোট (fill+drain = K−1=3 cycle, মোট ১১-এর একটা উল্লেখযোগ্য অংশ)।

2

আগের প্রশ্নের একই ৪-স্টেজ pipeline-এ যদি স্টেজ delay-গুলো হয় IF=150ps, EX=300ps, MEM=250ps, WB=100ps — pipelined ক্লক পিরিয়ড কত হবে, আর single-cycle (non-pipelined) ক্লক পিরিয়ড কত হবে? Ideal (N→∞) speedup কত?

প্রয়োগ

Single-cycle ক্লক পিরিয়ড = সব স্টেজের যোগফল = 150+300+250+100 = 800 ps

Pipelined ক্লক পিরিয়ড = সবচেয়ে ধীর স্টেজ = max(150,300,250,100) = 300 ps (EX স্টেজ বোতলনেক)।

Ideal speedup = 800/300 ≈ 2.67×K=4 স্টেজ থাকা সত্ত্বেও speedup -এর অনেক নিচে, কারণ EX স্টেজ বাকিদের তুলনায় অনেক বেশি ধীর (severely unbalanced)। এখানেই বাস্তব ডিজাইনে “EX স্টেজকে ভাগ করে দুইটা স্টেজ বানানো যায় কি না” — এই ধরনের প্রশ্ন ওঠে।

3

MUL R3, R1, R2 এর ঠিক পরেই ADD R5, R3, R4 থাকলে এটা কোন ধরনের hazard? যদি MUL-এর পরে দুইটা সম্পূর্ণ অসম্পর্কিত instruction থাকে, তারপর ADD R5, R3, R4 আসে, তাহলে কি hazard থেকেই যাবে?

যুক্তি

প্রথম ক্ষেত্রে এটা একটা data hazard (RAW)ADD সরাসরি MUL-এর ফলাফল (R3) দরকার, আর দুইটা পরপর instruction হওয়ায় MUL-এর WB (cycle 5, যদি MUL instruction 1 হয়) ADD-এর ID (cycle 3)-এর অনেক পরে ঘটে।

দ্বিতীয় ক্ষেত্রে — মাঝে দুইটা independent instruction থাকলে, ADD-এর ID স্টেজ পিছিয়ে যায় (এখন এটা instruction 4, তার ID cycle 5-এ)। MUL (instruction 1)-এর WB cycle 5-এ, মান উপলব্ধ cycle 6 থেকে — তবু ADD-এর ID (cycle 5) সেই সময়ের আগে! তাই এখনো একটা (ছোট) hazard থেকেই যায়, যদিও গ্যাপ ছোট হয়ে গেছে। এটাই compiler instruction scheduling-এর মূল কৌশল (লেসন ১১-এ বিস্তারিত) — মাঝে independent কাজ বসিয়ে হাজার্ডের গ্যাপ কমানো বা পুরোপুরি ঢেকে দেওয়া।

4

একটা CPU ডিজাইনার প্রস্তাব দিলেন: “structural hazard সমস্যা সমাধান করতে চলুন instruction memory আর data memory-র জন্য সবসময় সম্পূর্ণ আলাদা physical chip ব্যবহার করি, কখনো কোনো resource শেয়ার করব না।” এই approach-এর সুবিধা ও অসুবিধা কী?

ডিজাইন

সুবিধা: সব structural hazard একেবারে নির্মূল — কোনো stall লাগবে না, ডিজাইন সরল হবে, timing predictable হবে।

অসুবিধা: প্রতিটা resource duplicate করার খরচ (silicon area, power) বাস্তব — আর এই খরচ সবসময় লাভজনক না। একটা বিরল ব্যবহৃত functional unit (যেমন একটা floating-point divide unit, যা কালেভদ্রে ব্যবহৃত হয়) duplicate করার চেয়ে মাঝে মাঝে stall করাই সস্তা — কারণ সেই hazard খুব কম ঘটে, আর duplicate করা silicon area প্রায় সবসময় অব্যবহৃত পড়ে থাকবে।

সঠিক নীতি: যে resource প্রায় প্রতি cycle-েই ব্যবহৃত হয় (instruction fetch, যা প্রতি cycle-েই ঘটে) — সেটা duplicate করা প্রায় সবসময় লাভজনক (এবং তাই split I-cache/D-cache universal)। যে resource কালেভদ্রে ব্যবহৃত হয় — সেখানে stall মেনে নেওয়াই বেশি ব্যয়সাশ্রয়ী। এটা একটা cost-benefit সিদ্ধান্ত, একটা নিয়মে সবার জন্য একই উত্তর নেই।

5

Data hazard-এর তিন প্রকার (RAW, WAR, WAW)-এর মধ্যে in-order ৫-স্টেজ pipeline-এ কোনটা আসলে সমস্যা তৈরি করে, আর কেন বাকি দুটো করে না?

স্মরণ

শুধু RAW (Read-After-Write) সমস্যা তৈরি করে — কারণ এটাই একমাত্র “true dependency”, প্রোগ্রামের প্রকৃত অর্থের অংশ।

WAR (Write-After-Read) আর WAW (Write-After-Write) হলো “false dependency” — শুধু register নামের পুনর্ব্যবহারের কারণে তৈরি, প্রকৃত ডেটা প্রবাহের অংশ না। In-order pipeline-এ প্রতিটা instruction তার স্টেজগুলো ঠিক প্রোগ্রাম অর্ডারেই পার হয় — তাই একটা instruction-এর read সবসময় তার পরের instruction-এর write-এর আগেই ঘটে (কারণ আগের instruction pipeline-এ আগে ঢুকেছে, ID স্টেজও আগে পার করবে)। WAR/WAW শুধু তখনই সমস্যা, যখন instruction-গুলো প্রোগ্রাম অর্ডার থেকে সরে গিয়ে চলতে পারে — যা লেসন ১৩-এর out-of-order execution-এর বিষয়।

6

একটা প্রোগ্রামে মোট ২০০০ instruction আছে। এর মধ্যে ৪০০টা instruction-এ data hazard-এর কারণে গড়ে ৩ cycle করে stall লাগে (এই লেসনের timing model অনুযায়ী, কোনো fix ছাড়া)। ৫-স্টেজ পাইপলাইনে (K=5, clock period = 200 ps) এই প্রোগ্রামের মোট execution time কত? CPI কত?

প্রয়োগ

বেস cycle সংখ্যা (hazard ছাড়া, ideal): N + K - 1 = 2000 + 5 - 1 = 2004 cycle।

Stall থেকে বাড়তি cycle: 400 × 3 = 1200 cycle।

মোট cycle: 2004 + 1200 = 3204 cycle।

Execution time:

3204×200 ps=640800 ps640.8 ns3204 \times 200 \text{ ps} = 640800 \text{ ps} \approx 640.8 \text{ ns}

CPI: 3204 / 2000 = 1.602 — অর্থাৎ প্রতি instruction গড়ে 1.602 cycle নিচ্ছে, ideal 1-এর তুলনায় প্রায় ৬০% বেশি। এই একই সংখ্যা ব্যবহার করে বলা যায় বাস্তব speedup ideal -এর বদলে 4 / 1.602 ≈ 2.5×-এ নেমে এসেছে — hazard-এর প্রকৃত খরচ এখানে স্পষ্ট। পরের লেসনে forwarding দিয়ে এই ৪০০টা stall-এর বেশিরভাগ কীভাবে দূর করা যায় তা দেখব।

এরপর কী

এরপর কী

এই লেসনে আমরা সমস্যাটা চিনেছি — এখনো একটাও সমাধান করিনি। তিনটা hazard-এর তিনটা ভিন্ন সমাধান পথ:

  • Structural hazard — সাধারণত ডিজাইন সময়েই এড়ানো হয় (split cache), বা মাঝে মাঝে stall মেনে নেওয়া হয়
  • Data hazard — পরের লেসনের পুরো বিষয়: forwarding (সরাসরি ওয়্যার দিয়ে ফলাফল পাঠানো, বেশিরভাগ ক্ষেত্রে stall ছাড়াই সমাধান) আর stalling (যখন forwarding-ও যথেষ্ট না, বিশেষত load-use hazard-এ)
  • Control hazard — লেসন ১২-এর পুরো বিষয়: branch prediction, যেখানে CPU একটা শিক্ষিত অনুমান করে, ভুল হলে সংশোধন করে

পরের লেসনে আমরা ঠিক এই লেসনের ADD R1,R2,R3 / SUB R4,R1,R5 উদাহরণে ফিরে যাব — আর দেখব কীভাবে একটা মাত্র বাড়তি তার (forwarding path) এই hazard-এর বেশিরভাগ খরচ শূন্যে নামিয়ে আনতে পারে, শুধু একটা ব্যতিক্রম বাদে।

আরও পড়ুন

  • Computer Organization and Design (RISC-V Edition), Chapter 4 — The Processor — David A. Patterson, John L. Hennessy · ৫-স্তরের pipeline আর hazard-এর প্রামাণ্য আলোচনা — এই লেসনের numeric উদাহরণগুলো এখান থেকে অনুপ্রাণিত
  • Computer Architecture: A Quantitative Approach, Appendix C — Pipelining: Basic and Intermediate Concepts — John L. Hennessy, David A. Patterson · Hazard classification আর pipeline performance model-এর গভীর আলোচনা
  • "A Description of the Instruction Sets of the STRETCH Computer" এবং পরবর্তী retrospective — W. Buchholz (ed.), IBM · IBM 7030 Stretch (1961) — প্রথম বাণিজ্যিক pipelined কম্পিউটার, এই লেসনের real-world অংশে আলোচিত