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

Instruction-level parallelism — superscalar আর VLIW

Instruction-Level Parallelism — Superscalar and VLIW Processors

Taught in Bangla, this lesson explains instruction-level parallelism: true, name and control dependences, dependence graphs, critical paths and ideal IPC, static versus dynamic multiple issue, the issue-packet rules of a 2-way in-order RISC-V superscalar, scheduling a loop cycle by cycle with reordering, loop unrolling and register renaming, VLIW bundles and Itanium's lessons, front-end width versus real IPC, and why ILP hit a wall, with dual-issue and VLIW simulators.

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

  • ILP কী, আর true, name ও control dependence কীভাবে তার সীমা ঠিক করে
  • একটি কোডের dependence graph আঁকা, critical path আর ideal IPC হিসাব করা
  • Static (VLIW) আর dynamic (superscalar) multiple issue-এর পার্থক্য, আর ২-way in-order RISC-V pipeline-এর issue packet-এর নিয়ম
  • Dual-issue-এ একটি লুপ cycle ধরে সাজানো — ক্রম বদল, loop unrolling আর register renaming-এর আগে-পরে cycle ও IPC
  • VLIW bundle আর NOP, Itanium-এর শিক্ষা, superscalar front end-এর width আর কেন ILP একসময় দেয়ালে ঠেকল

১ · ILP কী — একসাথে চালানো যায় এমন instruction খোঁজা

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

প্রসেসরেও একই প্রশ্ন। একটি প্রোগ্রামের instruction-গুলো লেখা থাকে একটি সারিতে, কিন্তু তাদের অনেকেই একে অপরের ফল ব্যবহার করে না। এমন স্বাধীন instruction একই সময়ে চালানোর সুযোগকে বলে instruction-level parallelism বা ILP. পর্ব ৩৫-এর pipeline-ও এক ধরনের ILP — সেখানে পাঁচটি instruction পাঁচটি ভিন্ন stage-এ একসাথে থাকে। কিন্তু সেখানে প্রতি cycle-এ সর্বোচ্চ একটি instruction শেষ হয়: ideal CPI = ১.

এই পর্বের প্রশ্ন আরও এক ধাপ বড়: প্রতি cycle-এ একাধিক instruction শুরু আর শেষ করা যায় কি? তখন আমরা CPI-এর বদলে তার উল্টো সংখ্যাটি ব্যবহার করি — IPC (instructions per cycle):

IPC = মোট instruction / মোট cycle = 1 / CPI
প্রতি cycle-এ ৪টি পর্যন্ত instruction শুরু করতে পারে এমন প্রসেসরে সর্বোচ্চ IPC = ৪, অর্থাৎ সর্বনিম্ন CPI = ০.২৫

পর্ব ০৩-এর লৌহসূত্র মনে করুন: CPU time = instruction সংখ্যা × CPI × clock cycle time. গতি বাড়ানোর তিনটি রাস্তা — কম instruction, ছোট CPI, দ্রুত clock. Pipeline গভীর করা (superpipelining) clock বাড়ায়; ILP-এর মূল লক্ষ্য CPI কমানো, প্রতি cycle-এ বেশি কাজ করিয়ে। ১৯৯০-এর দশক থেকে ২০০০-এর দশকের শুরু পর্যন্ত প্রসেসরের গতির বড় একটি অংশ এসেছে এই দুই কৌশলের মিলন থেকে।

মূল ধারণা

ILP প্রোগ্রামের ধর্ম, আর issue width hardware-এর ধর্ম। একটি ৮-চুলার রান্নাঘরও এক মিনিটে বেশি রান্না করতে পারবে না, যদি রেসিপির প্রতিটি ধাপ আগের ধাপের ওপর নির্ভর করে। তাই এই পর্বে আমরা দুটি জিনিস আলাদা করে মাপব: কোডে কতটা ILP আছে (dependence graph দিয়ে), আর একটি নির্দিষ্ট hardware তার কতটা কাজে লাগায় (scheduling দিয়ে)।

২ · ILP-এর সীমা — তিন রকম dependence

পর্ব ৩৬-এ data dependence-এর তিন রূপ দেখেছেন — RAW, WAR আর WAW — আর প্রমাণ করেছেন যে in-order ৫-stage pipeline-এ শুধু RAW বিপদ। একাধিক instruction একসাথে চালাতে গেলে ছবিটা বদলায়: এখন instruction-এর ক্রমই আমরা বদলাতে চাই, তাই তিনটি dependence-ই সীমা হয়ে দাঁড়ায়। সাথে যোগ হয় চতুর্থ একটি — branch-এর কারণে।

True dependence (RAW)

পরের instruction আগেরটির মান ব্যবহার করে: add x7, x5, x6 চলতে পারে না যতক্ষণ না lw x5-এর মান আসে। এটিই আসল ডেটা-প্রবাহ — একে সরানো যায় না, শুধু latency কমানো যায় (forwarding).

অপরিবর্তনীয় বাধা

Name dependence (WAR, WAW)

দুটি instruction একই register-এর নাম ব্যবহার করে, কিন্তু কোনো মান আদান-প্রদান হয় না। WAR-কে anti-dependence, WAW-কে output dependence বলে। আলাদা register নিলেই এরা মিলিয়ে যায় — এটাই register renaming.

নাম বদলালেই শেষ

Control dependence

Branch-এর পরের instruction চলবে কি না, তা branch-এর ফলের ওপর নির্ভর করে। তাই branch-এর আগের কোডের সাথে পরের কোড মেশানো কঠিন। Branch prediction (পর্ব ৩৭) আর speculation (অনুমানের ভিত্তিতে আগেভাগে চালানো) এই বাধা পার হওয়ার অস্ত্র।

পথ অজানা

মেমরির ক্ষেত্রেও dependence আছে: একটি sw আর পরের একটি lw যদি একই ঠিকানা ছোঁয়, তাদের ক্রম বদলানো যায় না। সমস্যা হলো, ঠিকানা প্রায়ই compile-এর সময় জানা থাকে না — দুটি pointer একই জায়গা দেখাচ্ছে কি না (aliasing), compiler নিশ্চিত হতে না পারলে সাবধানে পুরোনো ক্রমই রাখে।

রান্নাঘরে ফিরে যাই। “সবজি না কেটে ভাজি হবে না” — এটি true dependence; কোনো কৌশলেই কাটার আগে ভাজা যায় না। “ডাল আর মাংস দুটোই বড় হাঁড়িতে রাঁধতে হবে, অথচ বড় হাঁড়ি একটি” — এটি name dependence; আরেকটি হাঁড়ি আনলেই দুটি একসাথে চড়ানো যায়। আর “অতিথি মাছ খাবেন কি না, ফোনে জানাবেন” — এটি control dependence; উত্তর আসার আগে মাছ কাটা শুরু করা একটি অনুমান, ভুল হলে শ্রম নষ্ট।

কয়েকটি গুরুত্বপূর্ণ পর্যবেক্ষণ। এক, সাধারণ integer প্রোগ্রামে প্রায় প্রতি চার থেকে সাতটি instruction-এ একটি branch আসে বলে বহু পাঠ্যবইয়ে উল্লেখ আছে। তাই দুই branch-এর মাঝের একটানা কোড — basic block — খুব ছোট, আর তার ভেতরে স্বাধীন instruction খুব বেশি থাকে না। দুই, বড় ILP পেতে হলে branch পেরিয়ে, অর্থাৎ কয়েকটি basic block মিলিয়ে, instruction খুঁজতে হয়। তিন, compiler বা hardware যত বেশি instruction-এর একটি “জানালা” দেখতে পায়, তত বেশি স্বাধীন কাজ খুঁজে পায় — কিন্তু জানালা বড় করার দামও তত বেশি।

৩ · Dataflow limit — dependence graph আর critical path

একটি কোডে কতটা ILP আছে, মাপার সবচেয়ে পরিষ্কার উপায় হলো তার dependence graph আঁকা। প্রতিটি instruction একটি node. i থেকে j-তে একটি তীর মানে j-কে i-এর পরে চলতে হবে। তীরের গায়ে লেখা থাকে কত cycle পরে: RAW-এ i-এর latency (এই পর্বে lw-এর ২ cycle, বাকি সবার ১ cycle); WAR-এ ০ (একই cycle-এ চলতে পারে, কারণ পড়া হয় শুরুতে আর লেখা পরে); WAW-এ ১.

এখন ধরুন hardware অসীম — যত খুশি ALU, যত খুশি memory port, আর branch নেই। তাহলে প্রতিটি instruction শুরু হবে তার সব পূর্বসূরির শর্ত মিটলেই:

start(j) = max(1, max over edges i→j of [start(i) + w(i→j)])
L = max over j of [start(j) + latency(j) − 1]   ·   ideal IPC = n / L

কেন এটিই সর্বনিম্ন সময়? যেকোনো একটি পথ i1 → i2 → … → ik নিন। প্রতিটি তীর বলে পরেরটি অন্তত w cycle পরে শুরু হবে, তাই পুরো পথটি শেষ হতে অন্তত তীরগুলোর ওজনের যোগফল আর শেষ instruction-এর নিজের latency লাগে — যত ALU-ই থাকুক। অর্থাৎ যেকোনো schedule-এর সময় ≥ সবচেয়ে ভারী পথের দৈর্ঘ্য। উল্টোদিকে, অসীম hardware-এ উপরের “যত তাড়াতাড়ি সম্ভব” schedule ঠিক সেই দৈর্ঘ্যেই শেষ হয় (প্রতিটি instruction-এর start তার সবচেয়ে ভারী আগমন-পথের দৈর্ঘ্য, induction দিয়ে প্রমাণ)। তাই সবচেয়ে ভারী পথ — critical path — হলো সময়ের নিখুঁত নিম্নসীমা। একে বলে dataflow limit.

একটি ছোট উদাহরণ নিই। C-তে y = (a + b) & (c − d), যেখানে a, b, c, d, y মেমরিতে x10 থেকে ০, ৪, ৮, ১২ আর ১৬ bytes দূরে। একজন অসাবধান programmer (বা register কম থাকা compiler) x5 আর x6 দুবার ব্যবহার করেছে:

I1: lw  x5, 0(x10)     # x5 = a
I2: lw  x6, 4(x10)     # x6 = b
I3: add x7, x5, x6     # x7 = a + b
I4: lw  x5, 8(x10)     # x5 = c   (x5 আবার!)
I5: lw  x6, 12(x10)    # x6 = d   (x6 আবার!)
I6: sub x8, x5, x6     # x8 = c - d
I7: and x9, x7, x8     # x9 = (a + b) & (c - d)
I8: sw  x9, 16(x10)    # y = x9
WAW x5 WAW x6 WAR x5 WAR x6 x5 · 2 x6 · 2 x5 · 2 x6 · 2 x7 · 1 x8 · 1 x9 · 1 I1: lw x5 I2: lw x6 I4: lw x5 I5: lw x6 I3: add x7 I6: sub x8 I7: and x9 I8: sw x9 cycle 1 cycle 1 cycle 1 (নাম বদলে) cycle 1 (নাম বদলে) cycle 3 cycle 3 cycle 4 cycle 5
ছবি ৩৯.১ — উদাহরণের dependence graph. নীল কঠিন তীর = RAW (গায়ে register আর latency), কমলা ড্যাশ তীর = name dependence. সবুজ সংখ্যা হলো renaming-এর পরে (শুধু নীল তীর মেনে) শুরুর cycle. কমলা তীরগুলোও মানলে I4 আর I5-কে I3-এর জন্য cycle ৩ পর্যন্ত অপেক্ষা করতে হয়, আর পুরো কাজ ৫-এর বদলে ৭ cycle নেয়।

নিচের stepper-এ graph-টি ধাপে ধাপে গড়ে তুলে critical path বের করি:

