পাঠ ৩৪ · ৫৭-এর মধ্যে · মডিউল ৭
Home / Courses / Computer Architecture & Digital Logic / ডেটা হ্যাজার্ড ও ফরওয়ার্ডিং

ডেটা হ্যাজার্ড ও ফরওয়ার্ডিং

Data hazards & forwarding
৯ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • ডেটা হ্যাজার্ড ঠিক কীভাবে ঘটে — ADD r1,r2,r3 ও ADD r4,r1,r5-এর কনক্রিট উদাহরণ দিয়ে
  • ফরওয়ার্ডিং ছাড়া স্টেল-ভ্যালু বাগ কোড দিয়ে প্রদর্শন করা এবং ফরওয়ার্ডিং দিয়ে তা সমাধান করা
  • লোড-ইউজ হ্যাজার্ড কেন ফরওয়ার্ডিং দিয়েও পুরোপুরি এড়ানো যায় না, এবং কেন ঠিক ১ সাইকেল স্টল যথেষ্ট
  • উভয় পরিস্থিতিতে (নিয়মিত ফরওয়ার্ডিং ও লোড-ইউজ) চূড়ান্ত গণনা করা মান সঠিক কিনা তা কোড দিয়ে নিশ্চিত করা

১ · ডেটা হ্যাজার্ড কী

L33-এর স্ট্রাকচারাল হ্যাজার্ড ছিল খাঁটি হার্ডওয়্যার-রিসোর্স সমস্যা। ডেটা হ্যাজার্ড সম্পূর্ণ ভিন্ন — এটি ঘটে যখন একটি ইনস্ট্রাকশনের দরকারি মান একটি আগের, পাইপলাইনে এখনও চলমান ("ইন-ফ্লাইট") ইনস্ট্রাকশন তখনও সম্পূর্ণভাবে গণনা/লেখা শেষ করেনি। কনক্রিট উদাহরণ —

I1:  ADD r1, r2, r3    # r1 = r2 + r3 (নতুন মান)
I2:  ADD r4, r1, r5    # r4 = r1 + r5 -- এই r1 কি নতুন, নাকি পুরনো মান?

নিয়মমাফিক পাইপলাইনে I1-এর Write-backM6/L26-এ শেখা পঞ্চম স্টেজ, যেখানে ফলাফল আসলে রেজিস্টার ফাইলে লেখা হয়। (রেজিস্টার ফাইলে r1 আপডেট করা, L26-এর প্রসঙ্গ) I1-এর Decodeযে স্টেজে ইনস্ট্রাকশন তার সোর্স রেজিস্টার পড়ে। স্টেজের বেশ কয়েক সাইকেল পরে ঘটে — I2-এর Decode স্টেজ (যেখানে সে স্বাভাবিকভাবে r1 পড়ত) I1-এর Write-back-এর আগেই চলে আসে। ফরওয়ার্ডিং ছাড়া, I2 তখন পুরনো, ভুল r1 পড়ে ফেলবে — একটি বাস্তব করেক্টনেস বাগ।

২ · সমাধান — ফরওয়ার্ডিং (বাইপাসিং)

ফরওয়ার্ডিংForwardingরেজিস্টার ফাইলে লেখা-পড়ার সম্পূর্ণ চক্র সম্পন্ন হওয়ার অপেক্ষা না করে, ফলাফল সরাসরি এক স্টেজ থেকে আরেক স্টেজে পাঠানোর অতিরিক্ত ডেটাপাথ। যোগ করে একটি অতিরিক্ত ডেটাপাথ — I1-এর ALU ঠিক Execute স্টেজেই ফলাফল তৈরি করে ফেলে (Write-back-এর আগেই), তাই সেই মান সরাসরি (EX/MEM ল্যাচ থেকে) I2-এর Execute স্টেজে "ফরওয়ার্ড" করে দেওয়া যায় — I2-কে রেজিস্টার ফাইল থেকে পড়ার জন্য অপেক্ষা করতে হয় না। যেহেতু I2-এর EX স্টেজ ঠিক I1-এর EX স্টেজের এক সাইকেল পরে ঘটে, এই ফরওয়ার্ডিং ঠিক সময়মতো পৌঁছায় — কোনো স্টল ছাড়াই।

