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

Out-of-order execution — Tomasulo, register renaming আর reorder buffer

Out-of-Order Execution — Dynamic Scheduling, Tomasulo's Algorithm, Reorder Buffer, Physical Register Renaming and the Modern OoO Core

Taught in Bangla, this lesson shows why one stalled instruction blocks independent work in an in-order core and how dynamic scheduling fixes it: in-order front end, out-of-order execution, in-order commit. It compares scoreboard and Tomasulo, fills Tomasulo tables cycle by cycle with reservation stations, register status and the CDB, then covers the reorder buffer and recovery, physical register renaming with a map table and free list, load/store queues and modern OoO cores.

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

  • কেন in-order stall স্বাধীন কাজও আটকায়, আর dynamic scheduling-এর তিন ভাগ: in-order সামনে, out-of-order মাঝে, in-order পেছনে
  • Scoreboard আর Tomasulo-র পার্থক্য; reservation station-এর ঘর (Op, Vj, Vk, Qj, Qk, Busy, A), register status আর CDB
  • Tomasulo-র টেবিল cycle ধরে পূরণ করা — issue, execute, write result — আর tag কীভাবে WAR/WAW দূর করে
  • Reorder buffer দিয়ে in-order commit, ভুল branch আর exception থেকে recovery
  • Physical register file, map table আর free list দিয়ে renaming; load/store queue; আধুনিক OoO core-এর ধাপ আর তার দাম

১ · একজন আটকে গেলে সবাই দাঁড়িয়ে — in-order stall-এর দাম

পর্ব ৩৫-এ পাঁচ ধাপের pipeline দেখেছেন, আর পর্ব ৩৬-এ দেখেছেন একটি instruction তার আগের instruction-এর ফলের জন্য অপেক্ষা করলে stall লাগে। সেখানে অপেক্ষা ছিল এক-দুই cycle-এর। কিন্তু বাস্তবের কিছু instruction অনেক ধীর। Floating point-এর ভাগ কয়েক ডজন cycle নিতে পারে। আর যে load-এর ডেটা cache-এ নেই, তাকে main memory থেকে আনতে শত cycle-এর বেশিও লাগতে পারে (cache-এর হিসাব আসবে পর্ব ৪৪ থেকে ৪৭-এ)। In-order core-এর নিয়ম কড়া: পেছনের কেউ সামনের জনকে টপকাতে পারে না। ফলে একটি ধীর instruction আটকালে তার পেছনের সবাই দাঁড়িয়ে থাকে — এমনকি যাদের ওই ফলের কোনো দরকারই নেই।

একটি রেস্তোরাঁর রান্নাঘর ভাবুন, যেখানে নিয়ম হলো অর্ডার আসার ক্রমেই রান্না হবে। প্রথম অর্ডার কাচ্চি বিরিয়ানি — দমে বসাতে চল্লিশ মিনিট। পরের অর্ডারগুলো চা, ডিম ভাজা, সালাদ — প্রতিটি পাঁচ মিনিটের কাজ, আর কাচ্চির সাথে এদের কোনো সম্পর্ক নেই। কড়া নিয়মে চুলা ফাঁকা থাকলেও চা বসবে না, কারণ কাচ্চি এখনো শেষ হয়নি। বুদ্ধিমান রান্নাঘর কী করে? অর্ডারগুলো একটা বোর্ডে টাঙিয়ে রাখে। যে অর্ডারের উপকরণ তৈরি, সে আগে রান্না হয়। কিন্তু টেবিলে পরিবেশন হয় আবার অর্ডারের ক্রমেই, যাতে কোনো খদ্দের বিভ্রান্ত না হন। এই পর্বের পুরো গল্প এই তিন বাক্যে: বোর্ডে টাঙানো (issue), প্রস্তুত হলে রান্না (out-of-order execute), ক্রমে পরিবেশন (in-order commit).

একটি worked example — ছয়টি instruction

পুরো পর্বে আমরা একটিই ছোট floating point প্রোগ্রাম ব্যবহার করব (RISC-V-এর double-precision instruction; নিয়মের খুঁটিনাটি অংশ ৩-এ)। প্রতিটি instruction-এর execute ধাপের দৈর্ঘ্য ধরা হলো: fld ২ cycle (ঠিকানা হিসাব আর মেমরি পড়া), fadd.d/fsub.d ২ cycle, fmul.d ৬ cycle, fdiv.d ১২ cycle. এগুলো শেখানোর জন্য বেছে নেওয়া সংখ্যা, কোনো নির্দিষ্ট chip-এর নয়।

I1  fld    f2, 0(x1)        # f2 = মেমরি থেকে
I2  fdiv.d f0, f2, f4       # f0 = f2 / f4     (I1-এর ওপর নির্ভর, ১২ cycle)
I3  fadd.d f6, f0, f8       # f6 = f0 + f8     (I2-এর ওপর নির্ভর)
I4  fsub.d f8, f10, f14     # f8 = f10 - f14   (স্বাধীন!)
I5  fmul.d f6, f10, f8      # f6 = f10 * f8    (I4-এর ওপর নির্ভর)
I6  fadd.d f12, f6, f4      # f12 = f6 + f4    (I5-এর ওপর নির্ভর)

প্রথমে dependence-গুলো আলাদা করি (পর্ব ৩৬-এর ভাষায়):

  • সত্যিকারের data dependence (RAW): I1 → I2 (f2), I2 → I3 (f0), I4 → I5 (f8), I5 → I6 (f6). এখানে সত্যিই একটি মান এক instruction থেকে আরেকটিতে যায়।
  • WAR: I3 f8 পড়ে, পরে I4 f8-এ লেখে। I4 যদি I3-এর আগে লিখে ফেলে, I3 ভুল f8 পাবে।
  • WAW: I3 আর I5 দুজনেই f6-এ লেখে; শেষে I5-এর মান থাকতে হবে।

এবার একটি সরল in-order মেশিন ধরুন: প্রতি cycle-এ সর্বোচ্চ একটি instruction issue (চালানোর জন্য পাঠানো), প্রোগ্রামের ক্রমে; কোনো source এখনো লেখা না হলে issue থেমে থাকে (stall-on-use), আর পেছনের সবাইও থামে। Execution unit-গুলো pipelined, তাই একাধিক instruction একসাথে চলতে পারে; ফল লেখার পথ একটি। অংশ ৪-এর simulator যে হিসাব দেবে, তার in-order অংশ এই:

InstructionIssueExecuteফল লেখাকেন দেরি
I1 fld f2১২–৩৪—
I2 fdiv.d f0৪৫–১৬১৭f2-এর জন্য অপেক্ষা (RAW)
I3 fadd.d f6১৭১৮–১৯২০f0-এর জন্য ১২ cycle অপেক্ষা
I4 fsub.d f8১৮১৯–২০২১নিজে স্বাধীন, তবু I3-এর পেছনে আটকা
I5 fmul.d f6২১২২–২৭২৮f8-এর জন্য অপেক্ষা
I6 fadd.d f12২৮২৯–৩০৩১f6-এর জন্য অপেক্ষা

মোট ৩১ cycle. লক্ষ করুন I4-কে: তার ইনপুট f10 আর f14 শুরু থেকেই তৈরি, তবু সে cycle ১৮ পর্যন্ত অপেক্ষা করল, শুধু কারণ সামনের I3 ভাগের ফলের জন্য দাঁড়িয়ে। I4 আটকে থাকায় I5, আর I5 আটকে থাকায় I6 — পুরো শৃঙ্খলটি ভাগের পেছনে সারি দিল। অথচ I4 → I5 → I6 শৃঙ্খলটি ভাগের সাথে সম্পূর্ণ সম্পর্কহীন; সেটি cycle ৫ থেকেই চলতে পারত, ভাগের সমান্তরালে।

মূল ধারণা

Compiler আগে থেকে instruction সাজিয়ে কিছুটা সাহায্য করতে পারে (static scheduling — পর্ব ৩৯-এ VLIW প্রসঙ্গে দেখেছেন)। কিন্তু compiler জানে না কোন load-এ cache miss হবে, আর একই binary অনেক রকম chip-এ চলে। তাই hardware-কেই চলার সময় সিদ্ধান্ত নিতে হয়: dynamic scheduling. লক্ষ্য একটাই — dependence-এর শৃঙ্খল যতটা অনুমতি দেয়, ঠিক ততটা সমান্তরালে চালানো।

২ · Dynamic scheduling-এর ছক — সামনে ক্রমে, মাঝে ক্রমের বাইরে, পেছনে আবার ক্রমে

আধুনিক out-of-order execution-এর প্রায় সব নকশা মোটামুটি তিন ভাগে সাজানো (মূল Tomasulo-তে তৃতীয় ভাগটি ছিল না, তা অংশ ৪-এর শেষে দেখবেন)। এই ছকটি মনে রাখলে পর্বের বাকি সব যন্ত্রাংশ কোথায় বসে, তা পরিষ্কার হয়ে যাবে।

১. সামনের অংশ — in-order

Fetch আর decode প্রোগ্রামের ক্রমে, branch prediction-এর পথ ধরে (পর্ব ৩৭)। Decode-এর পর instruction একটি অপেক্ষার জায়গায় বসে। এখানেই register-এর নাম বদলানো হয়।

২. মাঝের অংশ — out-of-order

অপেক্ষারত instruction-গুলোর মধ্যে যার সব operand তৈরি আর execution unit খালি, সে চলে — প্রোগ্রামে আগে থাকুক বা পরে। এই দলটিকে বলে instruction window.

৩. পেছনের অংশ — in-order

শেষ হওয়া instruction-এর ফল চূড়ান্ত হয় (commit বা retire) প্রোগ্রামের ক্রমে। এতে exception precise থাকে আর ভুল অনুমানের কাজ ফেলে দেওয়া যায়।

মাঝের অংশে দুটি বিপদ। এক, সত্যিকারের dependence (RAW) — ফল তৈরি না হলে চালানো যাবে না; এটি কেউ দূর করতে পারে না, শুধু অপেক্ষা করা যায়। দুই, name dependence (WAR আর WAW) — ক্রম ভেঙে চালালে এরা সত্যিকারের ভুল ঘটাতে পারে। পর্ব ৩৬-এ দেখেছেন, এরা আসলে নামের সংঘাত: register-এর সংখ্যা কম বলে compiler একই নাম বারবার ব্যবহার করে। দুটি সমাধান ইতিহাসে এসেছে — অপেক্ষা করানো, নয়তো নাম বদলে ফেলা।

Scoreboard বনাম Tomasulo — সংক্ষেপে

১৯৬৪ সালের CDC 6600-এর scoreboard (পর্ব ৩৬-এ পরিচয় হয়েছে) প্রথম পথ নিয়েছিল: একটি কেন্দ্রীয় টেবিল হিসাব রাখে কোন unit ব্যস্ত আর কোন register কে লিখবে, আর hazard দেখলে instruction-কে অপেক্ষা করায়। ১৯৬৭ সালে IBM-এর Robert Tomasulo System/360 Model 91-এর floating point unit-এর জন্য যে পদ্ধতি প্রকাশ করেন, তা দ্বিতীয় পথ নিল: নাম বদলে দেওয়া। দুটির তুলনা (সাধারণভাবে বইয়ে যেভাবে বর্ণিত; দুই মেশিনের সব খুঁটিনাটি এখানে নেই):

বিষয়Scoreboard (CDC 6600)Tomasulo (IBM 360/91 FPU)
RAWoperand তৈরি না হওয়া পর্যন্ত অপেক্ষাঅপেক্ষা, কিন্তু ফল CDB থেকে সরাসরি পায় (forwarding-এর মতো)
WARফল লেখা পিছিয়ে দেয়, যতক্ষণ না আগের পাঠক পড়ে ফেলেসমস্যাই নেই — পাঠক issue-এর সময়ই মান কপি করে নেয় বা tag ধরে রাখে
WAWissue থামিয়ে দেয়সমস্যাই নেই — register status শুধু সবচেয়ে নতুন লেখকের tag রাখে
অপেক্ষার জায়গাপ্রতিটি functional unit-এ একটিপ্রতি unit-এর সামনে কয়েকটি reservation station
ফল কীভাবে পৌঁছায়register file-এ লেখা, তারপর পড়াcommon data bus-এ একসাথে সবার কাছে
নিয়ন্ত্রণকেন্দ্রীয়ছড়ানো — প্রতিটি station নিজে CDB শোনে

একটি ঐতিহাসিক কারণও প্রায়ই উল্লেখ করা হয়: System/360-এর ISA-তে floating point register ছিল মাত্র চারটি। এত কম নামে compiler-কে বারবার একই register ব্যবহার করতেই হতো, তাই WAR আর WAW ছিল সর্বত্র। Hardware-এ নাম বদলানো ছাড়া সমান্তরালতা পাওয়ার উপায় ছিল না।

৩ · Tomasulo-র যন্ত্রপাতি — reservation station, tag আর common data bus

Tomasulo-র algorithm-এ তিনটি যন্ত্রাংশ। প্রথমটি reservation station (RS) — প্রতিটি execution unit-এর সামনে কয়েকটি অপেক্ষার ঘর, প্রতিটির একটি নাম (Add1, Add2, Mult1 …)। এই নামটিই tag. প্রতিটি RS-এ এই ঘরগুলো:

ঘরঅর্থ
Busyএই station-এ কোনো instruction বসে আছে কি না
Opকোন কাজ — যোগ, বিয়োগ, গুণ, ভাগ
Vj, Vkদুই source operand-এর মান, যদি পাওয়া গিয়ে থাকে
Qj, Qkমান এখনো না থাকলে, কোন RS সেই মান তৈরি করবে তার নাম (tag); মান থাকলে ফাঁকা। প্রতিটি operand-এর জন্য V বা Q — একটিই ভরা থাকে
ALoad/store-এর জন্য: শুরুতে immediate offset, পরে হিসাব করা কার্যকর ঠিকানা

দ্বিতীয় যন্ত্রাংশ register status — প্রতিটি register-এর জন্য একটি ঘর Qi, যা বলে “এই register-এর পরের মান কোন RS লিখবে”. ফাঁকা থাকলে register file-এর মানই ঠিক। তৃতীয়টি common data bus (CDB): কোনো unit কাজ শেষ করলে ফলটি নিজের tag-সহ এই bus-এ ঘোষণা করে। প্রতিটি RS আর register status নিজে শোনে — “আমি কি এই tag-এর অপেক্ষায় ছিলাম?” — আর মিললে মানটি তুলে নেয়।

