চূড়ান্ত প্রকল্প — একটি সিম্পল CPU সিমুলেটর বানানো
এই পাঠে যা শিখবেন
- কোর্সের M1-M12-এর মূল উপাদানগুলো কীভাবে একটি একক, সুসংগত সিমুলেটরে একসাথে কাজ করে তা প্রত্যক্ষ করা
- একটি বাস্তব প্রোগ্রামের ফেচ-ডিকোড-এক্সিকিউট-মেমরি-রাইটব্যাক ট্রেস ধাপে ধাপে অনুসরণ করা
- একটি বাস্তব প্রোগ্রামে ক্যাশ হিট ও মিস ঠিক কখন ঘটে তা প্রত্যক্ষ করা
- প্রকৃত এক্সিকিউশন থেকে একটি genuine CPI কম্পিউট করা ও তার অর্থ বোঝা
১ · এই ক্যাপস্টোন কী — নতুন তত্ত্ব নয়, সংশ্লেষণ
এতদূর আসার পথে আপনি ধাপে ধাপে ডিজিটাল লজিকের গেট থেকে শুরু করে একটি সম্পূর্ণ CPU-এর প্রতিটি অংশ আলাদা
আলাদাভাবে শিখেছেন। এই শেষ পাঠে কোনো নতুন ধারণা নেই — বরং সেই সব টুকরো একত্র করে একটি সত্যিকার, চলমান
SimpleCPU ক্লাস বানানো হবে, যা একটি বাস্তব প্রোগ্রাম চালিয়ে সঠিক ফলাফল দেবে।
২ · উপাদানগুলো — কোন অংশ কোন পাঠ থেকে এসেছে
সাধারণ-উদ্দেশ্য রেজিস্টার + PC (প্রোগ্রাম কাউন্টার) + IR (ইনস্ট্রাকশন রেজিস্টার) + flags — r0 সবসময় ০ পড়ে, ঠিক বাস্তব RISC ISA-র মতো।
ইনস্ট্রাকশন মেমরি ও ডেটা মেমরি; ডেটা মেমরির সামনে একটি ডিরেক্ট-ম্যাপড ক্যাশ, যা প্রতিটি অ্যাক্সেসে হিট/মিস ট্র্যাক করে।
অ্যাডার/সাবট্রাক্টর লজিকের ওপর ভিত্তি করে ADD, SUB ও কম্পেয়ার (CMP) অপারেশন সমর্থন করে।
মাল্টি-সাইকেল-স্টাইল FSM — FETCH → DECODE → EXECUTE → (MEMORY) → (WRITEBACK), ইনস্ট্রাকশন-টাইপ অনুযায়ী অপ্রয়োজনীয় স্টেজ বাদ দিয়ে।
মোট সাইকেল ও মোট ইনস্ট্রাকশন গুনে প্রকৃত, কম্পিউট-করা CPI বের করা।
৩ · হাতে-অ্যাসেম্বল করা প্রোগ্রাম — যোগফল ১+২+৩+৪+৫
M5/L27-এর অ্যাসেম্বলি ভাষার ধারণা পুনরায় ব্যবহার করে, নিচের প্রোগ্রামটি হাতে "অ্যাসেম্বল" করা হয়েছে — একটি লুপের মাধ্যমে ১ থেকে ৫ পর্যন্ত যোগ করে, ফলাফল মেমরিতে সংরক্ষণ করে, তারপর যাচাইয়ের জন্য আবার লোড করে:
PC ইনস্ট্রাকশন মন্তব্য
-- ------------------------ -----------------------------
0 ADDI r1, r0, 0 i = 0
1 ADDI r2, r0, 0 sum = 0
2 ADDI r3, r0, 5 limit = 5
3 ADDI r4, r0, 1 const_one = 1
4 ADD r1, r1, r4 <-- LOOP: i = i + 1
5 ADD r2, r2, r1 sum = sum + i
6 BEQ r1, r3, 1 i == limit হলে PC=8 (এক্সিটে যাও)
7 BEQ r0, r0, -4 নাহলে PC=4 (LOOP-এ ফিরে যাও, অ্যানকন্ডিশনাল জাম্প)
8 STORE r2, 0(r0) mem[0] = sum
9 LOAD r5, 0(r0) r5 = mem[0] (যাচাইয়ের জন্য আবার লোড)
10 HALT
লক্ষ্য করুন লাইন ৭-এ BEQ r0, r0, -4 একটি "অ্যানকন্ডিশনাল জাম্প" হিসেবে ব্যবহৃত হয়েছে — যেহেতু
r0 সবসময় r0-এর সমান (M5/L26-এর হার্ডওয়্যার্ড-জিরো রেজিস্টার), এই BEQ সবসময় ট্রু হয়, ঠিক একটি জাম্প
ইনস্ট্রাকশনের মতো আচরণ করে — আলাদা কোনো JUMP ইনস্ট্রাকশন ছাড়াই। এই একই কৌশল বাস্তব RISC ISA-তেও ব্যবহৃত
হয়।
৪ · সম্পূর্ণ সিমুলেটর — চালিয়ে দেখুন
নিচের কোড সেলে সম্পূর্ণ SimpleCPU ক্লাস (এবং তার সহায়ক DirectMappedCache,
ALU, RegisterFile ক্লাস) সংজ্ঞায়িত করে, উপরের প্রোগ্রামটি চালিয়ে একটি
সম্পূর্ণ এক্সিকিউশন ট্রেস প্রিন্ট করা হবে।
class DirectMappedCache:
"""M8/L38 -- ডেটা মেমরির সামনে বসানো ডিরেক্ট-ম্যাপড ক্যাশ, হিট/মিস ট্র্যাক করে"""
def __init__(self, num_lines=4):
self.num_lines = num_lines
self.lines = [{"valid": False, "tag": None, "data": 0} for _ in range(num_lines)]
self.hits = 0
self.misses = 0
def _index_tag(self, address):
return address % self.num_lines, address // self.num_lines
def read(self, address, data_memory):
index, tag = self._index_tag(address)
line = self.lines[index]
if line["valid"] and line["tag"] == tag:
self.hits += 1
return line["data"], "HIT"
self.misses += 1
value = data_memory.get(address, 0)
line.update(valid=True, tag=tag, data=value)
return value, "MISS"
def write(self, address, value, data_memory):
index, tag = self._index_tag(address)
line = self.lines[index]
status = "HIT" if (line["valid"] and line["tag"] == tag) else "MISS"
if status == "HIT":
self.hits += 1
else:
self.misses += 1
line.update(valid=True, tag=tag, data=value) # write-through: ক্যাশ + মেমরি দুটোই আপডেট
data_memory[address] = value
return status
class ALU:
"""M2/L09 + M4/L19 -- অ্যাডার/সাবট্রাক্টর-ভিত্তিক ALU, তুলনাসহ"""
@staticmethod
def execute(op, a, b):
if op == "ADD":
result = a + b
elif op == "SUB":
result = a - b
elif op == "CMP":
result = a - b
else:
raise ValueError(f"অজানা ALU অপারেশন: {op}")
flags = {"zero": result == 0, "negative": result < 0}
return result, flags
class RegisterFile:
"""M5/L26 -- সাধারণ-উদ্দেশ্য রেজিস্টার, r0 সবসময় ০, প্লাস PC/IR/flags"""
def __init__(self, n=8):
self.regs = [0] * n
self.pc = 0
self.ir = None
self.flags = {"zero": False, "negative": False}
def read(self, i):
return 0 if i == 0 else self.regs[i]
def write(self, i, value):
if i != 0:
self.regs[i] = value
class SimpleCPU:
"""M6/L31 -- মাল্টি-সাইকেল-স্টাইল ফেচ-ডিকোড-এক্সিকিউট-মেমরি-রাইটব্যাক কন্ট্রোল লুপ"""
STAGE_CYCLES = {
"ADD": ["FETCH", "DECODE", "EXECUTE", "WRITEBACK"],
"SUB": ["FETCH", "DECODE", "EXECUTE", "WRITEBACK"],
"ADDI": ["FETCH", "DECODE", "EXECUTE", "WRITEBACK"],
"LOAD": ["FETCH", "DECODE", "EXECUTE", "MEMORY", "WRITEBACK"],
"STORE": ["FETCH", "DECODE", "EXECUTE", "MEMORY"],
"BEQ": ["FETCH", "DECODE", "EXECUTE"],
"HALT": ["FETCH", "DECODE"],
}
def __init__(self, instruction_memory, cache_lines=4):
self.imem = instruction_memory
self.dmem = {}
self.cache = DirectMappedCache(num_lines=cache_lines)
self.rf = RegisterFile()
self.alu = ALU()
self.total_cycles = 0
self.instructions_executed = 0
self.trace = []
self.halted = False
def step(self):
if self.halted:
return False
pc_before = self.rf.pc
instr = self.imem[pc_before]
op = instr["op"]
stages = self.STAGE_CYCLES[op]
rec = {"pc": pc_before, "instr": instr, "stages": ["FETCH", "DECODE"], "mem_status": None}
self.rf.ir = instr # FETCH: ইনস্ট্রাকশন রেজিস্টারে লোড
pc_next = pc_before + 1
if op == "HALT": # DECODE-এই থেমে যায়
self.halted = True
self.total_cycles += len(stages)
self.instructions_executed += 1
self.trace.append(rec)
return False
rec["stages"].append("EXECUTE")
result, branch_taken = None, False
if op in ("ADD", "SUB"):
a, b = self.rf.read(instr["rs1"]), self.rf.read(instr["rs2"])
result, self.rf.flags = self.alu.execute(op, a, b)
elif op == "ADDI":
a = self.rf.read(instr["rs1"])
result, self.rf.flags = self.alu.execute("ADD", a, instr["imm"])
elif op in ("LOAD", "STORE"):
rec["addr"] = self.rf.read(instr["rs1"]) + instr["offset"]
elif op == "BEQ":
a, b = self.rf.read(instr["rs1"]), self.rf.read(instr["rs2"])
_, cmp_flags = self.alu.execute("CMP", a, b)
branch_taken = cmp_flags["zero"]
if branch_taken:
pc_next = pc_before + 1 + instr["offset"]
if "MEMORY" in stages:
rec["stages"].append("MEMORY")
if op == "LOAD":
result, rec["mem_status"] = self.cache.read(rec["addr"], self.dmem)
elif op == "STORE":
rec["mem_status"] = self.cache.write(rec["addr"], self.rf.read(instr["rs2"]), self.dmem)
if "WRITEBACK" in stages:
rec["stages"].append("WRITEBACK")
self.rf.write(instr["rd"], result)
self.total_cycles += len(stages)
self.instructions_executed += 1
self.rf.pc = pc_next
rec["branch_taken"] = branch_taken
self.trace.append(rec)
return True
def run(self, max_steps=200):
steps = 0
while not self.halted and steps < max_steps:
self.step()
steps += 1
@property
def cpi(self):
return self.total_cycles / self.instructions_executed if self.instructions_executed else 0
# --- হাতে-অ্যাসেম্বল করা প্রোগ্রাম: ১ থেকে ৫ পর্যন্ত যোগফল ---
program = [
{"op": "ADDI", "rd": 1, "rs1": 0, "imm": 0}, # 0: i = 0
{"op": "ADDI", "rd": 2, "rs1": 0, "imm": 0}, # 1: sum = 0
{"op": "ADDI", "rd": 3, "rs1": 0, "imm": 5}, # 2: limit = 5
{"op": "ADDI", "rd": 4, "rs1": 0, "imm": 1}, # 3: const_one = 1
{"op": "ADD", "rd": 1, "rs1": 1, "rs2": 4}, # 4: LOOP: i = i + 1
{"op": "ADD", "rd": 2, "rs1": 2, "rs2": 1}, # 5: sum = sum + i
{"op": "BEQ", "rs1": 1, "rs2": 3, "offset": 1}, # 6: i == limit -> PC 8
{"op": "BEQ", "rs1": 0, "rs2": 0, "offset": -4}, # 7: অ্যানকন্ডিশনাল জাম্প -> PC 4
{"op": "STORE", "rs2": 2, "rs1": 0, "offset": 0}, # 8: mem[0] = sum
{"op": "LOAD", "rd": 5, "rs1": 0, "offset": 0}, # 9: r5 = mem[0]
{"op": "HALT"}, # 10
]
cpu = SimpleCPU(program)
cpu.run()
print("=== এক্সিকিউশন ট্রেস ===")
for rec in cpu.trace:
mem = f" [ক্যাশ: {rec['mem_status']}]" if rec["mem_status"] else ""
print(f"PC={rec['pc']:>2} {rec['instr']['op']:<6} {'>'.join(rec['stages']):<40}{mem}")
print()
print("চূড়ান্ত রেজিস্টার:", cpu.rf.regs)
print("r2 (sum) =", cpu.rf.read(2))
print("r5 (মেমরি থেকে রি-লোড করা sum) =", cpu.rf.read(5))
print("ডেটা মেমরি:", cpu.dmem)
print()
print(f"ক্যাশ -- হিট: {cpu.cache.hits}, মিস: {cpu.cache.misses}")
print(f"মোট সাইকেল: {cpu.total_cycles}")
print(f"এক্সিকিউট হওয়া ইনস্ট্রাকশন: {cpu.instructions_executed}")
print(f"CPI = {cpu.total_cycles} / {cpu.instructions_executed} = {cpu.cpi:.4f}")
assert cpu.rf.read(2) == 15, "যোগফল ভুল!"
assert cpu.rf.read(5) == 15, "রি-লোড করা মান ভুল!"
assert cpu.cache.misses == 1 and cpu.cache.hits == 1, "ক্যাশ হিট/মিস প্রত্যাশিত মতো নয়!"
print()
print("সব যাচাই সফল -- ১+২+৩+৪+৫ = ১৫, সঠিকভাবে কম্পিউট হয়েছে।")
৫ · ফলাফল ব্যাখ্যা
ট্রেস লক্ষ্য করলে দেখবেন PC ৪-৭ পাঁচবার পুনরাবৃত্তি হয় (লুপের ৫টি ইটারেশন), তারপর PC ৮-এ পৌঁছে
STORE চালানো হয় — এটাই প্রথম ডেটা-মেমরি অ্যাক্সেস, তাই ক্যাশে এখনো কিছু নেই এবং এটি একটি
MISS হিসেবে রেকর্ড হয়। ঠিক পরের ইনস্ট্রাকশন LOAD একই অ্যাড্রেস (০) থেকে
পড়ে — যেহেতু STORE ইতিমধ্যে সেই লাইনটি ক্যাশে বসিয়ে দিয়েছে (write-through নীতিতে), এই অ্যাক্সেসটি একটি
HIT। চূড়ান্ত CPI ১-এর চেয়ে উল্লেখযোগ্যভাবে বেশি — কারণ এটি M7-এর পাইপলাইনড ডিজাইন নয়,
বরং M6/L31-এর মাল্টি-সাইকেল ডিজাইন, যেখানে প্রতিটি ইনস্ট্রাকশনের প্রতিটি স্টেজ সিরিয়ালিভাবে, একটির পর
একটি ঘটে — কোনো ওভারল্যাপ ছাড়াই।
ADD/ADDI/BEQ ইনস্ট্রাকশনগুলো MEMORY স্টেজ
সম্পূর্ণ এড়িয়ে যায় — এটাই M6/L31-এর মাল্টি-সাইকেল ডিজাইনের মূল দক্ষতা: প্রতিটি ইনস্ট্রাকশন শুধু তার
প্রয়োজনীয় স্টেজ দিয়েই যায়, M6/L28-এর সিঙ্গেল-সাইকেল ডিজাইনের মতো সবাইকে জোর করে সবচেয়ে ধীর
ইনস্ট্রাকশনের সমান দীর্ঘ সময় নিতে হয় না।
এই SimpleCPU-ই আসলে এই পুরো কোর্সের গল্প — লজিক গেট থেকে শুরু করে একটি সম্পূর্ণ,
প্রোগ্রামযোগ্য প্রসেসর পর্যন্ত। প্রতিটি রেজিস্টার-রিড, প্রতিটি ক্যাশ-লুকআপ, প্রতিটি ALU-অপারেশন, প্রতিটি
সাইকেল-গণনা — সবকিছুই এই কোর্সের কোনো না কোনো আগের পাঠের সরাসরি প্রয়োগ। অভিনন্দন — আপনি এখন জানেন
সফটওয়্যারের একটি লাইন শেষ পর্যন্ত কীভাবে হার্ডওয়্যারে প্রকৃতপক্ষে চলে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ কেন এই সিমুলেটরের ক্যাশ (M8) একটি লুপযুক্ত প্রোগ্রামের জন্য গুরুত্বপূর্ণ — লুপ কী ধরনের অ্যাক্সেস প্যাটার্ন তৈরি করে, আর সেটা M8/L37-এর locality-of-reference ধারণার সাথে কীভাবে যুক্ত?
একটি লুপ বারবার একই, ছোট্ট একগুচ্ছ ইনস্ট্রাকশন ও ডেটা অ্যাক্সেস করে — এই প্রোগ্রামে PC ৪-৭ পাঁচবার পুনরাবৃত্তি হয়, এটাই M8/L37-এর টেম্পোরাল লোকালিটি-র ক্লাসিক উদাহরণ। আমাদের এই টয় সিমুলেটরের ক্যাশ শুধু ডেটা মেমরির সামনে বসানো (ইনস্ট্রাকশন মেমরির সামনে নয়), তাই লুপ-বডির বারবার ফেচ হওয়া ইনস্ট্রাকশনগুলো এই সিমুলেশনে ক্যাশ হয় না — কিন্তু ঠিক এই একই বারবার-অ্যাক্সেসের প্যাটার্নই বাস্তব CPU-তে ইনস্ট্রাকশন ক্যাশিং-কে এত কার্যকর করে তোলে। আমাদের ডেটা-সাইডে দেখা একমাত্র MISS-তারপর-HIT প্যাটার্নও একই নীতির ছোট্ট, concrete প্রমাণ।
প্র ০২ চূড়ান্ত CPI ১-এর তুলনায় বেশ বেশি (একের চেয়ে অনেক বড়) কেন হলো, অথচ M7-এর পাইপলাইনিং তত্ত্ব অনুযায়ী CPI ১-এর কাছাকাছি হওয়ার কথা?
কারণ এই সিমুলেটর M7-এর পাইপলাইনড ডিজাইন নয় — এটি M6/L31-এর মাল্টি-সাইকেল ডিজাইন, যেখানে একটি ইনস্ট্রাকশনের সব স্টেজ (FETCH→DECODE→EXECUTE→...) সম্পূর্ণ শেষ হওয়ার পরই পরের ইনস্ট্রাকশন শুরু হয় — কোনো ওভারল্যাপ নেই। পাইপলাইনিং-এ ভিন্ন ইনস্ট্রাকশনের ভিন্ন স্টেজ একই সাইকেলে সমান্তরালে চলে, যা CPI-কে ১-এর কাছাকাছি নামিয়ে আনে। এই দুটো সম্পূর্ণ ভিন্ন আর্কিটেকচারাল কৌশল — মাল্টি-সাইকেল ডিজাইন একক-ইনস্ট্রাকশন দক্ষতা বাড়ায় (M6/L28-এর তুলনায়), কিন্তু পাইপলাইনিং-এর মতো ইনস্ট্রাকশন-ওভারল্যাপ প্রদান করে না।
প্র ০৩ যদি ADD/ADDI-এর জন্যও MEMORY স্টেজ বাদ না দিয়ে সবসময় চালানো হতো, CPI-তে কী প্রভাব পড়ত?
এই প্রোগ্রামে ADD ৯ বার (৫ বার লুপে i=i+1-এর জন্য, ৫ বার sum=sum+i-এর জন্য) ও
ADDI ৪ বার চলে — মোট ১৩টি ইনস্ট্রাকশন প্রতিটিতে একটি করে অতিরিক্ত অপ্রয়োজনীয় সাইকেল যোগ হতো, অর্থাৎ
মোট সাইকেল ১৩ বেড়ে যেত, ইনস্ট্রাকশন-সংখ্যা একই থেকে যেত — তাই CPI আরও বেড়ে যেত। এটাই ঠিক দেখায় কেন
M6/L31-এর "প্রতিটি ইনস্ট্রাকশন শুধু প্রয়োজনীয় স্টেজ দিয়ে যায়" নীতিটা genuinely কার্যকর — অপ্রয়োজনীয়
স্টেজ বাদ দেওয়াই মাল্টি-সাইকেল ডিজাইনের মূল দক্ষতার উৎস।
অনুশীলন
-
চিন্তা করুন: যদি এই প্রোগ্রামে
limit৫-এর বদলে ১০ হতো (১ থেকে ১০ পর্যন্ত যোগফল), এক্সিকিউশন ট্রেসে কী কী পরিবর্তন হতো —instructions_executed,total_cycles, এবং ক্যাশ হিট/মিস সংখ্যায়?লুপ-বডি (PC ৪-৭, ৪টি ইনস্ট্রাকশন) ৫ বারের বদলে ১০ বার চলত — অর্থাৎ
instructions_executedওtotal_cyclesদুটোই লুপ-অংশের জন্য প্রায় দ্বিগুণ বেড়ে যেত। কিন্তু ক্যাশ হিট/মিস সংখ্যা একই থাকত — কারণ STORE ও LOAD এখনো ঠিক একই একটি অ্যাড্রেসে (মেমরি অ্যাড্রেস ০) মাত্র একবার করেই ঘটে, লুপ-ইটারেশন সংখ্যা নির্বিশেষে। এটাই দেখায় ক্যাশ-আচরণ নির্ভর করে কোন অ্যাড্রেস কতবার অ্যাক্সেস হয় তার ওপর, শুধু মোট ইনস্ট্রাকশন সংখ্যার ওপর নয়। -
পরীক্ষা করুন (কোড পরিবর্তন করবেন না): এই প্রোগ্রামে ছোট ক্যাশ (মাত্র ৪টি লাইন) থাকা সত্ত্বেও L38-এর মতো কোনো কনফ্লিক্ট-মিস দেখা যায়নি কেন?
কনফ্লিক্ট মিস তখনই ঘটে যখন দুই বা ততোধিক আলাদা, ব্যস্তভাবে-ব্যবহৃত অ্যাড্রেস একই ক্যাশ-লাইন ইনডেক্সে ম্যাপ হয় এবং একে অপরকে বারবার উচ্ছেদ করে। কিন্তু এই প্রোগ্রামে ডেটা মেমরির মাত্র একটি অ্যাড্রেস (০) ব্যবহৃত হয়েছে — তাই কোনো দ্বিতীয় অ্যাড্রেসের সাথে সংঘর্ষের প্রশ্নই ওঠে না। L38-এর কনফ্লিক্ট দেখতে হলে অন্তত দুটো ভিন্ন অ্যাড্রেস দরকার হতো যাদের
address % num_linesমান একই।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস আবার দেখুন ৫৭টি পাঠ গেট থেকে CPU পর্যন্ত পুরো যাত্রাটা আরেকবার দেখে নিন — প্রতিটি টুকরো কীভাবে একসাথে ফিট করে।
- Operating Systems কোর্স সহোদর কোর্স এই কোর্স যে হার্ডওয়্যার বানায়, সেই হার্ডওয়্যার সফটওয়্যার-স্তরে কীভাবে ব্যবস্থাপনা করা হয় — এই SimpleCPU-এর ওপরই ঠিক সেই OS-স্তর দাঁড়িয়ে আছে।
- Computer Networks কোর্স সঙ্গী কোর্স একটি মেশিনের ভেতরের হার্ডওয়্যার এই কোর্সে, আর মেশিনে-মেশিনে যোগাযোগের হার্ডওয়্যার/প্রোটোকল সেই কোর্সে — দুটো মিলিয়েই সম্পূর্ণ কম্পিউটিং হার্ডওয়্যার-চিত্র।