I1…I8: y = (a + b) & (c − d), lw-এর latency ২, বাকিদের ১
এখন কী করছি
RAW (true)
WAR / WAW (name)
start cycle I1…I8
দৈর্ঘ্য L, ideal IPC
  1. ধাপ ১. প্রতিটি instruction কোন register লেখে আর কোনগুলো পড়ে, তা লিখি। sw x9, 16(x10) কোনো register লেখে না — সে x9 আর x10 পড়ে। x10 কেউ বদলায় না, তাই তার কোনো তীর নেই।
  2. ধাপ ২. RAW: প্রতিটি source register-এর সবচেয়ে কাছের আগের লেখককে খুঁজি। I6-এর x5 এসেছে I4 থেকে, I1 থেকে নয় — কারণ মাঝখানে I4 আবার x5 লিখেছে। মোট ৭টি true dependence, তীরের ওজন producer-এর latency.
  3. ধাপ ৩. Name dependence: I3 x5 আর x6 পড়ে, তারপর I4 আর I5 সেগুলোতে লেখে — দুটি WAR. I1 আর I4 দুজনেই x5 লেখে, I2 আর I5 দুজনেই x6 — দুটি WAW. মোট ৪টি কমলা তীর।
  4. ধাপ ৪. সব তীর মেনে start: I1, I2 cycle ১-এ; I3 cycle ৩-এ (load-এর ২ cycle); I4, I5-কে I3-এর পড়া শেষ হওয়া পর্যন্ত অপেক্ষা করতে হয় (WAR, ওজন ০) — cycle ৩; I6 cycle ৫, I7 cycle ৬, I8 cycle ৭. মোট ৭ cycle, ideal IPC ৮/৭ ≈ ১.১৪.
  5. ধাপ ৫. I4 আর I5-এর destination বদলে x15 আর x16 করি, I6-কেও সেভাবে পড়তে বলি। কমলা তীর সব উধাও। এখন চারটি load একসাথে cycle ১-এ, I3 আর I6 cycle ৩-এ, I7 cycle ৪-এ, I8 cycle ৫-এ। ৮টি instruction ৫ cycle-এ — ideal IPC ১.৬.
  6. ধাপ ৬. Critical path I1 → I3 → I7 → I8: ২ + ১ + ১ + ১ = ৫ cycle (I2, I4 বা I5 থেকে শুরু পথগুলোও সমান লম্বা)। শুধু true dependence দিয়ে গড়া এই পথ কোনো renaming বা scheduling-এ ছোট হয় না — এটাই এই কোডের dataflow limit.

হাতে এভাবে হিসাব করা ছোট কোডে চলে, কিন্তু বড় কোডে ভুল হবেই। নিচের analyser একই কাজ করে: instruction-এর তালিকা পড়ে, সব RAW, WAR, WAW জোড়া খোঁজে (মাঝখানে অন্য লেখক থাকলে বাদ দেয়), তারপর দুইভাবে start cycle হিসাব করে — সব তীর মেনে, আর শুধু RAW মেনে (renaming-এর পরে যা থাকে)। Run চাপুন, তারপর নিজের কোড বসিয়ে দেখুন:

// Dependence-graph analyser: RAW / WAR / WAW, critical path and ideal IPC
const prog = ["lw  x5, 0(x10)", "lw  x6, 4(x10)", "add x7, x5, x6", "lw  x5, 8(x10)",
              "lw  x6, 12(x10)", "sub x8, x5, x6", "and x9, x7, x8", "sw  x9, 16(x10)"];
const LAT = { lw: 2 };                                // load: 2 cycles, everything else: 1
const I = prog.map(s => {
  const [op, ...a] = s.replace(/[(),]/g, " ").trim().split(/\s+/);
  const r = a.filter(t => /^x\d+$/.test(t)).map(t => +t.slice(1)).filter(x => x);
  const st = op === "sw";                             // sw rs2, imm(rs1): writes no register
  return { op, rd: st ? 0 : r[0], src: st ? r : r.slice(1), lat: LAT[op] || 1 };
});
const between = (i, j, r) => I.slice(i + 1, j).some(q => q.rd === r);   // another writer of r?
const edges = [];                                      // [from, to, type, reg]
I.forEach((b, j) => I.slice(0, j).forEach((a, i) => {
  if (a.rd && b.src.includes(a.rd) && !between(i, j, a.rd)) edges.push([i, j, "RAW", a.rd]);
  for (const r of new Set(a.src)) if (b.rd === r && !between(i, j, r)) edges.push([i, j, "WAR", r]);
  if (a.rd && a.rd === b.rd && !between(i, j, a.rd)) edges.push([i, j, "WAW", a.rd]);
}));
for (const t of ["RAW", "WAR", "WAW"])
  console.log(t + ": " + edges.filter(e => e[2] === t).map(([i, j, , r]) => `I${i + 1}->I${j + 1}(x${r})`).join("  "));
function earliest(types) {                             // unlimited hardware: start = max over edges
  const s = [];
  I.forEach((q, j) => {
    s[j] = 1;
    for (const [a, b, t] of edges) if (b === j && types.includes(t))
      s[j] = Math.max(s[j], s[a] + (t === "RAW" ? I[a].lat : t === "WAW" ? 1 : 0));
  });
  const len = Math.max(...I.map((q, k) => s[k] + q.lat - 1));
  console.log(`\n${types.join("+")}: start cycles ${s.join(" ")}`);
  console.log(`  critical path = ${len} cycles, ideal IPC = ${I.length}/${len} = ${(I.length / len).toFixed(2)}`);
}
earliest(["RAW", "WAR", "WAW"]);                       // registers as written
earliest(["RAW"]);                                     // after renaming: true dependences only

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

RAW: I1->I3(x5)  I2->I3(x6)  I4->I6(x5)  I5->I6(x6)  I3->I7(x7)  I6->I7(x8)  I7->I8(x9)
WAR: I3->I4(x5)  I3->I5(x6)
WAW: I1->I4(x5)  I2->I5(x6)

RAW+WAR+WAW: start cycles 1 1 3 3 3 5 6 7
  critical path = 7 cycles, ideal IPC = 8/7 = 1.14

RAW: start cycles 1 1 3 1 1 3 4 5
  critical path = 5 cycles, ideal IPC = 8/5 = 1.60

হাতের হিসাবের সাথে হুবহু মিলে গেল: ৭টি RAW, ২টি WAR, ২টি WAW. সব তীর মানলে start cycle 1 1 3 3 3 5 6 7 — critical path ৭ cycle, ideal IPC ১.১৪. শুধু RAW মানলে 1 1 3 1 1 3 4 5 — ৫ cycle, IPC ১.৬০. অর্থাৎ শুধু দুটি register-এর নাম বদলে এই কোডের সম্ভাব্য গতি ৭/৫ = ১.৪ গুণ বাড়ল। লক্ষ করুন, এই “ideal” IPC অসীম hardware ধরে — cycle ১-এ চারটি load চলছে, অর্থাৎ চারটি memory port লাগবে। বাস্তব hardware-এ IPC এর চেয়ে কম বা সমান হবে, কখনো বেশি নয়। LAT-এ lw: 4 বসিয়ে দেখুন — load ধীর হলে critical path কীভাবে বাড়ে।

৪ · Multiple issue — CPI যখন ১-এর নিচে নামে

কোডে ILP আছে জানলাম। এখন hardware-কে প্রতি cycle-এ একাধিক instruction শুরু (issue) করতে দিতে হবে — একে বলে multiple issue. প্রতি cycle-এ সর্বোচ্চ কয়টি instruction issue হতে পারে, তাকে বলে issue width; প্রতিটি জায়গাকে বলে একটি issue slot. ২-way প্রসেসরে প্রতি cycle-এ ২টি slot, ৪-way-তে ৪টি। কোনো slot-এ কাজ না পেলে সেটি নষ্ট হয় — আর এই নষ্ট slot-ই multiple issue-এর আসল শত্রু।

কোন instruction কোন cycle-এ একসাথে যাবে — এই সিদ্ধান্ত কে নেয়, তার ওপর ভিত্তি করে দুটি বড় পরিবার:

দিকStatic multiple issue (VLIW)Dynamic multiple issue (superscalar)
কে সিদ্ধান্ত নেয়Compiler — compile-এর সময়ই instruction-গুলো packet বা bundle-এ সাজায়Hardware — চলার সময় প্রতি cycle-এ দেখে কোনগুলো একসাথে যেতে পারে
Hazard কে সামলায়মূলত compiler; ক্লাসিক VLIW-এ hardware interlock কম বা নেইHardware (hazard detection, forwarding, প্রয়োজনে stall)
পুরোনো binary নতুন chip-এSlot আর latency বদলালে প্রায়ই নতুন করে compile করতে হয়একই binary চলে; নতুন chip নিজেই বেশি সমান্তরালতা খোঁজে
Hardware-এর জটিলতাকম — dependence পরীক্ষার লজিক ছোটবেশি, বিশেষত out-of-order হলে
কোথায় দেখা যায়DSP, কিছু embedded আর বিশেষ chip; Intel Itanium (EPIC)প্রায় সব আধুনিক PC, server আর ফোনের CPU

Superscalar-এর ভেতরেও দুটি ধরন আছে। In-order superscalar প্রোগ্রামের ক্রম ঠিক রাখে: পরপর instruction-গুলো থেকে যতগুলো একসাথে যেতে পারে পাঠায়, প্রথম যে আটকায় সেখানেই থামে। Out-of-order superscalar আটকে থাকা instruction-কে পাশ কাটিয়ে পরের স্বাধীন instruction আগে চালায় — এর বিস্তারিত (Tomasulo, renaming, reorder buffer) আছে পর্ব ৪০-এ। এই পর্বে আমরা in-order superscalar আর VLIW নিয়ে কাজ করব, যেখানে ভালো ফলের জন্য compiler-এর scheduling সবচেয়ে জরুরি।

ঢাকার একটি দুই-লেনের টোল প্লাজা ভাবুন: একটি লেন প্রাইভেট গাড়ির, একটি ট্রাকের। সামনের গাড়ি আর ট্রাক একসাথে পার হতে পারে। কিন্তু পরপর দুটি প্রাইভেট গাড়ি এলে একটিকে অপেক্ষা করতে হয় — ট্রাকের লেন খালি থাকলেও। আর ট্রাকটি যদি সামনের গাড়ির কাগজ হাতে না পাওয়া পর্যন্ত ছাড়তে না পারে (dependence), তাহলেও দুই লেনের সুবিধা নষ্ট। Superscalar-এর issue slot ঠিক এই লেনগুলোর মতো।

৫ · দুই-পথের in-order superscalar RISC-V — issue packet-এর নিয়ম

Patterson–Hennessy-র বইয়ের পরিচিত উদাহরণের ধাঁচে একটি সরল dual-issue RISC-V pipeline নিই। প্রতি cycle-এ দুটি instruction একসাথে fetch, decode আর issue হয় — এই জোড়াকে বলে issue packet. নিয়মগুলো:

  1. Slot ১: ALU বা branch (add, addi, sub, bne …); slot ২: load বা store (lw, sw). দুটি ALU instruction বা দুটি load একসাথে যেতে পারে না।
  2. একই packet-এর মধ্যে RAW চলবে না: packet-এর দুটি instruction একই সময়ে register পড়ে, তাই একটি অন্যটির ফল চাইলে তাকে পরের cycle-এ যেতে হবে। একই packet-এ WAR চলে — পড়া হয় শুরুতে, লেখা পরে।
  3. Latency: ALU-র ফল forwarding দিয়ে পরের cycle-এর packet পায় (latency ১)। lw-এর ফল এক cycle দেরিতে আসে (latency ২) — অর্থাৎ load-use-এর একটি ফাঁকা cycle.
  4. Branch packet শেষ করে: লুপের branch লুপের শেষ packet-এ বসে। ধরে নেওয়া হচ্ছে branch prediction নিখুঁত, তাই branch-এর জন্য বাড়তি penalty নেই (পর্ব ৩৭-এর বিষয় আলাদা রাখছি)।

Hardware-এর দাম: port, adder আর forwarding-এর তার

একসাথে একটি ALU instruction আর একটি store চালাতে register file-কে এক cycle-এ চারটি register পড়তে হয় — ALU-র জন্য দুটি (rs1, rs2), store-এর জন্য দুটি (base আর data). আর একটি ALU ফল ও একটি load ফল একই cycle-এ লিখতে হতে পারে। তাই লাগে ৪টি read port আর ২টি write port, যেখানে scalar pipeline-এ ছিল ২ আর ১. Register file-এর আয়তন port-এর সংখ্যার সাথে দ্রুত বাড়ে, কারণ প্রতিটি bit cell-এ প্রতিটি port-এর জন্য আলাদা তার আর transistor লাগে। Load/store-এর ঠিকানা হিসাবের জন্য আলাদা একটি adder লাগে, কারণ ALU-টি তখন অন্য instruction-এর কাজে ব্যস্ত। আর forwarding-এর পথ প্রায় দ্বিগুণ: দুটি pipeline-এর ফল এখন চারটি operand ইনপুটের যেকোনোটিতে পৌঁছাতে পারে।

Latency-র অর্থও বদলায়। Scalar pipeline-এ load-use মানে একটি instruction-এর জায়গা নষ্ট; dual-issue-এ এক cycle ফাঁকা মানে দুটি issue slot নষ্ট। ALU-র ফল scalar pipeline-এ পরের instruction-ই পেত; এখন একই packet-এর সঙ্গী সেটি পায় না, তাই নির্ভরশীল load/store-কে পরের packet-এ যেতে হয়। অর্থাৎ width বাড়লে একই latency “বেশি instruction-এর সমান” হয়ে যায় — scheduling তাই আরও জরুরি।

