পাঠ ৩৪ · ৫৬-এর মধ্যে · মডিউল ৮
Home / Courses / Operating Systems (OS) / পেজ রিপ্লেসমেন্ট — LRU

পেজ রিপ্লেসমেন্ট অ্যালগরিদম — LRU

Page replacement algorithms — LRU
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • LRU অ্যালগরিদমের যুক্তি এবং locality of reference নীতির সাথে এর সম্পর্ক
  • LRU বাস্তবায়নের দুটি সাধারণ কৌশল — টাইমস্ট্যাম্প-ভিত্তিক ও stack/OrderedDict-ভিত্তিক
  • Python-এ OrderedDict দিয়ে প্রকৃত LRU ক্যাশ-প্যাটার্ন বাস্তবায়ন
  • FIFO, Optimal ও LRU-এর ফল্ট সংখ্যা একসাথে গণনা করে একটি চূড়ান্ত তুলনা সারণি তৈরি

১ · LRU (Least Recently Used) কী

L33-এ আমরা দেখেছি FIFO ব্যবহারের ধরন সম্পূর্ণ উপেক্ষা করে, আর Optimal ভবিষ্যৎ জানা লাগে বলে বাস্তবে অচল। LRU (Least Recently Used)Least Recently Usedযে পেজ সবচেয়ে বেশিদিন ধরে অ্যাক্সেস হয়নি তাকে সরানোর নিয়ম — শুধু অতীতের ব্যবহারের ইতিহাসের ওপর ভিত্তি করে। এই দুটোর মাঝামাঝি একটি বাস্তবসম্মত সমাধান দেয়: এটি সেই পেজ সরায় যেটি সবচেয়ে বেশিদিন ধরে অ্যাক্সেস হয়নি।

এর পেছনের ধারণাটি হলো locality of referenceLocality of Referenceএকটি অভিজ্ঞতাগত পর্যবেক্ষণ — সাম্প্রতিক অতীতে ব্যবহৃত ডেটা নিকট ভবিষ্যতেও আবার ব্যবহৃত হওয়ার সম্ভাবনা বেশি। — একটি অভিজ্ঞতাগত (empirically observed) পর্যবেক্ষণ যে সাম্প্রতিক অতীতে ব্যবহৃত ডেটা নিকট ভবিষ্যতেও আবার ব্যবহৃত হওয়ার সম্ভাবনা বেশি। তাই যে পেজ দীর্ঘদিন ব্যবহৃত হয়নি, সেটি ভবিষ্যতেও শীঘ্রই ব্যবহৃত হওয়ার সম্ভাবনা কম — সরানোর জন্য একটি যুক্তিসঙ্গত পছন্দ। গুরুত্বপূর্ণ পার্থক্য: LRU শুধু অতীতের তথ্য (যা OS-এর কাছে সবসময় পাওয়া যায়) ব্যবহার করে Optimal-এর ভবিষ্যৎ-নির্ভরতা এড়িয়ে যায় — এবং সাধারণত FIFO-এর চেয়ে অনেক ভালো পারফর্ম করে, Belady's Anomaly-এর মতো সমস্যাও LRU-তে ঘটে না।

২ · বাস্তবায়ন কৌশল

LRU বাস্তবায়নের দুটি সাধারণ পদ্ধতি আছে। প্রথমত, প্রতিটি পেজ অ্যাক্সেসে একটি টাইমস্ট্যাম্প/কাউন্টার আপডেট করা যায় — রিপ্লেসমেন্ট দরকার হলে সবচেয়ে পুরনো টাইমস্ট্যাম্পের পেজ খুঁজে বের করা হয় (নির্ভুল, কিন্তু প্রতিবার পুরো স্ক্যান লাগে)। দ্বিতীয়ত, একটি stack/linked-list রাখা যায় যেখানে প্রতিটি অ্যাক্সেসে পেজটিকে একদম সামনে/শেষে সরিয়ে আনা হয় — রিপ্লেসমেন্ট দরকার হলে অন্য প্রান্তের পেজটি সরানো হয়। Python-এ এই দ্বিতীয় পদ্ধতিটিই সবচেয়ে স্বাভাবিকভাবে collections.OrderedDict দিয়ে করা যায় — move_to_end(key) দিয়ে সাম্প্রতিক ব্যবহৃত পেজকে শেষে আনা, আর popitem(last=False) দিয়ে সবচেয়ে কম-সাম্প্রতিক (সামনের) পেজ সরানো।

