পাঠ ৫২ · ৫৮-এর মধ্যে · মডিউল ১২
Home / Courses / Concepts of Programming Languages & Compiler Design / কোড জেনারেশন

কোড জেনারেশন বেসিকস

Code generation basics
৮ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

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

  • IR থেকে টার্গেট মেশিন কোডে যাওয়ার পথ — ইনস্ট্রাকশন সিলেকশনের ধারণা
  • রেজিস্টার অ্যালোকেশন কেন একটি গুরুত্বপূর্ণ, রিসোর্স-সীমাবদ্ধ সমস্যা
  • একটি সহজ, অর্ডার-অফ-ফার্স্ট-ডেফিনিশন রেজিস্টার অ্যালোকেশন কৌশল ও তার পেছনের "লাইভনেস" ধারণা
  • ছোট রেজিস্টার বাজেটে বাস্তব রেজিস্টার অ্যালোকেশন চালিয়ে, কোনো কনফ্লিক্ট নেই তা কোড দিয়েই যাচাই করা

১ · IR থেকে টার্গেট মেশিন কোড — ইনস্ট্রাকশন সিলেকশন

M12/L51-এ আমরা দেখেছি সোর্স এক্সপ্রেশন (a + b) * (c - d) কীভাবে থ্রি-অ্যাড্রেস কোডে (3AC) পরিণত হয় — t1 = a + b, t2 = c - d, t3 = t1 * t2। কোড জেনারেশনের কাজ হলো এই IR-কে আসল টার্গেট মেশিনের জন্য বাস্তব ইনস্ট্রাকশনে রূপান্তর করা — সরাসরি Computer Architecture কোর্সের M5-এর ইনস্ট্রাকশন-ফরম্যাট ও অ্যাড্রেসিং-মোড উপাদান এখানেই কাজে লাগে।

ইনস্ট্রাকশন সিলেকশনInstruction Selectionএকটি IR অপারেশন সঠিকভাবে বাস্তবায়ন করার জন্য কোন টার্গেট-মেশিন ইনস্ট্রাকশন(গুলো) বেছে নিতে হবে তা ঠিক করার ধাপ। — কখনো সরাসরি এক-এক (3AC-এর ADD সরাসরি একটি মেশিন ADD ইনস্ট্রাকশনে ম্যাপ হয়), কখনো একটি IR অপারেশনের জন্য একাধিক মেশিন ইনস্ট্রাকশন প্রয়োজন হয়, বা একাধিক সম্ভাব্য বিকল্প থাকে ভিন্ন ভিন্ন খরচসহ (সম্পূর্ণ ইনস্ট্রাকশন-সিলেকশন অ্যালগরিদম এই কোর্সের পরিসরের বাইরে, শুধু ধারণাটি জানা যথেষ্ট)।

থ্রি-অ্যাড্রেস কোড (M12/L51) ইনস্ট্রাকশন সিলেকশন রেজিস্টার অ্যালোকেশন টার্গেট মেশিন কোড / অ্যাসেম্বলি CPU-তে এক্সিকিউশন
3AC-এর প্রতিটি অপারেশন প্রথমে একটি মেশিন ইনস্ট্রাকশনে ম্যাপ হয়, তারপর প্রতিটি temporary-কে একটি রেজিস্টার (বা মেমরি স্লট) বরাদ্দ দেওয়া হয় — এই পাঠ মূলত দ্বিতীয় ধাপে ফোকাস করে।

২ · রেজিস্টার অ্যালোকেশন সমস্যা

একটি বাস্তব মেশিনে রেজিস্টারের সংখ্যা সীমিত ও নির্দিষ্ট (Computer Architecture কোর্সের M5/L26-এর রেজিস্টার ফাইল উপাদান — যেমন ৩২টি জেনারেল-পারপাস রেজিস্টার), কিন্তু একটি 3AC প্রোগ্রামে তার চেয়ে অনেক বেশি temporary variable থাকতে পারে। কোড জেনারেটরকে সিদ্ধান্ত নিতে হয় — কোন temporary একটি আসল রেজিস্টার পাবে, আর কোনটিকে মেমরিতে "স্পিল" করতে হবে (প্রয়োজনমতো লোড/স্টোর করা, যা অতিরিক্ত মেমরি-অ্যাক্সেস খরচ যোগ করে) — একটি জেনুইন, পারফরম্যান্স-প্রভাবিত সিদ্ধান্ত।

৩ · একটি সহজ রেজিস্টার অ্যালোকেশন কৌশল