একটি লুপ cycle ধরে সাজানো

C-তে for (p = a; p != end; p++) *p += s; — প্রতিটি উপাদানের সাথে একটি সংখ্যা যোগ। x10 = p, x11 = s, x12 = end. RV32I-এ লুপের body:

Loop: lw   x5, 0(x10)      # x5 = *p
      add  x5, x5, x11     # x5 = *p + s
      sw   x5, 0(x10)      # *p = x5
      addi x10, x10, 4     # p++ (৪ bytes)
      bne  x10, x12, Loop  # p != end হলে আবার

লেখা ক্রমেই packet বানালে: cycle ১-এ lw; add-কে load-এর জন্য cycle ৩ পর্যন্ত অপেক্ষা করতে হয়, তাই cycle ২ পুরো ফাঁকা; cycle ৪-এ sw আর addi জোড়া বাঁধে (ভিন্ন slot, আর তাদের মধ্যে শুধু WAR); cycle ৫-এ bne. ৫টি instruction ৫ cycle-এ — IPC ১.০০, dual-issue-এর কোনো লাভই হলো না! এবার compiler একটি কৌশল করে: addi-কে ওপরে তুলে প্রথম packet-এ lw-এর ঠিক পরে বসায়, আর তার পরে যে sw চলবে তার offset ০ থেকে −৪ করে দেয়, যাতে ঠিকানা একই থাকে। নিচের stepper-এ নতুন schedule দেখুন (শুরুতে x10 = 1000, x11 = 3, Mem[1000] = 7):

slot ১ = ALU/branch, slot ২ = load/store · lw latency ২, ALU latency ১
Cycle
Slot ১ (ALU/branch)
Slot ২ (load/store)
কেন এখানে
মান
  1. Cycle ১. addi slot ১-এ, lw slot ২-এ। দুজনে একই cycle-এ register পড়ে, তাই lw পুরোনো x10 = ১০০০ ব্যবহার করে; addi-এর নতুন মান ১০০৪ পরে লেখা হয়। এটি একটি WAR, আর একই packet-এ WAR নিরাপদ।
  2. Cycle ২. পরের কাজ add, কিন্তু তার x5 আসবে lw থেকে, যার latency ২ — মান পাওয়া যাবে cycle ৩-এ। অন্য কোনো স্বাধীন instruction নেই, তাই দুটি slot-ই ফাঁকা। Dual-issue-এ এক cycle load-use মানে দুটি slot নষ্ট।
  3. Cycle ৩. add x5 = ৭ + ৩ = ১০ হিসাব করে। sw-কে এই ফলটি লাগবে, আর একই packet-এ RAW নিষেধ, তাই slot ২ এবারও ফাঁকা।
  4. Cycle ৪. bne x10 (১০০৪, cycle ১-এর addi থেকে) আর x12 তুলনা করে; সাথে sw x5, -4(x10) forwarding দিয়ে ১০ পায় আর ঠিকানা ১০০৪ − ৪ = ১০০০-এ লেখে। Branch শেষ packet-এ, ঠিক নিয়মমতো।
  5. সারাংশ. ৫টি instruction ৪ cycle-এ: IPC = ১.২৫ (CPI ০.৮)। ৮টি slot-এর ৫টি কাজে লাগল, ৩টি নষ্ট — সবগুলো এই এক উপাদানের dependence শিকলের কারণে। শিকল ভাঙতে লাগবে আরও স্বাধীন কাজ — পরের অংশের unrolling.

নিচের scheduler এই নিয়মগুলো মেনে প্রতিটি cycle-এর packet সাজায়। reorder = false হলে hardware-এর মতো লেখা ক্রমে চলে — প্রথম যে instruction আটকায়, সেখানেই সেই cycle শেষ। reorder = true হলে compiler-এর মতো list scheduling করে: যে instruction-এর পরের পথ সবচেয়ে লম্বা, তাকে আগে বসায়, আর pointer-এর addi-এর পরে বসা load/store-এর offset ঠিক করে দেয়। চারটি সংস্করণ চলবে — মূল ক্রম, ক্রম বদলানো, ৪ বার unroll করা কিন্তু শুধু x5 দিয়ে, আর ৪ বার unroll করে নাম বদলানো:

// 2-way superscalar: per cycle one ALU/branch + one load/store slot; latency ALU 1, lw 2
function run(name, body, reorder) {
  const P = body.map(s => { const [op, ...a] = s.replace(/[(),]/g, " ").trim().split(/\s+/);
    const r = a.filter(t => /^x\d+$/.test(t)).map(t => +t.slice(1)), none = op === "sw" || op === "bne";
    return { s, op, mem: op === "lw" || op === "sw", rd: none ? 0 : r[0], src: none ? r : r.slice(1) }; });
  const ptr = P.findIndex(q => q.op === "addi"), imm = +body[ptr].split(",")[2], E = []; // [i, j, gap]
  P.forEach((b, j) => P.slice(0, j).forEach((a, i) => {
    if (b.op === "bne") E.push([i, j, 0]);                                   // branch closes the body
    if (a.rd && b.src.includes(a.rd)) E.push([i, j, a.op === "lw" ? 2 : 1]); // RAW
    if (b.rd && a.src.includes(b.rd) && !(reorder && j === ptr && a.mem)) E.push([i, j, 0]); // WAR
    if (a.rd && a.rd === b.rd) E.push([i, j, 1]);                            // WAW
  }));
  const h = P.map(() => 0); for (let j = P.length - 1; j >= 0; j--) for (const [i, k, d] of E) if (k === j) h[i] = Math.max(h[i], h[j] + d);
  const order = P.map((_, j) => j).sort((x, y) => reorder ? h[y] - h[x] || x - y : x - y);
  const at = [], rows = []; for (let c = 1; at.filter(Boolean).length < P.length; c++) {
    const row = {};
    for (const j of order) {
      if (at[j]) continue;
      const slot = P[j].mem ? "mem" : "alu", inOrder = reorder || P.slice(0, j).every((_, i) => at[i]);
      if (inOrder && !row[slot] && E.every(([i, k, d]) => k !== j || (at[i] && c >= at[i] + d))) {
        at[j] = c;  // a load/store issued after the pointer bump gets its offset corrected
        row[slot] = reorder && at[ptr] < c ? P[j].s.replace(/(-?\d+)\(/, (m, o) => `${o - imm}(`) : P[j].s;
      } else if (!reorder) break;                               // in-order: nobody may overtake
    }
    rows.push(row);
  }
  console.log(`${name}: ${P.length} instructions, ${rows.length} cycles, IPC = ${(P.length / rows.length).toFixed(2)}`);
  rows.forEach((r, c) => console.log(`  ${String(c + 1).padStart(2)}  ${(r.alu || "-").padEnd(19)}${r.mem || "-"}`));
}
const loop = ["lw x5, 0(x10)", "add x5, x5, x11", "sw x5, 0(x10)", "addi x10, x10, 4", "bne x10, x12, Loop"];
const unroll = rename => [0, 1, 2, 3].flatMap(k => { const r = rename ? 5 + k : 5;
  return [`lw x${r}, ${4 * k}(x10)`, `add x${r}, x${r}, x11`, `sw x${r}, ${4 * k}(x10)`]; })
  .concat(["addi x10, x10, 16", "bne x10, x12, Loop"]);
run("A) original order", loop, false);  run("B) reordered", loop, true);
run("C) unrolled x4, only x5", unroll(false), true);  run("D) unrolled x4, renamed", unroll(true), true);

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

A) original order: 5 instructions, 5 cycles, IPC = 1.00
   1  -                  lw x5, 0(x10)
   2  -                  -
   3  add x5, x5, x11    -
   4  addi x10, x10, 4   sw x5, 0(x10)
   5  bne x10, x12, Loop -
B) reordered: 5 instructions, 4 cycles, IPC = 1.25
   1  addi x10, x10, 4   lw x5, 0(x10)
   2  -                  -
   3  add x5, x5, x11    -
   4  bne x10, x12, Loop sw x5, -4(x10)
C) unrolled x4, only x5: 14 instructions, 16 cycles, IPC = 0.88
   1  addi x10, x10, 16  lw x5, 0(x10)
   2  -                  -
   3  add x5, x5, x11    -
   4  -                  sw x5, -16(x10)
   5  -                  lw x5, -12(x10)
   6  -                  -
   7  add x5, x5, x11    -
   8  -                  sw x5, -12(x10)
   9  -                  lw x5, -8(x10)
  10  -                  -
  11  add x5, x5, x11    -
  12  -                  sw x5, -8(x10)
  13  -                  lw x5, -4(x10)
  14  -                  -
  15  add x5, x5, x11    -
  16  bne x10, x12, Loop sw x5, -4(x10)
D) unrolled x4, renamed: 14 instructions, 8 cycles, IPC = 1.75
   1  addi x10, x10, 16  lw x5, 0(x10)
   2  -                  lw x6, -12(x10)
   3  add x5, x5, x11    lw x7, -8(x10)
   4  add x6, x6, x11    lw x8, -4(x10)
   5  add x7, x7, x11    sw x5, -16(x10)
   6  add x8, x8, x11    sw x6, -12(x10)
   7  -                  sw x7, -8(x10)
   8  bne x10, x12, Loop sw x8, -4(x10)

A-তে লেখা ক্রমে ৫ cycle, IPC ১.০০ — হাতের হিসাবের মতো। B-তে stepper-এর schedule: ৪ cycle, IPC ১.২৫. C একটি সতর্কবার্তা: লুপ ৪ বার unroll করলেও সব কপি যদি x5 ব্যবহার করে, প্রতিটি কপিকে আগেরটির sw শেষ হওয়া পর্যন্ত অপেক্ষা করতে হয় (WAR আর WAW). ১৪টি instruction ১৬ cycle-এ, IPC ০.৮৮ — প্রতি উপাদানে ৪ cycle, B-এর চেয়ে একটুও ভালো নয়। D-তে চারটি কপি x5, x6, x7, x8 পায়: চারটি load পরপর যায়, তাদের add আর store পেছনে পেছনে, আর bne শেষ sw-এর সাথে জোড়া বাঁধে। ১৪টি instruction ৮ cycle-এ — IPC ১.৭৫, প্রতি উপাদানে ২ cycle. এর চেয়ে ভালো সম্ভব নয়: ৮টি load/store-এর জন্য slot ২ একটিই, তাই অন্তত ৮ cycle লাগবেই। Patterson–Hennessy-র বইয়ের ৬৪-bit (ld/sd) উদাহরণেও একই ধাঁচের সংখ্যা আসে — ৪ cycle-এ IPC ১.২৫, আর unroll করে ৮ cycle-এ ১৪টি instruction.

৬ · Loop unrolling আর compiler-এর register renaming

একটি উপাদানের কাজ একটি শিকল: load → add → store. শিকলের ভেতরে ILP নেই। কিন্তু ভিন্ন উপাদানের শিকলগুলো একে অপরের ওপর নির্ভর করে না — a[0]-এর হিসাব আর a[1]-এর হিসাব সম্পূর্ণ স্বাধীন। Loop unrolling লুপের body কয়েকবার পাশাপাশি লিখে এই স্বাধীন শিকলগুলোকে একই basic block-এ নিয়ে আসে, যাতে scheduler তাদের মিশিয়ে সাজাতে পারে। তবে D আর C-এর পার্থক্য দেখাল: unrolling একা যথেষ্ট নয়, সাথে register-এর নাম বদলাতে হবে।

