কোড জেনারেশন বেসিকস
এই পাঠে যা শিখবেন
- 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 অপারেশনের জন্য একাধিক মেশিন ইনস্ট্রাকশন প্রয়োজন হয়, বা
একাধিক সম্ভাব্য বিকল্প থাকে ভিন্ন ভিন্ন খরচসহ (সম্পূর্ণ ইনস্ট্রাকশন-সিলেকশন অ্যালগরিদম এই কোর্সের পরিসরের বাইরে,
শুধু ধারণাটি জানা যথেষ্ট)।
২ · রেজিস্টার অ্যালোকেশন সমস্যা
একটি বাস্তব মেশিনে রেজিস্টারের সংখ্যা সীমিত ও নির্দিষ্ট (Computer Architecture কোর্সের M5/L26-এর রেজিস্টার ফাইল উপাদান — যেমন ৩২টি জেনারেল-পারপাস রেজিস্টার), কিন্তু একটি 3AC প্রোগ্রামে তার চেয়ে অনেক বেশি temporary variable থাকতে পারে। কোড জেনারেটরকে সিদ্ধান্ত নিতে হয় — কোন temporary একটি আসল রেজিস্টার পাবে, আর কোনটিকে মেমরিতে "স্পিল" করতে হবে (প্রয়োজনমতো লোড/স্টোর করা, যা অতিরিক্ত মেমরি-অ্যাক্সেস খরচ যোগ করে) — একটি জেনুইন, পারফরম্যান্স-প্রভাবিত সিদ্ধান্ত।
৩ · একটি সহজ রেজিস্টার অ্যালোকেশন কৌশল
এই পাঠ একটি বেসিক, শেখার-উপযোগী কৌশল দেখাবে (state-of-the-art নয়) — প্রতিটি temporary যে ক্রমে প্রথম সংজ্ঞায়িত হয় সেই ক্রমে একটি ফ্রি রেজিস্টার বরাদ্দ দেওয়া, এবং যখনই একটি temporary-এর মান আর কখনো পরে দরকার হবে না (তার লাইভ রেঞ্জLive Rangeএকটি variable/temporary-এর মান যে সংজ্ঞা থেকে তার শেষ ব্যবহার পর্যন্ত "জীবিত" (দরকারি) থাকে, সেই ব্যাপ্তি — এই ব্যাপ্তির বাইরে তার রেজিস্টার নিরাপদে রিইউজ করা যায়। শেষ হয়ে যায়), তার রেজিস্টার তখনই মুক্ত করে পরের temporary-কে দেওয়া — একটি সরলীকৃত লিনিয়ার-স্ক্যান-ধাঁচের ধারণা।
নিচের কোড সেল 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 কি ভুলবশত একই রেজিস্টার পেয়ে গেছে?
# 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 সঠিকভাবে
অ্যালোকেট হয়েছে — এটাই ছোট বাজেটে রিইউজের আসল প্রয়োজনীয়তা।
কোড জেনারেশন 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 যতক্ষণ পর্যন্ত তার মান দরকারি থাকে ততক্ষণ তার জন্য স্টোরেজ (এখানে, একটি রেজিস্টার) বরাদ্দ রাখতে হয়, লাইফটাইম শেষ হলে সেই স্টোরেজ নিরাপদে পুনর্ব্যবহারযোগ্য। রেজিস্টার অ্যালোকেশন আসলে লাইফটাইম/লাইভনেস ধারণাটিকেই কম্পাইলার-ব্যাক-এন্ডের একটি নির্দিষ্ট, রিসোর্স-সীমাবদ্ধ প্রয়োগে রূপান্তর করে।
অনুশীলন
-
চিন্তা করুন: যদি 3AC-তে একটি চতুর্থ instruction
t4 = t3 + aযোগ করা হয় (t3-কে আবার ব্যবহার করে), তাহলে t3-এর "শেষ ব্যবহার মুহূর্ত" কীভাবে বদলাবে, এবং এর ফলে রেজিস্টার বরাদ্দে কী প্রভাব পড়বে?t3-এর
last_use_pointএখন instr3 (2.5, তার নিজের জন্ম মুহূর্ত) থেকে বেড়ে instr4 (3) হয়ে যাবে — অর্থাৎ t3-এর লাইভ রেঞ্জ দীর্ঘ হবে, এবং t3-এর রেজিস্টার instr4 শেষ না হওয়া পর্যন্ত মুক্ত হবে না। এর ফলে t4-এর জন্য একটি সম্পূর্ণ নতুন ফ্রি রেজিস্টার প্রয়োজন হবে (t3-এর রেজিস্টার তখনো দখলে) — বাজেট ২ হলে এটি একটি নতুন স্পিল-প্রয়োজনীয় পরিস্থিতি তৈরি করতে পারে। -
পরীক্ষা করুন: উপরের কোড সেলে
NUM_REGISTERS-এর মান ৩ করে Run চেপে দেখুন অ্যালোকেশন ট্রেস ও চূড়ান্ত বরাদ্দ কীভাবে বদলায়।বাজেট ৩ হলে
t1 -> R1,t2 -> R2,t3 -> R3— প্রতিটি temp নিজস্ব, কখনো-রিইউজ-না-হওয়া রেজিস্টার পাবে। ট্রেসে কোনো "মুক্ত হলো" বার্তাt3বরাদ্দের ঠিক আগে দেখা যাবে ঠিকই (t1, t2 উভয়ের শেষ ব্যবহার এখনো instr3-তেই ঘটে), কিন্তু যেহেতু R3 তখনো ফ্রি-তালিকায় আছে, তাইt3R1 নয়, R3 নেবে (তালিকা থেকে সবচেয়ে ছোট নম্বরের ফ্রি রেজিস্টার বেছে নেওয়ার নিয়ম অনুযায়ী) —reusedভ্যারিয়েবল তখনFalseহবে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ পরের পাঠে সঠিকতার শর্ত বজায় রেখে কোড কীভাবে আরও দ্রুত/ছোট করা যায় — কোড অপ্টিমাইজেশন টেকনিক।
-
ইন্টারমিডিয়েট রিপ্রেজেন্টেশন ও থ্রি-অ্যাড্রেস কোড (L51) M12 · আগের পাঠ
এই পাঠের 3AC উদাহরণ যেখান থেকে তৈরি হয়েছে —
(a+b)*(c-d)-এর সম্পূর্ণ জেনারেশন প্রক্রিয়া। - কোড অপ্টিমাইজেশন টেকনিক (L53) পরের পাঠ কনস্ট্যান্ট ফোল্ডিং, ডেড কোড এলিমিনেশন ও কমন সাবএক্সপ্রেশন এলিমিনেশন — একই 3AC-তে তিনটি অপ্টিমাইজেশন একসাথে।