এই পাঠ একটি বেসিক, শেখার-উপযোগী কৌশল দেখাবে (state-of-the-art নয়) — প্রতিটি temporary যে ক্রমে প্রথম সংজ্ঞায়িত হয় সেই ক্রমে একটি ফ্রি রেজিস্টার বরাদ্দ দেওয়া, এবং যখনই একটি temporary-এর মান আর কখনো পরে দরকার হবে না (তার লাইভ রেঞ্জLive Rangeএকটি variable/temporary-এর মান যে সংজ্ঞা থেকে তার শেষ ব্যবহার পর্যন্ত "জীবিত" (দরকারি) থাকে, সেই ব্যাপ্তি — এই ব্যাপ্তির বাইরে তার রেজিস্টার নিরাপদে রিইউজ করা যায়। শেষ হয়ে যায়), তার রেজিস্টার তখনই মুক্ত করে পরের temporary-কে দেওয়া — একটি সরলীকৃত লিনিয়ার-স্ক্যান-ধাঁচের ধারণা।

M12/L51-এর সাথে সরাসরি সংযোগ

নিচের কোড সেল M12/L51-এ জেনারেট হওয়া (a+b)*(c-d)-এর 3AC-ই ব্যবহার করে — t1 = a + b, t2 = c - d, t3 = t1 * t2। লক্ষ্য করুন t1 ও t2 উভয়েই instr3-তে ব্যবহৃত হয় (একসাথে জীবিত থাকে), কিন্তু t3 জন্ম নেয় ঠিক তখনই যখন t1, t2 উভয়ে মারা যায় — এটাই সেই মুহূর্ত যেখানে রিইউজ ঘটতে বাধ্য হয় মাত্র ২টি রেজিস্টারের বাজেটে।

৪ · বাস্তবায়ন — ছোট রেজিস্টার বাজেটে রিইউজ যাচাই

নিচের কোড সেলে allocate_registers ফাংশন প্রতিটি temp-এর "জন্ম মুহূর্ত" ও "শেষ ব্যবহার মুহূর্ত" হিসাব করে, তারপর মাত্র ২টি রেজিস্টার দিয়ে বরাদ্দ চালায়। শেষে কোড নিজেই পরীক্ষা করে — কোনো দুটো একসাথে-জীবিত temp কি ভুলবশত একই রেজিস্টার পেয়ে গেছে?

Python
# L51-এর (a+b)*(c-d) থেকে জেনারেট হওয়া থ্রি-অ্যাড্রেস কোড (3AC)
code = [
    {"result": "t1", "op": "+", "arg1": "a",  "arg2": "b"},
    {"result": "t2", "op": "-", "arg1": "c",  "arg2": "d"},
    {"result": "t3", "op": "*", "arg1": "t1", "arg2": "t2"},
]

def compute_live_points(code, temp_names):
    """প্রতিটি temp-এর জীবনকাল বের করে -- def_point = instruction i-তে অপারেন্ড পড়ার
    *পরে* (i + 0.5); use_point = instruction i-তে অপারেন্ড পড়ার মুহূর্তে (i)। এই আধা-ধাপ
    টাইমিং একই instruction-এর ভেতরে "আগে পুরনো অপারেন্ড ব্যবহার শেষ, পরে নতুন ফলাফলের জন্ম"
    এই সঠিক ক্রম ধরে রাখে -- নইলে একই instruction-index হওয়ায় ভুলভাবে কনফ্লিক্ট মনে হবে।"""
    def_point = {}
    last_use_point = {}
    for i, instr in enumerate(code):
        for arg in (instr["arg1"], instr["arg2"]):
            if arg in temp_names:
                last_use_point[arg] = i
        if instr["result"] in temp_names:
            def_point[instr["result"]] = i + 0.5
    for t in temp_names:
        if t not in last_use_point:
            last_use_point[t] = def_point[t]
    return def_point, last_use_point

def allocate_registers(code, num_available_registers):
    """অর্ডার-অফ-ফার্স্ট-ডেফিনিশন রেজিস্টার অ্যালোকেশন -- প্রতিটি temp সংজ্ঞায়িত হওয়ার সময়
    একটি ফ্রি রেজিস্টার পায়; একটি temp-এর শেষ ব্যবহার শেষ হলে তার রেজিস্টার মুক্ত হয়ে পরের
    temp রিইউজ করতে পারে।"""
    temp_names = {instr["result"] for instr in code if instr["result"].startswith("t")}
    def_point, last_use_point = compute_live_points(code, temp_names)

    free_regs = list(range(1, num_available_registers + 1))
    active = {}
    allocation = {}
    trace = []

    for i, instr in enumerate(code):
        for arg in (instr["arg1"], instr["arg2"]):
            if arg in temp_names and arg in active and last_use_point[arg] == i:
                freed = active.pop(arg)
                free_regs.append(freed)
                free_regs.sort()
                trace.append(f"  instr{i+1}: {arg}-এর শেষ ব্যবহার শেষ -> R{freed} মুক্ত হলো")

        result = instr["result"]
        if result in temp_names and result not in active:
            if not free_regs:
                raise RuntimeError(f"রেজিস্টার বাজেট শেষ! {result}-এর জন্য spill প্রয়োজন")
            reg = free_regs.pop(0)
            active[result] = reg
            allocation[result] = reg
            trace.append(f"  instr{i+1}: {result} সংজ্ঞায়িত -> R{reg} বরাদ্দ")

    return allocation, def_point, last_use_point, trace