মূল ধারণা

OrderedDict-ভিত্তিক এই প্যাটার্নটিই সাধারণ "LRU ক্যাশ" ডেটা স্ট্রাকচারের ভিত্তি — শুধু OS-এর পেজ রিপ্লেসমেন্টেই নয়, ওয়েব ব্রাউজার ক্যাশ, ডেটাবেজ বাফার পুল, এমনকি CDN-এও একই ধারণা ব্যবহৃত হয়। এখানে আমরা এটিকে সরাসরি পেজ রিপ্লেসমেন্টের প্রেক্ষাপটে প্রয়োগ করছি।

৩ · কোডে যাচাই: FIFO বনাম Optimal বনাম LRU

নিচের কোড সেলে L33-এর FIFO ও Optimal ফাংশন দুটোর পাশাপাশি একটি বাস্তব LRU ফাংশন যোগ করা হয়েছে — OrderedDict ব্যবহার করে। তিনটিই ঠিক একই রেফারেন্স স্ট্রিং ও ফ্রেম সংখ্যায় চালানো হয়েছে, যাতে ফলাফল সরাসরি তুলনাযোগ্য হয়।

Python
# FIFO বনাম Optimal বনাম LRU -- একই রেফারেন্স স্ট্রিং ও ফ্রেম সংখ্যায় তিনটি বাস্তব অ্যালগরিদম

from collections import OrderedDict

# L33-এর সাথে অভিন্ন -- তুলনা যাতে ন্যায্য হয়
REFERENCE_STRING = [7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1]
NUM_FRAMES = 3


def fifo(reference_string, num_frames):
    frames = []
    faults = 0
    for page in reference_string:
        if page in frames:
            continue
        faults += 1
        if len(frames) < num_frames:
            frames.append(page)
        else:
            frames.pop(0)
            frames.append(page)
    return faults


def optimal(reference_string, num_frames):
    frames = []
    faults = 0
    n = len(reference_string)
    for i, page in enumerate(reference_string):
        if page in frames:
            continue
        faults += 1
        if len(frames) < num_frames:
            frames.append(page)
        else:
            future = reference_string[i + 1:]
            farthest_use, evict_idx = -1, 0
            for idx, resident_page in enumerate(frames):
                next_use = future.index(resident_page) if resident_page in future else n
                if next_use > farthest_use:
                    farthest_use, evict_idx = next_use, idx
            frames[evict_idx] = page
    return faults


def lru(reference_string, num_frames):
    frames = OrderedDict()   # বাম প্রান্ত = সবচেয়ে কম-সাম্প্রতিক, ডান প্রান্ত = সবচেয়ে সাম্প্রতিক
    faults = 0
    for page in reference_string:
        if page in frames:
            frames.move_to_end(page)      # HIT -- সবচেয়ে সাম্প্রতিক হিসেবে চিহ্নিত করা হলো
            continue
        faults += 1                        # FAULT
        if len(frames) >= num_frames:
            frames.popitem(last=False)     # সবচেয়ে কম-সাম্প্রতিক পেজ সরানো হলো
        frames[page] = None
    return faults


fifo_faults = fifo(REFERENCE_STRING, NUM_FRAMES)
optimal_faults = optimal(REFERENCE_STRING, NUM_FRAMES)
lru_faults = lru(REFERENCE_STRING, NUM_FRAMES)

print("রেফারেন্স স্ট্রিং:", REFERENCE_STRING)
print("ফ্রেম সংখ্যা:", NUM_FRAMES)
print()
print(f"{'অ্যালগরিদম':<12}{'মোট পেজ ফল্ট':>14}")
print("-" * 26)
print(f"{'Optimal':<12}{optimal_faults:>14}")
print(f"{'LRU':<12}{lru_faults:>14}")
print(f"{'FIFO':<12}{fifo_faults:>14}")
print()
if optimal_faults <= lru_faults <= fifo_faults:
    print(f"যাচাই সফল -> Optimal ({optimal_faults}) <= LRU ({lru_faults}) <= FIFO ({fifo_faults})")
else:
    print("অপ্রত্যাশিত ক্রম -- সংখ্যা পুনরায় যাচাই করুন")

    