রেলস্টেশনের ঘোষণার কথা ভাবুন। যাত্রীরা ট্রেনের নম্বর নিয়ে বসে থাকেন, ট্রেন কোথা থেকে আসছে তা জানার দরকার নেই। মাইকে ঘোষণা হয় “৭০৩ নম্বর ট্রেন ২ নম্বর প্ল্যাটফর্মে এসেছে” — যাঁদের টিকিটে ৭০৩, তাঁরা সবাই একসাথে ওঠেন। এখানে ট্রেনের নম্বর হলো tag, মাইক হলো CDB, আর একই মাইকে একসাথে দুটি ঘোষণা চলে না — CDB-ও এক cycle-এ একটিই ফল বহন করে।

তিনটি ধাপ

  1. Issue: instruction queue-এর মাথা থেকে প্রোগ্রামের ক্রমে, প্রতি cycle-এ একটি। তার ধরনের একটি RS খালি না থাকলে issue থামে (structural hazard). প্রতিটি source-এর জন্য register status দেখা হয়: Qi ফাঁকা হলে register file থেকে মান Vj/Vk-তে কপি; নইলে সেই tag Qj/Qk-তে। তারপর destination register-এর Qi-তে এই RS-এর নাম বসে।
  2. Execute: দুটি operand-ই মান হিসেবে হাতে এলে execution শুরু। অপেক্ষার সময় RS CDB শোনে। Load-এর ক্ষেত্রে প্রথমে কার্যকর ঠিকানা হিসাব হয়ে A-তে বসে, তারপর মেমরি পড়া।
  3. Write result: ফল তৈরি হলে CDB-তে tag-সহ ঘোষণা। যে সব RS-এর Qj বা Qk এই tag, তারা মান পায়; যে register-এর Qi এই tag, register file-এ মান লেখা হয় আর Qi ফাঁকা হয়। RS-টি খালি হয়।

এই পর্বের হিসাবের নিয়ম (বইভেদে cycle গোনার নিয়ম সামান্য আলাদা হয়, তাই পরীক্ষায় প্রশ্নের নিয়ম আগে পড়ে নেবেন):

  • Cycle t-তে issue হলে সবচেয়ে আগে execute শুরু t + ১-এ। কোনো operand cycle W-তে CDB থেকে এলে execute শুরু W + ১-এ। Latency L হলে execute চলে শুরু থেকে শুরু + L − ১ পর্যন্ত।
  • Execute শেষের পরের cycle-এ write result, যদি CDB খালি থাকে। এক cycle-এ একটিই write; প্রতিযোগিতা হলে প্রোগ্রামে পুরোনোটি আগে।
  • এক cycle-এর ভেতরে ক্রম: আগে write result (CDB), তারপর execute শুরু, তারপর issue. তাই একই cycle-এ CDB-তে আসা মান issue হওয়া instruction সরাসরি পায়, আর ওই cycle-এ খালি হওয়া RS নতুন instruction নিতে পারে।
  • Unit-গুলো pipelined (বা যথেষ্ট সংখ্যায়), তাই operand তৈরি হলেই execute শুরু হতে পারে। RS: Load ২টি, Add ৩টি (যোগ-বিয়োগ), Mult ২টি (গুণ-ভাগ; ভেতরে আলাদা multiplier আর divider).

Tag কেন WAR আর WAW মুছে দেয় — একটি যুক্তি

WAR: ধরুন i একটি register R পড়ে, আর পরে j সেই R-এ লেখে। Issue প্রোগ্রামের ক্রমে, তাই i, j-এর আগে issue হয়। Issue-এর মুহূর্তে i R-এর মান কপি করে নেয়, নয়তো তখনকার লেখকের tag ধরে রাখে। পরে j যাই লিখুক, i আর register file থেকে R পড়বে না — তার মান বা tag আগেই ঠিক হয়ে গেছে। আর j-এর tag i-এর Q-তে কখনো বসে না, কারণ j পরে issue হয়েছে। অতএব j যত আগেই লিখুক, i-এর ইনপুট বদলায় না। WAW: i আর j দুজনেই R-এ লেখে, j পরে। j-এর issue-এর সময় Qi[R] = j-এর tag, i-এর tag মুছে গেল। পরে i-এর ফল CDB-তে এলে register status-এ তার tag আর নেই, তাই R-এ লেখা হয় না — i-এর ফল যায় শুধু যারা i-এর tag-এর অপেক্ষায় ছিল তাদের কাছে। ফলে R-এ শেষ পর্যন্ত j-এর মানই থাকে, i আর j যে ক্রমেই শেষ হোক। অর্থাৎ RS-এর tag-গুলো আসলে বাড়তি, অদৃশ্য register — register renaming, যা কোনো আলাদা টেবিল ছাড়াই ঘটে বলে একে implicit renaming বলা হয়। RAW অবশ্য থেকে যায় — তা থাকারই কথা, কারণ সেটি সত্যিকারের ডেটা-প্রবাহ।

এবার উদাহরণের প্রথম দিকের cycle-গুলো টেবিল ধরে দেখুন। প্রতিটি ছবি সেই cycle-এর শেষের অবস্থা; “5–…” মানে execute শুরু হয়েছে, শেষ হয়নি। প্রাথমিক মান: f4 = 2, f8 = 1.5, f10 = 5, f14 = 1, আর মেমরিতে 0(x1)-এ 6.

Cycle ২CDB ফাঁকা
Instruction            Issue Exec  Write
I1 fld f2, 0(x1)       1     2–…
I2 fdiv.d f0, f2, f4   2
I3 fadd.d f6, f0, f8
I4 fsub.d f8, f10, f14
I5 fmul.d f6, f10, f8
I6 fadd.d f12, f6, f4

RS     Busy Op      Vj   Vk   Qj    Qk    A
Load1  yes  fld     —    —    —     —     0
Load2  no
Add1   no
Add2   no
Add3   no
Mult1  yes  fdiv.d  —    2    Load1 —
Mult2  no

Qi: f0=Mult1  f2=Load1  f6=—  f8=—  f12=—
Cycle ৩WAR-এর বীজ: I3 f8-এর মান কপি করল
Instruction            Issue Exec  Write
I1 fld f2, 0(x1)       1     2–…
I2 fdiv.d f0, f2, f4   2
I3 fadd.d f6, f0, f8   3
I4 fsub.d f8, f10, f14
I5 fmul.d f6, f10, f8
I6 fadd.d f12, f6, f4

RS     Busy Op      Vj   Vk   Qj    Qk    A
Load1  yes  fld     —    —    —     —     0
Load2  no
Add1   yes  fadd.d  —    1.5  Mult1 —
Add2   no
Add3   no
Mult1  yes  fdiv.d  —    2    Load1 —
Mult2  no

Qi: f0=Mult1  f2=Load1  f6=Add1  f8=—  f12=—
Cycle ৪CDB: Load1 → 6
Instruction            Issue Exec  Write
I1 fld f2, 0(x1)       1     2–3   4
I2 fdiv.d f0, f2, f4   2
I3 fadd.d f6, f0, f8   3
I4 fsub.d f8, f10, f14 4
I5 fmul.d f6, f10, f8
I6 fadd.d f12, f6, f4

RS     Busy Op      Vj   Vk   Qj    Qk    A
Load1  no
Load2  no
Add1   yes  fadd.d  —    1.5  Mult1 —
Add2   yes  fsub.d  5    1    —     —
Add3   no
Mult1  yes  fdiv.d  6    2    —     —
Mult2  no

Qi: f0=Mult1  f2=—  f6=Add1  f8=Add2  f12=—
RF: f2=6
Cycle ৫ভাগ আর বিয়োগ একসাথে শুরু; WAW-এ f6-এর tag বদলাল
Instruction            Issue Exec  Write
I1 fld f2, 0(x1)       1     2–3   4
I2 fdiv.d f0, f2, f4   2     5–…
I3 fadd.d f6, f0, f8   3
I4 fsub.d f8, f10, f14 4     5–…
I5 fmul.d f6, f10, f8  5
I6 fadd.d f12, f6, f4

RS     Busy Op      Vj   Vk   Qj    Qk    A
Load1  no
Load2  no
Add1   yes  fadd.d  —    1.5  Mult1 —
Add2   yes  fsub.d  5    1    —     —
Add3   no
Mult1  yes  fdiv.d  6    2    —     —
Mult2  yes  fmul.d  5    —    —     Add2

Qi: f0=Mult1  f2=—  f6=Mult2  f8=Add2  f12=—
Cycle ৭CDB: Add2 → 4 (বিয়োগ শেষ, ভাগ চলছে)
Instruction            Issue Exec  Write
I1 fld f2, 0(x1)       1     2–3   4
I2 fdiv.d f0, f2, f4   2     5–…
I3 fadd.d f6, f0, f8   3
I4 fsub.d f8, f10, f14 4     5–6   7
I5 fmul.d f6, f10, f8  5
I6 fadd.d f12, f6, f4  6

RS     Busy Op      Vj   Vk   Qj    Qk    A
Load1  no
Load2  no
Add1   yes  fadd.d  —    1.5  Mult1 —
Add2   no
Add3   yes  fadd.d  —    2    Mult2 —
Mult1  yes  fdiv.d  6    2    —     —
Mult2  yes  fmul.d  5    4    —     —

Qi: f0=Mult1  f2=—  f6=Mult2  f8=—  f12=Add3
RF: f2=6  f8=4
Cycle ১৪CDB: Mult2 → 20 (গুণ শেষ, ভাগ তখনো চলছে)
Instruction            Issue Exec  Write
I1 fld f2, 0(x1)       1     2–3   4
I2 fdiv.d f0, f2, f4   2     5–…
I3 fadd.d f6, f0, f8   3
I4 fsub.d f8, f10, f14 4     5–6   7
I5 fmul.d f6, f10, f8  5     8–13  14
I6 fadd.d f12, f6, f4  6

RS     Busy Op      Vj   Vk   Qj    Qk    A
Load1  no
Load2  no
Add1   yes  fadd.d  —    1.5  Mult1 —
Add2   no
Add3   yes  fadd.d  20   2    —     —
Mult1  yes  fdiv.d  6    2    —     —
Mult2  no

Qi: f0=Mult1  f2=—  f6=—  f8=—  f12=Add3
RF: f2=6  f6=20  f8=4
Cycle ১৭CDB: Mult1 → 3; I6 প্রস্তুত, কিন্তু bus দখলে
Instruction            Issue Exec  Write
I1 fld f2, 0(x1)       1     2–3   4
I2 fdiv.d f0, f2, f4   2     5–16  17
I3 fadd.d f6, f0, f8   3
I4 fsub.d f8, f10, f14 4     5–6   7
I5 fmul.d f6, f10, f8  5     8–13  14
I6 fadd.d f12, f6, f4  6     15–16 (অপেক্ষা)

RS     Busy Op      Vj   Vk   Qj    Qk    A
Load1  no
Load2  no
Add1   yes  fadd.d  3    1.5  —     —
Add2   no
Add3   yes  fadd.d  20   2    —     —
Mult1  no
Mult2  no

Qi: f0=—  f2=—  f6=—  f8=—  f12=Add3
RF: f0=3  f2=6  f6=20  f8=4
Cycle ২০CDB: Add1 → 4.5, কিন্তু f6-এ লেখা হলো না
Instruction            Issue Exec  Write
I1 fld f2, 0(x1)       1     2–3   4
I2 fdiv.d f0, f2, f4   2     5–16  17
I3 fadd.d f6, f0, f8   3     18–19 20
I4 fsub.d f8, f10, f14 4     5–6   7
I5 fmul.d f6, f10, f8  5     8–13  14
I6 fadd.d f12, f6, f4  6     15–16 18

RS     সব খালি

Qi: সব ফাঁকা
RF: f0=3  f2=6  f6=20  f8=4  f12=22
  1. Cycle ১–২. Cycle ১-এ I1 (fld) Load1-এ issue হলো, A = 0 (offset), আর Qi[f2] = Load1. Cycle ২-এ Load1-এর execute শুরু (ঠিকানা হিসাব), আর I2 (fdiv.d) Mult1-এ issue হলো। f2-এর Qi = Load1, তাই Mult1-এ Qj = Load1 (মান নেই, tag আছে); f4 প্রস্তুত, তাই Vk = 2. Qi[f0] = Mult1.
  2. Cycle ৩ — I3 issue. I3 (fadd.d f6, f0, f8) Add1-এ। f0-এর Qi = Mult1, তাই Qj = Mult1. f8-এর Qi ফাঁকা, তাই register file থেকে মান কপি: Vk = 1.5. এই কপিটাই পরে WAR থেকে বাঁচাবে। Qi[f6] = Add1.
  3. Cycle ৪ — প্রথম CDB ঘোষণা। Load1 মেমরি থেকে 6 পড়ে CDB-তে “Load1 → 6” ঘোষণা করল। Mult1-এর Qj = Load1 মিলে গেল, তাই Vj = 6; register file-এ f2 = 6, Qi[f2] ফাঁকা; Load1 খালি। একই cycle-এ I4 (fsub.d f8) Add2-তে issue হলো — f10 = 5, f14 = 1 সরাসরি মান। আর Qi[f8] = Add2: f8-এর পরের মান এখন Add2 দেবে, কিন্তু Add1 আগেই 1.5 নিয়ে রেখেছে।
  4. Cycle ৫ — দুটি execute একসাথে। Mult1 (ভাগ, operand এসেছে cycle ৪-এ) আর Add2 (বিয়োগ, issue cycle ৪-এ) দুজনেই এখন execute শুরু করল — ভাগ চলবে ৫–১৬, বিয়োগ ৫–৬. In-order মেশিনে বিয়োগ শুরু হতো cycle ১৯-এ! I5 (fmul.d f6) Mult2-তে: f10 = 5 মান, f8-এর জন্য Qk = Add2. সবচেয়ে গুরুত্বপূর্ণ: Qi[f6] = Mult2 — আগের লেখক Add1-এর tag মুছে গেল। এটাই WAW-এর সমাধান।
  5. Cycle ৬–৭. Cycle ৬-এ I6 (fadd.d f12, f6, f4) Add3-এ issue: f6-এর Qi = Mult2, তাই Qj = Mult2 — সে ঠিক লেখকের (I5) অপেক্ষায়, I3-এর নয়। Cycle ৭-এ বিয়োগের ফল “Add2 → 4” CDB-তে: Mult2 Vk = 4 পেল, register file-এ f8 = 4. দেখুন Add1-এর Vk এখনো 1.5 — I4 f8 বদলে দিলেও I3-এর ইনপুট অক্ষত, WAR কোনো ক্ষতি করল না।
  6. Cycle ৮–১৪ — গুণ শেষ। Mult2 ৮–১৩ execute করে cycle ১৪-এ “Mult2 → 20” ঘোষণা করল (5 × 4). Add3 Vj = 20 পেল। Qi[f6] = Mult2 ছিল, তাই register file-এ f6 = 20. ভাগ (Mult1) তখনো চলছে, আর Add1 (I3) এখনো তার অপেক্ষায় — কিন্তু পেছনের তিনটি instruction-এর কাজ প্রায় শেষ।
  7. Cycle ১৭ — CDB-এর ঠেলাঠেলি। ভাগ শেষ হলো ১৬-তে, আর I6-এর যোগ (১৫–১৬)-ও ঠিক একই সময়ে শেষ। দুজনেই cycle ১৭-এ CDB চায়, কিন্তু bus একটি। নিয়ম অনুযায়ী পুরোনো I2 জিতল: “Mult1 → 3” (6 ÷ 2). Add1-এর Vj = 3, f0 = 3. I6 এক cycle অপেক্ষা করে cycle ১৮-এ লিখবে (f12 = 20 + 2 = 22). এটাই CDB-এর bandwidth-এর সীমা।
  8. Cycle ২০ — শেষ, আর WAW-এর প্রমাণ। I3 ১৮–১৯ execute করে cycle ২০-এ “Add1 → 4.5” (3 + 1.5) ঘোষণা করল। কিন্তু Qi[f6] অনেক আগেই Mult2 হয়ে ফাঁকা হয়েছে, Add1-এর নামে নেই — তাই f6-এ লেখা হলো না, f6 = 20-ই থাকল, যা প্রোগ্রামের ক্রমে সঠিক (I5 শেষ লেখক)। মোট ২০ cycle; চূড়ান্ত f0 = 3, f2 = 6, f6 = 20, f8 = 4, f12 = 22 — ক্রমে চালালে যা হতো, হুবহু তাই।