সাইকেল → ১ ২ ৩ ৪ ৫ ৬ I1 I2 IF ID EX ✓ MEM WB IF ID ✗স্টেল EX ✓ফরওয়ার্ড MEM WB EX/MEM → EX ফরওয়ার্ড পাথ (সাইকেল ৩ → ৪)
ফরওয়ার্ডিং ছাড়া I2-এর ID (সাইকেল ৩) স্টেল মান পড়ে (লাল) — ফরওয়ার্ডিং সহ I1-এর EX ফলাফল সরাসরি I2-এর EX (সাইকেল ৪)-এ পৌঁছায় (নীল)।

৩ · লোড-ইউজ হ্যাজার্ড — ফরওয়ার্ডিং যা সমাধান করতে পারে না

ফরওয়ার্ডিং সর্বশক্তিমান নয়। যদি দরকারি মানটি একটি LOAD ইনস্ট্রাকশন থেকে আসে —

I1:  LOAD r1, 0(r2)    # মেমরি থেকে r1-এ মান লোড হয় -- MEM স্টেজ শেষে
I2:  ADD  r4, r1, r5   # r4 = r1 + r5

LOAD-এর মান MEM স্টেজ সম্পূর্ণ শেষ হওয়ার আগে তৈরিই হয় না। কিন্তু নিয়মিত ফরওয়ার্ডিং পাথ (EX/MEM→EX) কাজ করার জন্য I2-এর EX স্টেজ I1-এর EX স্টেজের ঠিক পরের সাইকেলে হতে হবে — LOAD-এর ক্ষেত্রে সেটি I1-এর MEM স্টেজেরই একই সাইকেল, যেখানে লোড হওয়া মান তখনও প্রস্তুত হয়নি। তাই এমনকি ফরওয়ার্ডিং হার্ডওয়্যার থাকা সত্ত্বেও, I2-কে ঠিক ১ সাইকেল স্টল করতে হয়, যাতে LOAD-এর MEM স্টেজ শেষ হওয়ার পর MEM/WB ল্যাচ থেকে মানটি ফরওয়ার্ড করা যায়।

নিয়মিত ফরওয়ার্ডিং (EX→EX)
ADD-then-ADD-এর মতো ক্ষেত্রে, ফলাফল ঠিক পরের সাইকেলেই দরকার হয় — কোনো স্টল ছাড়াই ফরওয়ার্ড করা যায়।
লোড-ইউজ (MEM→EX)
মান MEM স্টেজ শেষেই তৈরি হয় — পরের ইনস্ট্রাকশনের EX ঠিক তার আগেই পড়ে যায়, তাই ফরওয়ার্ডিং সহও ১ সাইকেল স্টল অনিবার্য।

৪ · কোড দিয়ে যাচাই

নিচের কোড সেলে দুটি দৃশ্য সিমুলেট করা হয়েছে — প্রথমে ADD r1,r2,r3 তারপর ADD r4,r1,r5 (ফরওয়ার্ডিং সহ ও ছাড়া তুলনা), তারপর LOAD r1,0(r2) তারপর ADD r4,r1,r5 (স্টল সহ ও ছাড়া তুলনা) — প্রতিটি ক্ষেত্রেই চূড়ান্ত গণনা করা মান সঠিক কিনা তা কোড নিজেই নিশ্চিত করে।

Python
# ডেটা হ্যাজার্ড ও ফরওয়ার্ডিং -- পাইপলাইন স্টেজ-টাইমিং সিমুলেশন
IF, ID, EX, MEM, WB = "IF", "ID", "EX", "MEM", "WB"
STAGES = [IF, ID, EX, MEM, WB]

def stage_at_cycle(start_cycle, stage_name):
    """start_cycle-এ IF শুরু হওয়া ইনস্ট্রাকশন stage_name স্টেজে কোন সাইকেলে পৌঁছাবে"""
    return start_cycle + STAGES.index(stage_name)

