পাঠ ৪০ · ৫৭-এর মধ্যে · মডিউল ৮
Home / Courses / Computer Architecture & Digital Logic / রিপ্লেসমেন্ট ও রাইট পলিসি

ক্যাশ রিপ্লেসমেন্ট ও রাইট পলিসি

Cache replacement & write policy
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • রিপ্লেসমেন্ট পলিসি কী এবং কেন সেট ভর্তি হলে এই সিদ্ধান্ত দরকার হয়
  • LRU (Least Recently Used) পলিসি কীভাবে কাজ করে — OS কোর্সের পেজ রিপ্লেসমেন্টের সাথে সরাসরি সমান্তরাল
  • write-through বনাম write-back — এই দুই ভিন্ন লেখা-নীতির পার্থক্য ও প্রতিটির খরচ-সুবিধা
  • dirty bit কী এবং কীভাবে এটি write-back-এ মেমরি আপডেট পিছিয়ে দেওয়া সম্ভব করে

১ · রিপ্লেসমেন্ট পলিসি — সেট ভর্তি হলে কে যাবে?

L39-এর কোড সেলে সেট ভর্তি থাকলে "প্রথম স্লট প্রতিস্থাপন করো" — এই সরলীকৃত নিয়ম ব্যবহার করা হয়েছিল। বাস্তব ক্যাশ ডিজাইনে এই সিদ্ধান্তটা নেয় একটা রিপ্লেসমেন্ট পলিসিReplacement Policyএকটি সেট ভর্তি থাকলে নতুন ব্লকের জন্য জায়গা করতে কোন বিদ্যমান লাইন evict হবে তা ঠিক করার নিয়ম — সবচেয়ে সাধারণ ও বাস্তবিক পছন্দ হলো LRULeast Recently Usedসবচেয়ে দীর্ঘ সময় ধরে ব্যবহৃত হয়নি এমন লাইনটিকে evict করে — যে লাইনটি সবচেয়ে দীর্ঘ সময় ধরে ব্যবহৃত হয়নি, ঠিক সেটাই evict করা হয়। যুক্তিটা সরাসরি L37-এর টেম্পোরাল লোকালিটি থেকে আসে — যদি একটা লাইন অনেকক্ষণ ব্যবহৃত না হয়ে থাকে, নিকট ভবিষ্যতেও তার আবার লাগার সম্ভাবনা কম।

Operating Systems কোর্সের সাথে সরাসরি সমান্তরাল

এই LRU যুক্তিটা ঠিক Operating Systems কোর্সের LRU পেজ রিপ্লেসমেন্ট পাঠের সাথে হুবহু একই ধারণা — শুধু সেখানে "পেজ" OS সফটওয়্যার দিয়ে ম্যানেজ হয় (ভার্চুয়াল মেমরির প্রেক্ষাপটে), এখানে "ক্যাশ লাইন" সরাসরি হার্ডওয়্যার সার্কিট দিয়ে ম্যানেজ হয়। নিচের কোড সেলে একই OrderedDict-স্টাইল প্যাটার্ন ব্যবহার করা হয়েছে, যা সেই পাঠেও ব্যবহৃত হয়েছিল।

২ · রাইট পলিসি — লেখার সময় কী ঘটে

রিপ্লেসমেন্ট পলিসি বলে দেয় কে evict হবে — কিন্তু একটা সম্পূর্ণ ভিন্ন প্রশ্ন হলো: যখন CPU ক্যাশে থাকা কোনো ডেটায় লেখে (write), তখন মেইন মেমরির পুরনো কপিটার কী হবে? দুটো ভিন্ন নীতি আছে —

write-through
প্রতিটি লেখা সাথে সাথে ক্যাশ ও মেইন মেমরি — দুটোতেই আপডেট হয়। সবসময় সিঙ্ক থাকে, বাস্তবায়ন সহজ, কিন্তু প্রতিটি লেখাই ধীর মেমরি-অ্যাক্সেসের খরচ বহন করে।
write-back
লেখা শুধু ক্যাশে হয়, লাইনটাকে dirty চিহ্নিত করা হয় — মেইন মেমরি আপডেট পিছিয়ে দেওয়া হয়, ঘটে শুধু ওই লাইন evict হওয়ার সময়। লেখা-ভারী কাজে অনেক দ্রুত।

write-back-এর জন্য প্রতিটি লাইনে একটা অতিরিক্ত dirty bitDirty Bitএকটি ক্যাশ লাইনে এমন ডেটা আছে যা মেইন মেমরিতে এখনো লেখা হয়নি, তা নির্দেশ করা একটি ফ্ল্যাগ দরকার — এটা ট্র্যাক রাখে লাইনটাতে এমন কোনো পরিবর্তন আছে কি না যা এখনো মেইন মেমরিতে প্রতিফলিত হয়নি। যখন একটি dirty লাইন evict হয়, evict করার আগেই তার ডেটা মেমরিতে লিখে দিতে হয় — নাহলে সেই পরিবর্তন চিরতরে হারিয়ে যাবে।

