পাঠ ৩৯ · ৫৭-এর মধ্যে · মডিউল ৮
Home / Courses / Computer Architecture & Digital Logic / সেট-অ্যাসোসিয়েটিভ ক্যাশ

সেট-অ্যাসোসিয়েটিভ ও ফুলি-অ্যাসোসিয়েটিভ ক্যাশ

Set-associative & fully associative cache
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • ফুলি-অ্যাসোসিয়েটিভ ক্যাশ কীভাবে কাজ করে এবং কেন বড় আকারে ব্যবহারিকভাবে অসম্ভব
  • N-way সেট-অ্যাসোসিয়েটিভ ক্যাশের ম্যাপিং নিয়ম — সেট নির্বাচনে মডুলো, সেটের ভেতরে যেকোনো লাইন
  • ডিরেক্ট ম্যাপড, সেট-অ্যাসোসিয়েটিভ ও ফুলি-অ্যাসোসিয়েটিভ — এই তিনটি আসলে একই স্পেকট্রামের তিনটি বিন্দু, তা বোঝা
  • Python দিয়ে L38-এর সমস্যা সমাধান হতে সরাসরি দেখা — একই address, ভিন্ন ফলাফল

১ · ফুলি-অ্যাসোসিয়েটিভ ক্যাশ — সম্পূর্ণ স্বাধীনতা, কিন্তু চড়া মূল্যে

L38-এর conflict miss সমস্যার সবচেয়ে চরম সমাধান হলো ফুলি-অ্যাসোসিয়েটিভ ক্যাশFully Associative Cacheএকটি মেমরি ব্লক ক্যাশের যেকোনো লাইনে বসতে পারে — কোনো নির্দিষ্ট ইনডেক্স-ম্যাপিং নেই — কোনো নির্দিষ্ট ইনডেক্স-নিয়ম নেই, একটি মেমরি ব্লক ক্যাশের যেকোনো লাইনে বসতে পারে। এতে দুটো ব্লক কখনোই "বাধ্য হয়ে" একে অপরকে evict করে না — conflict miss পুরোপুরি দূর হয়ে যায়। কিন্তু এর মূল্য বাস্তব: যেহেতু কোনো নির্দিষ্ট ইনডেক্স নেই, একটি অ্যাক্সেস হিট না মিস তা যাচাই করতে প্রতিটি লাইনের ট্যাগ একসাথে (parallel) তুলনা করতে হয় — এই তুলনা-হার্ডওয়্যার প্রতি লাইনের সাথে বাড়ে, তাই বড় ক্যাশে (হাজার হাজার লাইন) এটা ব্যবহারিকভাবে অত্যন্ত ব্যয়বহুল ও ধীর হয়ে যায়।

২ · N-way সেট-অ্যাসোসিয়েটিভ ক্যাশ — বাস্তব মধ্যম পথ

প্রায় সব আধুনিক CPU যা আসলে ব্যবহার করে তা হলো N-way সেট-অ্যাসোসিয়েটিভ ক্যাশN-way Set-Associative Cacheলাইনগুলো N-টার সেটে ভাগ করা — একটি ব্লক একটি নির্দিষ্ট সেটে ম্যাপ হয়, কিন্তু সেই সেটের যেকোনো লাইনে বসতে পারে — ডিরেক্ট ম্যাপড ও ফুলি-অ্যাসোসিয়েটিভের মধ্যবর্তী একটা ব্যবহারিক সমঝোতা। এখানে ক্যাশের লাইনগুলো N-টা করে সেটে ভাগ করা থাকে। একটি মেমরি ব্লক ঠিক একটি নির্দিষ্ট সেটে ম্যাপ হয় (মডুলো দিয়ে, ঠিক ডিরেক্ট ম্যাপডের মতোই), কিন্তু সেই সেটের ভেতরে সে N-টা লাইনের যেকোনো একটিতে বসতে পারে।

$\text{set\_index} = \text{block\_address} \bmod \text{number\_of\_sets}$

একই স্পেকট্রামের দুই প্রান্ত

লক্ষ্য করুন — N=1 হলে প্রতিটি সেটে মাত্র একটি লাইন, অর্থাৎ এটাই L38-এর ডিরেক্ট ম্যাপড ক্যাশ হুবহু। আর N = মোট লাইন সংখ্যা হলে পুরো ক্যাশটাই একটি একক সেট — অর্থাৎ এটাই ফুলি-অ্যাসোসিয়েটিভ ক্যাশ। তিনটে আলাদা ডিজাইন মনে হলেও, আসলে এরা একই ধারণার তিনটি বিন্দু — associativity (N-এর মান) যত বাড়ে, conflict তত কমে, কিন্তু হার্ডওয়্যার খরচও তত বাড়ে।