def simulate_two_add(forwarding_enabled):
    """I1: ADD r1,r2,r3  তারপর  I2: ADD r4,r1,r5 -- ডেটা হ্যাজার্ড"""
    registers = {"r1": 10, "r2": 20, "r3": 30, "r5": 5}  # শুরুর (পুরনো) রেজিস্টার স্টেট
    i1_start, i2_start = 1, 2
    i1_result = registers["r2"] + registers["r3"]  # I1 আসলে যা লিখবে (নতুন r1)

    i1_ex_cycle = stage_at_cycle(i1_start, EX)
    i1_wb_cycle = stage_at_cycle(i1_start, WB)
    i2_id_cycle = stage_at_cycle(i2_start, ID)
    i2_ex_cycle = stage_at_cycle(i2_start, EX)

    if forwarding_enabled:
        # EX/MEM -> EX ফরওয়ার্ড পাথ: I1-এর EX ফলাফল ঠিক I2-এর EX-এ পৌঁছায় কিনা যাচাই
        r1_used = i1_result if i1_ex_cycle == i2_ex_cycle - 1 else registers["r1"]
        source = "ফরওয়ার্ড করা (EX/MEM -> EX)"
    else:
        # রেজিস্টার ফাইল থেকেই সরাসরি পড়া -- WB সম্পন্ন না হওয়া পর্যন্ত পুরনো মান
        r1_used = i1_result if i1_wb_cycle <= i2_id_cycle else registers["r1"]
        source = "রেজিস্টার ফাইল থেকে সরাসরি"

    r4_result = r1_used + registers["r5"]
    correct = i1_result + registers["r5"]
    return {"r1_used": r1_used, "source": source, "r4_result": r4_result,
            "correct": correct, "is_correct": r4_result == correct}

print("=== ADD-then-ADD, ফরওয়ার্ডিং ছাড়া ===")
no_fwd = simulate_two_add(forwarding_enabled=False)
for k, v in no_fwd.items(): print(f"  {k}: {v}")

print("\n=== ADD-then-ADD, ফরওয়ার্ডিং সহ ===")
with_fwd = simulate_two_add(forwarding_enabled=True)
for k, v in with_fwd.items(): print(f"  {k}: {v}")

assert not no_fwd["is_correct"], "ফরওয়ার্ডিং ছাড়া ভুল (স্টেল) মান পাওয়ার কথা"
assert with_fwd["is_correct"], "ফরওয়ার্ডিং সহ সঠিক মান পাওয়ার কথা"
print(f"\nযাচাই: ফরওয়ার্ডিং ছাড়া r4={no_fwd['r4_result']} (ভুল, প্রত্যাশিত {no_fwd['correct']}), "
      f"ফরওয়ার্ডিং সহ r4={with_fwd['r4_result']} (সঠিক)")

def simulate_load_use(with_stall):
    """I1: LOAD r1,0(r2)  তারপর  I2: ADD r4,r1,r5 -- লোড-ইউজ হ্যাজার্ড"""
    memory = {20: 99}
    registers = {"r1": 0, "r2": 20, "r5": 5}
    i1_start = 1
    i1_mem_cycle = stage_at_cycle(i1_start, MEM)  # লোড হওয়া মান এই সাইকেল শেষে প্রস্তুত হয়
    loaded_value = memory[registers["r2"]]

    i2_start = i1_start + 2 if with_stall else i1_start + 1  # ১ সাইকেল বাবল যোগ হলো কিনা
    i2_ex_cycle = stage_at_cycle(i2_start, EX)

    # MEM/WB -> EX ফরওয়ার্ড: I2-এর EX, I1-এর MEM-এর পরের সাইকেলে হলে তবেই মান প্রস্তুত থাকে
    r1_used = loaded_value if i2_ex_cycle > i1_mem_cycle else registers["r1"]
    r4_result = r1_used + registers["r5"]
    correct = loaded_value + registers["r5"]
    return {"i1_mem_cycle": i1_mem_cycle, "i2_ex_cycle": i2_ex_cycle,
            "r1_used": r1_used, "r4_result": r4_result,
            "correct": correct, "is_correct": r4_result == correct}