write-back-এর একটা বাস্তব ঝুঁকিও আছে (সংক্ষেপে): যদি eviction-এর আগেই হঠাৎ পাওয়ার চলে যায়, dirty ডেটা মেমরিতে কখনো পৌঁছায় না — একটা প্রকৃত ডেটা-লস ঝুঁকি যা write-through-এ ঘটে না (কারণ সেখানে মেমরি সবসময় আপ-টু-ডেট)। বাস্তব সিস্টেমে এই ঝুঁকি সামলাতে ব্যাটারি-ব্যাকড ক্যাশ বা নিয়মিত ফ্লাশের মতো ব্যবস্থা নেওয়া হয়।

৩ · কোডে — LRU eviction এবং দুই রাইট পলিসি

নিচের কোড সেলে প্রথমে একটি ২-way সেট-অ্যাসোসিয়েটিভ ক্যাশে OrderedDict দিয়ে LRU বাস্তবায়ন করা হয়েছে — সেট ভর্তি থাকলে সবচেয়ে least-recently-used ট্যাগটাই evict হয়। তারপর write-through ও write-back আলাদাভাবে সিমুলেট করা হয়েছে, dirty bit-সহ।

Python
from collections import OrderedDict

NUM_SETS = 4
WAYS = 2

class LRUSetAssociativeCache:
    def __init__(self, num_sets, ways):
        self.num_sets = num_sets
        self.ways = ways
        # প্রতিটি সেট একটি OrderedDict -- key=tag, সবচেয়ে বাম দিকেরটাই সবচেয়ে least-recently-used
        self.sets = [OrderedDict() for _ in range(num_sets)]

    def access(self, address):
        set_index = address % self.num_sets
        tag = address // self.num_sets
        cur = self.sets[set_index]

        if tag in cur:
            cur.move_to_end(tag)  # সদ্য ব্যবহৃত হলো -- সবচেয়ে ডানে (most-recently-used) সরাও
            return set_index, tag, "HIT", None

        evicted = None
        if len(cur) >= self.ways:
            evicted, _ = cur.popitem(last=False)  # সবচেয়ে বামেরটাই least-recently-used -- evict করো

        cur[tag] = True
        return set_index, tag, "MISS", evicted

cache = LRUSetAssociativeCache(NUM_SETS, WAYS)

# সেট index 0-এ পড়ে এমন ৪টি ভিন্ন address (address মড 4 == 0): 0, 4, 8, 12 -> tag 0,1,2,3
sequence = [0, 4, 0, 8, 4]
print("LRU রিপ্লেসমেন্ট -- ২-way সেট, address সিকোয়েন্স:", sequence)
print()
print("address | set | tag | ফলাফল | evicted tag")
print("-" * 46)
for addr in sequence:
    set_index, tag, result, evicted = cache.access(addr)
    evicted_str = str(evicted) if evicted is not None else "-"
    print(f"{addr:>7} | {set_index:>3} | {tag:>3} | {result:>5} | {evicted_str}")

print()
print("address=0 (tag=0) দ্বিতীয়বার HIT হওয়ায় সেটের ভেতরে MRU হয়ে যায়, ফলে tag=1 (address=4)")
print("LRU থেকে যায় -- তাই address=8 (tag=2) আসার সময় tag=1-ই evict হয়েছে, tag=0 নয়।")

print()
print("=" * 46)
print("write-through বনাম write-back")
print("=" * 46)

memory = {100: "OLD"}

def write_through(cache_line, memory, address, value):
    cache_line["data"] = value
    memory[address] = value  # সাথে সাথেই মেমরিও আপডেট

def write_back(cache_line, value):
    cache_line["data"] = value
    cache_line["dirty"] = True  # মেমরি এখনো পুরনো -- শুধু dirty flag সেট হলো

def evict_writeback(cache_line, memory, address):
    if cache_line.get("dirty"):
        memory[address] = cache_line["data"]
        cache_line["dirty"] = False

wt_line = {"data": "OLD", "dirty": False}
write_through(wt_line, memory, 100, "NEW-WT")
print(f"write-through-এ লেখার পরপরই memory[100] = {memory[100]!r}  (সাথে সাথে আপডেট হয়ে গেছে)")

memory_wb = {100: "OLD"}
wb_line = {"data": "OLD", "dirty": False}
write_back(wb_line, "NEW-WB")
print(f"write-back-এ লেখার পরপরই memory[100] = {memory_wb[100]!r}  (এখনো পুরনো! dirty={wb_line['dirty']})")

evict_writeback(wb_line, memory_wb, 100)
print(f"eviction-এর পর memory[100] = {memory_wb[100]!r}  (dirty={wb_line['dirty']}) -- এখন মেমরি আপডেট হলো")

    
লক্ষ্য করুন write-through-এ memory[100] লেখার সাথে সাথেই বদলে গেছে, কিন্তু write-back-এ লেখার পরপরই memory এখনো পুরনো মান "OLD" ধরে আছে — শুধু dirty=True হয়েছে। মেমরি আসলে বদলায় একমাত্র evict_writeback কল হওয়ার পরে — ঠিক যেভাবে বাস্তব হার্ডওয়্যারে eviction-এর সময় dirty ডেটা মেমরিতে ফ্লাশ হয়।
মূল কথা · Key takeaway