৪ · Simulator-এ Tomasulo, পাশে in-order — আর Tomasulo-র সীমা

হাতে যা মেলালেন, এবার একটি ছোট simulator-এ চালিয়ে দেখুন। কোডটি ঠিক উপরের নিয়ম মানে: প্রতি cycle-এ প্রথমে CDB-তে একটি write (পুরোনোটি আগে), তারপর execute শুরু, তারপর issue. RS-এর সংখ্যা আর latency উপরে বদলে নিজে পরীক্ষা করতে পারেন।

// Tomasulo: reservation stations (RS) + register status (Qi) + ONE common data bus (CDB)
const LAT = { "fld": 2, "fadd.d": 2, "fsub.d": 2, "fmul.d": 6, "fdiv.d": 12 };   // execute cycles
const UNIT = { "fld": "Load", "fadd.d": "Add", "fsub.d": "Add", "fmul.d": "Mult", "fdiv.d": "Mult" };
const NRS = { Load: 2, Add: 3, Mult: 2 };                                          // RS per unit type
const prog = [["fld", "f2", 0], ["fdiv.d", "f0", "f2", "f4"], ["fadd.d", "f6", "f0", "f8"],
              ["fsub.d", "f8", "f10", "f14"], ["fmul.d", "f6", "f10", "f8"], ["fadd.d", "f12", "f6", "f4"]];
const RF = { f0: 0, f2: 0, f4: 2, f6: 0, f8: 1.5, f10: 5, f12: 0, f14: 1 }, MEM = { 0: 6 }, x1 = 0;
const ALU = { "fadd.d": (a, b) => a + b, "fsub.d": (a, b) => a - b, "fmul.d": (a, b) => a * b, "fdiv.d": (a, b) => a / b };
const Qi = {}, RS = [], t = prog.map(() => ({}));       // Qi[reg] = name of the RS that will write reg
for (const u in NRS) for (let k = 1; k <= NRS[u]; k++) RS.push({ name: u + k, unit: u, busy: false });
const src = x => Qi[x] ? [null, Qi[x]] : [RF[x], null];  // value now, or the tag to wait for
for (let c = 1, next = 0; t.some(x => !x.w); c++) {
  const d = RS.filter(r => r.busy && r.end < c).sort((a, b) => a.i - b.i)[0];  // 1) WRITE: oldest gets the CDB
  if (d) {
    const v = d.op === "fld" ? MEM[d.A] : ALU[d.op](d.Vj, d.Vk);
    for (const r of RS) for (const [V, Q] of [["Vj", "Qj"], ["Vk", "Qk"]])            // every waiting RS listens
      if (r[Q] === d.name) { r[V] = v; r[Q] = null; r.rdy = c; }
    for (const reg in Qi) if (Qi[reg] === d.name) { RF[reg] = v; delete Qi[reg]; }  // only if still newest writer
    t[d.i].w = c; d.busy = false;
  }
  for (const r of RS) if (r.busy && !r.start && !r.Qj && !r.Qk && r.rdy < c) {  // 2) EXECUTE: operands ready earlier
    r.start = t[r.i].s = c; r.end = t[r.i].e = c + LAT[r.op] - 1; if (r.op === "fld") r.A += x1;  // A = address
  }
  const I = prog[next], r = I && RS.find(s => !s.busy && s.unit === UNIT[I[0]]); // 3) ISSUE in order, needs free RS
  if (r) {
    const [op, dst, j, k] = I, ld = op === "fld";
    Object.assign(r, { busy: true, op, i: next, rdy: c, start: 0, end: Infinity, A: ld ? j : null });
    [[r.Vj, r.Qj], [r.Vk, r.Qk]] = ld ? [[null, null], [null, null]] : [src(j), src(k)];
    Qi[dst] = r.name; t[next++].iss = c;                                        // rename: dst now waits for r
  }
}
const txt = I => I[0] === "fld" ? `fld ${I[1]}, ${I[2]}(x1)` : `${I[0]} ${I.slice(1).join(", ")}`;
console.log("instruction             issue  execute   write");
prog.forEach((I, i) => console.log(`${txt(I).padEnd(24)}${String(t[i].iss).padEnd(7)}${(t[i].s + "-" + t[i].e).padEnd(10)}${t[i].w}`));
console.log(`total: ${Math.max(...t.map(x => x.w))} cycles   final: ` + Object.entries(RF).map(([k, v]) => `${k}=${v}`).join(" "));

আসল আউটপুট:

instruction             issue  execute   write
fld f2, 0(x1)           1      2-3       4
fdiv.d f0, f2, f4       2      5-16      17
fadd.d f6, f0, f8       3      18-19     20
fsub.d f8, f10, f14     4      5-6       7
fmul.d f6, f10, f8      5      8-13      14
fadd.d f12, f6, f4      6      15-16     18
total: 20 cycles   final: f0=3 f2=6 f4=2 f6=20 f8=4 f10=5 f12=22 f14=1

Stepper-এর সাথে প্রতিটি সংখ্যা মিলছে। লক্ষ করুন তিনটি জিনিস। এক, issue কলাম ১ থেকে ৬ — একটানা, কারণ কোনো RS ভরে যায়নি; dynamic scheduling-এ issue আর কারও জন্য অপেক্ষা করে না, অপেক্ষা হয় RS-এর ভেতরে। দুই, execute কলাম আর প্রোগ্রামের ক্রম মানে না: I4 শুরু ৫-এ, I5 ৮-এ, I6 ১৫-তে — সবই I3-এর (১৮) আগে। তিন, I6 execute শেষ করল ১৬-তে, অথচ write ১৮-তে: cycle ১৭-এর CDB পুরোনো ভাগ নিয়ে নিয়েছিল। আর শেষ লাইনের f6=20 প্রমাণ করে WAW ঠিকঠাক সামলানো হয়েছে — I3-এর ৪.৫ cycle ২০-এ CDB-তে এলেও f6-এ বসেনি; f12=22 প্রমাণ করে I6 সঠিক লেখক I5-এর মানই পেয়েছে।

এবার একই প্রোগ্রাম পাশাপাশি দুই যন্ত্রে। নিচের কোড শুধু সময়ের হিসাব করে: in-order মেশিনে source তৈরি না হলে issue থামে (আর আগের কোনো লেখা একই register-এ বাকি থাকলেও, WAW এড়াতে); out-of-order মেশিনে issue একটানা, execute শুরু হয় source তৈরি হলেই। দুটিতেই ফল লেখার পথ একটি।

// Same program, two machines: in-order stall-on-use vs dynamic scheduling (Tomasulo timing, 1 CDB)
const LAT = { "fld": 2, "fadd.d": 2, "fsub.d": 2, "fmul.d": 6, "fdiv.d": 12 };
const prog = [["fld", "f2", []], ["fdiv.d", "f0", ["f2", "f4"]], ["fadd.d", "f6", ["f0", "f8"]],
              ["fsub.d", "f8", ["f10", "f14"]], ["fmul.d", "f6", ["f10", "f8"]], ["fadd.d", "f12", ["f6", "f4"]]];
function run(inOrder) {
  const W = {}, bus = new Set(), rows = []; let last = 0;   // W[reg] = cycle the newest value of reg is written
  for (const [op, d, src] of prog) {
    const ready = Math.max(0, ...src.map(r => W[r] || 0));  // cycle when the last source is written
    let issue = last + 1;                                   // one issue per cycle, in program order
    if (inOrder) issue = Math.max(issue, ready, W[d] || 0); // stall-on-use (RAW) and wait for older write (WAW)
    const start = Math.max(issue, ready) + 1, end = start + LAT[op] - 1;
    let w = end + 1; while (bus.has(w)) w++;                // one result bus: older instructions booked first
    bus.add(w); W[d] = w; last = issue; rows.push([op, d, issue, start, end, w]);
  }
  return rows;
}
const A = run(true), B = run(false);
console.log("instruction  dst  | in-order: issue exec  write | out-of-order: issue exec  write");
A.forEach((a, i) => { const b = B[i];
  console.log(`${a[0].padEnd(12)} ${a[1].padEnd(4)} |          ${String(a[2]).padEnd(5)} ${(a[3] + "-" + a[4]).padEnd(5)} ${String(a[5]).padEnd(5)} |              ${String(b[2]).padEnd(5)} ${(b[3] + "-" + b[4]).padEnd(5)} ${b[5]}`); });
const tA = Math.max(...A.map(a => a[5])), tB = Math.max(...B.map(b => b[5]));
console.log(`total cycles: in-order ${tA}, out-of-order ${tB}, speedup ${(tA / tB).toFixed(2)}x`);

আসল আউটপুট:

instruction  dst  | in-order: issue exec  write | out-of-order: issue exec  write
fld          f2   |          1     2-3   4     |              1     2-3   4
fdiv.d       f0   |          4     5-16  17    |              2     5-16  17
fadd.d       f6   |          17    18-19 20    |              3     18-19 20
fsub.d       f8   |          18    19-20 21    |              4     5-6   7
fmul.d       f6   |          21    22-27 28    |              5     8-13  14
fadd.d       f12  |          28    29-30 31    |              6     15-16 18
total cycles: in-order 31, out-of-order 20, speedup 1.55x

ডান দিকের কলাম আগের simulator-এর সাথে হুবহু মেলে (এই কোড RS-এর সংখ্যা গোনে না; উদাহরণে RS কখনো ভরেনি বলে ফল একই)। বাঁ দিকে in-order: I4 issue ১৮-তে, ডানে ৪-এ — চোদ্দো cycle আগে। মোট ৩১ বনাম ২০, speedup প্রায় ১.৫৫ গুণ, আর এটা কোনো বাড়তি execution unit ছাড়াই, শুধু অপেক্ষার জায়গা আর tag দিয়ে। তবে লক্ষ করুন, out-of-order মেশিনেও মোট সময় ঠিক করছে I1 → I2 → I3 শৃঙ্খল (৪ + ১২ + …)। Dynamic scheduling সত্যিকারের dependence-এর শৃঙ্খল ছোট করতে পারে না; শুধু অন্য কাজ তার ফাঁকে ঢুকিয়ে দেয়।

Tomasulo-র সীমা

  • CDB-এর bandwidth. প্রতি cycle-এ একটিই ফল ঘোষণা — উপরে I6 এক cycle হারাল। একাধিক CDB দিলে প্রতিটি RS-এর প্রতিটি operand-কে প্রতিটি bus-এর tag মেলাতে হয়: তার আর তুলনাকারী সার্কিট দুটোই গুণে গুণে বাড়ে।
  • RS-এর সংখ্যা. অপেক্ষারত instruction RS দখল করে বসে থাকে। Add RS মাত্র একটি হলে কী হয়, তা অনুশীলন ৪-এ দেখবেন — ফল চমকে দেওয়ার মতো।
  • Imprecise exception. ফল CDB থেকে সরাসরি register file-এ যায়, শেষ হওয়ার ক্রমে। উপরের উদাহরণে cycle ১৪-তে f6 = 20 লেখা হয়ে গেছে, অথচ I2 (ভাগ) তখনো শেষ হয়নি। এখন যদি ভাগে কোনো exception হতো, register file-এ পরের instruction-এর ফল আগেই বসে আছে — পর্ব ৩৮-এর precise exception-এর শর্ত ভঙ্গ। Model 91-এর floating point exception তাই imprecise ছিল বলে পরিচিত (imprecise exception).
  • Branch পার হওয়া যায় না। একই কারণে branch-এর পরের instruction-কে চালিয়ে register-এ লিখতে দেওয়া বিপজ্জনক — অনুমান ভুল হলে লেখা ফেরানোর উপায় নেই। তাই মূল Tomasulo-তে branch-এর পেছনের instruction ফল লেখে না, যতক্ষণ না branch নিষ্পত্তি হয়।

শেষ দুটি সমস্যার একটিই সমাধান: ফল সরাসরি register-এ না পাঠিয়ে মাঝে একটি সারিতে রাখা, যেখান থেকে প্রোগ্রামের ক্রমে চূড়ান্ত হবে।

৫ · Hardware speculation আর reorder buffer — ক্রমের বাইরে চালানো, ক্রমে চূড়ান্ত করা