print("\n=== LOAD-then-ADD, স্টল ছাড়া (ফরওয়ার্ডিং থাকা সত্ত্বেও) ===")
no_stall = simulate_load_use(with_stall=False)
for k, v in no_stall.items(): print(f"  {k}: {v}")

print("\n=== LOAD-then-ADD, ১ সাইকেল স্টল সহ ===")
stalled = simulate_load_use(with_stall=True)
for k, v in stalled.items(): print(f"  {k}: {v}")

assert not no_stall["is_correct"], "স্টল ছাড়া লোড-ইউজ ভুল মান দেওয়ার কথা"
assert stalled["is_correct"], "১ সাইকেল স্টল সহ সঠিক মান পাওয়ার কথা"
print(f"\nযাচাই: স্টল ছাড়া r4={no_stall['r4_result']} (ভুল), ১ সাইকেল স্টল সহ r4={stalled['r4_result']} (সঠিক)")

    
লক্ষ্য করুন — ADD-then-ADD-এর ক্ষেত্রে শুধু ফরওয়ার্ডিং যোগ করলেই যথেষ্ট, কোনো স্টল লাগে না। কিন্তু LOAD-then-ADD-এর ক্ষেত্রে ফরওয়ার্ডিং একাই যথেষ্ট নয় — i2_ex_cycle > i1_mem_cycle শর্তটি স্টল ছাড়া পূরণ হয় না, তাই কম্পাইলার/হার্ডওয়্যারকে অবশ্যই একটি বাবল ঢোকাতে হবে।
মূল কথা · Key takeaway

ফরওয়ার্ডিং বেশিরভাগ ডেটা হ্যাজার্ড কোনো পারফরম্যান্স খরচ ছাড়াই সমাধান করে দেয় — কিন্তু এটি সময়ের সমস্যা, শুধু ডেটাপাথের সমস্যা নয়। যখন মান আক্ষরিক অর্থেই সময়মতো প্রস্তুত থাকে না (লোড-ইউজ-এর মতো), তখন হার্ডওয়্যারকে (বা কম্পাইলারকে ইনস্ট্রাকশন পুনর্বিন্যাস করে) অবশ্যই অপেক্ষা করতে হবে — এবং প্রতিটি এমন স্টল সরাসরি L32-এর আদর্শ স্পিডআপ থেকে খরচ হয়।

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

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

প্র ০১ ADD-then-ADD-এর ক্ষেত্রে ফরওয়ার্ডিং কেন কোনো স্টল ছাড়াই কাজ করে, কিন্তু LOAD-then-ADD-এর ক্ষেত্রে ঠিক ১ সাইকেল স্টল লাগে — মূল পার্থক্যটা কী?

পার্থক্য হলো ফলাফল কোন স্টেজে "তৈরি" হয়। ADD-এর ফলাফল তার নিজের EX স্টেজেই তৈরি হয়, এবং I2-এর EX ঠিক তার পরের সাইকেলে ঘটে — ফরওয়ার্ডিং পাথ যথেষ্ট সময় পায়। কিন্তু LOAD-এর ফলাফল তার MEM স্টেজে তৈরি হয় (এক স্টেজ পরে), অথচ I2-এর EX (যেখানে মানটি দরকার) সেই একই সাইকেলে পড়ে যায় — ডেটা "ভবিষ্যতে" পৌঁছানোর সময় পায় না, তাই একটি সাইকেল অপেক্ষা করাতেই হয়।

প্র ০২ যদি ADD r1,r2,r3 ও ADD r4,r1,r5-এর মাঝে একটি অসম্পর্কিত ইনস্ট্রাকশন (যেমন ADD r6,r7,r8) থাকত, তাহলে কি এখনও ফরওয়ার্ডিং দরকার হতো?