৩ · কোডে — L38-এর সমস্যাটাই এখন সমাধান হচ্ছে দেখা

নিচের কোড সেলে ঠিক আগের পাঠের মতো মোট ৮টি লাইনের ক্যাশ — কিন্তু এবার ২-way সেট-অ্যাসোসিয়েটিভ ভাবে সংগঠিত (৪টি সেট, প্রতি সেটে ২টি লাইন)। L38-এর ঠিক একই address সিকোয়েন্স (৩ ও ১১, যারা একই ইনডেক্সে সংঘর্ষ করছিল) এখানে আবার চালানো হচ্ছে।

Python
TOTAL_LINES = 8
WAYS = 2
NUM_SETS = TOTAL_LINES // WAYS  # = 4

class NWaySetAssociativeCache:
    def __init__(self, num_sets, ways):
        self.num_sets = num_sets
        self.ways = ways
        # প্রতিটি সেট হলো N-টা {valid, tag} স্লটের একটি লিস্ট
        self.sets = [[{"valid": False, "tag": None} for _ in range(ways)] for _ in range(num_sets)]

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

        for slot in current_set:
            if slot["valid"] and slot["tag"] == tag:
                return set_index, tag, "HIT"

        # মিস -- এই সেটের প্রথম ফাঁকা (invalid) স্লটে বসাও
        for slot in current_set:
            if not slot["valid"]:
                slot["valid"] = True
                slot["tag"] = tag
                return set_index, tag, "MISS"

        # সব স্লট ভর্তি হলে সরল নিয়মে প্রথম স্লটটি প্রতিস্থাপন করো (L40-এ real replacement policy আসবে)
        current_set[0]["tag"] = tag
        return set_index, tag, "MISS"

cache = NWaySetAssociativeCache(NUM_SETS, WAYS)

# L38-এর ঠিক একই conflict address sequence -- মোট লাইন সংখ্যা একই (৮টি), শুধু organization পাল্টেছে
conflict_sequence = [3, 11, 3, 11, 3, 11]

print(f"২-way set-associative cache -- {NUM_SETS}টি সেট, প্রতি সেটে {WAYS}টি লাইন (মোট {TOTAL_LINES}টি লাইন, L38-এর সমান)")
print()
print("address | set | tag | ফলাফল")
print("-" * 32)
results = []
for addr in conflict_sequence:
    set_index, tag, result = cache.access(addr)
    results.append(result)
    print(f"{addr:>7} | {set_index:>3} | {tag:>3} | {result}")

miss_count = results.count("MISS")
hit_count = results.count("HIT")
print()
print(f"মোট {len(conflict_sequence)} বার অ্যাক্সেসে {miss_count} বার MISS, {hit_count} বার HIT।")
print("L38-এ একই sequence-এ ৬/৬ MISS হয়েছিল -- এখানে দুটি address একই সেটে গেলেও")
print("সেটের ভেতরে ২টি ওয়ে থাকায় দুজনেই সহাবস্থান করতে পারছে -- প্রথমবারের পর সবই HIT।")

    
মোট লাইন সংখ্যা (৮টি) দুই পাঠেই একই — শুধু সংগঠন ভিন্ন। L38-এ ৬/৬ মিস হয়েছিল, এখানে মাত্র ২/৬ মিস (শুধু প্রথমবার প্রতিটি address-এর জন্য) — বাকি ৪টি অ্যাক্সেসই হিট। এটাই associativity বাড়ানোর বাস্তব সুফল, কোনো হার্ডওয়্যার বাড়ানো ছাড়াই — শুধু organization পাল্টে।
মূল কথা · Key takeaway

associativity যত বেশি, conflict miss তত কম — কিন্তু হার্ডওয়্যার তুলনা-লজিকও তত বেশি। বাস্তব CPU-গুলো সাধারণত ২-way থেকে ১৬-way-এর মধ্যে কোথাও থামে — সম্পূর্ণ ফুলি-অ্যাসোসিয়েটিভ পর্যন্ত সাধারণত যায় না, কারণ ব্যয়টা সুবিধার তুলনায় অনেক বেশি হয়ে যায়। কোন লাইন evict হবে তা এখনো সরল নিয়মে ঠিক করা হয়েছে (প্রথম স্লট) — পরের পাঠে (L40) একটি বাস্তব রিপ্লেসমেন্ট পলিসি (LRU) ও রাইট পলিসি শেখা হবে।

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

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

প্র ০১ যদি ফুলি-অ্যাসোসিয়েটিভ ক্যাশ conflict miss পুরোপুরি দূর করে দেয়, তাহলে সব CPU কেন ফুলি-অ্যাসোসিয়েটিভ ব্যবহার করে না?