উপরের কোড Run করলে ঠিক এই ক্রমটাই পাবেন — Optimal সবচেয়ে কম ফল্ট দেয় (তাত্ত্বিক সেরা হওয়ায়), LRU তার বেশ কাছাকাছি থাকে (শুধু অতীতের তথ্য দিয়েও), আর FIFO সবচেয়ে বেশি ফল্ট দেয় (ব্যবহারের ধরন সম্পূর্ণ উপেক্ষা করার কারণে)। এই ক্রমটি — Optimal ≤ LRU ≤ FIFO — সাধারণত সত্যি হয়, যদিও এটি প্রতিটি সম্ভাব্য রেফারেন্স স্ট্রিং-এর জন্য গাণিতিকভাবে গ্যারান্টিযুক্ত নয় (শুধু Optimal ≤ যেকোনো অ্যালগরিদম গ্যারান্টিযুক্ত)।

৪ · সারসংক্ষেপ সারণি — তিনটি অ্যালগরিদম পাশাপাশি

উপরের কোডের আউটপুট অনুযায়ী তিনটি অ্যালগরিদমের একটি সংক্ষিপ্ত তুলনা:

অ্যালগরিদম তথ্যের উৎস বাস্তবায়নযোগ্য? মোট পেজ ফল্ট
Optimal ভবিষ্যৎ (তাত্ত্বিক) না — বেঞ্চমার্ক মাত্র ৯
LRU অতীত (সাম্প্রতিকতা) হ্যাঁ — বাস্তবে ব্যবহৃত ১২
FIFO প্রবেশের ক্রম হ্যাঁ — সরল কিন্তু দুর্বল ১৫

সংখ্যাগুলো উপরের কোড সেলটি Run করলে যা প্রিন্ট হয় তারই সংক্ষিপ্তসার — একই ২০-অ্যাক্সেসের রেফারেন্স স্ট্রিং ও ৩টি ফ্রেমে।

মূল কথা · Key takeaway

LRU দেখায় কীভাবে শুধু অতীতের তথ্য ব্যবহার করেও একটি বাস্তবায়নযোগ্য অ্যালগরিদম Optimal-এর তাত্ত্বিক সীমার অনেক কাছাকাছি পৌঁছাতে পারে — এই কারণেই LRU (এবং এর বিভিন্ন বাস্তব variants, যেমন Clock/Second-Chance অ্যালগরিদম) আধুনিক OS-এ ব্যাপকভাবে ব্যবহৃত হয়। কিন্তু LRU নিখুঁত নয়: যথেষ্ট ফ্রেম না থাকলে (বা প্রসেসগুলো একসাথে বেশি মেমরি চাইলে) তখনও পারফরম্যান্স ভেঙে পড়তে পারে — যাকে বলে থ্র্যাশিং, ঠিক পরের পাঠের বিষয়।

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

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

প্র ০১ LRU "অতীতের ব্যবহার ভবিষ্যতেরও ইঙ্গিত দেয়" এই অনুমানের ওপর নির্ভর করে — এই অনুমান কখন ভুল প্রমাণিত হতে পারে?

যদি একটি প্রোগ্রামের অ্যাক্সেস প্যাটার্নে কোনো locality না থাকে (যেমন সম্পূর্ণ র‍্যান্ডম বা ঘুরেফিরে একটি বড় ডেটাসেট পুরোপুরি বারবার স্ক্যান করা, যেখানে সাম্প্রতিক ব্যবহার ভবিষ্যতের ব্যবহারের কোনো ইঙ্গিত দেয় না), LRU-এর সিদ্ধান্তগুলো এলোমেলো অনুমানের চেয়ে খুব একটা ভালো হয় না। বাস্তবে বেশিরভাগ প্রোগ্রামেই যথেষ্ট locality থাকে বলে LRU গড়ে ভালো পারফর্ম করে, কিন্তু এটি কোনো গাণিতিক গ্যারান্টি নয় — শুধু একটি অভিজ্ঞতাগত প্রবণতার ওপর নির্ভরশীল।

প্র ০২ উপরের কোডে frames.move_to_end(page) লাইনটি বাদ দিলে (শুধু HIT-এ কিছু না করে continue করলে) কী সমস্যা হতো?