print("3AC:")
for instr in code:
    print(f"  {instr['result']} = {instr['arg1']} {instr['op']} {instr['arg2']}")

NUM_REGISTERS = 2   # ইচ্ছাকৃতভাবে ছোট বাজেট -- মাত্র ২টি রেজিস্টার, ৩টি temp-এর জন্য
allocation, def_point, last_use_point, trace = allocate_registers(code, NUM_REGISTERS)

print(f"\nরেজিস্টার বাজেট: {NUM_REGISTERS}")
print("অ্যালোকেশন ট্রেস:")
for line in trace:
    print(line)

print("\nচূড়ান্ত বরাদ্দ:")
for t, r in allocation.items():
    print(f"  {t} -> R{r}  (জন্ম {def_point[t]}, শেষ ব্যবহার {last_use_point[t]})")

# সঠিকতা যাচাই: কোনো দুটো একসাথে-জীবিত (simultaneously live) temp কি একই রেজিস্টার শেয়ার করছে?
temps = list(allocation.keys())
conflict_found = False
for i in range(len(temps)):
    for j in range(i + 1, len(temps)):
        t1, t2 = temps[i], temps[j]
        s1, e1 = def_point[t1], last_use_point[t1]
        s2, e2 = def_point[t2], last_use_point[t2]
        overlap = max(s1, s2) <= min(e1, e2)
        if overlap and allocation[t1] == allocation[t2]:
            conflict_found = True
            print(f"  ✗ কনফ্লিক্ট: {t1} ও {t2} একসাথে জীবিত থেকেও R{allocation[t1]} শেয়ার করছে!")

print("\nসঠিকতা যাচাই:", "FAIL" if conflict_found else "PASS -- কোনো দুটো একসাথে-জীবিত temp একই রেজিস্টার শেয়ার করেনি")
reused = len(set(allocation.values())) < len(allocation)
print("রিইউজ ঘটেছে কিনা:", "হ্যাঁ -- t1 মারা যাওয়ার পর t3 আবার সেই একই রেজিস্টার নিয়েছে" if reused else "না")

    
লক্ষ্য করুন — t1 ও t2 উভয়ে instr3-এ একসাথে দরকার হওয়ায় তারা ভিন্ন রেজিস্টার (R1, R2) পায় (কোনো উপায় নেই তাদের একই রেজিস্টারে রাখার, তারা একসাথে জীবিত)। কিন্তু t3 জন্ম নেয় ঠিক সেই মুহূর্তে যখন t1 ও t2 উভয়ের শেষ ব্যবহার ঘটে যায় — তাই t3 নিরাপদে R1 রিইউজ করতে পারে, তৃতীয় কোনো রেজিস্টারের প্রয়োজন ছাড়াই। মাত্র ২টি রেজিস্টার দিয়ে ৩টি temp সঠিকভাবে অ্যালোকেট হয়েছে — এটাই ছোট বাজেটে রিইউজের আসল প্রয়োজনীয়তা।
মূল কথা · Key takeaway

কোড জেনারেশন IR-কে বাস্তব মেশিন ইনস্ট্রাকশনে রূপান্তর করে, কিন্তু রেজিস্টারের সীমিত সংখ্যা একটি বাস্তব বাধা তৈরি করে। লাইভ রেঞ্জ ট্র্যাক করে, একটি temporary-এর প্রয়োজন ফুরোনোর সাথে সাথেই তার রেজিস্টার রিইউজ করা — এমনকি একটি খুবই ছোট রেজিস্টার বাজেটেও — এই সরল কৌশলের মূল শক্তি।

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

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

প্র ০১ উপরের কোড সেলে যদি রেজিস্টার বাজেট ৩ (বা তার বেশি) করা হতো, তাহলে কি এখনো রিইউজ ঘটত?