কারণ হিট/মিস যাচাই করতে প্রতিটি লাইনের ট্যাগ একসাথে তুলনা করতে হয় — এই তুলনা-হার্ডওয়্যার (parallel comparator) লাইন সংখ্যার সাথে সরাসরি বাড়ে। মাত্র কয়েকশো লাইনের একটি বড় L2/L3 ক্যাশে এটা এত বেশি ট্রানজিস্টর ও পাওয়ার লাগাবে যে ব্যবহারিকভাবে অচল হয়ে যাবে। তাই বাস্তবে N-way সেট-অ্যাসোসিয়েটিভ (ছোট N) দিয়ে conflict অনেকাংশে কমিয়ে একটা ব্যবহারিক ভারসাম্য রাখা হয়।

প্র ০২ কোড সেলে দেখা ২-way ক্যাশ যদি আরও বড় associativity (যেমন ৪-way) হতো, conflict সমস্যাটা কি আরও কম হতো?

এই নির্দিষ্ট উদাহরণে দুটোমাত্র address (৩ ও ১১) সংঘর্ষ করছিল, তাই ২-way-ই যথেষ্ট ছিল সমস্যা সমাধানে — ৪-way হলেও একই ফলাফল আসত। কিন্তু বাস্তবে যদি একই সেটে তিন বা তার বেশি ব্যস্ত ব্লক প্রতিযোগিতা করত, তখন ২-way যথেষ্ট হতো না (তৃতীয় ব্লকটা প্রথম দুটোর একটিকে evict করে দিত) — সেক্ষেত্রে বেশি associativity (৪-way, ৮-way) সত্যিই আরও conflict কমাত।

প্র ০৩ কোড সেলে সেট ভর্তি হয়ে গেলে "প্রথম স্লট প্রতিস্থাপন করো" — এই নিয়মে কোনো বাস্তব সমস্যা আছে কি?

হ্যাঁ, এটা এখনো একটা সরলীকরণ — যদি প্রথম স্লটের ব্লকটাই আসলে সবচেয়ে বেশি ব্যবহৃত হতো (এখনই দরকার হতে পারত), তাহলে এই নিয়ম সেটাকে অযথা evict করে ফেলবে, যেখানে অন্য স্লটের ব্লকটা হয়তো অনেকদিন ব্যবহৃতই হয়নি। এই সমস্যাটাই সমাধান করে একটা বুদ্ধিমান রিপ্লেসমেন্ট পলিসি — যেমন LRU (Least Recently Used) — যা পরের পাঠে (L40) বাস্তবায়ন করা হবে।

অনুশীলন

  1. চিন্তা করুন: একটি ১৬-লাইনের ক্যাশকে ৪-way সেট-অ্যাসোসিয়েটিভ হিসেবে সংগঠিত করলে মোট কয়টি সেট হবে?

    সেট সংখ্যা = মোট লাইন ÷ ওয়ে = ১৬ ÷ ৪ = ৪টি সেট, প্রতিটি সেটে ৪টি করে লাইন। একটি মেমরি ব্লক ৪টি সেটের একটিতে (address mod 4 দিয়ে) ম্যাপ হবে, তারপর সেই সেটের ৪টি লাইনের যেকোনোটিতে বসতে পারবে।

  2. পরীক্ষা করুন: কোড সেলে যদি তৃতীয় একটি address 19 (যা ৩ mod 4 = 3-এর সাথে একই সেটে পড়ে, যেহেতু 19 mod 4 = 3) যোগ করে [3, 11, 19, 3, 11, 19] চালানো হতো, ফলাফল কেমন হতো ভাবুন (কোড পরিবর্তন করবেন না)?

    তিনটি address (3, 11, 19) সবাই একই সেটে (set_index=3) পড়ে, কিন্তু সেটে মাত্র ২টি ওয়ে আছে। প্রথম তিনটি অ্যাক্সেসই MISS হবে (3, 11 বসবে দুই স্লটে, তারপর 19 আসলে সেট ভর্তি থাকায় কোডের সরল নিয়ম অনুযায়ী প্রথম স্লট প্রতিস্থাপিত হবে, address 3-কে হারিয়ে) — এরপর 3 আবার আসলে সেটা আবার MISS হবে, কারণ তাকে সবেমাত্র evict করা হয়েছে। এটাই দেখায় associativity সীমিত হলে যথেষ্ট বেশি প্রতিযোগী ব্লক থাকলে conflict এখনো ফিরে আসতে পারে — শুধু কম ঘটে।

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

আগের পাঠ
ক্যাশ মেমরি বেসিকস — ডিরেক্ট ম্যাপড