লুপ: *p += s · ২-way dual-issue · প্রতি উপাদানের হিসাব
ধাপ
কোড
Instruction / উপাদান
Branch / উপাদান
কপিগুলোর মধ্যে name dependence
Cycle / উপাদান (dual-issue)
  1. মূল লুপ. প্রতি উপাদানে ৫টি instruction, যার একটি branch. ক্রম বদলেও প্রতি উপাদানে ৪ cycle লাগে, কারণ load → add → store শিকলটি ছোট করা যায় না।
  2. কপি. Body চারবার লেখা হলো; মাঝের তিনটি bne আর লাগে না (ধরে নিচ্ছি উপাদান-সংখ্যা ৪-এর গুণিতক)। প্রতিটি সরানো branch মানে একটি control dependence কম। কিন্তু এখন চারটি addi x10 একটি শিকল বানায়, আর চারটি কপিই x5 ব্যবহার করে।
  3. Pointer মেলানো. চারটি addi x10, x10, 4-এর বদলে শেষে একটি addi x10, x10, 16, আর কপিগুলোর offset ০, ৪, ৮, ১২. ১৭টি থেকে ১৪টি instruction. এখন বাকি name dependence শুধু x5-এ: কপি k-এর add আর sw x5 পড়ে, তারপর কপি k + ১-এর lw x5 লেখে — প্রতি জোড়া কপির মধ্যে ২টি WAR আর ১টি WAW, তিন জোড়ায় ৬টি WAR আর ৩টি WAW.
  4. শুধু x5 দিয়ে schedule. কপি k + ১-এর lw কপি k-এর sw-এর আগে যেতে পারে না, তাই চারটি শিকল আবার একটার পর একটা চলে: ১৬ cycle, প্রতি উপাদানে ৪ — unrolling-এর লাভ শূন্য।
  5. Renaming. Compiler প্রতিটি কপিকে নিজস্ব register দেয় — x5, x6, x7, x8. কপিগুলোর মধ্যে কোনো name dependence রইল না, শুধু প্রতিটি কপির ভেতরের true dependence. চারটি শিকল এখন পাশাপাশি চলে: ৮ cycle, প্রতি উপাদানে ২ cycle, IPC ১.৭৫.
  6. দাম. কোড ৫ থেকে ১৪টি instruction (প্রায় ২.৮ গুণ), register ১ থেকে ৪টি। উপাদান-সংখ্যা ৪-এর গুণিতক না হলে বাকি ১–৩টির জন্য আলাদা একটি ছোট লুপ লাগে। বেশি unroll করলে register ফুরিয়ে যেতে পারে, তখন মান মেমরিতে রাখতে হয় (spill) — যা লাভ খেয়ে ফেলে।

এখানে register renaming করেছে compiler — compile-এর সময়, ISA-র দৃশ্যমান register দিয়ে। RV32I-এ মাত্র ৩২টি integer register, তার কিছু আবার stack pointer, return address, zero-র মতো নির্দিষ্ট কাজে বাঁধা। তাই compiler-এর renaming-এর একটি স্বাভাবিক সীমা আছে, যাকে বলে register pressure. Out-of-order hardware চলার সময় একই কাজ করে ISA-র চোখের আড়ালে, শতাধিক physical register দিয়ে — সেটি পর্ব ৪০-এর বিষয়।

সতর্কতা: Unrolling সবসময় লাভজনক নয়। বড় কোড instruction cache-এ বেশি জায়গা নেয় — code bloat; ছোট লুপে প্রতি branch-এর খরচ যদি এমনিতেই কম হয় (ভালো branch predictor আর out-of-order core-এ), তাহলে unrolling-এর লাভও কম। GCC-তে সাধারণ -O2-এ ঢালাও loop unrolling সাধারণত চালু থাকে না; -funroll-loops দিয়ে চাইতে হয় — আর তাতে কিছু প্রোগ্রাম ধীরও হতে পারে। মাপুন, অনুমান করবেন না।

Unrolling + renaming + instruction scheduling — এই তিনটি একসাথে static ILP-এর মূল অস্ত্র। Compiler-এর scheduler-টি আসলে পর্ব ৩৬-এর list scheduling-এরই প্রশস্ত রূপ: প্রতিটি cycle-এ প্রস্তুত instruction-গুলো থেকে slot অনুযায়ী বেছে নেওয়া, যার সামনের পথ সবচেয়ে লম্বা তাকে অগ্রাধিকার দিয়ে।

৭ · VLIW আর EPIC — compiler-ই যখন scheduler

Dual-issue-এর ধারণা চরমে নিলে পাওয়া যায় VLIW — very long instruction word. এখানে একটি “instruction” আসলে কয়েকটি operation-এর একটি লম্বা গুচ্ছ — bundle. প্রতিটি bundle-এ নির্দিষ্ট সংখ্যক slot, প্রতিটি slot একটি নির্দিষ্ট functional unit-এর জন্য। Compiler আগে থেকেই ঠিক করে কোন operation কোন bundle-এর কোন slot-এ যাবে। Hardware শুধু প্রতি cycle-এ একটি bundle তুলে সব slot একসাথে চালায় — dependence পরীক্ষার জটিল লজিক প্রায় নেই।

কিন্তু একটি slot-এ বসানোর মতো কাজ না পেলে? তখন সেখানে একটি nop বসাতে হয়। ক্লাসিক VLIW-এ hardware interlock না থাকায় latency-র ফাঁকটুকুও compiler-কে পুরো nop bundle দিয়ে ভরতে হয়। নিচের packer অংশ ৩-এর কোডটি (নাম বদলানো সংস্করণ) N টি একই রকম slot-এর bundle-এ সাজায় আর nop গোনে:

// VLIW bundle packer: N identical slots per bundle, no hardware interlocks,
// so every empty slot must be filled with an explicit nop by the compiler.
const block = ["lw  x5, 0(x10)", "lw  x6, 4(x10)", "add x7, x5, x6", "lw  x15, 8(x10)",
               "lw  x16, 12(x10)", "sub x8, x15, x16", "and x9, x7, x8", "sw  x9, 16(x10)"];
const B = block.map(s => {
  const [op, ...a] = s.replace(/[(),]/g, " ").trim().split(/\s+/);
  const r = a.filter(t => /^x\d+$/.test(t)).map(t => +t.slice(1));
  return { s, lat: op === "lw" ? 2 : 1, rd: op === "sw" ? 0 : r[0], src: op === "sw" ? r : r.slice(1) };
});
const pred = B.map((b, j) => B.slice(0, j).map((a, i) => i).filter(i => B[i].rd && b.src.includes(B[i].rd)));
const h = B.map(() => 0);                       // height: longest latency path to the end
for (let j = B.length - 1; j >= 0; j--) for (const i of pred[j]) h[i] = Math.max(h[i], h[j] + B[i].lat);
function pack(N) {
  const at = [], bundles = [];
  for (let c = 1; at.filter(Boolean).length < B.length; c++) {
    const ready = B.map((_, j) => j).filter(j => !at[j] && pred[j].every(i => at[i] && at[i] + B[i].lat <= c));
    const pick = ready.sort((x, y) => h[y] - h[x] || x - y).slice(0, N);
    pick.forEach(j => at[j] = c);
    bundles.push([...pick.map(j => B[j].s), ...Array(N - pick.length).fill("nop")]);
  }
  const nops = bundles.length * N - B.length;
  console.log(`N = ${N}: ${bundles.length} bundles, ${bundles.length * N} slots, ${nops} nops ` +
              `(${Math.round(100 * nops / (bundles.length * N))}% empty), IPC = ${(B.length / bundles.length).toFixed(2)}`);
  bundles.forEach((b, k) => console.log(`  ${k + 1}: { ${b.map(s => s.padEnd(16)).join(" | ")} }`));
}
[1, 2, 4].forEach(pack);

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

N = 1: 8 bundles, 8 slots, 0 nops (0% empty), IPC = 1.00
  1: { lw  x5, 0(x10)   }
  2: { lw  x6, 4(x10)   }
  3: { lw  x15, 8(x10)  }
  4: { lw  x16, 12(x10) }
  5: { add x7, x5, x6   }
  6: { sub x8, x15, x16 }
  7: { and x9, x7, x8   }
  8: { sw  x9, 16(x10)  }
N = 2: 6 bundles, 12 slots, 4 nops (33% empty), IPC = 1.33
  1: { lw  x5, 0(x10)   | lw  x6, 4(x10)   }
  2: { lw  x15, 8(x10)  | lw  x16, 12(x10) }
  3: { add x7, x5, x6   | nop              }
  4: { sub x8, x15, x16 | nop              }
  5: { and x9, x7, x8   | nop              }
  6: { sw  x9, 16(x10)  | nop              }
N = 4: 5 bundles, 20 slots, 12 nops (60% empty), IPC = 1.60
  1: { lw  x5, 0(x10)   | lw  x6, 4(x10)   | lw  x15, 8(x10)  | lw  x16, 12(x10) }
  2: { nop              | nop              | nop              | nop              }
  3: { add x7, x5, x6   | sub x8, x15, x16 | nop              | nop              }
  4: { and x9, x7, x8   | nop              | nop              | nop              }
  5: { sw  x9, 16(x10)  | nop              | nop              | nop              }

N = ১: ৮টি bundle, একটিও nop নেই — packer চারটি load আগে বসিয়ে latency লুকিয়েছে, কিন্তু IPC ১-এর বেশি হওয়া সম্ভব নয়। N = ২: ৬টি bundle, ১২টি slot-এর ৪টি nop (৩৩%), IPC ১.৩৩. N = ৪: ৫টি bundle — অংশ ৩-এর critical path-এর সমান, তাই এর চেয়ে কম সম্ভব নয় — কিন্তু ২০টি slot-এর ১২টি nop (৬০%), আর bundle ২ পুরোটাই ফাঁকা: চারটি load-এর latency-র অপেক্ষা। Width দ্বিগুণ করে (২ → ৪) bundle মাত্র ৬ থেকে ৫, অথচ কোডের আকার (slot সংখ্যা) ১২ থেকে ২০. VLIW-এর মূল টানাপোড়েন এটাই: বেশি slot মানে বেশি সম্ভাব্য গতি, কিন্তু ILP কম হলে বেশি nop আর বড় কোড। বাস্তব VLIW-এ তাই প্রায়ই compressed encoding থাকে, যেখানে nop-গুলো আলাদা করে জায়গা নেয় না।

VLIW-এর দুর্বলতা

  • Binary compatibility: latency আর slot-এর সংখ্যা ISA-তে প্রায় স্থায়ী হয়ে যায়। পরের প্রজন্মে একটি unit দ্রুত বা ধীর হলে, বা slot বাড়লে, পুরোনো binary হয় ভুল চলে, নয়তো সুবিধা পায় না — নতুন করে compile করতে হয়। পর্ব ৩৬-এর MIPS load delay slot-এর শিক্ষারই বড় রূপ।
  • অজানা latency: compile-এর সময় compiler জানে না কোন load cache-এ পাবে আর কোনটি miss করবে। Miss হলে পুরো যন্ত্র থামে, কারণ পরের bundle ধরে নিয়েছে মান এসে গেছে। Out-of-order superscalar ঠিক এখানে এগিয়ে থাকে — miss-এর সময়ও সে অন্য স্বাধীন কাজ খুঁজে চালায়।
  • Branch: basic block ছোট বলে compiler-কে branch পেরিয়ে schedule করতে হয় — trace scheduling, predication আর speculative load-এর মতো কৌশল লাগে, যা compiler-কে জটিল করে।

ইতিহাস — Itanium আর EPIC: ১৯৮০-এর দশকে Josh Fisher-এর trace scheduling-এর কাজ আর Multiflow, Cydrome-এর মতো কোম্পানির VLIW যন্ত্র এই ধারণাকে এগিয়ে নেয়। ১৯৯০-এর দশকে HP আর Intel মিলে একটি নতুন ৬৪-bit ISA বানায় — IA-64, যার দর্শনের নাম EPIC (explicitly parallel instruction computing). এতে ১২৮-bit-এর bundle-এ তিনটি instruction থাকে, সাথে কয়েকটি template bit, আর “stop” চিহ্ন দিয়ে compiler স্পষ্ট বলে দেয় কোন instruction-গুলো একসাথে চলতে পারে — ফলে পরের প্রজন্মের চওড়া chip পুরোনো binary থেকেও বেশি সমান্তরালতা নিতে পারত। ছিল ১২৮টি integer register, ব্যাপক predication আর speculative load. প্রথম Itanium বাজারে আসে ২০০১ সালে। কিন্তু বাণিজ্যিকভাবে এটি প্রত্যাশা পূরণ করেনি; ২০২০-এর দশকের শুরুতে Intel এর উৎপাদন বন্ধ করে বলে জানা যায়। কারণ নিয়ে বিতর্ক আছে — compiler সাধারণ প্রোগ্রামে যথেষ্ট static ILP খুঁজে পায়নি, cache miss-এর অনিশ্চয়তা, x86 প্রোগ্রাম চালানোয় দুর্বলতা, দেরিতে বাজারে আসা, আর AMD-এর x86-64 (২০০৩) পুরোনো সফটওয়্যার রেখেই ৬৪-bit দিয়ে দেওয়া — প্রায়ই এগুলো একসাথে উল্লেখ করা হয়।

তবু VLIW মরে যায়নি। যেখানে কোড অনুমানযোগ্য — সংকেত প্রক্রিয়াকরণ, অডিও, ছবি, modem — সেখানে লুপ বড়, branch কম, আর latency জানা। অনেক DSP তাই VLIW ধাঁচের; Texas Instruments-এর C6000 পরিবার আর Qualcomm-এর Hexagon DSP-কে প্রায়ই এমন নকশার উদাহরণ হিসেবে উল্লেখ করা হয়। একটি নির্দিষ্ট ধরনের কাজের জন্য বানানো chip-এ compiler-এর পক্ষে কোড আগে থেকে সাজানো সহজ, আর hardware সরল থাকায় power কম লাগে — ব্যাটারিচালিত ডিভাইসে যা বড় সুবিধা।