না — বাজেট ৩ হলে t1, t2, t3 প্রতিটি নিজস্ব রেজিস্টার (R1, R2, R3) পেয়ে যেত, কারণ যথেষ্ট রেজিস্টার থাকায় কখনো ফ্রি-রেজিস্টার-তালিকা খালি হতো না। রিইউজ ঘটে শুধুমাত্র তখনই যখন বরাদ্দযোগ্য রেজিস্টারের সংখ্যা একসাথে-জীবিত temp-এর সংখ্যার চেয়ে কম বা সমান — এই পাঠে ইচ্ছাকৃতভাবে বাজেট ২ রাখা হয়েছে ঠিক এই পরিস্থিতি তৈরি করার জন্য।

প্র ০২ যদি রেজিস্টার বাজেট মাত্র ১ করা হতো, তাহলে উপরের কোড সেলে কী হতো?

t1 R1 পেত। instr2-এ t2 সংজ্ঞায়িত হওয়ার সময় R1 ইতিমধ্যে t1-এর দখলে (এবং t1 এখনো জীবিত, যেহেতু instr3-এ দরকার হবে) — তাই free_regs খালি থাকবে এবং কোড RuntimeError("রেজিস্টার বাজেট শেষ!") তুলবে। এটাই বাস্তব কম্পাইলারদের "স্পিল" প্রয়োজনের মুহূর্ত — যখন কোনো রেজিস্টার-বাজেটই যথেষ্ট নয়, তখন একটি temporary-কে সাময়িকভাবে মেমরিতে রাখতে হয়।

প্র ০৩ রেজিস্টার অ্যালোকেশনে ব্যবহৃত "লাইভ রেঞ্জ" ধারণাটি M8/L39-এর কোন ধারণার সাথে সরাসরি সম্পর্কিত?

M8/L39-এর "লাইফটাইম" ধারণার সাথে — একটি ভ্যারিয়েবল/temporary যতক্ষণ পর্যন্ত তার মান দরকারি থাকে ততক্ষণ তার জন্য স্টোরেজ (এখানে, একটি রেজিস্টার) বরাদ্দ রাখতে হয়, লাইফটাইম শেষ হলে সেই স্টোরেজ নিরাপদে পুনর্ব্যবহারযোগ্য। রেজিস্টার অ্যালোকেশন আসলে লাইফটাইম/লাইভনেস ধারণাটিকেই কম্পাইলার-ব্যাক-এন্ডের একটি নির্দিষ্ট, রিসোর্স-সীমাবদ্ধ প্রয়োগে রূপান্তর করে।

অনুশীলন

  1. চিন্তা করুন: যদি 3AC-তে একটি চতুর্থ instruction t4 = t3 + a যোগ করা হয় (t3-কে আবার ব্যবহার করে), তাহলে t3-এর "শেষ ব্যবহার মুহূর্ত" কীভাবে বদলাবে, এবং এর ফলে রেজিস্টার বরাদ্দে কী প্রভাব পড়বে?

    t3-এর last_use_point এখন instr3 (2.5, তার নিজের জন্ম মুহূর্ত) থেকে বেড়ে instr4 (3) হয়ে যাবে — অর্থাৎ t3-এর লাইভ রেঞ্জ দীর্ঘ হবে, এবং t3-এর রেজিস্টার instr4 শেষ না হওয়া পর্যন্ত মুক্ত হবে না। এর ফলে t4-এর জন্য একটি সম্পূর্ণ নতুন ফ্রি রেজিস্টার প্রয়োজন হবে (t3-এর রেজিস্টার তখনো দখলে) — বাজেট ২ হলে এটি একটি নতুন স্পিল-প্রয়োজনীয় পরিস্থিতি তৈরি করতে পারে।

  2. পরীক্ষা করুন: উপরের কোড সেলে NUM_REGISTERS-এর মান ৩ করে Run চেপে দেখুন অ্যালোকেশন ট্রেস ও চূড়ান্ত বরাদ্দ কীভাবে বদলায়।

    বাজেট ৩ হলে t1 -> R1, t2 -> R2, t3 -> R3 — প্রতিটি temp নিজস্ব, কখনো-রিইউজ-না-হওয়া রেজিস্টার পাবে। ট্রেসে কোনো "মুক্ত হলো" বার্তা t3 বরাদ্দের ঠিক আগে দেখা যাবে ঠিকই (t1, t2 উভয়ের শেষ ব্যবহার এখনো instr3-তেই ঘটে), কিন্তু যেহেতু R3 তখনো ফ্রি-তালিকায় আছে, তাই t3 R1 নয়, R3 নেবে (তালিকা থেকে সবচেয়ে ছোট নম্বরের ফ্রি রেজিস্টার বেছে নেওয়ার নিয়ম অনুযায়ী) — reused ভ্যারিয়েবল তখন False হবে।

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

আগের পাঠ
ইন্টারমিডিয়েট রিপ্রেজেন্টেশন ও থ্রি-অ্যাড্রেস কোড