পর্ব ৩৮-এ একটি প্রতিশ্রুতি ছিল: শত শত instruction ক্রমের বাইরে চললেও exception precise রাখা যায়, reorder buffer (ROB) দিয়ে। ধারণাটি ১৯৮০-এর দশকের মাঝামাঝি James Smith আর Andrew Pleszkun-এর গবেষণায় এসেছিল, আর Tomasulo-র সাথে মিলিয়ে speculative execution-এর ভিত্তি হয়েছে। ROB একটি বৃত্তাকার সারি (circular queue): issue-এর সময় প্রতিটি instruction প্রোগ্রামের ক্রমে লেজে একটি entry পায়, আর শুধু মাথা থেকেই বের হয়।

ঘরঅর্থ
Typebranch (কোনো ফল নেই), store (ফল মেমরিতে যাবে), বা register-এ লেখা instruction (ALU, load)
Destinationregister-এর নাম, বা store-এর মেমরি-ঠিকানা
Valueফল, যতক্ষণ না commit হয়
Readyexecute শেষ, মান তৈরি কি না
(বাস্তবে আরও)PC, exception হয়েছে কি না আর তার কারণ, branch-এর অনুমান ঠিক ছিল কি না

ROB যোগ হলে Tomasulo-র ধাপ চারটি হয়, আর একটি বদল আসে: tag এখন RS-এর নাম নয়, ROB entry-এর নম্বর। Register status-এর Qi-তেও ROB-এর নম্বর থাকে।

  1. Issue: একটি খালি RS আর একটি খালি ROB entry লাগে — যেকোনোটি না থাকলে issue থামে। Operand register file-এ, নয়তো ROB-এ তৈরি অবস্থায় থাকলে সেখান থেকে, নয়তো ROB-নম্বরের tag.
  2. Execute: আগের মতোই, operand এলে।
  3. Write result: ফল CDB-তে ROB-নম্বরসহ — অপেক্ষারত RS আর ROB entry-তে, register file-এ নয়। Entry-র Ready = হ্যাঁ।
  4. Commit: শুধু মাথার entry, আর শুধু Ready হলে। Register-লেখা instruction হলে মান register file-এ বসে; store হলে এখন, এবং কেবল এখনই, মেমরিতে লেখা হয়; ঠিকভাবে অনুমান করা branch হলে শুধু সরানো হয়। আর মাথার branch-এর অনুমান ভুল প্রমাণিত হয়ে থাকলে, বা মাথার instruction-এর exception থাকলে: ROB-এর বাকি সব entry flush, RS আর register status খালি, আর fetch শুরু সঠিক ঠিকানা থেকে (exception-এর ক্ষেত্রে handler থেকে, mepc = ওই instruction-এর PC).
মূল ধারণা

Architectural অবস্থা — register file আর মেমরি — বদলায় শুধু commit-এ, প্রোগ্রামের ক্রমে। ROB-এর ভেতরের যা কিছু, তা “হয়তো” — অনুমাননির্ভর (speculative). তাই ভুল পথের কাজ ফেলে দেওয়া মানে শুধু ROB-এর কিছু entry মুছে দেওয়া; কারও কোনো ছাপ register বা মেমরিতে থাকে না। পর্ব ৩৮-এর commit point-এরই বড় সংস্করণ: সেখানে ছিল pipeline-এর একটি ধাপ, এখানে একটি সারির মাথা।

Worked example: cache miss, ভুল branch আর squash

নিচের ছয়টি instruction ধরুন, প্রতি cycle-এ একটি করে ROB-এ ঢোকে। fld-এ cache miss, তাই তার ফল আসে cycle ৯-এ। beq-কে “not taken” অনুমান করা হয়েছিল, তাই fetch সোজা নিচে চলেছে; কিন্তু cycle ৪-এ execute করে দেখা গেল branch আসলে taken — #৪ থেকে #৬ ভুল পথের, এদের শেষে squash (বাতিল) করতে হবে। প্রাথমিক মান: x1 = 0, x5 = 0, f2 = 2, f10 = 9, f12 = 4; মেমরিতে M[0] = 3, M[8] = 0. প্রতি cycle-এ সর্বোচ্চ একটি commit. হিসাব সরল রাখতে এখানে অংশ ৩-এর মতো আলাদা write-result cycle ধরা হয়নি: কোনো ফল যে cycle-এ তৈরি হয়, তার পরের cycle থেকেই নির্ভরশীল instruction তা ব্যবহার করে, আর entry মাথায় থাকলে commit হয়।

Cycle ৩তিনটি entry, মাথা #1
#  Instruction          Dest  Value  Ready
1  fld    f6, 0(x1)    f6    ?      no    ← মাথা (cache miss)
2  fmul.d f4, f6, f2    f4    ?      no
3  beq    x5, x0, L     —     ?      no    (অনুমান: not taken)

Register file: f2=2  f4=0  f6=0  f8=0   M[8]=0
Cycle ৬ছয়টি entry; branch-এর ভুল ধরা পড়েছে cycle ৪-এ
#  Instruction          Dest  Value  Ready
1  fld    f6, 0(x1)    f6    ?      no    ← মাথা
2  fmul.d f4, f6, f2    f4    ?      no
3  beq    x5, x0, L     —     taken  yes   ✗ ভুল অনুমান
4  fadd.d f8, f4, f4    f8    ?      no    (ভুল পথ)
5  fsub.d f2, f10, f12  f2    ?      no    (ভুল পথ)
6  fsd    f2, 8(x1)     M[8]  ?      no    (ভুল পথ)

Register file: f2=2  f4=0  f6=0  f8=0   M[8]=0
Cycle ৮ভুল পথের দুটি কাজ শেষ — কিন্তু কোথাও লেখা হয়নি
#  Instruction          Dest  Value  Ready
1  fld    f6, 0(x1)    f6    ?      no    ← মাথা
2  fmul.d f4, f6, f2    f4    ?      no
3  beq    x5, x0, L     —     taken  yes   ✗
4  fadd.d f8, f4, f4    f8    ?      no
5  fsub.d f2, f10, f12  f2    5      yes
6  fsd    f2, 8(x1)     M[8]  5      yes

Register file: f2=2  f4=0  f6=0  f8=0   M[8]=0
Cycle ১০মাথা প্রস্তুত: #1 commit
#  Instruction          Dest  Value  Ready
1  fld    f6, 0(x1)    f6    3      ✓ commit হয়েছে
2  fmul.d f4, f6, f2    f4    ?      no    ← মাথা (execute ১০–১৫)
3  beq    x5, x0, L     —     taken  yes   ✗
4  fadd.d f8, f4, f4    f8    ?      no
5  fsub.d f2, f10, f12  f2    5      yes
6  fsd    f2, 8(x1)     M[8]  5      yes

Register file: f2=2  f4=0  f6=3  f8=0   M[8]=0
Cycle ১৬#2 commit
#  Instruction          Dest  Value  Ready
2  fmul.d f4, f6, f2    f4    6      ✓ commit হয়েছে
3  beq    x5, x0, L     —     taken  yes   ✗ ← মাথা
4  fadd.d f8, f4, f4    f8    ?      no
5  fsub.d f2, f10, f12  f2    5      yes
6  fsd    f2, 8(x1)     M[8]  5      yes

Register file: f2=2  f4=6  f6=3  f8=0   M[8]=0
Cycle ১৭–১৮ভুল branch মাথায়: squash
#  Instruction          Dest  Value  Ready
3  beq    x5, x0, L     → ভুল অনুমান, মাথায় পৌঁছাল
4  fadd.d f8, f4, f4    ✗ squash
5  fsub.d f2, f10, f12  ✗ squash (মান 5 ফেলে দেওয়া)
6  fsd    f2, 8(x1)     ✗ squash (মেমরি ছোঁয়নি)

Register file: f2=2  f4=6  f6=3  f8=0   M[8]=0
cycle 18: fetch শুরু L থেকে
  1. Cycle ৩ — সারি ভরছে। #১ fld, #২ fmul.d, #৩ beq প্রোগ্রামের ক্রমে ROB-এর লেজে ঢুকল। মাথা #১, যার load-এ cache miss — মান আসবে cycle ৯-এ। Branch predictor “not taken” বলেছে, তাই fetch L-এ না গিয়ে সোজা নিচের instruction আনছে।
  2. Cycle ৬ — ভুল জানা গেছে, কিন্তু তাড়াহুড়ো নেই। #৩ cycle ৪-এ execute হয়ে দেখল x5 = 0, branch taken — অনুমান ভুল। এই সরল (Hennessy–Patterson-ধাঁচের) নকশায় সেটা শুধু #৩-এর entry-তে লিখে রাখা হলো; পদক্ষেপ নেওয়া হবে #৩ মাথায় পৌঁছালে। এর মধ্যে ভুল পথের #৪, #৫, #৬ সারিতে ঢুকে পড়েছে।
  3. Cycle ৮ — ভুল পথের কাজ শেষ, কিন্তু অদৃশ্য। #৫ (fsub.d f2: 9 − 4 = 5) আর #৬ (fsd, মান 5) প্রস্তুত — মাথার #১-এর অনেক আগেই। কিন্তু তাদের মান আছে শুধু ROB-এ: register file-এ f2 এখনো 2, M[8] এখনো 0. Store মেমরিতে লেখে কেবল commit-এ।
  4. Cycle ১০ — প্রথম commit. #১-এর মান (3) cycle ৯-এ এসেছে, তাই cycle ১০-এ মাথা থেকে commit: register file-এ f6 = 3. মাথা এখন #২, যে f6 পেয়ে ১০–১৫ গুণ করছে। লক্ষ করুন, #৫ আর #৬ প্রস্তুত থাকলেও commit করতে পারে না — মাথা না হওয়া পর্যন্ত সবাইকে অপেক্ষা করতে হয়।
  5. Cycle ১৬ — #২ commit. গুণফল 3 × 2 = 6, তাই f4 = 6. মাথা এখন ভুল-অনুমানের branch #৩.
  6. Cycle ১৭ — squash. #৩ মাথায়, আর তার entry বলছে অনুমান ভুল। তাই #৪, #৫, #৬ — মাথার পেছনের সবকিছু — flush; RS আর register status খালি; cycle ১৮-এ fetch শুরু L থেকে। Architectural অবস্থা: f6 = 3, f4 = 6, f2 = 2 (ভুল পথের 5 কোথাও পৌঁছায়নি), M[8] = 0. Exception-এর ক্ষেত্রে হুবহু একই নাচ, শুধু fetch যায় handler-এ। আধুনিক core ভুল ধরা পড়ার সাথে সাথেই (এখানে cycle ৫-এ) recover করে — অংশ ৬-এর checkpoint দিয়ে।

একই ঘটনা একটি ছোট simulator-এ। প্রতিটি entry-র “done” মানে কোন cycle-এর শেষে তার ফল তৈরি; commit হয় মাথা থেকে, প্রতি cycle-এ একটি, আর মাথা তৈরি না হলে সবাই অপেক্ষা করে।

// Reorder buffer: results finish out of order, commit strictly in order from the head
// Each entry: text, dest, value, cycle its result is ready ("done"); beq was predicted NOT taken but IS taken
const rob = [
  { t: "fld    f6, 0(x1)",   dest: "f6",  val: 3,   done: 9  },   // cache miss: slow
  { t: "fmul.d f4, f6, f2",  dest: "f4",  val: 6,   done: 15 },   // waits for f6
  { t: "beq    x5, x0, L",   dest: "-",   val: "taken", done: 4, mispredict: true },
  { t: "fadd.d f8, f4, f4",  dest: "f8",  val: null, done: 17 },  // wrong path, waits for f4
  { t: "fsub.d f2, f10, f12", dest: "f2", val: 5,   done: 7  },   // wrong path, finishes early
  { t: "fsd    f2, 8(x1)",   dest: "M[8]", val: 5,  done: 8  }];  // wrong path store: waits in ROB
const show = c => rob.forEach((e, i) => console.log(`  #${i + 1} ${e.t.padEnd(20)} dest=${e.dest.padEnd(5)}` +
  ` value=${String(c >= e.done ? e.val : "?").padEnd(6)} ready=${c >= e.done ? "yes" : "no"}`));
console.log("ROB at the end of cycle 8 (head = #1):"); show(8);
const RF = { f2: 2, f4: 0, f6: 0, f8: 0 }, MEM = { 8: 0 };
let head = 0;
for (let c = 1; head < rob.length; c++) {          // commit at most one entry per cycle
  const e = rob[head];
  if (e.done >= c) continue;                       // head not ready: everyone behind it waits
  if (e.mispredict) {
    console.log(`cycle ${c}: commit #${head + 1} ${e.t} -> MISPREDICTED: squash ` +
      rob.slice(head + 1).map((x, k) => `#${head + 2 + k}`).join(" ") + `, fetch from L in cycle ${c + 1}`);
    break;
  }
  if (e.dest.startsWith("M")) MEM[8] = e.val; else RF[e.dest] = e.val;
  console.log(`cycle ${c}: commit #${head + 1} ${e.t} -> ${e.dest} = ${e.val}`);
  head++;
}
console.log("architectural state:", JSON.stringify(RF), "MEM[8] =", MEM[8]);
console.log(`early recovery (resolve at cycle ${rob[2].done}, checkpointed map): squash in cycle ${rob[2].done + 1}, fetch L in cycle ${rob[2].done + 1}`);

আসল আউটপুট:

ROB at the end of cycle 8 (head = #1):
  #1 fld    f6, 0(x1)     dest=f6    value=?      ready=no
  #2 fmul.d f4, f6, f2    dest=f4    value=?      ready=no
  #3 beq    x5, x0, L     dest=-     value=taken  ready=yes
  #4 fadd.d f8, f4, f4    dest=f8    value=?      ready=no
  #5 fsub.d f2, f10, f12  dest=f2    value=5      ready=yes
  #6 fsd    f2, 8(x1)     dest=M[8]  value=5      ready=yes
cycle 10: commit #1 fld    f6, 0(x1) -> f6 = 3
cycle 16: commit #2 fmul.d f4, f6, f2 -> f4 = 6
cycle 17: commit #3 beq    x5, x0, L -> MISPREDICTED: squash #4 #5 #6, fetch from L in cycle 18
architectural state: {"f2":2,"f4":6,"f6":3,"f8":0} MEM[8] = 0
early recovery (resolve at cycle 4, checkpointed map): squash in cycle 5, fetch L in cycle 5

প্রথম টেবিলটি stepper-এর cycle ৮-এর ছবি: #৩, #৫, #৬ ready, মাথা #১ নয়। Commit-এর লাইনগুলো দেখায় ক্রম কঠোর — #১ cycle ১০, #২ cycle ১৬, তারপর #৩-এ squash, যদিও #৫ আর #৬ cycle ৮ থেকেই প্রস্তুত ছিল। শেষের architectural state-এ f2 = 2 আর MEM[8] = 0: ভুল পথের কোনো চিহ্ন নেই। আর শেষ লাইনটি এই সরল নকশার দাম দেখায় — ভুল ধরা পড়েছিল cycle ৪-এ, অথচ recovery হলো ১৭-তে, কারণ cache miss-ওয়ালা load মাথা আটকে রেখেছিল। মাথার জন্য অপেক্ষা না করে cycle ৫-এই squash করতে পারলে ঠিক পথের fetch তেরো cycle আগে (১৮-এর বদলে ৫-এ) শুরু হতো। আধুনিক core এটাই করে, অংশ ৬-এর checkpoint দিয়ে।

কেন এটা এত শক্তিশালী

ROB-এর ফলে branch-এর ওপারেও নির্ভয়ে চালানো যায়। একটি সূক্ষ্ম কথা মনে রাখবেন: flush হওয়া instruction register বা মেমরিতে কিছু রাখে না, কিন্তু তারা cache-এ কোনো লাইন এনে ফেলতে পারে — “ছাপহীন” ঠিক নয়। Spectre আর Meltdown-এর মতো দুর্বলতার শিকড় এখানেই; ধারণা আর প্রতিরক্ষা পর্ব ৪১-এ (speculation-security.html).

৬ · Explicit register renaming — physical register file, map table আর free list

H&P-ধাঁচের ROB-এ মান থাকে ROB-এর ভেতরে, আর commit-এ register file-এ কপি হয়। একে বলা যায় “ROB-value style”. বেশিরভাগ আধুনিক বড় core আরেকটি পথ নেয়, যার একটি পরিচিত প্রথম দিকের উদাহরণ ১৯৯০-এর দশকের মাঝামাঝি MIPS R10000: একটি বড় physical register file (PRF), যেখানে নিশ্চিত আর অনুমাননির্ভর সব মানই থাকে; ROB শুধু হিসাব রাখে, মান নয়।

  • Architectural register: প্রোগ্রামারের চোখের নাম — RISC-V-এ x1 … x31.
  • Map table (RAT): প্রতিটি architectural register এখন কোন physical register-এ আছে।
  • Free list: খালি physical register-এর তালিকা।

প্রতিটি instruction rename ধাপে তিনটি কাজ করে, এই ক্রমে: (১) source-এর নাম map table দিয়ে অনুবাদ; (২) destination-এর জন্য free list থেকে একটি নতুন physical register; (৩) map table-এ destination-এর ঘরে নতুনটি বসানো, আর পুরোনো mapping-টি ROB entry-তে লিখে রাখা। পুরোনো physical register কখন ফেরত যাবে? যখন এই instruction commit করে — কারণ তখন নিশ্চিত যে পুরোনো মানের আর কোনো পাঠক বাকি নেই (সব পাঠক প্রোগ্রামে আগে, তাই আগেই commit করেছে), আর কোনো recovery তাকে আর ফিরিয়ে আনবে না।

একটি ছোট RISC-V integer উদাহরণ নিন। শুরুতে x1 … x8 আছে p1 … p8-এ; free list = p9 … p14.

I1  mul  x5, x1, x2
I2  add  x6, x5, x3     # RAW: x5 (I1)
I3  sub  x5, x4, x7     # WAR: I2 পড়ে x5, I3 লেখে · WAW: I1 আর I3 দুজনেই x5
I4  add  x8, x5, x6     # RAW: x5 (I3), x6 (I2)
I5  addi x6, x8, 1      # WAR: I4 পড়ে x6 · WAW: I2 আর I5 দুজনেই x6
1mul  x5, x1, x2  →  mul  p9,  p1,  p22add  x6, x5, x3  →  add  p10, p9,  p33sub  x5, x4, x7  →  sub  p11, p4,  p74add  x8, x5, x6  →  add  p12, p11, p105addi x6, x8, 1   →  addi p13, p12, 1
Map table (শুধু বদলানো ঘর) আর free list
x5
x6
x8
free list
commit-এ ফেরত যাবে
  1. শুরু। প্রতিটি architectural register xi আছে physical register pi-তে (এখানে শুধু x5, x6, x8-এর ঘর দেখানো হচ্ছে, কারণ এরাই বদলাবে)। Free list-এ p9 থেকে p14 — ছয়টি খালি ঘর।
  2. I1 mul x5, x1, x2. Source x1 → p1, x2 → p2. Destination x5-এর জন্য free list-এর মাথা থেকে p9. Map table-এ x5 = p9; পুরোনো p5 এই instruction-এর ROB entry-তে লেখা থাকল — I1 commit করলে p5 ফেরত যাবে।
  3. I2 add x6, x5, x3. Source x5 এখন map table অনুযায়ী p9 — অর্থাৎ ঠিক I1-এর ফল; সত্যিকারের dependence (RAW) রয়ে গেল, যেমন থাকা উচিত। x3 → p3. Destination x6 পেল p10, পুরোনো p6 মনে রাখা হলো।
  4. I3 sub x5, x4, x7 — নামের সংঘাতের অবসান। I3 আবার x5-এ লেখে, কিন্তু পায় একেবারে নতুন p11. I2 যে p9 পড়বে, তাতে I3 কখনো হাত দেয় না (WAR শেষ); আর I1 লেখে p9-এ, I3 লেখে p11-এ — দুই লেখকের দুই আলাদা ঘর (WAW শেষ)। তাই I3 চাইলে I1 আর I2-এর আগেও চলতে পারে। পুরোনো mapping p9 ফেরত যাবে I3-এর commit-এ।
  5. I4 add x8, x5, x6. x5 → p11 (I3-এর ফল, I1-এর নয় — সঠিক), x6 → p10 (I2-এর ফল)। x8 পেল p12, পুরোনো p8 মনে রাখা হলো।
  6. I5 addi x6, x8, 1. x8 → p12. x6 পেল p13 — I4 যে p10 পড়ে, সেটি অক্ষত। শেষ অবস্থা: x5 = p11, x6 = p13, x8 = p12; free list-এ শুধু p14. পাঁচটি commit-এর পর p5, p6, p9, p8, p10 — এই ক্রমে — আবার free list-এ ফিরবে। Free list ফুরিয়ে গেলে rename থেমে যায়, তাই physical register-এর সংখ্যা ঠিক করে কতগুলো লেখক একসাথে উড়ন্ত থাকতে পারে।

Renaming-এর আগে আর পরে WAR/WAW গুনে দেখুন একটি ছোট program দিয়ে:

// Register renaming with a map table (RAT) + free list of physical registers
const prog = [["mul", "x5", "x1", "x2"], ["add", "x6", "x5", "x3"], ["sub", "x5", "x4", "x7"],
              ["add", "x8", "x5", "x6"], ["addi", "x6", "x8", "1"]];
const map = {}; for (let i = 1; i <= 8; i++) map["x" + i] = "p" + i;   // start: xi lives in pi
const free = ["p9", "p10", "p11", "p12", "p13", "p14"];
const isReg = s => /^[xp]\d+$/.test(s);
function hazards(code) {                    // count WAR and WAW pairs (i before j)
  let war = 0, waw = 0;
  code.forEach((a, i) => code.slice(i + 1).forEach(b => {
    if (b[1] === a[1]) waw++;                                   // both write the same register
    if (a.slice(2).includes(b[1])) war++;                       // a reads what b later writes
  }));
  return `${war} WAR, ${waw} WAW`;
}
const out = [];
prog.forEach(([op, d, s1, s2], i) => {
  const p1 = isReg(s1) ? map[s1] : s1, p2 = isReg(s2) ? map[s2] : s2;   // 1) read sources through the map
  const pd = free.shift(), old = map[d];                                 // 2) new physical reg for the dest
  map[d] = pd; out.push([op, pd, p1, p2]);                               // 3) update the map
  console.log(`I${i + 1}  ${`${op} ${d}, ${s1}, ${s2}`.padEnd(17)} ->  ${`${op} ${pd}, ${p1}, ${p2}`.padEnd(20)}` +
              `map ${d}->${pd}  (old ${old} freed when I${i + 1} commits)   free: [${free.join(" ")}]`);
});
console.log(`\nbefore renaming: ${hazards(prog)}   after renaming: ${hazards(out)}`);
console.log("final map:", Object.entries(map).map(([a, p]) => `${a}=${p}`).join(" "));

আসল আউটপুট:

I1  mul x5, x1, x2    ->  mul p9, p1, p2      map x5->p9  (old p5 freed when I1 commits)   free: [p10 p11 p12 p13 p14]
I2  add x6, x5, x3    ->  add p10, p9, p3     map x6->p10  (old p6 freed when I2 commits)   free: [p11 p12 p13 p14]
I3  sub x5, x4, x7    ->  sub p11, p4, p7     map x5->p11  (old p9 freed when I3 commits)   free: [p12 p13 p14]
I4  add x8, x5, x6    ->  add p12, p11, p10   map x8->p12  (old p8 freed when I4 commits)   free: [p13 p14]
I5  addi x6, x8, 1    ->  addi p13, p12, 1    map x6->p13  (old p10 freed when I5 commits)   free: [p14]

before renaming: 2 WAR, 2 WAW   after renaming: 0 WAR, 0 WAW
final map: x1=p1 x2=p2 x3=p3 x4=p4 x5=p11 x6=p13 x7=p7 x8=p12

প্রতিটি লাইন stepper-এর একটি ধাপ। মূল কোডে ২টি WAR (I2 → I3-এ x5, I4 → I5-এ x6) আর ২টি WAW (I1/I3-এ x5, I2/I5-এ x6); rename-এর পরে দুটিই শূন্য, কারণ প্রতিটি destination আলাদা physical register (p9 থেকে p13). RAW গোনা হয়নি — সেগুলো থেকে যায়, rename-করা কোডেও (যেমন I4 পড়ে p11, যা I3 লেখে)। শেষ map-এ x5 = p11, x6 = p13, x8 = p12; এই মানগুলোই এখন “x5”, “x6”, “x8”.

দুই ধাঁচের তুলনা, আর ভুল branch-এ map table ফেরানো

বিষয়ROB-value style (H&P-র ROB, Intel P6-এর মতো নকশা বলে পরিচিত)Physical register file (MIPS R10000, Alpha 21264 আর আজকের বেশিরভাগ বড় core)
অনুমাননির্ভর মান কোথায়ROB entry-তেPhysical register-এ
Commit-এ কী হয়মান ROB থেকে architectural register file-এ কপিকোনো কপি নয়; শুধু পুরোনো physical register free list-এ ফেরত
Operand পড়াROB, নয়তো register file — দুই জায়গা থেকেসবসময় একটি PRF থেকে
সুবিধাধারণায় সরল; register file ছোটডেটার নাড়াচাড়া কম, যা power আর port-এর দিক থেকে বড় সুবিধা
অসুবিধাপ্রতিটি মান দুবার লেখা; ROB-কে অনেক port-এর ডেটা-ভান্ডার হতে হয়Map table ফেরানো আর free list-এর হিসাব জটিল

ভুল branch-এ PRF-ধাঁচে কী ফেরাতে হয়? Map table — কারণ ভুল পথের instruction-গুলো এতে নতুন mapping বসিয়ে গেছে। দুটি উপায়: (১) ROB-এর লেজ থেকে উল্টো দিকে হেঁটে প্রতিটি entry-র “পুরোনো mapping” আবার বসানো আর তার নতুন register free list-এ ফেরত দেওয়া — সময় লাগে entry-র সংখ্যা অনুযায়ী; (২) প্রতিটি branch-এর সময় map table-এর একটি কপি (checkpoint) রেখে দেওয়া, যাতে ভুল ধরা পড়ামাত্র এক ধাপে ফেরানো যায় — ROB-এর মাথার অপেক্ষা ছাড়াই।

৭ · মেমরির ক্রম — load/store queue আর memory disambiguation

Register-এর নাম decode-এর সময়ই জানা, তাই rename করা সহজ। মেমরির ঠিকানা কিন্তু জানা যায় অনেক পরে — sw x5, 0(x10)-এর ঠিকানা x10 হিসাব না হওয়া পর্যন্ত অজানা। তাই মেমরির dependence আলাদাভাবে সামলাতে হয়, একটি load/store queue (LSQ) দিয়ে, যেখানে চলমান load আর store প্রোগ্রামের ক্রমে থাকে। তিনটি নিয়ম:

  1. Store মেমরিতে লেখে কেবল commit-এর পরে। তার আগে ঠিকানা আর ডেটা store queue-তে বসে থাকে — ভুল পথের store যেন কখনো মেমরি না ছোঁয় (অংশ ৫-এর #৬)।
  2. Load আগের store-গুলোর সাথে ঠিকানা মেলায়। কোনো আগের store একই ঠিকানায় থাকলে আর তার ডেটা তৈরি থাকলে, load cache-এ না গিয়ে সরাসরি সেই ডেটা নেয় — store-to-load forwarding. না মিললে cache থেকে পড়ে।
  3. আগের কোনো store-এর ঠিকানা অজানা হলে? এটাই memory disambiguation-এর প্রশ্ন। সাবধানী পথ: অপেক্ষা। সাহসী পথ: ধরে নেওয়া যে ঠিকানা মিলবে না, আর load আগে চালিয়ে দেওয়া (speculative load). পরে store-এর ঠিকানা জানা গেলে hardware পরীক্ষা করে; মিলে গেলে load আর তার পরের সব instruction squash করে আবার চালায় — ভুল branch-এর মতোই।

একটি ছোট উদাহরণ, প্রোগ্রামের ক্রমে (x10 = 0x1000, x5 = 42):

Instructionঠিকানাকী হয়
S1 sw x5, 0(x10)0x1000, ডেটা 42Store queue-তে বসে, commit-এর অপেক্ষায়
S2 sw x6, 0(x12)অজানা (x12 এখনো একটি ধীর load-এর ফলের অপেক্ষায়)Store queue-তে, ঠিকানা “?”
L1 lw x7, 0(x11), x11 = 0x10000x1000S1-এর সাথে মিলল। কিন্তু মাঝে S2-এর ঠিকানা অজানা — সাবধানী নকশা অপেক্ষা করবে; সাহসী নকশা S1 থেকে 42 forward করে এগোবে, আর S2-এর ঠিকানা পরে 0x1000 বেরোলে L1 আর পরের সবাইকে আবার চালাবে
L2 lw x8, 8(x13), x13 = 0x20000x2008জানা কোনো store-এর সাথে মেলে না; সাহসী নকশায় cache থেকে আগেই পড়ে নেয়

বাস্তবে বেশিরভাগ load আগের অজানা store-এর ওপর নির্ভর করে না, তাই সাহসী পথ সাধারণত লাভজনক। কিন্তু কিছু নির্দিষ্ট load বারবার ধরা খায় (যেমন একটি লুপে প্রতিবার আগের ঘুরের লেখা পড়া)। এর জন্য memory dependence prediction: hardware মনে রাখে কোন load আগে কোন store-এর কাছে ধরা খেয়েছিল, আর শুধু সেগুলোকে অপেক্ষা করায়। ১৯৯০-এর দশকের শেষ দিকের “store sets” গবেষণা এই ধারণার একটি পরিচিত উদাহরণ; Intel ২০০৬ সালের দিকে Core microarchitecture-এ load-কে আগের অজানা store পেরিয়ে চালানোর ব্যবস্থাকে “memory disambiguation” নামেই প্রচার করেছিল বলে জানা যায়।

৮ · আধুনিক out-of-order core — ধাপ, আকার আর দাম

সব যন্ত্রাংশ এক জায়গায় বসালে আজকের একটি বড় core-এর pipeline মোটামুটি এমন দেখায় (নাম আর সীমারেখা নির্মাতাভেদে আলাদা):

ধাপক্রমকাজএই পর্বের কোন অংশ
Fetchin-orderBranch predictor-এর দেখানো পথে প্রতি cycle-এ কয়েকটি instruction আনাপর্ব ৩৭
Decodein-orderInstruction-কে এক বা একাধিক micro-op-এ ভাঙা (x86-এ বিশেষ জরুরি; RISC-V আর ARM-এ বেশিরভাগ এক-এক)পর্ব ৩১, ৩৪
Renamein-orderMap table আর free list দিয়ে physical register বসানোঅংশ ৬
Dispatchin-orderROB-এ entry, আর issue queue বা load/store queue-তে জায়গাঅংশ ৫, ৭
Issueout-of-orderOperand তৈরি এমন micro-op বেছে execution unit-এ পাঠানো (wakeup আর select)অংশ ৩ (RS-এর আধুনিক রূপ)
Register read, executeout-of-orderPRF পড়া, ALU, load/store, FP, vector unit-এ কাজপর্ব ৩৯, ৪২
Writebackout-of-orderফল PRF-এ, tag ঘোষণা করে অপেক্ষারতদের জাগানো (CDB-এর আধুনিক রূপ)অংশ ৩
Retirein-orderROB-এর মাথা থেকে প্রতি cycle-এ কয়েকটি commit; exception আর ভুল branch এখানে চূড়ান্তঅংশ ৫

একটি বিভ্রান্তি আগেই মিটিয়ে রাখুন: “issue” আর “dispatch” শব্দ দুটি বইভেদে উল্টো অর্থে ব্যবহৃত হয়। Hennessy–Patterson-এ Tomasulo-র “issue” হলো RS-এ বসানো; অনেক শিল্প-নথিতে সেটাকেই “dispatch” বলা হয়, আর “issue” মানে execution unit-এ পাঠানো। উত্তর লেখার সময় নিজের সংজ্ঞা লিখে দেবেন।

আকার কত বড়? আজকের বড় desktop, server আর ফোনের “performance” core-এ ROB আর issue queue মিলিয়ে একসাথে শত শত micro-op উড়ন্ত থাকতে পারে। একটি নির্দিষ্ট উদাহরণ দিলে: Intel ২০২১ সালে তার Golden Cove core-এর ROB ৫১২ entry-র বলে জানিয়েছিল। Physical register-ও কয়েকশো, architectural register-এর কয়েক গুণ। এর অর্থ — একটি cache miss হওয়া load-এর পেছনে core কয়েকশো instruction এগিয়ে কাজ খুঁজতে পারে, পরের cache miss-গুলোও আগেভাগে শুরু করে দিতে পারে। একে বলা হয় memory-level parallelism, আর বড় window-এর লাভের বড় অংশ আসে এখান থেকেই।

দাম — power, area আর জটিলতা

এই সমান্তরালতা বিনামূল্যে আসে না। প্রতি cycle-এ প্রতিটি ঘোষিত tag-কে issue queue-র প্রতিটি অপেক্ষারত source-এর সাথে মেলাতে হয়। উদাহরণ হিসেবে, ১০০ entry-র queue, প্রতি entry-তে ২টি source আর প্রতি cycle-এ ৪টি ফল হলে ১০০ × ২ × ৪ = ৮০০টি তুলনা — প্রতি cycle-এ। Wakeup আর select-কে এক cycle-এ শেষ করতে হয়, নইলে পরস্পর-নির্ভর দুটি যোগ পরপর চলতে পারে না; তাই এই সার্কিট clock-এর গতিও সীমিত করে। Rename-এ প্রতি cycle-এ কয়েকটি instruction-কে একে অপরের সাথে মেলাতে হয় (একই cycle-এ পরেরটি আগেরটির ফল পড়লে)। বড় PRF-এ অনেক read/write port, ROB আর LSQ-এর জন্য বিপুল হিসাব, ভুল অনুমানে ফেলে দেওয়া কাজের শক্তি — সব মিলিয়ে একটি বড় out-of-order core একটি সরল in-order core-এর চেয়ে কয়েক গুণ বেশি জায়গা আর শক্তি নিতে পারে, যেখানে গতি বাড়ে তার চেয়ে কম হারে। পর্ব ২-এর power wall-এর যুগে এই হিসাবটি গুরুত্বপূর্ণ।

তাই সবাই out-of-order নয়। ছোট microcontroller (ARM Cortex-M, অনেক RISC-V embedded core) প্রায় সবসময় in-order: সেখানে নির্ভরযোগ্য সময় আর অল্প শক্তিই মুখ্য। ফোনের chip-এ প্রায়ই বড় out-of-order core-এর পাশে ছোট “efficiency” core থাকে; ARM-এর Cortex-A53 আর A55-এর মতো বহুল ব্যবহৃত ছোট core in-order বলে পরিচিত (সব নির্মাতার efficiency core অবশ্য in-order নয়)। Intel-এর প্রথম Atom (২০০৮ সালের দিকে) in-order ছিল, পরের প্রজন্মে out-of-order-এ সরে যায় বলে জানা যায়।

আরও গভীরে: Window আরও বাড়াতে গবেষক আর chip-নির্মাতারা কয়েকটি পথ খুঁজেছেন — issue queue-কে কয়েকটি ছোট অংশে ভাগ করা (প্রতিটি কয়েকটি unit-এর জন্য), যাতে tag-তুলনার তার ছোট থাকে; “runahead execution”, যেখানে লম্বা cache miss-এর সময় core ফল ছাড়াই সামনে চালিয়ে শুধু পরের miss-গুলো আগেভাগে শুরু করে; আর এমন নকশা যেখানে ROB-এর মাথা আটকে থাকলেও নিশ্চিত হয়ে যাওয়া instruction-এর সম্পদ আগে ছেড়ে দেওয়া যায়। কোন বাণিজ্যিক core ঠিক কোন কৌশল নিয়েছে, তা প্রায়ই প্রকাশিত হয় না; বাইরের বিশ্লেষণ থেকে যে সংখ্যা পাওয়া যায়, তা অনুমাননির্ভর ধরে নেবেন।

৯ · AI-এর যুগে — Tomasulo-র টেবিলে AI, আর যাচাইয়ের অভ্যাস

Tomasulo, ROB আর renaming নিয়ে AI-এর ব্যাখ্যা সাধারণত মসৃণ, কিন্তু cycle-ধরা টেবিলে আর ঐতিহাসিক খুঁটিনাটিতে ভুল প্রায়ই চোখে পড়ে:

AI-এর সাধারণ ভুলকীভাবে ধরবেন
Issue, execute আর write-এর cycle গুনতে এক cycle এদিক-ওদিক; একই cycle-এ CDB থেকে মান পেয়ে সেই cycle-এই execute শুরুপ্রশ্নের নিয়ম আগে লিখে নিন; ছোট simulator-এ চালিয়ে মেলান
দুটি instruction একই cycle-এ CDB-তে লেখাপ্রতিটি write cycle-এর কলাম দেখুন — CDB একটি হলে একই সংখ্যা দুবার থাকতে পারে না
“Tomasulo precise exception দেয়” বা “scoreboard renaming করে”মূল Tomasulo-তে ROB নেই; scoreboard শুধু অপেক্ষা করায়
WAW-এ পুরোনো লেখকের ফলও register-এ বসিয়ে দেওয়াRegister status-এ ঘোষণার মুহূর্তে কোন tag আছে, দেখুন
Rename-এ পুরোনো physical register “এখনই” free করে দেওয়াপুরোনোটি ফেরত যায় প্রতিস্থাপনকারী instruction-এর commit-এ
বানানো ROB বা window-এর আকার, বা ভুল সালের ইতিহাসনির্মাতার প্রকাশিত নথি খুঁজুন; না পেলে “অজানা” ধরুন

AI-কে Tomasulo-র প্রশ্ন করার সময় সব নিয়ম স্পষ্ট দিন আর যাচাইযোগ্য টেবিল চান:

আমি Tomasulo-র algorithm (ROB ছাড়া, Hennessy–Patterson-এর ধাঁচে) অনুশীলন করছি। নিয়মগুলো হুবহু মেনে চলবে: - প্রতি cycle-এ একটি issue, প্রোগ্রামের ক্রমে; RS খালি না থাকলে issue থামে। - Cycle t-তে issue হলে execute সবচেয়ে আগে t+1-এ; CDB থেকে operand cycle W-তে এলে execute W+1-এ। - Latency: fld 2, fadd/fsub 2, fmul 6, fdiv 12. Execute শেষের পরের cycle-এ write; CDB একটি, পুরোনো instruction আগে। - RS: Load 2, Add 3, Mult 2. প্রোগ্রাম: fld f2,0(x1); fdiv.d f0,f2,f4; fadd.d f6,f0,f8; fsub.d f8,f10,f14; fmul.d f6,f10,f8; fadd.d f12,f6,f4 ১. প্রতিটি instruction-এর issue, execute (শুরু–শেষ) আর write cycle-এর টেবিল দাও। ২. Cycle 5-এর শেষে প্রতিটি RS-এর Busy, Op, Vj, Vk, Qj, Qk আর register status দেখাও। ৩. কোথায় WAR আর WAW ছিল আর tag কীভাবে সমস্যা এড়াল, এক লাইনে করে বলো। ৪. কোনো নিয়ম অস্পষ্ট লাগলে অনুমান না করে আমাকে জিজ্ঞেস করো।

AI-এর দেওয়া Tomasulo বা ROB-এর উত্তর হাতে পেলে — চেকলিস্ট

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

  • Scoreboard আর Tomasulo-র মূল পার্থক্য? — Scoreboard WAR/WAW-এ অপেক্ষা করায়; Tomasulo RS-এর tag দিয়ে register-এর নাম বদলে এগুলো দূর করে, আর ফল CDB-তে সবার কাছে একসাথে পাঠায়।
  • Reservation station-এর Qj আর Vj-এর সম্পর্ক? — একটি operand-এর জন্য একটিই ভরা থাকে: মান থাকলে Vj, না থাকলে যে RS মান দেবে তার নাম Qj.
  • ROB কেন লাগে, যদি Tomasulo-ই ক্রমের বাইরে চালাতে পারে? — In-order commit-এর জন্য: precise exception আর ভুল branch-এর পর অনুমাননির্ভর কাজ ফেলে দেওয়া।
  • Physical register কখন free হয়? — একই architectural register-এ পরের লেখক commit করলে, তার আগের mapping-এর register.
  • Store-to-load forwarding কী? — Load-এর ঠিকানা store queue-র আগের store-এর সাথে মিললে, cache-এ না গিয়ে store-এর ডেটা সরাসরি load পায়।

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

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

প্র ০১Tomasulo-র algorithm ১৯৬৭ সালে প্রকাশিত, অথচ সাধারণ কম্পিউটারে তা ছড়াল প্রায় তিন দশক পরে। মাঝের সময়টায় কী ঘটেছিল?

IBM System/360 Model 91 ছিল বিজ্ঞানের হিসাবের জন্য বানানো একটি উচ্চ-গতির মেশিন, আর Robert Tomasulo-র পদ্ধতি তার floating point unit-এই সীমিত ছিল। মেশিনটি খুব অল্প সংখ্যায় তৈরি হয়েছিল বলে জানা যায়, আর সেই যুগের transistor-বাজেটে এত জটিল নিয়ন্ত্রণ-সার্কিট বানানো ছিল ব্যয়বহুল। একই সময়ে আরেকটি সমাধান বাজার দখল করছিল: cache. IBM System/360 Model 85 (১৯৬৮ সালের দিকে) প্রথম বাণিজ্যিক cache-এর একটি হিসেবে পরিচিত, আর মেমরির ধীরগতির বড় অংশ cache-ই লুকিয়ে ফেলল। ফলে dynamic scheduling-এর জরুরি প্রয়োজন কিছুটা কমল।

১৯৮০-এর দশকে RISC আন্দোলন (পর্ব ২৫) ভিন্ন দর্শন নিল: hardware সরল রাখা, pipeline ছোট রাখা, আর instruction সাজানোর দায়িত্ব compiler-কে দেওয়া। এক chip-এ তখন যত transistor ধরত, তাতে একটি পরিষ্কার পাঁচ ধাপের pipeline আর cache বসানোই ছিল সবচেয়ে লাভজনক। Compiler-এর static scheduling নিয়মিত, অনুমানযোগ্য latency-র কাজে ভালোই চলত।

১৯৯০-এর দশকে তিনটি জিনিস একসাথে বদলাল। এক, chip-এ transistor-এর সংখ্যা এত বাড়ল যে জটিল নিয়ন্ত্রণ আর রাখা সম্ভব হলো। দুই, processor আর মেমরির গতির ফারাক বাড়তে থাকল — cache miss হলে অপেক্ষা হলো শত cycle-এর, যা compiler আগে থেকে জানে না। তিন, দুটি অনুপস্থিত অংশ ততদিনে তৈরি: precise exception-এর জন্য reorder buffer (Smith ও Pleszkun-এর কাজ, ১৯৮০-এর দশকের মাঝামাঝি) আর নির্ভরযোগ্য dynamic branch prediction (পর্ব ৩৭)। এর ফলে ১৯৯০-এর দশকের মাঝামাঝি Intel-এর Pentium Pro (P6), MIPS R10000, পরে DEC Alpha 21264 — সবাই Tomasulo-র মূল ধারণা (renaming আর tag দিয়ে dataflow) আর ROB মিলিয়ে বাণিজ্যিক out-of-order core বানাল। Tomasulo নিজে ১৯৯৭ সালে Eckert–Mauchly পুরস্কার পান বলে জানা যায় — আবিষ্কারের তিন দশক পরে।

মূল উপলব্ধি: একটি ভালো ধারণা সময়ের আগে এলে অপেক্ষা করে — transistor-এর দাম, মেমরির ফারাক আর সহায়ক কৌশলগুলো (ROB, branch prediction) তৈরি হলে তবেই সেটি সর্বত্র ছড়ায়।

প্র ০২মান ROB-এ রাখা, নাকি বড় physical register file-এ? শিল্প কেন ধীরে ধীরে দ্বিতীয় পথে সরে গেল?

দুই নকশাই একই সমস্যার সমাধান — অনুমাননির্ভর মান রাখার একটি জায়গা, যা ভুল হলে ফেলে দেওয়া যায়। ROB-value style-এ (Hennessy–Patterson-এর বর্ণনায়, আর Intel-এর P6 বংশের নকশায় বলে পরিচিত) প্রতিটি instruction-এর ফল ROB entry-তে বসে; commit-এ সেই মান architectural register file-এ কপি হয়। পড়ার সময় operand হয় architectural file থেকে, নয়তো ROB থেকে আসে। ধারণাটি সরল, আর register file ছোট থাকে।

কিন্তু window যত বড় হলো, এর খরচ চোখে পড়ল। প্রতিটি মান দুবার লেখা হয় — একবার ROB-এ, একবার commit-এ register file-এ — আর প্রতিবার ৬৪ বা আরও বেশি বিটের ডেটা নাড়াচাড়া মানে শক্তি। ROB-কে একসাথে instruction-এর হিসাব-খাতা আর বহু port-ওয়ালা ডেটা-ভান্ডার হতে হয়: প্রতি cycle-এ কয়েকটি ফল লেখা, কয়েকটি operand পড়া, কয়েকটি commit-এর কপি। Vector বা SIMD register (পর্ব ৪২) ২৫৬ বা ৫১২ বিটের হলে এই কপি আরও ব্যয়বহুল।

Physical register file-এ মান একবারই লেখা হয়, নিজের জায়গায় থাকে; commit মানে শুধু একটি পুরোনো register-এর নম্বর free list-এ ফেরত দেওয়া — কয়েকটি বিটের কাজ। ROB হালকা হয়ে যায়, শুধু হিসাব রাখে। দাম হলো জটিলতা: map table-এর checkpoint, free list-এর সঠিক হিসাব, ভুল branch-এ ঠিক কোন register ফেরত দিতে হবে। MIPS R10000 আর Alpha 21264 এই পথের পরিচিত প্রথম দিকের উদাহরণ; Intel-এর Pentium 4 এবং পরে Sandy Bridge (২০১১ সালের দিকে) physical register file-এ সরে যায় বলে ব্যাপকভাবে বর্ণিত। আজকের বেশিরভাগ বড় core এই ধাঁচের।

মূল উপলব্ধি: বড় window-এর যুগে ডেটা নাড়াচাড়াই আসল খরচ; তাই নকশা এমনভাবে বদলেছে যাতে মান একবার লিখে শুধু নামের হিসাব বদলানো যায়।

প্র ০৩Instruction window অসীম করলে কি সমস্যা মিটে যায়? Window-এর আকারের সীমা কে ঠিক করে?

প্রথম সীমা branch prediction. ধরুন প্রতি ৫টি instruction-এ একটি branch, আর predictor প্রতিটিতে ৯৫% ঠিক। ১০০টি instruction-এর window-এ ২০টি branch, আর সবগুলো ঠিক হওয়ার সম্ভাবনা ০.৯৫২০ ≈ ০.৩৬. ২০০টির window-এ ৪০টি branch: ০.৯৫৪০ ≈ ০.১৩. অর্থাৎ window-এর দূরের অংশ বেশিরভাগ সময় ভুল পথের কাজে ভরা থাকে, যা শেষে ফেলে দিতে হয়। Window বড় করতে হলে predictor-কেও আরও নিখুঁত হতে হয় — এজন্যই আধুনিক predictor এত জটিল।

দ্বিতীয় সীমা প্রোগ্রামের নিজের সমান্তরালতা। ১৯৯১ সালে DEC-এর গবেষক David Wall “Limits of Instruction-Level Parallelism” নামের একটি বহুল উদ্ধৃত গবেষণায় দেখান, নিখুঁত branch prediction আর অসীম register ধরে নিলে অনেক প্রোগ্রামে বিপুল ILP থাকে, কিন্তু বাস্তবসম্মত অনুমানে সাধারণ integer প্রোগ্রামের ILP অনেক কম — মোটামুটি এক অঙ্কের ঘরে। Pointer-নির্ভর কোড (linked list ঘোরা) একটি শৃঙ্খল — প্রতিটি load আগেরটির ফলের ওপর নির্ভর করে, তাই কোনো window তাকে সমান্তরাল করতে পারে না।

তৃতীয় সীমা মেমরি। এখানে একটি সহজ হিসাব কাজে লাগে (Little-এর সূত্রের মতো): একটি cache miss-এর সময় core যদি থেমে না থেকে প্রতি cycle-এ গড়ে ৪টি instruction এগোতে চায়, আর miss-এর latency ২০০ cycle হয়, তাহলে window-এ ৪ × ২০০ = ৮০০টি instruction-এর জায়গা লাগবে। তাই শত শত entry-র ROB-ও লম্বা DRAM miss পুরোপুরি ঢাকতে পারে না; এর লাভ মূলত একাধিক miss একসাথে শুরু করা।

মূল উপলব্ধি: Window-এর আকার একসাথে বেঁধে রাখে branch-এর নির্ভুলতা, প্রোগ্রামের dependence-শৃঙ্খল, মেমরির latency আর সার্কিটের power — যেকোনো একটি ঠিক না করে শুধু window বাড়ালে লাভ কমে আসে।

প্র ০৪ফোনের একই chip-এ কেন একসাথে বড় out-of-order core আর ছোট in-order core রাখা হয়?

দুই ধরনের core দুই ধরনের হিসাবে সেরা। বড় out-of-order core একটি thread-কে দ্রুততম চালায় — ওয়েব পেজ খোলা, অ্যাপ চালু হওয়া, গেমের একটি দৃশ্য। কিন্তু অংশ ৮-এ দেখেছেন, তার area আর power গতির চেয়ে বেশি হারে বাড়ে: প্রতিটি instruction-এ বাড়তি rename, wakeup, ROB-এর হিসাব আর ভুল পথের ফেলে দেওয়া কাজের শক্তি। ছোট in-order core প্রতি instruction-এ অনেক কম শক্তি খরচ করে, যদিও ধীরে।

ফোনের বেশিরভাগ সময়ের কাজ কিন্তু ধীর হলেও চলে: পটভূমিতে মেসেজ আসা দেখা, গান বাজানো, সেন্সর পড়া। এগুলো বড় core-এ চালানো মানে ব্যাটারি অপচয়। তাই ARM ২০১১ সালের দিকে “big.LITTLE” নামে একটি ধারণা প্রচার করে: একই chip-এ বড় আর ছোট core, একই ISA, যাতে operating system কাজের চাপ বুঝে thread-কে এক core থেকে আরেকটিতে সরাতে পারে। প্রথম দিকের জুটির একটি উদাহরণ Cortex-A15 (out-of-order) আর Cortex-A7 (in-order); পরে Cortex-A53/A55 বহু বছর ধরে ছোট core হিসেবে বহুল ব্যবহৃত হয়েছে বলে জানা যায়।

কাজটি শুধু hardware-এর নয় — OS scheduler-কে জানতে হয় কোন core কত শক্তিশালী আর কত শক্তি খরচ করে, আর কোন thread-কে কোথায় রাখলে মোট শক্তি কম লাগবে অথচ ব্যবহারকারী দেরি টের পাবেন না। Operating Systems কোর্সের multiprocessor scheduling পর্বে এই দিকটি আছে। Intel-ও পরে desktop-এ “performance” আর “efficient” core মিলিয়েছে, যদিও তার efficient core নিজেও out-of-order. অর্থাৎ “ছোট core মানেই in-order” — এটি সাধারণ প্রবণতা, নিয়ম নয়।

মূল উপলব্ধি: Out-of-order হলো গতি কেনার একটি দামি উপায়; যেখানে গতির চেয়ে শক্তি বেশি মূল্যবান, সেখানে সরল in-order core আজও যুক্তিসংগত পছন্দ।

প্র ০৫Compiler-ই তো instruction সাজাতে পারে (VLIW, EPIC). তাহলে general-purpose CPU-তে hardware-এর dynamic scheduling কেন জিতল?

VLIW-এর দর্শন (পর্ব ৩৯) আকর্ষণীয়: dependence খোঁজা, সমান্তরাল instruction বাছা, renaming-এর মতো কাজ compiler একবার করে দিক, hardware শুধু নির্দেশ মেনে চলুক — তাহলে issue queue, ROB, rename-এর বিশাল সার্কিট লাগে না, শক্তি বাঁচে। Intel আর HP এই দর্শনে বড় বাজি ধরেছিল: Itanium (EPIC), যার প্রথম chip ২০০১ সালের দিকে বাজারে আসে। কিন্তু general-purpose বাজারে এটি প্রত্যাশিত সাফল্য পায়নি, আর ২০২০-এর দশকের শুরুর দিকে বন্ধ করে দেওয়া হয় বলে জানা যায়।

কারণগুলো এই পর্বের সাথেই যুক্ত। এক, compiler জানে না কোন load-এ cache miss হবে — একই কোড একবার ৪ cycle, আরেকবার ২০০ cycle নেয়। Dynamic scheduling চলার সময় দেখে সিদ্ধান্ত নেয়, compiler-কে আগে থেকে অনুমান করতে হয়। দুই, memory dependence: দুটি pointer একই জায়গা দেখায় কি না, compiler প্রায়ই প্রমাণ করতে পারে না, তাই সাবধানী হতে হয়; hardware চলার সময় আসল ঠিকানা মিলিয়ে নেয় (অংশ ৭)। তিন, binary compatibility: compiler একটি নির্দিষ্ট latency আর unit-সংখ্যা ধরে সাজায়; নতুন প্রজন্মের chip-এ সংখ্যা বদলালে পুরোনো binary আর সর্বোত্তম থাকে না। Out-of-order core একই পুরোনো binary-কে নিজে থেকে নতুন chip-এ ভালোভাবে চালায়।

তবু VLIW হারিয়ে যায়নি। যেখানে কাজের ধরন নিয়মিত আর latency অনুমানযোগ্য — digital signal processor, কিছু AI accelerator আর GPU-র ভেতরের অংশ — সেখানে compiler-নির্ভর scheduling এখনো শক্তি-দক্ষ পছন্দ। মাঝামাঝি পথও এসেছে: Transmeta-র Crusoe (২০০০ সালের দিকে) VLIW hardware-এর ওপরে software দিয়ে x86 কোড অনুবাদ করত, চলার সময়ের তথ্য দেখে। আর আধুনিক out-of-order core-এর জন্যও compiler-এর scheduling এখনো সাহায্য করে — শুধু শেষ সিদ্ধান্ত hardware-এর।

মূল উপলব্ধি: যেখানে চলার সময়ের অনিশ্চয়তা বেশি (cache miss, pointer, ভিন্ন প্রজন্মের chip), সেখানে hardware-এর চোখ compiler-এর অনুমানকে হারায়; যেখানে অনিশ্চয়তা কম, সেখানে compiler-ই সস্তা সমাধান।

অনুশীলন

পরীক্ষার ধাঁচে: Tomasulo-র টেবিল পূরণ, renaming, ROB recovery আর simulator-এর আউটপুট অনুমান। আগে নিজে চেষ্টা করুন; প্রতিটি উত্তর এই পর্বের simulator বদলে চালিয়ে মেলানো।

  1. Tomasulo-র টেবিল পূরণ (হাতে trace): অংশ ৩-এর নিয়ম, একটি CDB. Latency: fld ২, fadd.d/fsub.d ২, fmul.d ১০, fdiv.d ২০. RS: Load ৩, Add ৩, Mult ২. প্রাথমিক মান: f4 = 2.0, মেমরিতে 32(x2) = 2.0 আর 44(x3) = 6.0. প্রোগ্রাম: I1 fld f6, 32(x2); I2 fld f2, 44(x3); I3 fmul.d f0, f2, f4; I4 fsub.d f8, f2, f6; I5 fdiv.d f10, f0, f6; I6 fadd.d f6, f8, f2. (ক) প্রতিটির issue, execute আর write cycle বের করুন। (খ) Cycle ৫-এর শেষে I5-এর RS-এ Vj, Vk, Qj, Qk কী? (গ) I6 f6-এ লেখে cycle ১১-এ, অথচ I5 f6 পড়ে execute শুরু করে আরও পরে। I5 কোন f6 ব্যবহার করে, আর চূড়ান্ত f0, f6, f8, f10 কত?
    InstructionRSIssueExecuteWrite
    I1 fld f6Load1১২–৩৪
    I2 fld f2Load2২৩–৪৫
    I3 fmul.d f0Mult1৩৬–১৫১৬
    I4 fsub.d f8Add1৪৬–৭৮
    I5 fdiv.d f10Mult2৫১৭–৩৬৩৭
    I6 fadd.d f6Add2৬৯–১০১১

    (ক) I3 আর I4 দুজনেই f2-এর অপেক্ষায়, যা CDB-তে আসে cycle ৫-এ, তাই দুজনের execute শুরু ৬-এ। I4-এর f6 cycle ৪-এই এসেছে। I6 f8-এর জন্য অপেক্ষা করে (cycle ৮), তাই ৯–১০. I5 f0-এর জন্য (cycle ১৬), তাই ১৭–৩৬, write ৩৭. CDB-তে কোনো সংঘর্ষ নেই (৪, ৫, ৮, ১১, ১৬, ৩৭ — সব আলাদা)। মোট ৩৭ cycle.

    (খ) Cycle ৫-এ issue-এর সময় f0-এর Qi = Mult1 (I3), তাই Qj = Mult1, Vj ফাঁকা; f6 cycle ৪-এ CDB থেকে register file-এ এসেছে, তাই Vk = 2.0, Qk ফাঁকা.

    (গ) I5 issue-এর সময়ই f6-এর মান 2.0 Vk-তে কপি করে নিয়েছে, তাই I6-এর লেখা (cycle ১১-এ f6 = 4.0 + 6.0 = 10.0) তাকে ছোঁয় না — WAR নিরাপদ। চূড়ান্ত: f0 = 6 × 2 = 12, f8 = 6 − 2 = 4, f10 = 12 ÷ 2 = 6, f6 = 10. Scoreboard হলে I6-কে লেখা পিছিয়ে দিতে হতো যতক্ষণ না I5 f6 পড়ে — অর্থাৎ cycle ১৭ পর্যন্ত।

  2. Rename করুন: শুরুতে map table x1 → p1, x2 → p2, x3 → p3, x4 → p4; free list = p5, p6, p7, p8, p9 (এই ক্রমে নেওয়া হয়)। প্রোগ্রাম: I1 lw x1, 0(x2); I2 add x3, x1, x4; I3 addi x1, x1, 8; I4 sub x4, x3, x1; I5 add x3, x4, x4. (ক) মূল কোডের সব WAR আর WAW জোড়া লিখুন। (খ) প্রতিটি instruction rename করুন, শেষের map table আর free list দিন। (গ) প্রতিটি instruction commit করলে কোন physical register ফেরত যাবে?

    (ক) WAR: I2 পড়ে x1 → I3 লেখে x1; I2 পড়ে x4 → I4 লেখে x4; I4 পড়ে x3 → I5 লেখে x3 — মোট ৩টি। WAW: x1 (I1, I3), x3 (I2, I5) — ২টি। (I3 নিজে x1 পড়ে আর লেখে — একই instruction-এর ভেতরে কোনো hazard নেই, কারণ পড়া আগে।)

    I1  lw   x1, 0(x2)   ->  lw   p5, 0(p2)    x1 -> p5   (পুরোনো p1)
    I2  add  x3, x1, x4  ->  add  p6, p5, p4   x3 -> p6   (পুরোনো p3)
    I3  addi x1, x1, 8   ->  addi p7, p5, 8    x1 -> p7   (পুরোনো p5)
    I4  sub  x4, x3, x1  ->  sub  p8, p6, p7   x4 -> p8   (পুরোনো p4)
    I5  add  x3, x4, x4  ->  add  p9, p8, p8   x3 -> p9   (পুরোনো p6)

    (খ) শেষের map: x1 = p7, x2 = p2, x3 = p9, x4 = p8; free list খালি — ষষ্ঠ কোনো লেখক এলে rename থেমে যাবে যতক্ষণ না কেউ commit করে। Rename-এর পরে প্রতিটি destination আলাদা (p5 থেকে p9), তাই WAR আর WAW শূন্য; অংশ ৬-এর renamer-এ এই প্রোগ্রাম বসিয়ে চালালে “3 WAR, 2 WAW” থেকে “0 WAR, 0 WAW” আসে। লক্ষ করুন I3 x1 পড়ে p5 থেকে — I1-এর ফল — আর লেখে নতুন p7-এ।

    (গ) I1 commit → p1, I2 → p3, I3 → p5, I4 → p4, I5 → p6. p5 ফেরত যায় I3-এর commit-এ, কারণ তার শেষ পাঠক I2 আর I3 দুজনেই I3-এর আগে বা সাথেই শেষ।

  3. ROB recovery: একটি ROB-এ মাথা থেকে লেজ পর্যন্ত: #১ add x1, x2, x3 (PC 0x100, ready); #২ lw x4, 0(x9) (PC 0x104, ready, কিন্তু page fault চিহ্নিত); #৩ addi x5, x1, 1 (0x108, ready); #৪ beq x5, x0, L (0x10c, ready, ভুল অনুমান চিহ্নিত); #৫ sw x5, 0(x10) (0x110, ready); #৬ add x6, x5, x5 (0x114, ready). প্রতি cycle-এ একটি commit, H&P-ধাঁচের নকশা। (ক) পরপর কী কী ঘটবে? (খ) Register file আর মেমরিতে কার কার ফল পৌঁছাবে? mepc কত? (গ) Branch-এর ভুল আগে ধরা পড়লেও কেন সেটি এখানে কোনো ভূমিকা রাখে না?

    (ক) প্রথম cycle-এ #১ commit: x1 লেখা হলো। পরের cycle-এ #২ মাথায়, আর তার entry-তে exception — তাই commit-এর বদলে trap: #২ থেকে #৬ সব flush, register status আর RS খালি, fetch শুরু trap handler থেকে।

    (খ) শুধু #১-এর x1 register file-এ। #২-এর load কিছু লেখে না, #৩-এর x5, #৬-এর x6 ফেলে দেওয়া হয়, আর #৫-এর store কখনো মেমরিতে লেখে না — store লেখে কেবল commit-এ। mepc = 0x104 (দোষী load-এর PC). Handler page আনার পর mret 0x104-এ ফিরবে (Linux-এর মতো S-mode-এ সামলালে একই কাজ sepc আর sret দিয়ে); তখন load, addi, beq আবার চলবে, আর branch-এর অনুমান আবার করা হবে।

    (গ) Branch-এর ভুল একটি অনুমানের ভুল, আর তার নিষ্পত্তি হয় সে মাথায় পৌঁছালে (বা আধুনিক নকশায় আগে, checkpoint দিয়ে)। কিন্তু প্রোগ্রামের ক্রমে #২ আগে — precise exception মানে সবচেয়ে পুরোনো সমস্যাটি আগে। #২-এর trap #৪-কেও মুছে দেয়, তাই branch-এর প্রশ্নই আর ওঠে না।

  4. আউটপুট অনুমান: অংশ ৪-এর tomasulo.js-এ শুধু NRS = { Load: 2, Add: 1, Mult: 2 } করে (Add RS মাত্র একটি) চালালে টেবিল কেমন হবে? (ক) কোন instruction-এর issue প্রথম বদলায় আর কেন? (খ) মোট cycle কত? (গ) অংশ ৪-এর in-order মেশিনের ৩১ cycle-এর সাথে তুলনা করে ব্যাখ্যা করুন। চালানোর আগে হাতে হিসাব করুন।
    instruction             issue  execute   write
    fld f2, 0(x1)           1      2-3       4
    fdiv.d f0, f2, f4       2      5-16      17
    fadd.d f6, f0, f8       3      18-19     20
    fsub.d f8, f10, f14     20     21-22     23
    fmul.d f6, f10, f8      21     24-29     30
    fadd.d f12, f6, f4      23     31-32     33
    total: 33 cycles   final: f0=3 f2=6 f4=2 f6=20 f8=4 f10=5 f12=22 f14=1

    (ক) I3 (fadd.d f6) cycle ৩-এ একমাত্র Add RS-এ বসে, আর ভাগের ফলের জন্য সেখানে বসে থাকে cycle ২০ পর্যন্ত। I4 (fsub.d) একটি Add RS চায়, পায় না — issue থেমে যায়, আর issue প্রোগ্রামের ক্রমে বলে পেছনের সবাইও থামে। I3 cycle ২০-এ CDB-তে লিখে RS ছাড়ে, একই cycle-এ I4 issue (নিয়ম অনুযায়ী একই cycle-এ খালি হওয়া RS নেওয়া যায়)। I5 পায় Mult1 (ভাগ সেটি cycle ১৭-এ ছেড়েছে), ২১-এ; I6 আবার Add RS-এর অপেক্ষায়, যা I4 ছাড়ে cycle ২৩-এ।

    (খ) মোট ৩৩ cycle; চূড়ান্ত মান অপরিবর্তিত (f6 = 20, f12 = 22) — সঠিকতা কখনো RS-এর সংখ্যার ওপর নির্ভর করে না, শুধু সময়।

    (গ) এটি in-order-এর ৩১-এর চেয়েও খারাপ! কারণ অংশ ৪-এর in-order মডেলে I3 অপেক্ষা করে issue-এর আগে, কোনো জায়গা দখল না করে, আর pipelined adder প্রতি cycle-এ নতুন কাজ নেয়; অথচ এখানে অপেক্ষারত I3 একমাত্র Add RS দখল করে রাখে issue থেকে write পর্যন্ত — একটি structural hazard. শিক্ষা: out-of-order-এর লাভ আসে অপেক্ষার জায়গা থেকে; জায়গা কম হলে লাভ উবে যায়। এজন্যই আধুনিক core-এ issue queue আর ROB-এর entry-সংখ্যা এত গুরুত্বপূর্ণ নকশা-সিদ্ধান্ত।

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

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

ক্রমএকটি আধুনিক out-of-order core-এ একটি instruction-এর যাত্রা সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📥 Fetch আর decode — প্রোগ্রামের ক্রমে, micro-op-এ ভাঙা
  2. 🏷️ Rename — map table থেকে source, free list থেকে নতুন destination
  3. 📋 Dispatch — ROB-এ entry আর issue queue-তে জায়গা
  4. ⏳ Wakeup ও issue — operand তৈরি হলে execution unit-এ পাঠানো
  5. ⚙️ Execute আর writeback — ফল physical register-এ, tag ঘোষণা
  6. ✅ Retire — ROB-এর মাথা থেকে প্রোগ্রামের ক্রমে commit

Tomasulo-র algorithm WAR আর WAW hazard কীভাবে দূর করে?

  • WAR আর WAW দেখলে issue থামিয়ে দেয়
  • Issue-এর সময় মান বা RS-এর tag ধরে রেখে, আর register status-এ শুধু সবচেয়ে নতুন লেখকের tag রেখে
  • Compiler-কে আলাদা register ব্যবহার করতে বাধ্য করে
  • Reorder buffer-এ সব ফল ক্রমে সাজিয়ে

Tomasulo-র নিয়ম: issue হলে execute পরের cycle থেকে; CDB থেকে operand cycle W-তে এলে execute W + 1 থেকে; execute শেষের পরের cycle-এ write. একটি fmul.d cycle ৩-এ issue হলো, তার শেষ operand CDB-তে এলো cycle ৫-এ, latency ৬, আর CDB খালি। Execute আর write কোন cycle-এ?

  • Execute ৫–১০, write ১১
  • Execute ৪–৯, write ১০
  • Execute ৬–১১, write ১২
  • Execute ৬–১১, write ১১

একটি ROB-এ মাথার entry #৫ একটি load, cache miss-এর কারণে এখনো ready নয়। #৭ একটি store, যার ঠিকানা আর ডেটা দুটোই তৈরি। Store মেমরিতে কখন লিখবে?

  • এখনই, কারণ ঠিকানা আর ডেটা তৈরি
  • নিজের execute শেষ হলেই
  • #৫-এর ডেটা cache-এ এলেই
  • #৫ আর #৬ commit করার পর, #৭ নিজে মাথায় এসে commit করলে

Map table-এ x5 → p9 আর x1 → p1; free list-এর মাথায় p12, তারপর p13. Instruction add x5, x5, x1 rename করলে কী হয়?

  • add p12, p9, p1; x5 → p12; p9 free হবে এই instruction-এর commit-এ
  • add p12, p12, p1; x5 → p12
  • add p9, p9, p1; map বদলায় না
  • add p12, p9, p1; p9 সাথে সাথে free list-এ ফেরত

Register renaming কোন dependence দূর করতে পারে না?

  • WAR (anti-dependence)
  • WAW (output dependence)
  • RAW (true data dependence)
  • একই register-এ পরপর দুটি লেখা

Store queue-তে একটি আগের store আছে: ঠিকানা 0x1000, ডেটা 42, এখনো commit হয়নি। পরের একটি load-এর ঠিকানা 0x1000. মাঝে আর কোনো store নেই। ভালো একটি out-of-order core কী করবে?

  • Cache থেকে 0x1000 পড়বে
  • Store queue থেকে সরাসরি 42 নেবে (store-to-load forwarding)
  • Store commit হয়ে মেমরিতে লেখা পর্যন্ত অপেক্ষা করবে
  • Memory exception তুলবে

শব্দকোষ

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

মনে রাখুন

  1. In-order stall স্বাধীন কাজও আটকায়; dynamic scheduling-এর ছক: in-order fetch/decode → out-of-order execute → in-order commit.
  2. Tomasulo: RS-এ Op, Vj/Vk (মান) বা Qj/Qk (tag); register status-এ সবচেয়ে নতুন লেখকের tag; ফল CDB-তে tag-সহ। Tag-ই implicit renaming — WAR, WAW শেষ, RAW থাকে।
  3. ধাপ: issue (ক্রমে, RS লাগে) → execute (operand এলে, পরের cycle থেকে) → write (CDB, এক cycle-এ একটি, পুরোনো আগে)। সীমা: CDB, RS-এর সংখ্যা, imprecise exception.
  4. ROB: type, destination, value, ready; commit শুধু মাথা থেকে; ভুল branch বা exception মাথায় এলে পেছনের সব flush; store মেমরিতে লেখে commit-এ।
  5. আধুনিক core: PRF + map table + free list (পুরোনো register free হয় প্রতিস্থাপনকারীর commit-এ), checkpoint দিয়ে দ্রুত recovery, LSQ-তে forwarding আর memory disambiguation; শত শত micro-op উড়ন্ত, দাম power আর জটিলতা।

পরের পর্বে: ROB ভুল পথের কাজ register আর মেমরি থেকে মুছে দেয় — কিন্তু cache থেকে নয়। এই ছোট ফাঁক থেকেই ২০১৮ সালে প্রকাশিত Spectre আর Meltdown. Speculative execution-এর নিরাপত্তা আর প্রতিরক্ষা।

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

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