৮ · Superscalar-এর front end — width বনাম আসল IPC

একটি আধুনিক superscalar core-কে কয়েকটি চওড়া ধাপ হিসেবে ভাবা যায়, আর প্রতিটির নিজস্ব width আছে:

ধাপWidth মানেকী সীমা দেয়
Fetchপ্রতি cycle-এ instruction cache থেকে কয়টি instruction (বা কত bytes) আনা যায়Taken branch-এ fetch group শেষ হয়; পরের ঠিকানা অনুমান করতে branch predictor লাগে
Decodeপ্রতি cycle-এ কয়টি instruction decode হয়x86-এর ভিন্ন দৈর্ঘ্যের instruction decode করা কঠিন; তাই micro-op cache
Issueপ্রতি cycle-এ কয়টি instruction execution unit-এ পাঠানো যায়Dependence, unit-এর ধরন (কয়টি ALU, কয়টি load port)
Retireপ্রতি cycle-এ কয়টি instruction চূড়ান্তভাবে শেষ (commit) হয়প্রোগ্রামের ক্রমে শেষ করতে হয় (পর্ব ৪০-এর reorder buffer)

যে অংশ পরের ঠিকানা অনুমান করে, instruction আনে আর decode করে, তাকে বলে front end. গড়ে IPC কখনো সবচেয়ে সরু ধাপের width ছাড়াতে পারে না — যেন চার লেনের মহাসড়ক, কিন্তু একটি সেতু দুই লেনের। আর আসল IPC সাধারণত width-এর চেয়ে অনেক কম। প্রকাশিত তথ্যমতে অনেক আধুনিক উচ্চ-পারফরম্যান্স core প্রতি cycle-এ ৪ থেকে ৮টি বা তারও বেশি instruction decode বা issue করতে পারে; অথচ সাধারণ প্রোগ্রামে মাপা গড় IPC প্রায়ই ১ থেকে ৩-এর মধ্যে থাকে বলে দেখা যায় — প্রোগ্রাম আর chip-ভেদে এটি অনেক বদলায়। কারণগুলো এই পর্বেই দেখেছেন: dependence শিকল, cache miss-এর লম্বা অপেক্ষা, branch-এর ভুল অনুমান, আর fetch-এর ভাঙন।

Fetch-এর ভাঙন: taken branch

Instruction cache সাধারণত একটি সারিবদ্ধ টুকরো (যেমন ১৬ বা ৩২ bytes) একবারে দেয়। যদি টুকরোর মাঝখানে একটি taken branch থাকে, তার পরের instruction-গুলো কাজে লাগে না — পরের cycle-এ branch-এর লক্ষ্য থেকে নতুন করে আনতে হয়। ধরুন fetch width ৪, আর গড়ে প্রতি ৬টি instruction-এ একটি taken branch. তাহলে একটি fetch group গড়ে ৪-এর কম কাজের instruction আনবে — অর্থাৎ পেছনের ধাপগুলো যত চওড়াই হোক, front end তাদের পুরো খাওয়াতে পারে না। এজন্য উচ্চ-পারফরম্যান্স core-এ এক cycle-এ একাধিক branch অনুমান, decode করা micro-op জমিয়ে রাখা, আর ছোট লুপকে আলাদা buffer থেকে চালানোর মতো কৌশল দেখা যায়।

নিজে মেপে দেখুন: এক শিকল বনাম চার শিকল

ILP-এর প্রভাব চোখে দেখার সহজ পরীক্ষা: একটি array-এর যোগফল। দুটি লুপই হুবহু একই কাজ করে, একই রকম unroll করা — শুধু প্রথমটিতে সব যোগ একটি চলক s-এ (এক শিকল), দ্বিতীয়টিতে চারটি আলাদা চলকে (চারটি স্বাধীন শিকল)। এটি অংশ ৬-এর renaming-এর C-রূপ। Wandbox-এর gcc এখানে optimization ছাড়া (-O0-এর মতো) compile করে:

#include <stdio.h>
#include <time.h>
#define N 4096
#define REPS 10000
static double a[N];
static double now(void) {
    struct timespec t;
    clock_gettime(CLOCK_MONOTONIC, &t);
    return (double)t.tv_sec + (double)t.tv_nsec * 1e-9;
}
int main(void) {
    for (int i = 0; i < N; i++) a[i] = (double)(i % 7);   /* small whole numbers: sums stay exact */
    double s = 0, t0 = now();
    for (int r = 0; r < REPS; r++)
        for (int i = 0; i < N; i += 4) {    /* one chain: every add waits for the previous one */
            s += a[i]; s += a[i + 1]; s += a[i + 2]; s += a[i + 3];
        }
    double t1 = now(), p0 = 0, p1 = 0, p2 = 0, p3 = 0;
    for (int r = 0; r < REPS; r++)
        for (int i = 0; i < N; i += 4) {    /* four independent chains */
            p0 += a[i]; p1 += a[i + 1]; p2 += a[i + 2]; p3 += a[i + 3];
        }
    double t2 = now(), s4 = p0 + p1 + p2 + p3;
    printf("same sum      : %s (%.0f)\n", s == s4 ? "yes" : "NO", s);
    printf("1 accumulator : %6.1f ms\n", (t1 - t0) * 1e3);
    printf("4 accumulators: %6.1f ms\n", (t2 - t1) * 1e3);
    printf("ratio         : %.1f times faster with 4 chains\n", (t1 - t0) / (t2 - t1));
    return 0;
}

একটি cloud সার্ভারের x86-64 CPU-তে কয়েকবার চালানোর একটি আউটপুট:

same sum      : yes (122850000)
1 accumulator :  199.2 ms
4 accumulators:   50.6 ms
ratio         : 3.9 times faster with 4 chains

কয়েকবার চালিয়ে অনুপাত এসেছে ৩.৫ থেকে ৪.১-এর মধ্যে — মোট সময়ও প্রতিবার আলাদা (কখনো প্রায় দ্বিগুণ), আপনার চালানোয়ও আলাদা আসবে, কারণ সার্ভারটি অন্যদের সাথে ভাগ করা আর CPU-র ধরন বদলাতে পারে। কিন্তু ধাঁচ স্থির: চার শিকল প্রায় ৩.৫–৪ গুণ দ্রুত, যদিও দুটি লুপে instruction প্রায় সমান। কারণ: optimization ছাড়া compiler s-কে মেমরিতে (stack-এ) রাখে, তাই প্রতিটি s += a[i] মানে s পড়া, floating-point যোগ, আবার s লেখা — আর পরের যোগকে আগের লেখা মান আবার পড়তে হয়। এই store থেকে load-এ মান পৌঁছানো (store-to-load forwarding) আর FP যোগ মিলিয়ে প্রতিটি ধাপ কয়েক cycle নেয়, আর শিকলের প্রতিটি ধাপকে আগেরটির জন্য অপেক্ষা করতে হয়। চার চলক থাকলে চারটি শিকল একসাথে এগোয় — out-of-order core তাদের latency একে অপরের ওপর মিলিয়ে দেয়। যোগফল দুটিই সমান (yes), কারণ সব সংখ্যা ছোট পূর্ণসংখ্যা, তাই rounding হয়নি। ভগ্নাংশ থাকলে চার শিকলের যোগফল সামান্য আলাদা হতে পারত, কারণ floating-point যোগ associative নয় (পর্ব ২৪) — আর ঠিক এই কারণেই -O2-তেও gcc নিজে থেকে এক শিকলকে চার শিকলে ভাঙে না, যদি না -ffast-math-এর মতো flag দিয়ে অনুমতি দেওয়া হয়। -O2-তে সময় অনেক কমবে, অনুপাতও বদলাতে পারে, তবে এক শিকলের লুপ সাধারণত FP যোগের latency দিয়েই বাঁধা থাকে।

৯ · ILP-এর দেয়াল — limit study, power আর multicore

কোডে আসলে কতটা ILP আছে? এই প্রশ্নের উত্তর খুঁজতে কয়েক দশক ধরে limit study করা হয়েছে: বাস্তব প্রোগ্রামের instruction trace নিয়ে একটি কাল্পনিক যন্ত্রে চালানো, যার কিছু বা সব সীমা তুলে দেওয়া — অসীম register (নিখুঁত renaming), নিখুঁত branch prediction, নিখুঁত memory-ঠিকানা জ্ঞান, অসীম জানালা। ইতিহাসটি মোটামুটি এমন (সংখ্যাগুলো পদ্ধতি আর benchmark-ভেদে অনেক আলাদা, তাই শুধু ধাঁচটুকু নিন):

  • ১৯৭০ সালের দিকে Tjaden ও Flynn-এর একটি গবেষণায় basic block-এর ভেতরে গড়ে মাত্র দুইয়ের কাছাকাছি instruction একসাথে চালানো যায় বলে দেখা যায় — এই সীমাকে পরে কেউ কেউ “Flynn bottleneck” বলেছেন।
  • ১৯৭২ সালের দিকে Riseman ও Foster দেখান, branch পেরিয়ে দেখতে পারলে অনেক বেশি ILP আছে — কিন্তু তা পেতে hardware অবাস্তব রকম বড় লাগে।
  • ১৯৯১ সালে DEC-এর David Wall-এর বহুল উদ্ধৃত “Limits of Instruction-Level Parallelism” গবেষণা দেখায়, নিখুঁত যন্ত্রে ILP বিশাল হলেও বাস্তবসম্মত ধারণায় (সীমিত জানালা, বাস্তব branch prediction, সীমিত renaming) গড় ILP সাধারণত একক অঙ্কে নেমে আসে। Hennessy–Patterson-এর বইয়েও এমন হিসাবের আলোচনা আছে।

অর্থাৎ ILP আছে, কিন্তু তা তুলে আনার খরচ দ্রুত বাড়ে। কয়েকটি কারণ:

  • জটিলতা width-এর চেয়ে দ্রুত বাড়ে: প্রতিটি নতুন instruction-কে একই cycle-এর বাকি সবার সাথে dependence-এর জন্য তুলনা করতে হয় — তুলনার সংখ্যা মোটামুটি width-এর বর্গের হারে বাড়ে। Register file-এর port, bypass network-এর তার আর out-of-order-এর অপেক্ষমাণ instruction-এর সারিও একইভাবে বাড়ে।
  • Power: dynamic power ≈ α·C·V²·f. প্রতিটি অনুমান-ভিত্তিক কাজ, প্রতিটি তুলনা, প্রতিটি লম্বা তার energy খায় — অথচ IPC-র লাভ ক্রমশ কম। ২০০০-এর দশকের মাঝামাঝি Dennard scaling থেমে গেলে voltage আর কমানো যাচ্ছিল না, আর chip power wall-এ ঠেকে।
  • ক্রমহ্রাসমান লাভ: width ৪ থেকে ৮ করলে transistor আর power অনেক বাড়ে, কিন্তু সাধারণ প্রোগ্রামে IPC বাড়ে সামান্য — কারণ কোডেই অতটা ILP নেই, আর memory-র অপেক্ষা সবকিছু থামিয়ে দেয়।

ফলে শিল্প দিক বদলায়। একটি বিশাল core-কে আরও চওড়া করার বদলে একই chip-এ কয়েকটি core বসানো — multicore. ২০০৫ সালে Intel আর AMD দুজনেই প্রথম dual-core desktop প্রসেসর বাজারে আনে। এতে সমান্তরালতার দায়িত্ব hardware থেকে আবার programmer-এর কাছে যায়: instruction-স্তরের বদলে thread-স্তরের সমান্তরালতা। একটি core-এর নষ্ট issue slot কাজে লাগানোর আরেকটি উপায় হলো একাধিক thread-এর instruction মিশিয়ে চালানো — পর্ব ৪৩-এর SMT; আর একই instruction দিয়ে অনেক ডেটা চালানো — পর্ব ৪২-এর SIMD. Multicore আর cache coherence-এর পুরো আলোচনা মডিউল ৯-এ, পর্ব ৫৩ থেকে। আর বেশি speculation-এর একটি অপ্রত্যাশিত দাম — নিরাপত্তার ফাঁক — দেখবেন পর্ব ৪১-এ।