রিপ্লেসমেন্ট পলিসি ঠিক করে "কে যাবে," রাইট পলিসি ঠিক করে "লেখা কবে মেমরিতে পৌঁছাবে" — দুটো সম্পূর্ণ আলাদা সিদ্ধান্ত যা প্রতিটি বাস্তব ক্যাশ ডিজাইনেই একসাথে কাজ করে। এখন হিট/মিস, LRU ও রাইট পলিসি সবই হাতে আছে — পরের পাঠে (L41) এই সবকিছুকে একটা একক পারফরম্যান্স সংখ্যায় (AMAT) মেলানো হবে।

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

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

প্র ০১ LRU-এর যুক্তি "যা দীর্ঘদিন ব্যবহৃত হয়নি তা আর লাগবে না" — এটা কি সবসময় সঠিক হবে?

না, সবসময় নয় — এটা একটা পরিসংখ্যানগত অনুমান (হিউরিস্টিক), নিশ্চয়তা নয়। কোনো অ্যাক্সেস প্যাটার্নে এমনও হতে পারে যেখানে দীর্ঘদিন ব্যবহৃত না হওয়া কোনো ব্লক ঠিক পরের মুহূর্তেই আবার দরকার হয় — তখন LRU ভুল সিদ্ধান্ত নেয়। কিন্তু বাস্তব প্রোগ্রামের বেশিরভাগ অ্যাক্সেস প্যাটার্নে (L37-এর টেম্পোরাল লোকালিটি অনুযায়ী) এই অনুমান গড়ে ভালো কাজ করে বলেই এটা এত জনপ্রিয় — নিখুঁত নয়, কিন্তু ব্যবহারিকভাবে কার্যকর।

প্র ০২ একটা প্রোগ্রাম যদি প্রধানত পড়ে (read-heavy), লেখে কম, তাহলে write-through বনাম write-back-এর পার্থক্য কি কম গুরুত্বপূর্ণ হয়ে যায়?

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

প্র ০৩ কোড সেলের LRU উদাহরণে address=0 যদি দ্বিতীয়বার আবার অ্যাক্সেস না হতো, তাহলে address=8 আসার সময় কোন tag evict হতো?

tag=0 (address=0) evict হতো — কারণ সেক্ষেত্রে insertion-এর ক্রম অনুযায়ী tag=0-ই সবচেয়ে আগে ঢোকানো হয়েছিল এবং কখনো আবার অ্যাক্সেস (এবং তাই MRU-তে সরানো) হয়নি, তাই সে-ই থেকে যেত সবচেয়ে বামে (least-recently-used)। কোড সেলে দ্বিতীয়বারের HIT-টাই আসলে tag=0-কে বাঁচিয়ে দিয়েছিল, tag=1-কে ঠেলে দিয়েছিল evict হওয়ার দিকে।

অনুশীলন

  1. চিন্তা করুন: একটি ২-way সেটে ক্রমান্বয়ে A, B, C, A, D অ্যাক্সেস হলো (A, B, C, D সবাই একই সেটে পড়ে)। LRU নিয়মে D আসার সময় কে evict হবে?

    ক্রম ট্রেস করুন: A (MISS, সেট=[A]) → B (MISS, সেট=[A,B]) → C (MISS, সেট ভর্তি, LRU=A evict, সেট=[B,C]) → A (MISS আবার, LRU=B evict, সেট=[C,A]) → D আসার সময় সেটে আছে [C,A], LRU হলো C (কারণ A সদ্য অ্যাক্সেস হয়েছে) — তাই D আসলে C evict হবে।

  2. পরীক্ষা করুন: কোড সেলে evict_writeback ফাংশনটা যদি dirty চেক না করে সবসময় মেমরি আপডেট করত, তাতে কী ভুল হতো (কোড পরিবর্তন করবেন না, শুধু চিন্তা করুন)?

    সরাসরি কোনো ভুল ফলাফল আসত না (যেহেতু cache_line["data"]-ই সর্বশেষ মান ধরে রাখে), কিন্তু এটা একটা অপ্রয়োজনীয় মেমরি-লেখা ঘটাত এমনকি যখন লাইনটা কখনো লেখাই হয়নি (শুধু পড়া হয়েছে) — write-back-এর পুরো লক্ষ্যই হলো অপ্রয়োজনীয় মেমরি অ্যাক্সেস এড়ানো, তাই dirty চেক ছাড়া এই ফাংশনটা তার নিজের উদ্দেশ্যই নষ্ট করে ফেলত।

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

আগের পাঠ
সেট-অ্যাসোসিয়েটিভ ও ফুলি-অ্যাসোসিয়েটিভ ক্যাশ