কোড অপ্টিমাইজেশন টেকনিক
এই পাঠে যা শিখবেন
- কোড অপ্টিমাইজেশনের সংজ্ঞা এবং এর অলঙ্ঘনীয় সঠিকতার শর্ত
- কনস্ট্যান্ট ফোল্ডিং, ডেড কোড এলিমিনেশন ও কমন সাবএক্সপ্রেশন এলিমিনেশন — প্রতিটির নির্দিষ্ট সংজ্ঞা
- একই 3AC উদাহরণে তিনটি টেকনিক একসাথে বাস্তবায়ন ও প্রয়োগ করা
- আগে-পরে কোড এক্সিকিউট করে সঠিকতা (ফলাফল অপরিবর্তিত) নিজে যাচাই করা
১ · অপ্টিমাইজেশনের সংজ্ঞা ও অলঙ্ঘনীয় শর্ত
কোড অপ্টিমাইজেশনCode OptimizationIR (বা জেনারেট হওয়া কোড)-কে সিমান্টিকালি সমতুল্য কিন্তু বেশি দক্ষ (দ্রুত, কম মেমরি) কোনো রূপে রূপান্তর করার প্রক্রিয়া। এর মানে M12/L51-এর IR-কে (বা M12/L52-এর জেনারেট হওয়া কোডকে) এমনভাবে বদলানো যাতে এটি আরও কম সময়ে বা কম মেমরিতে চলে। কিন্তু একটি শর্ত কখনো ভাঙা যাবে না — একটি সঠিক প্রোগ্রামের observable আচরণ/আউটপুট অপ্টিমাইজেশনের আগে ও পরে একদম অভিন্ন থাকতে হবে। যে "অপ্টিমাইজেশন" প্রোগ্রামের আচরণ বদলে দেয়, সেটি অপ্টিমাইজেশন নয় — সেটি স্রেফ একটি বাগ।
২ · কনস্ট্যান্ট ফোল্ডিং
যদি কোনো অপারেশনের উভয় অপারেন্ড কম্পাইল-টাইমেই জানা কনস্ট্যান্ট হয়, তাহলে ফলাফল কম্পাইল-টাইমেই
গণনা করে সরাসরি সেই কনস্ট্যান্ট দিয়ে অপারেশনটি প্রতিস্থাপন করা যায় — যেমন x = 3 + 4 হয়ে যায়
x = 7, রানটাইমে একটি যোগের হিসাব সম্পূর্ণ বাদ পড়ে যায়।
৩ · ডেড কোড এলিমিনেশন
যে কোডের ফলাফল প্রমাণযোগ্যভাবে কখনো ব্যবহৃত/observable হয় না (যেমন একটি ভ্যারিয়েবলে অ্যাসাইন করার পর সেটি আর কখনো পড়া হয় না, পুনরায় অ্যাসাইন হওয়ার আগেই বা স্কোপ শেষ হওয়ার আগেই — সরাসরি M8/L39-এর লাইফটাইম ধারণার পুনর্ব্যবহার), সেই কোড নিরাপদে বাদ দেওয়া যায়।
৪ · কমন সাবএক্সপ্রেশন এলিমিনেশন
যদি একই এক্সপ্রেশন একাধিকবার গণনা করা হয় এবং মাঝে তার অপারেন্ডগুলো প্রমাণযোগ্যভাবে অপরিবর্তিত থাকে, তাহলে সেটি একবার গণনা করে ফলাফল সংরক্ষণ করে পরবর্তী প্রতিটি ব্যবহারে সেই সংরক্ষিত ফলাফল পুনর্ব্যবহার করা যায় — অপ্রয়োজনীয় পুনরাবৃত্তি এড়ানো যায়। M12/L51-এর 3AC temporary-গুলো এই টেকনিকের জন্য স্বাভাবিক জায়গা, কারণ প্রতিটি উপ-ফলাফল ইতিমধ্যেই একটি নামযুক্ত temp-এ সংরক্ষিত থাকে।
তিনটি টেকনিকই 3AC ইনস্ট্রাকশন তালিকার উপর সরাসরি কাজ করে — M12/L51-এর তৈরি করা ফরম্যাটই এখানে অপ্টিমাইজেশনের ইনপুট/আউটপুট। এই কারণেই IR-কে সরল ও uniform রাখা (L05-এর front-end/back-end পৃথকীকরণের মূল যুক্তি) এত গুরুত্বপূর্ণ — একটি সরল, uniform ফরম্যাটেই এই ধরনের মেকানিক্যাল ট্রান্সফরমেশন লেখা সহজ।
৫ · বাস্তবায়ন — তিনটি টেকনিক একসাথে, একই উদাহরণে
নিচের উদাহরণে ইচ্ছাকৃতভাবে একটি অ-অপ্টিমাইজড 3AC তৈরি করা হয়েছে যাতে তিনটি সুযোগই একসাথে থাকে —
t1 = 2 + 3 (কনস্ট্যান্ট ফোল্ডিং), a + b দুইবার গণনা (কমন সাবএক্সপ্রেশন), এবং
t5 = a - b যা কখনো ব্যবহৃত হয় না (ডেড কোড)। তিনটি টেকনিক ক্রমান্বয়ে প্রয়োগ করে আগে ও পরের কোড
দেখানো হয়েছে, এবং শেষে a=5, b=3 দিয়ে দুই ভার্সনই এক্সিকিউট করে ফলাফল তুলনা করা হয়েছে।
def is_const(x):
if x is None:
return False
try:
int(x)
return True
except (ValueError, TypeError):
return False
def apply_op(op, x, y):
if op == "+": return x + y
if op == "-": return x - y
if op == "*": return x * y
if op == "/": return x // y
raise ValueError(op)
def constant_fold(code):
"""উভয় অপারেন্ড কম্পাইল-টাইমে জানা কনস্ট্যান্ট হলে, ফলাফল আগেই গণনা করে instruction-টিকে
একটি সরল 'copy' নির্দেশে বদলে দেয় -- রানটাইমে আর কোনো যোগ/বিয়োগ/গুণ করতে হয় না।"""
new_code = []
for instr in code:
op, a1, a2 = instr["op"], instr["arg1"], instr["arg2"]
if op != "copy" and is_const(a1) and is_const(a2):
val = apply_op(op, int(a1), int(a2))
new_code.append({"result": instr["result"], "op": "copy", "arg1": str(val), "arg2": None})
else:
new_code.append(dict(instr))
return new_code
def eliminate_common_subexpressions(code):
"""একই (op, arg1, arg2) আগে একবার গণনা হয়ে থাকলে, দ্বিতীয়বার আর গণনা না করে আগের
ফলাফল সরাসরি পুনর্ব্যবহার করে -- redundant instruction সম্পূর্ণ বাদ দেয়।"""
seen = {}
subst = {}
out = []
for instr in code:
op, a1, a2 = instr["op"], instr["arg1"], instr["arg2"]
a1r = subst.get(a1, a1)
a2r = subst.get(a2, a2) if a2 is not None else None
key = (op, a1r, a2r)
if op != "copy" and key in seen:
subst[instr["result"]] = seen[key]
continue
new_instr = {"result": instr["result"], "op": op, "arg1": a1r, "arg2": a2r}
out.append(new_instr)
if op != "copy":
seen[key] = instr["result"]
return out
def eliminate_dead_code(code, used_variables):
"""শেষ থেকে পেছনের দিকে হেঁটে -- যে instruction-এর ফলাফল কখনো ব্যবহৃত হয় না
(used_variables থেকে কোনো ট্রেস ছাড়াই), সেটি সম্পূর্ণ বাদ দেয়।"""
live = set(used_variables)
kept_reversed = []
for instr in reversed(code):
if instr["result"] in live:
kept_reversed.append(instr)
for arg in (instr["arg1"], instr["arg2"]):
if arg is not None and not is_const(arg):
live.add(arg)
return list(reversed(kept_reversed))
def execute(code, env0):
env = dict(env0)
for instr in code:
op = instr["op"]
if op == "copy":
env[instr["result"]] = int(instr["arg1"]) if is_const(instr["arg1"]) else env[instr["arg1"]]
else:
x = int(instr["arg1"]) if is_const(instr["arg1"]) else env[instr["arg1"]]
y = int(instr["arg2"]) if is_const(instr["arg2"]) else env[instr["arg2"]]
env[instr["result"]] = apply_op(op, x, y)
return env
# ইচ্ছাকৃতভাবে অ-অপ্টিমাইজড 3AC -- তিনটি সুযোগই একসাথে আছে:
# t1 = 2 + 3 -> কনস্ট্যান্ট ফোল্ডিং-এর সুযোগ
# t2, t3 = a + b -> একই এক্সপ্রেশন দুইবার -> কমন সাবএক্সপ্রেশন
# t5 = a - b -> কখনো ব্যবহৃত হয় না -> ডেড কোড
code = [
{"result": "t1", "op": "+", "arg1": "2", "arg2": "3"},
{"result": "t2", "op": "+", "arg1": "a", "arg2": "b"},
{"result": "t3", "op": "+", "arg1": "a", "arg2": "b"},
{"result": "t4", "op": "*", "arg1": "t3", "arg2": "t1"},
{"result": "t5", "op": "-", "arg1": "a", "arg2": "b"},
{"result": "result", "op": "copy", "arg1": "t4", "arg2": None},
]
print("আগে (BEFORE):")
for i in code:
print(" ", i)
folded = constant_fold(code)
csed = eliminate_common_subexpressions(folded)
final_code = eliminate_dead_code(csed, {"result"})
print("\nconstant_fold-এর পরে:")
for i in folded: print(" ", i)
print("\nCSE-এর পরে:")
for i in csed: print(" ", i)
print("\nডেড কোড এলিমিনেশনের পরে (চূড়ান্ত):")
for i in final_code: print(" ", i)
print(f"\nনির্দেশ সংখ্যা: আগে {len(code)}টি -> পরে {len(final_code)}টি")
env0 = {"a": 5, "b": 3}
before_result = execute(code, env0)["result"]
after_result = execute(final_code, env0)["result"]
print(f"\na=5, b=3 দিয়ে এক্সিকিউট করে: আগের কোডের ফলাফল = {before_result}, অপ্টিমাইজডের ফলাফল = {after_result}")
print("সঠিকতা যাচাই:", "PASS -- দুটো ফলাফল অভিন্ন" if before_result == after_result else "FAIL")
t1 = 2 + 3 সরাসরি t1 = copy 5-এ পরিণত হয়েছে (কনস্ট্যান্ট ফোল্ডিং),
t3 = a + b সম্পূর্ণ বাদ পড়েছে কারণ এটি t2-এর হুবহু পুনরাবৃত্তি (CSE — t4-এর
সংজ্ঞায় t3-এর বদলে t2 বসে গেছে), এবং t5 = a - b সম্পূর্ণ উধাও হয়ে গেছে
কারণ এটি কখনো ব্যবহৃতই হয় না (ডেড কোড)। ৬টি নির্দেশ থেকে মাত্র ৪টিতে নেমে এসেছে — অথচ চূড়ান্ত ফলাফল অভিন্ন,
কোড নিজেই এক্সিকিউট করে তা প্রমাণ করেছে।
সব কোড অপ্টিমাইজেশনের মূল কথা একটাই — কম কাজ করে একই ফলাফল দেওয়া, কখনো ভিন্ন ফলাফল নয়। কনস্ট্যান্ট ফোল্ডিং, ডেড কোড এলিমিনেশন ও কমন সাবএক্সপ্রেশন এলিমিনেশন — তিনটিই সরল, মেকানিক্যাল, কিন্তু বাস্তব কম্পাইলারে একসাথে প্রয়োগ করলে মূল কোডকে উল্লেখযোগ্যভাবে ছোট ও দ্রুত করে তোলে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
উপরের কোড সেলে eliminate_dead_code কেন সবার শেষে চালানো হয়েছে — constant_fold বা CSE-এর আগে নয় কেন?
কারণ constant_fold ও CSE উভয়ই কোডের গঠন বদলে দিতে পারে (যেমন CSE একটি temp-কে সম্পূর্ণ বাদ দিয়ে অন্য একটি দিয়ে প্রতিস্থাপন করে) — কোন ভ্যারিয়েবল আসলে "ব্যবহৃত" থাকছে তা সঠিকভাবে বলা যায় শুধুমাত্র বাকি সব ট্রান্সফরমেশনের পরে, চূড়ান্ত কোডের উপর। যদি dead-code এলিমিনেশন প্রথমে চালানো হতো, তাহলে এমন কোনো temp-কে ভুলবশত রেখে দেওয়া হতো যেটি পরবর্তী CSE পাস আসলে অপ্রয়োজনীয় করে দিত।
প্র ০২
যদি used_variables-এ "result"-এর বদলে ভুলবশত "t5" দেওয়া হতো, তাহলে ডেড কোড এলিমিনেশনের ফলাফলে কী পরিবর্তন হতো?
তখন t5 = a - b "জীবিত" (দরকারি) বলে গণ্য হতো এবং কখনো বাদ পড়ত না — উল্টো t4 ও
result-এর দিকে যাওয়া চেইন (যা আসলে চূড়ান্ত আউটপুট) সম্ভবত ডেড হিসেবে ভুলভাবে বাদ পড়ে যেত,
কারণ কেউ আর "result"-কে "দরকারি" বলে চিহ্নিত করছে না। এটি দেখায় dead-code এলিমিনেশনের
সঠিকতা সম্পূর্ণভাবে নির্ভর করে used_variables (কোন ভ্যারিয়েবল সত্যিকারের প্রোগ্রাম-আউটপুট)
সঠিকভাবে চিহ্নিত করার উপর।
প্র ০৩
কমন সাবএক্সপ্রেশন এলিমিনেশন কেন নিরাপদ ধরে নেওয়া হয় যে a ও b-এর মান দুইবার গণনার মাঝে বদলায়নি — বাস্তবে এই ধরে নেওয়া কখন ভুল হতে পারে?
এই কোড সেলে a, b সরাসরি ইনপুট ভ্যারিয়েবল, মাঝে কোনো অ্যাসাইনমেন্ট নেই — তাই ধরে
নেওয়া নিরাপদ। কিন্তু বাস্তব কম্পাইলারে যদি দুই গণনার মাঝে a বা b-তে নতুন মান
অ্যাসাইন হয় (বা কোনো ফাংশন কল থাকে যা এদের বদলাতে পারে, যেমন pass-by-reference — M11/L49), তাহলে দ্বিতীয়
গণনাটি আসলে ভিন্ন হতে পারে এবং CSE প্রয়োগ করলে ভুল ফলাফল আসবে — বাস্তব CSE বাস্তবায়নকে তাই মাঝের প্রতিটি
instruction পরীক্ষা করে দেখতে হয় অপারেন্ডগুলো সত্যিই অপরিবর্তিত ছিল কিনা।
অনুশীলন
-
চিন্তা করুন: যদি কোডে আরেকটি dead instruction
t6 = t1 * 2(কখনো ব্যবহৃত না) যোগ করা হয়, তাহলে সেটিও কি বাদ পড়বে? কেন?হ্যাঁ, বাদ পড়বে —
eliminate_dead_codeশেষ থেকে পেছনের দিকে হেঁটে প্রতিটি instruction-এর ফলাফলliveসেটে আছে কিনা পরীক্ষা করে;t6কখনো কোনো পরবর্তী instruction-এর অপারেন্ড হিসেবে ব্যবহৃত হয় না এবংused_variables-এও নেই, তাই এটি কখনোliveসেটে যুক্ত হবে না এবং বাদ পড়ে যাবে — instruction-এর সংখ্যা বা অবস্থান নির্বিশেষে নিয়মটি সবসময় প্রযোজ্য। -
পরীক্ষা করুন: উপরের কোড সেলে
env0-এর মান{"a": 10, "b": 1}করে Run চেপে দেখুন আগে ও পরের ফলাফল এখনো মেলে কিনা।হ্যাঁ, মিলবে —
a=10, b=1-এর জন্যresult = (a+b) * (2+3) = 11 * 5 = 55, এবং অপ্টিমাইজড ভার্সনও একই ৫৫ দেবে (শুধু কম ধাপে)। যেহেতু অপ্টিমাইজেশনগুলোa,b-এর নির্দিষ্ট মানের উপর নির্ভর করে না (তারা কাঠামোগতভাবে সঠিক যেকোনো ইনপুটের জন্য), তাই যেকোনোa,b-এর জন্যই ফলাফল মিলবে — এটাই একটি সাধারণ (input-independent) অপ্টিমাইজেশনের বৈশিষ্ট্য।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ পরের পাঠে প্রোগ্রাম চলাকালীন রানটাইম কী কী সাহায্য করে — কল স্ট্যাক, হিপ ও গার্বেজ কালেকশন।
- কোড জেনারেশন বেসিকস (L52) M12 · আগের পাঠ IR থেকে টার্গেট মেশিন কোডে যাওয়ার পথ ও রেজিস্টার অ্যালোকেশন — এই পাঠের অপ্টিমাইজেশন যে জেনারেটেড কোডকে আরও দক্ষ করে তোলে।
- রানটাইম এনভায়রনমেন্ট ও গার্বেজ কালেকশন (L54) পরের পাঠ M12-এর শেষ পাঠ — কল স্ট্যাক, হিপ, ম্যানুয়াল মেমরি ম্যানেজমেন্ট বনাম গার্বেজ কালেকশন।