তবে ILP-এর গল্প থেমে যায়নি। আজকের উচ্চ-পারফরম্যান্স core-গুলো এখনো চওড়া out-of-order নকশা, আর প্রতিটি নতুন প্রজন্মে IPC কিছুটা বাড়ানো হয় — কিন্তু সেই লাভ আসে ধীরে, অনেক transistor খরচ করে। ফোনের chip-এ তাই প্রায়ই দুই রকম core থাকে: কয়েকটি বড় চওড়া out-of-order core দ্রুত কাজের জন্য, আর কয়েকটি ছোট core (প্রায়ই in-order superscalar) কম power-এর কাজের জন্য।

আরও গভীরে: Compiler-এর static ILP-এর আরও দুটি শক্তিশালী কৌশল আছে। Software pipelining (modulo scheduling — B. Ramakrishna Rau-র কাজের সাথে জড়িত) লুপের বিভিন্ন iteration-এর ভিন্ন অংশ একই bundle-এ মিশিয়ে দেয়, যাতে unroll না করেও স্থির অবস্থায় প্রতি কয়েক cycle-এ একটি iteration শেষ হয়। Trace scheduling (Josh Fisher) সবচেয়ে সম্ভাব্য branch-পথ ধরে কয়েকটি basic block একসাথে schedule করে, আর ভুল পথের জন্য “সংশোধন কোড” বসায়। Hardware-এর দিকে, আধুনিক x86 আর ARM core পাশাপাশি কিছু instruction জোড়াকে (যেমন compare আর branch) একটি micro-op-এ মিলিয়ে দেয় — macro-op fusion — যাতে একই width-এ বেশি কাজ হয়।

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

  • Superscalar আর VLIW-এর মূল পার্থক্য কী? Superscalar-এ hardware চলার সময় ঠিক করে কোনগুলো একসাথে যাবে; VLIW-এ compiler আগে থেকেই bundle বানিয়ে দেয়।
  • কোন dependence register renaming দিয়ে দূর করা যায়? শুধু name dependence — WAR আর WAW; RAW (true) থাকে।
  • ৪-way প্রসেসরের সর্বনিম্ন CPI কত, আর বাস্তবে কেন পাওয়া যায় না? ০.২৫; dependence, cache miss, branch-এর ভুল অনুমান আর fetch-এর ভাঙনে slot নষ্ট হয়।
  • Loop unrolling কীভাবে ILP বাড়ায়, আর তার দাম কী? স্বাধীন iteration-কে এক basic block-এ এনে scheduling-এর সুযোগ দেয়, branch কমায়; দাম বড় কোড আর বেশি register.
  • Dependence graph-এর critical path কেন গুরুত্বপূর্ণ? অসীম hardware-এও কোডটি এর চেয়ে কম সময়ে শেষ হবে না — ideal IPC = n / L.

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

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

  • একই packet-এ নির্ভরশীল দুটি instruction বসিয়ে দেওয়া, বা slot-এর ধরন (ALU বনাম load/store) না মানা।
  • Load-use latency ভুলে যাওয়া, বা dual-issue-এ এক cycle ফাঁকা মানে যে দুটি slot নষ্ট, তা না ধরা; IPC আর CPI উল্টে ফেলা।
  • Unroll করে register-এর নাম না বদলেই বিশাল speedup দাবি করা; বা addi আগে তুলে load/store-এর offset ঠিক না করা — যা প্রোগ্রামকে ভুল ঠিকানায় পাঠায়।
  • MIPS আর RISC-V-এর রীতি মেশানো ($t0-ধাঁচের register, branch delay slot), বা RV32-এর জায়গায় RV64-এর ld/sd আর ৮-byte offset লেখা।
  • নির্দিষ্ট chip-এর width, latency বা Itanium-এর ইতিহাস নিয়ে বানানো সংখ্যা আর তারিখ দেওয়া।

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

আমি কম্পিউটার আর্কিটেকচার শিখছি। নিচের RISC-V (RV32I) লুপটি একটি ২-way static superscalar-এ schedule করো। নিয়ম: প্রতি cycle-এ একটি packet — slot ১-এ একটি ALU বা branch, slot ২-এ একটি lw বা sw; একই packet-এ RAW নিষেধ, WAR চলবে; ALU-র latency ১, lw-এর latency ২; branch থাকবে শেষ packet-এ; branch prediction নিখুঁত। ১. প্রথমে সব RAW, WAR, WAW dependence একটি টেবিলে দাও, আর dependence graph-এর critical path বলো। ২. লেখা ক্রমে packet টেবিল (cycle, slot ১, slot ২) আর IPC দাও। ৩. তারপর ক্রম বদলে, এবং ৪ বার unroll করে register-এর নাম বদলে — দুটির টেবিল আর IPC দাও। addi আগে তুললে প্রতিটি load/store-এর নতুন offset স্পষ্ট লেখো। ৪. প্রতিটি ফাঁকা slot কেন ফাঁকা, এক লাইনে বলো। ৫. শেষে একটি ছোট JavaScript স্ক্রিপ্ট দাও, যা দিয়ে আমি নিজে cycle সংখ্যা যাচাই করতে পারি। লুপ: [এখানে আপনার কোড লিখুন]

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

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

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

প্র ০১Itanium-এর পেছনে বিশাল বিনিয়োগ ছিল, তবু সাধারণ-উদ্দেশ্যের কম্পিউটারে এটি কেন জায়গা করতে পারেনি? এখান থেকে নকশাকারীরা কী শিখেছেন?

১৯৯০-এর দশকে অনেকের ধারণা ছিল, out-of-order superscalar-এর জটিলতা শিগগিরই অসহনীয় হয়ে উঠবে — প্রতিটি নতুন প্রজন্মে আরও বড় dependence-পরীক্ষার লজিক, আরও বেশি port, আরও বেশি power. যুক্তি ছিল: compiler পুরো প্রোগ্রাম দেখে, তার হাতে প্রচুর সময়, তাই সে-ই instruction সাজাক; hardware সরল থাকুক আর সেই বাঁচানো transistor দিয়ে আরও বেশি functional unit আর register বসানো হোক। HP-র গবেষকদের এই ধারণা আর Intel-এর উৎপাদন-শক্তি মিলে জন্ম নেয় IA-64 আর EPIC.

ধারণাটি নিখুঁতভাবে কাজ করে এমন কোডে, যেখানে latency আগে থেকে জানা — যেমন বৈজ্ঞানিক হিসাবের বড় লুপ, যেখানে software pipelining আর predication চমৎকার ফল দেয়। কিন্তু সাধারণ প্রোগ্রাম — database, web server, compiler, office সফটওয়্যার — অনিয়মিত। সেখানে pointer অনুসরণ, অনুমান-অযোগ্য branch আর cache miss প্রচুর। কোন load L1-এ পাবে আর কোনটি কয়েকশো cycle-এর memory অপেক্ষায় পড়বে, compiler তা জানে না। Out-of-order hardware চলার সময় সেটি দেখে আর অন্য কাজ চালিয়ে যায়; static schedule সেই সুযোগ পায় না। EPIC-এ speculative load আর advanced load-এর মতো কৌশল যোগ করা হয়েছিল, কিন্তু তাতে compiler আরও জটিল হয়।

বাজারের কারণও ছিল বলে প্রায়ই উল্লেখ করা হয়। প্রথম Itanium বাজারে আসে পরিকল্পনার চেয়ে দেরিতে, ২০০১ সালে। পুরোনো x86 প্রোগ্রাম চালানো যেত, কিন্তু ধীরে। আর ২০০৩ সালে AMD-এর x86-64 (AMD64) প্রমাণ করে যে পুরোনো সফটওয়্যার রেখেই ৬৪-bit পাওয়া যায় — Intel নিজেও পরে সেই পথ নেয়। ফলে Itanium ধীরে ধীরে কিছু উচ্চমূল্যের server-এর বিশেষ বাজারে সীমিত হয়, আর ২০২০-এর দশকের শুরুতে এর উৎপাদন বন্ধ হয় বলে জানা যায়। কোন কারণটি সবচেয়ে বড়, তা নিয়ে বিশেষজ্ঞরা আজও একমত নন।

তবে কাজটি বৃথা যায়নি। Predication, speculative load আর বড় register file নিয়ে গবেষণা compiler প্রযুক্তিকে সমৃদ্ধ করেছে; আর এই অভিজ্ঞতা দেখিয়েছে, ISA-তে microarchitecture-এর সময়-নির্ভর খুঁটিনাটি (latency, slot) বেঁধে দেওয়া দীর্ঘমেয়াদে ঝুঁকিপূর্ণ — যে শিক্ষা পর্ব ২৫-এর “ISA একটি দীর্ঘমেয়াদি চুক্তি” ধারণার সাথে মেলে।

মূল উপলব্ধি: Compile-এর সময়ের জ্ঞান স্থির আর অসম্পূর্ণ; চলার সময়ের জ্ঞান বদলায় কিন্তু সঠিক। অনিয়মিত সাধারণ প্রোগ্রামে চলার সময় সিদ্ধান্ত নেওয়া hardware এগিয়ে থাকে, আর নিয়মিত, অনুমানযোগ্য কোডে compiler-চালিত VLIW এখনো সাশ্রয়ী।

প্র ০২Limit study-গুলো নিখুঁত যন্ত্রে বিশাল ILP দেখায়, অথচ বাস্তব chip-এ IPC প্রায়ই ১–৩-এর মধ্যে আটকে থাকে। এই ফাঁক কোথা থেকে আসে?

Limit study-র কৌশল সরল: একটি বাস্তব প্রোগ্রাম চালিয়ে তার প্রতিটি instruction-এর একটি trace রাখা, তারপর একটি কাল্পনিক যন্ত্রে সেই trace-কে যত দ্রুত সম্ভব schedule করা। “নিখুঁত” যন্ত্রে সব সীমা তুলে দেওয়া হয় — অসীম register (তাই কোনো name dependence নেই), প্রতিটি branch-এর ফল আগেভাগে জানা, প্রতিটি load-store-এর ঠিকানা জানা, আর পুরো trace একটি জানালায় দেখা যায়। তখন শুধু true dependence থাকে — ঠিক এই পর্বের dataflow limit. সেখানে কিছু প্রোগ্রামে ILP কয়েক ডজন থেকে শতাধিক পর্যন্ত আসতে পারে বলে বিভিন্ন গবেষণায় দেখা গেছে।

তারপর একে একে বাস্তবতা ফেরানো হয়। জানালা সীমিত করুন — কয়েকশো বা কয়েক ডজন instruction — ILP অনেক কমে, কারণ দূরের স্বাধীন কাজ আর দেখা যায় না। Branch prediction বাস্তব করুন — প্রতিটি ভুল অনুমান জানালার পেছনের সব কাজ মুছে দেয়। Register সীমিত করুন — name dependence ফিরে আসে। মেমরির ঠিকানা অজানা ধরুন — সাবধানী load-store ক্রম আরও কমায়। David Wall-এর ১৯৯১ সালের বিখ্যাত গবেষণায় বাস্তবসম্মত ধারণায় গড় ILP সাধারণত একক অঙ্কে নেমে আসে বলে দেখা যায়, যদিও সংখ্যাগুলো benchmark আর ধারণাভেদে অনেক আলাদা।

বাস্তব chip এর চেয়েও কম পায়, কারণ limit study সাধারণত latency সরল ধরে আর cache miss বাদ দেয়। বাস্তবে main memory থেকে একটি load কয়েকশো cycle নিতে পারে; জানালা ভরে গেলে core থেমে যায়। প্রতিটি issue slot-এর জন্য উপযুক্ত unit-ও সবসময় খালি থাকে না। আর front end taken branch-এ ভাঙে। এসব যোগ হয়ে সাধারণ প্রোগ্রামে গড় IPC প্রায়ই ১ থেকে ৩-এর মধ্যে আসে বলে মাপা হয় — কিছু ছোট, cache-বান্ধব লুপে তার অনেক বেশি, আর pointer-নির্ভর কোডে ১-এর নিচেও।

এখান থেকে একটি গুরুত্বপূর্ণ শিক্ষা: ILP-এর সবচেয়ে বড় শত্রু প্রায়ই dependence নয়, বরং অনিশ্চয়তা — branch কোন দিকে যাবে, load কতক্ষণ নেবে, দুটি pointer একই জায়গা দেখায় কি না। আধুনিক core-এর বেশিরভাগ জটিলতা (branch predictor, memory dependence predictor, বড় reorder buffer) আসলে এই অনিশ্চয়তা কমানোর চেষ্টা।

মূল উপলব্ধি: Limit study বলে “কতটা সম্ভব”, chip বলে “কতটা সাশ্রয়ে পাওয়া যায়”। দুইয়ের ফাঁক তৈরি করে জানালার আকার, অনুমানের ভুল আর memory-র latency — আর এগুলোর প্রতিটি কমাতে খরচ দ্রুত বাড়ে।