তাহলে OrderedDict-এর ক্রম শুধু প্রতিটি পেজের প্রথমবার ঢোকার ক্রম ধরে রাখত, তার পরবর্তী কোনো ব্যবহারের হিসাব রাখত না — অর্থাৎ এটি আসলে LRU না হয়ে FIFO-তে পরিণত হয়ে যেত! ঠিক এই move_to_end() কলটিই প্রতিটি হিটে পেজকে "সবচেয়ে সাম্প্রতিক" হিসেবে চিহ্নিত করে, যা LRU-এর মূল সংজ্ঞার জন্য অপরিহার্য।

প্র ০৩ Optimal ≤ LRU ≤ FIFO — এই ক্রমটি কি প্রতিটি সম্ভাব্য রেফারেন্স স্ট্রিং-এর জন্যই গাণিতিকভাবে নিশ্চিত?

না। শুধু "Optimal সর্বোচ্চ ভালো (বা সমান)" এটিই গাণিতিকভাবে প্রমাণিত — Optimal সংজ্ঞা অনুযায়ীই সর্বনিম্ন সম্ভাব্য ফল্ট দেয়, তাই এটি সবসময় LRU ও FIFO উভয়ের চেয়ে কম বা সমান হবে। কিন্তু LRU সবসময় FIFO-এর চেয়ে ভালো হবে এমন কোনো সার্বজনীন গ্যারান্টি নেই — বিশেষ কিছু কৃত্রিমভাবে তৈরি রেফারেন্স স্ট্রিং-এ FIFO ভালো ফলও দিতে পারে। তবে বাস্তব, সাধারণ locality-সহ প্রোগ্রামের অ্যাক্সেস প্যাটার্নে LRU প্রায় সবসময়ই FIFO-এর চেয়ে ভালো বা সমান পারফর্ম করে — যেমনটা উপরের উদাহরণেও দেখা গেল।

অনুশীলন

  1. পরীক্ষা করুন: উপরের কোড সেলে REFERENCE_STRING-এর একদম শেষে 2 যোগ করুন এবং Run চেপে দেখুন তিনটি অ্যালগরিদমের ফল্ট সংখ্যা কীভাবে বদলায় — ক্রমটি (Optimal ≤ LRU ≤ FIFO) এখনও ঠিক থাকে কি না।

    স্ট্রিং-এর শেষে 2 যোগ করলে সেই মুহূর্তে 2 আগে থেকেই ফ্রেমে থাকার সম্ভাবনা বেশি (শেষ কয়েকটি অ্যাক্সেসে 0, 1, 7, 0, 1 থাকায়), তাই এটি প্রায় নিশ্চিতভাবে একটি অতিরিক্ত ফল্ট বা হিট যোগ করবে অ্যালগরিদমভেদে ভিন্নভাবে। প্রকৃত সংখ্যাটি জানতে কোডটি নিজে Run করে দেখাই সবচেয়ে নির্ভরযোগ্য উপায় — এটাই এই পাঠের মূল শিক্ষা: অনুমান না করে, কোড দিয়ে যাচাই করা।

  2. চিন্তা করুন: LRU বাস্তবায়নে প্রতিটি অ্যাক্সেসে move_to_end() কল করার একটি (ছোট হলেও বাস্তব) খরচ আছে। এই খরচ কেন Optimal-এর "ভবিষ্যৎ স্ক্যান করা" খরচের চেয়ে ব্যবহারিকভাবে অনেক কম গুরুত্বপূর্ণ?

    OrderedDict.move_to_end() একটি O(1) (স্থির সময়ের) অপারেশন — যতই পেজ থাকুক না কেন প্রতিটি অ্যাক্সেসে সমান খরচ। কিন্তু Optimal-এর মতো প্রতিবার সম্পূর্ণ ভবিষ্যৎ স্ক্যান করা তো দূরের কথা, বাস্তবে "ভবিষ্যৎ" বলে কিছুই পাওয়া যায় না — তাই তুলনাটাই অবাস্তব। LRU-এর প্রকৃত ব্যবহারিক সুবিধা ঠিক এখানেই: এটি দ্রুত (O(1)-এর কাছাকাছি) এবং শুধু অতীতের তথ্য দিয়েই কাজ চালাতে পারে।

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

আগের পাঠ
পেজ রিপ্লেসমেন্ট অ্যালগরিদম — FIFO ও Optimal