না — যদি একটি অসম্পর্কিত ইনস্ট্রাকশন মাঝে থাকে, তাহলে I2 (এখন তিন নম্বর ইনস্ট্রাকশন) এর ID স্টেজ আরও এক সাইকেল পরে ঘটবে, ঠিক I1-এর WB স্টেজের সাথে একই সাইকেলে বা তার পরে। রেজিস্টার ফাইল থেকে সরাসরি পড়লেও তখন সঠিক মান পাওয়া যাবে (ধরে নিলে WB আগে রাইট, তারপর ID রিড হয় একই সাইকেলে) — এই কারণেই হ্যাজার্ড শুধু কাছাকাছি ইনস্ট্রাকশনগুলোর মধ্যেই ঘটে, দূরত্ব বাড়লে সমস্যা নিজে থেকেই মিলিয়ে যায়।

প্র ০৩ লোড-ইউজ হ্যাজার্ডের ১ সাইকেল স্টল কি কম্পাইলার এড়াতে পারে, নাকি এটি সবসময় হার্ডওয়্যার-স্তরে ঘটবেই?

কম্পাইলার প্রায়ই এড়াতে পারে — "ইনস্ট্রাকশন শিডিউলিং" নামে একটি কৌশলে, কম্পাইলার LOAD-এর ঠিক পরে একটি অসম্পর্কিত (independent) ইনস্ট্রাকশন বসিয়ে দেয় (যদি প্রোগ্রামে এমন কোনো ইনস্ট্রাকশন থাকে যা LOAD-এর ফলাফলের উপর নির্ভরশীল নয়), যাতে হার্ডওয়্যারের বাস্তব স্টল প্রয়োজনই না পড়ে। কিন্তু যদি এমন কোনো স্বাধীন ইনস্ট্রাকশন খুঁজে না পাওয়া যায়, হার্ডওয়্যারকে অবশ্যই বাস্তব সাইকেল স্টল করতে হবে — এড়ানো সবসময় সম্ভব নয়।

অনুশীলন

  1. চিন্তা করুন: simulate_two_add ফাংশনে যদি I2 তিন নম্বর সাইকেলে না, বরং সরাসরি I1-এর ঠিক ২ সাইকেল পরে (i2_start = i1_start + 2) IF শুরু করত, তাহলে কি ফরওয়ার্ডিং তখনও দরকার হতো?

    না। i2_start=3 হলে i2_id_cycle = stage_at_cycle(3, ID) = 4, আর i1_wb_cycle = stage_at_cycle(1, WB) = 5। যেহেতু 5 > 4, রেজিস্টার-ফাইল-শর্ত i1_wb_cycle <= i2_id_cycle এখনও False (হ্যাজার্ড থেকেই যায়) — আসলে এই নির্দিষ্ট ব্যবধানে (২ সাইকেল দূরত্ব) এখনও ফরওয়ার্ডিং দরকার। দূরত্ব ৩ বা তার বেশি হলে তবেই WB আগেই ঘটে যেত।

  2. পরীক্ষা করুন: উপরের কোড সেলে memory = {20: 99}-এর মান {20: 250} করে Run চাপুন — স্টল সহ ও ছাড়া উভয় ক্ষেত্রে চূড়ান্ত r4_result কীভাবে বদলায়?

    loaded_value এখন 250 হবে, তাই সঠিক ফলাফল ($r5=5$ যোগ করে) হবে 255। স্টল সহ ক্ষেত্রে r4_result সঠিকভাবে 255 হবে (কারণ শর্ত পূরণ হয়ে r1_used = loaded_value = 250)। স্টল ছাড়া ক্ষেত্রে এখনও পুরনো registers["r1"] = 0 ব্যবহৃত হবে, তাই r4_result = 5 — সম্পূর্ণ ভুল, প্রায় ২৫০ কম। এই পার্থক্যই দেখায় স্টল না করলে বাগ কতটা গুরুতর হতে পারে, লোড হওয়া মান যত বড়ই হোক।

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

পূর্ববর্তী পাঠ
স্ট্রাকচারাল হ্যাজার্ড