প্র ০৩Issue width ৪ থেকে ৮ করলে transistor আর power অনেক বাড়ে, অথচ IPC সামান্য। ঠিক কোন কোন অংশ width-এর সাথে এত দ্রুত বড় হয়?

প্রথম অংশ register file. অংশ ৫-এ দেখেছি, ২-way-তেই read port ২ থেকে ৪ আর write port ১ থেকে ২ হয়। W-way-তে মোটামুটি ২W read আর W write port. প্রতিটি bit cell-এ প্রতিটি port-এর জন্য একটি করে তার আর access transistor লাগে, তাই cell-এর দৈর্ঘ্য ও প্রস্থ দুটোই port-এর সংখ্যার সাথে বাড়ে — আয়তন মোটামুটি port-এর বর্গের হারে। বড় register file মানে লম্বা তার, তাই পড়তে বেশি সময় আর বেশি energy. এজন্য অনেক চওড়া নকশা register file-কে কয়েকটি কপিতে ভাগ করে বা দুই cycle-এ পড়ে।

দ্বিতীয় অংশ dependence পরীক্ষা। In-order superscalar-এ একই cycle-এর W টি instruction-এর প্রতিটির source-কে আগের সবার destination-এর সাথে মেলাতে হয় — মোটামুটি W² তুলনা। Out-of-order-এ এর চেয়েও বড়: প্রতিটি ফল তৈরি হলে অপেক্ষমাণ সব instruction-কে জানাতে হয় (“wakeup”), তারপর প্রস্তুতদের মধ্য থেকে বেছে নিতে হয় (“select”) — আর এই কাজ এক cycle-এ শেষ করতে হয়, নইলে নির্ভরশীল instruction পরপর চলতে পারে না। ১৯৯৭ সালের দিকে Palacharla, Jouppi ও Smith-এর একটি পরিচিত গবেষণা দেখায়, এমন wakeup-select লজিক আর bypass-এর তারের delay width আর জানালার আকারের সাথে দ্রুত বাড়ে, আর প্রযুক্তি ছোট হলেও তারের delay সেই হারে কমে না।

তৃতীয় অংশ bypass network. প্রতিটি functional unit-এর ফলকে প্রতিটি unit-এর প্রতিটি ইনপুটে পৌঁছাতে হয় — তারের সংখ্যা মোটামুটি unit সংখ্যার বর্গ। এর সাথে যোগ করুন fetch-এর সমস্যা: ৮টি instruction প্রতি cycle-এ আনতে হলে প্রায় প্রতি cycle-এ অন্তত একটি branch পড়বে, তাই এক cycle-এ একাধিক branch অনুমান লাগবে।

অন্যদিকে লাভ সীমিত, কারণ প্রোগ্রামে অতটা ILP নেই (প্র ০২)। তাই চওড়া নকশায় প্রতি cycle-এ আরও বেশি transistor অলস বসে থাকে, আর সেগুলোও power খায় (leakage). এই হিসাবটিই ২০০০-এর দশকে multicore-এর দিকে যাত্রার একটি বড় চালিকা: দুটি মাঝারি core একটি বিশাল core-এর চেয়ে প্রায়ই বেশি মোট কাজ দেয়, একই power-এ — যদি সফটওয়্যার দুটি thread দিতে পারে।

মূল উপলব্ধি: Width রৈখিকভাবে বাড়লে register port, dependence পরীক্ষা আর bypass-এর খরচ প্রায় বর্গের হারে বাড়ে, আর IPC বাড়ে কোডের ILP যতটুকু দেয় ততটুকু — এই অসম লড়াই-ই ILP-এর দেয়াল।

প্র ০৪আসল Pentium থেকে আজকের অনেক ফোনের ছোট core — in-order dual-issue নকশা কেন এত দীর্ঘ সময় টিকে আছে?

১৯৯৩ সালের Intel Pentium ছিল x86-এর প্রথম দিকের একটি জনপ্রিয় superscalar প্রসেসর: দুটি integer pipeline, যাদের “U” আর “V” pipe বলা হতো। একটি জোড়া তখনই একসাথে যেত, যখন দুটি instruction নির্দিষ্ট সরল ধরনের আর একে অপরের ওপর নির্ভরশীল নয় — ঠিক এই পর্বের issue packet-এর নিয়মের মতো। সেই সময় compiler-গুলো Pentium-এর জন্য বিশেষ করে instruction জোড়া মিলিয়ে সাজাত, কারণ ভুল ক্রমে একটি pipe অলস বসে থাকত।

পরে x86-এর মূলধারা out-of-order-এ চলে যায় (Pentium Pro, ১৯৯৫)। কিন্তু in-order superscalar হারিয়ে যায়নি — energy-সাশ্রয়ী core-এ সে ফিরে এসেছে। ARM-এর Cortex-A53 আর A55-এর মতো core in-order dual-issue নকশার বলে ARM-এর প্রকাশিত বিবরণে দেখা যায়, আর এরা বহু বছর ধরে অনেক সাশ্রয়ী Android ফোনে ব্যবহৃত হয়েছে। Big.LITTLE বা এমন মিশ্র নকশায় একই chip-এ বড় out-of-order core আর এমন ছোট core থাকে; OS হালকা কাজ — notification, পটভূমির sync, গান চালানো — ছোট core-এ পাঠায়।

কারণ energy-র হিসাব। Out-of-order core-এর renaming টেবিল, reorder buffer, বড় scheduler আর অনুমান-ভিত্তিক কাজ — সব প্রতিটি instruction-এ energy খরচ করে। In-order dual-issue core সেই খরচ ছাড়াই scalar-এর চেয়ে বেশ খানিকটা বেশি IPC দেয়, বিশেষত যখন compiler ভালোভাবে schedule করে। প্রতি joule-এ কাজের হিসাবে এমন core প্রায়ই এগিয়ে থাকে, যদিও একক thread-এর সর্বোচ্চ গতিতে পিছিয়ে।

বাংলাদেশের প্রেক্ষাপটে ভাবুন: বাজারে যে কম দামের ফোন খুব প্রচলিত, তার chip-এ ব্যাটারি আর তাপ বড় সীমা। সেখানে কয়েকটি ছোট in-order core একটি বড় core-এর চেয়ে বাস্তবে বেশি উপযোগী হতে পারে। আর developer হিসেবে এর মানে — একই কোড বড় core-এ দ্রুত চললেও ছোট core-এ scheduling আর dependence শিকলের প্রভাব অনেক বেশি চোখে পড়ে; অংশ ৮-এর চার-accumulator কৌশল সেখানে আরও বেশি কাজে আসতে পারে।

মূল উপলব্ধি: সর্বোচ্চ গতি আর প্রতি energy-তে গতি দুটি ভিন্ন লক্ষ্য। In-order superscalar দ্বিতীয় লক্ষ্যে চমৎকার আপস — তাই সে বড় core-এর পাশে আজও জায়গা পায়।

প্র ০৫একটি numerical লাইব্রেরি লেখার সময় একাধিক accumulator আর loop unrolling কখন সাহায্য করে, কখন ক্ষতি করে? Compiler নিজে কেন সবসময় এটি করে না?

অংশ ৮-এর পরীক্ষায় দেখেছেন, এক শিকলের যোগফল প্রতিটি যোগের latency দিয়ে বাঁধা, আর চারটি স্বাধীন শিকল সেই latency একে অপরের ওপর মিলিয়ে দেয়। নিয়মটি সাধারণ: একটি core-এ FP যোগের latency L cycle আর প্রতি cycle-এ সে T টি FP যোগ শুরু করতে পারলে, পূর্ণ গতির জন্য অন্তত L × T টি স্বাধীন শিকল লাগে। এজন্য dot product, matrix গুণ বা যোগফলের মতো লুপে দক্ষ লাইব্রেরিগুলো (যেমন BLAS-এর অনেক বাস্তবায়ন) একাধিক accumulator আর unrolling ব্যবহার করে, প্রায়ই পর্ব ৪২-এর SIMD register-এর সাথে মিলিয়ে।

তাহলে compiler নিজে কেন করে না? Integer যোগের ক্ষেত্রে অনেক সময় করে — integer যোগ associative, ক্রম বদলালে ফল বদলায় না, তাই optimization চালু থাকলে compiler vectorize বা ভাগ করতে পারে। কিন্তু floating-point যোগ associative নয়: (a + b) + c আর a + (b + c)-এর rounding আলাদা হতে পারে (পর্ব ২৪)। C-এর নিয়ম অনুযায়ী compiler প্রোগ্রামের লেখা ক্রমের ফলই দিতে বাধ্য, তাই সে চুপচাপ চার শিকলে ভাঙতে পারে না। -ffast-math বা -fassociative-math-এর মতো flag সেই অনুমতি দেয় — কিন্তু তখন ফল সামান্য বদলাতে পারে, আর NaN, infinity বা খুব ছোট সংখ্যার আচরণও বদলাতে পারে। বৈজ্ঞানিক বা আর্থিক হিসাবে সেটি গ্রহণযোগ্য কি না, programmer-কেই ঠিক করতে হয়।

ক্ষতির দিকও আছে। বেশি accumulator আর বেশি unroll মানে বেশি register; register ফুরালে compiler মান মেমরিতে spill করে, আর লাভ উল্টে যেতে পারে। বড় কোড instruction cache আর micro-op cache থেকে উপচে পড়লে front end-এর গতি কমে। ছোট array-তে (ধরুন ১০টি উপাদান) unroll-এর বাকি অংশের লুপটাই প্রধান হয়ে যায়। আর লুপ যদি মেমরি থেকে ডেটা আনার গতিতেই বাঁধা থাকে (array cache-এর চেয়ে অনেক বড়), তাহলে ALU-র ILP বাড়িয়ে লাভ প্রায় শূন্য — তখন bottleneck memory, পর্ব ৪৭–৪৮-এর বিষয়।

তাই পেশাদার পদ্ধতি হলো: profiler দিয়ে দেখা লুপটি কীসে বাঁধা — latency, throughput না memory; তারপর একটি পরিবর্তন করে আবার মাপা। “বেশি unroll মানেই দ্রুত” — এটি অনুমান, নিয়ম নয়।

মূল উপলব্ধি: একাধিক accumulator dependence শিকল ভাঙে, কিন্তু floating-point-এ তা ফল বদলাতে পারে বলে compiler অনুমতি ছাড়া করে না; আর register, কোডের আকার ও memory-র সীমা ছাড়িয়ে গেলে লাভ ক্ষতিতে পরিণত হয়।

অনুশীলন

পরীক্ষার ধাঁচের সমস্যা — রীতি: lw-এর latency ২ cycle, বাকি instruction-এর ১ cycle; WAR-এ একই cycle চলে, WAW-এ পরের cycle. Dual-issue-এ slot ১ = ALU/branch, slot ২ = lw/sw, একই packet-এ RAW নিষেধ, branch শেষ packet-এ। আগে নিজে করুন, তারপর পূর্ণ সমাধান মিলিয়ে নিন।

  1. Dependence চিহ্নিত করুন আর critical path বের করুন:
    I1: lw  x5, 0(x10)
    I2: add x6, x5, x7
    I3: sub x5, x8, x9
    I4: lw  x7, 4(x10)
    I5: and x9, x6, x5
    I6: sw  x9, 8(x10)
    (ক) সব RAW, WAR, WAW জোড়া register-সহ লিখুন। (খ) অসীম hardware-এ সব dependence মেনে, আর renaming-এর পরে শুধু RAW মেনে — প্রতিটি instruction-এর শুরুর cycle, critical path আর ideal IPC কত? (গ) Renaming এখানে কেন সাহায্য করে না?

    (ক) RAW (৪টি): I1→I2 (x5), I2→I5 (x6), I3→I5 (x5), I5→I6 (x9). লক্ষ করুন I1→I5 RAW নয় — I5 যে x5 পড়ে, তা I3-এর লেখা। WAR (৩টি): I2→I3 (x5), I2→I4 (x7), I3→I5 (x9 — I3 x9 পড়ে, I5 লেখে)। WAW (১টি): I1→I3 (x5).

    (খ) সব dependence মেনে: I1 = ১; I2 = ১ + ২ = ৩; I3 = max(I2-এর WAR → ৩, I1-এর WAW → ২) = ৩; I4 = ৩ (I2-এর WAR); I5 = max(I2 + ১, I3 + ১) = ৪; I6 = ৫. শুরুর cycle 1 3 3 3 4 5, critical path ৫ cycle, ideal IPC = ৬/৫ = ১.২০. শুধু RAW মেনে: 1 3 1 1 4 5 — I3 আর I4 cycle ১-এ উঠে আসে, কিন্তু critical path এখনো ৫, IPC এখনো ১.২০.

    (গ) Critical path I1 → I2 → I5 → I6 (২ + ১ + ১ + ১ = ৫) পুরোটাই true dependence দিয়ে গড়া। Renaming শুধু name dependence মোছে, তাই I3 আর I4 আগে চলার সুযোগ পায়, কিন্তু সবচেয়ে লম্বা পথ ছোট হয় না। মূল শিক্ষা: renaming তখনই সময় বাঁচায়, যখন name dependence critical path-এর অংশ। (অংশ ৩-এর analyser-এ এই কোড বসিয়ে যাচাই করা।)

  2. Dual-issue-এ cycle আর IPC: একটি array-এর যোগফলের লুপ (x10 = pointer, x11 = যোগফল, x12 = শেষ):
    Loop: lw   x5, 0(x10)
          add  x11, x11, x5
          addi x10, x10, 4
          bne  x10, x12, Loop
    (ক) লেখা ক্রমে packet বানালে প্রতি iteration-এ কত cycle, IPC কত? (খ) Compiler ক্রম বদলালে? (গ) ৪ বার unroll করে চারটি load-কে x5–x8 দিলে (যোগফল একটিই, x11) প্রতি ৪ উপাদানে কত cycle, IPC কত? (ঘ) চারটি আলাদা যোগফল-register নিলে (x13–x16) কি আরও দ্রুত হবে?

    (ক) ৫ cycle, IPC = ৪/৫ = ০.৮০: cycle ১ lw; cycle ২ ফাঁকা (load-use); cycle ৩ add; cycle ৪ addi — সে ALU instruction, add-এর সাথে একই slot চায়, তাই আলাদা cycle; cycle ৫ bne (একই slot, তার ওপর addi-এর x10 চাই)।

    (খ) ৪ cycle, IPC = ১.০০: cycle ১-এ addi + lw (একই packet-এ WAR চলে), cycle ২ ফাঁকা, cycle ৩ add, cycle ৪ bne. এখানে কোনো load/store-এর offset বদলাতে হয়নি, কারণ addi-এর পরে কোনো মেমরি instruction নেই।

    (গ) ১০টি instruction, ৭ cycle, IPC ≈ ১.৪৩ (প্রতি উপাদানে ১.৭৫ cycle):

    cycle  slot ১ (ALU/branch)   slot ২ (load/store)
      1    addi x10, x10, 16     lw x5, 0(x10)
      2    -                     lw x6, -12(x10)
      3    add x11, x11, x5      lw x7, -8(x10)
      4    add x11, x11, x6      lw x8, -4(x10)
      5    add x11, x11, x7      -
      6    add x11, x11, x8      -
      7    bne x10, x12, Loop    -

    (ঘ) না — একই ৭ cycle. এখানে বাধা dependence নয়, slot ১: চারটি add, একটি addi আর একটি bne — ছয়টি ALU/branch instruction, আর প্রথম add cycle ৩-এর আগে যেতে পারে না, branch শেষ cycle-এ একা। তাই ৭-এর কম অসম্ভব। ALU latency ১ বলে x11-এর শিকলও প্রতি cycle-এ একটি করে এগোতে পারে। অংশ ৮-এর C প্রোগ্রামে চার accumulator সাহায্য করেছিল, কারণ সেখানে প্রতিটি যোগের latency কয়েক cycle (FP যোগ, আর -O0-তে মেমরি দিয়ে ঘোরা)। শিক্ষা: আগে দেখুন সীমা কোথায় — latency না resource. (অংশ ৫-এর scheduler-এ চারটি সংস্করণ চালিয়ে যাচাই করা।)

  3. VLIW bundle গুনুন: একটি VLIW যন্ত্রের প্রতিটি bundle-এ N টি একই রকম slot (যেকোনো operation যেকোনো slot-এ), hardware interlock নেই। নিচের basic block-এর জন্য (ক) dependence graph-এর critical path, (খ) N = ৩ হলে সর্বনিম্ন bundle সংখ্যা আর nop সংখ্যা, (গ) N = ২ হলে?
    lw  x5, 0(x10)
    lw  x6, 4(x10)
    lw  x7, 8(x10)
    add x8, x5, x6
    sub x9, x7, x5
    and x28, x8, x9
    or  x29, x8, x7
    sw  x28, 12(x10)
    sw  x29, 16(x10)

    (ক) সবচেয়ে লম্বা পথ lw → add x8 → and x28 → sw x28: ২ + ১ + ১ + ১ = ৫ cycle. ৯টি operation, তাই ideal IPC = ৯/৫ = ১.৮.

    (খ) N = ৩: ৫টি bundle, ১৫টি slot, ৬টি nop (৪০% ফাঁকা), IPC ১.৮০:

    1: { lw x5, 0(x10)   | lw x6, 4(x10)    | lw x7, 8(x10) }
    2: { nop             | nop              | nop           }
    3: { add x8, x5, x6  | sub x9, x7, x5   | nop           }
    4: { and x28, x8, x9 | or x29, x8, x7   | nop           }
    5: { sw x28, 12(x10) | sw x29, 16(x10)  | nop           }

    ৫ critical path-এর সমান, তাই এটিই সর্বনিম্ন। Bundle ২ পুরো nop — load-এর latency, interlock না থাকায় compiler-কে স্পষ্ট লিখতে হয়।

    (গ) N = ২: ৬টি bundle, ১২টি slot, ৩টি nop, IPC ১.৫০. ৯ ÷ ২ = ৪.৫, তাই অন্তত ৫ — কিন্তু ৫ সম্ভব নয়: তিনটি load-এর জন্য অন্তত দুটি bundle লাগে, তাই একটি load bundle ২ বা পরে যায়। সেটি x7 হলে sub x9 bundle ৪-এর আগে নয়; x5 বা x6 হলে add x8 bundle ৪-এর আগে নয়। দুই ক্ষেত্রেই and x28 অন্তত bundle ৫-এ, আর sw x28 অন্তত bundle ৬-এ। একটি সঠিক সাজানো: { lw x5 | lw x6 }, { lw x7 | nop }, { add x8 | nop }, { sub x9 | or x29 }, { and x28 | sw x29 }, { sw x28 | nop }. (অংশ ৭-এর packer-এ N = ২, ৩, ৪ দিয়ে যাচাই করা; N = ৪-এও ৫টি bundle, কিন্তু ১১টি nop.)

  4. কোড লিখুন আর ফল অনুমান করুন: অংশ ৫-এর *p += s লুপটি (lw, add, sw, addi, bne) ২ বার unroll করুন, register-এর নাম বদলে (x5 আর x6), একটি addi দিয়ে। তারপর dual-issue-এ হাতে schedule করুন: প্রতি iteration-এ কয়টি instruction, কয়টি cycle, IPC আর প্রতি উপাদানে কত cycle? অংশ ৫-এর scheduler-এ চালালে কী ছাপবে, অনুমান করুন।

    Unroll করা body (৮টি instruction):

    Loop: lw   x5, 0(x10)
          add  x5, x5, x11
          sw   x5, 0(x10)
          lw   x6, 4(x10)
          add  x6, x6, x11
          sw   x6, 4(x10)
          addi x10, x10, 8
          bne  x10, x12, Loop

    Schedule — ৫ cycle, IPC = ৮/৫ = ১.৬০, প্রতি উপাদানে ২.৫ cycle:

    cycle  slot ১ (ALU/branch)   slot ২ (load/store)
      1    addi x10, x10, 8      lw x5, 0(x10)
      2    -                     lw x6, -4(x10)
      3    add x5, x5, x11       -
      4    add x6, x6, x11       sw x5, -8(x10)
      5    bne x10, x12, Loop    sw x6, -4(x10)

    যুক্তি: চারটি load/store-এর জন্য slot ২ একটি, তাই অন্তত ৪ cycle; কিন্তু শেষ sw x6-কে add x6-এর (cycle ৪) পরে যেতে হয় — cycle ৫, আর branch তার সাথে। addi cycle ১-এ উঠে গেছে, তাই cycle ২ থেকে সব offset ৮ কমেছে: 4(x10) → -4(x10), 0(x10) → -8(x10). Scheduler-এ এই body দিলে ঠিক এই টেবিল আর 8 instructions, 5 cycles, IPC = 1.60 ছাপে। তুলনা: unroll ছাড়া প্রতি উপাদানে ৪ cycle, ২ বার unroll-এ ২.৫, ৪ বার unroll-এ ২ — লাভ কমতে কমতে slot ২-এর সীমায় (প্রতি উপাদানে দুটি মেমরি কাজ = ২ cycle) গিয়ে থামে।

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

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

ক্রমএকটি লুপ থেকে static ILP বের করতে compiler-এর কাজের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Body-র dependence graph বানানো আর critical path দেখা
  2. লুপ কয়েকবার unroll করা আর অতিরিক্ত branch সরানো
  3. প্রতিটি কপিকে আলাদা register দিয়ে name dependence মোছা
  4. Slot আর latency-র নিয়ম মেনে packet বা bundle-এ সাজানো, offset ঠিক করা
  5. মোট cycle আর IPC গুনে আগের সংস্করণের সাথে মেলানো

একটি প্রসেসর প্রতি cycle-এ সর্বোচ্চ ৪টি instruction issue করতে পারে। তার সর্বনিম্ন সম্ভাব্য CPI কত?

  • 4
  • 0.25
  • 1
  • 0.5

কোন ধরনের dependence register renaming দিয়ে দূর করা যায় না?

  • WAR
  • WAW
  • RAW (true dependence)
  • কোনোটিই নয় — renaming সব dependence মোছে

একটি basic block-এ ১২টি instruction. অসীম hardware ধরে তার dependence graph-এর critical path ৪ cycle. এই block-এর ideal IPC সর্বোচ্চ কত?

  • 3
  • 4
  • 12
  • 0.33

একটি ২-way static superscalar-এ প্রতি cycle-এ একটি ALU/branch আর একটি load/store slot. একটি unroll করা লুপের body-তে ১৪টি instruction, যার ৮টি load বা store. Body চলতে অন্তত কত cycle লাগবে, আর তখন IPC কত?

  • 7 cycle, IPC 2.00
  • 14 cycle, IPC 1.00
  • 6 cycle, IPC 2.33
  • 8 cycle, IPC 1.75

ক্লাসিক VLIW প্রসেসরে কোন operation-গুলো একই cycle-এ একসাথে চলবে, এই সিদ্ধান্ত মূলত কে নেয়?

  • চলার সময় hardware-এর issue logic
  • Compiler, compile-এর সময়
  • Operating system-এর scheduler
  • Branch predictor

২০০০-এর দশকের মাঝামাঝি থেকে প্রসেসর নির্মাতারা একটি core-কে আরও চওড়া করার বদলে একাধিক core-এর দিকে যান। প্রধান কারণ কোনটি?

  • প্রোগ্রামে আর কোনো ILP অবশিষ্ট ছিল না
  • Compiler superscalar কোড বানাতে পারত না
  • Width বাড়ানোর power আর জটিলতা IPC-র লাভের চেয়ে অনেক দ্রুত বাড়ছিল
  • Chip-এ আর transistor বাড়ানো যাচ্ছিল না

শব্দকোষ

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

মনে রাখুন

  1. ILP = একসাথে চালানো যায় এমন স্বাধীন instruction; লক্ষ্য IPC > ১ (CPI < ১), আর IPC = ১ / CPI.
  2. তিন সীমা: true (RAW) dependence থাকে; name (WAR, WAW) renaming-এ মোছে; control dependence branch prediction আর speculation দিয়ে পার হতে হয়।
  3. Dataflow limit: dependence graph-এর critical path L হলে ideal IPC = n / L; কোনো hardware এর চেয়ে দ্রুত নয়।
  4. Dual-issue RISC-V (ALU/branch + load/store): বাড়তি port ও forwarding লাগে, load-use-এ দুটি slot নষ্ট; উদাহরণ লুপ ৫ → ৪ cycle (ক্রম বদলে), আর ৪ বার unroll + renaming-এ ১৪টি instruction ৮ cycle-এ, IPC ১.৭৫.
  5. VLIW-এ compiler bundle বানায়, ফাঁকা slot-এ nop — সরল hardware, কিন্তু binary compatibility আর অজানা latency-র সমস্যা; বাস্তব IPC width-এর অনেক নিচে, আর power-জটিলতার দেয়াল শিল্পকে multicore-এ নিয়ে যায়।

পরের পর্বে: In-order core-এ একটি আটকে থাকা instruction পেছনের সবাইকে থামিয়ে দেয়। Hardware যদি নিজেই instruction-এর ক্রম বদলাতে পারত, চলার সময় register-এর নাম বদলে? পরের পর্বে out-of-order execution — Tomasulo, register renaming আর reorder buffer.

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

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