Foundationপ্রথম নীতি থেকে
LEVEL 3লেসন ৮/১৭কঠিন১ ঘণ্টা ৫ মিনিট

Cache Organization — বিশাল ঠিকানাকে ছোট array-তে গুঁজে দেওয়া

Cache Organization

একটা cache বিশাল address space-কে ছোট array-তে map করে address-কে tag+index+offset-এ ভেঙে — direct-mapped (এক ঠিকানা = এক line, সস্তা কিন্তু conflict-prone), fully-associative (যেকোনো line, conflict নেই কিন্তু ব্যয়বহুল parallel search), আর মাঝামাঝি, বাস্তবে সবচেয়ে ব্যবহৃত set-associative।

এই লেসন শেষে আপনি পারবেন

  • একটা memory address কীভাবে tag, index, আর offset — এই তিন ভাগে বিভক্ত হয় সেটা বিট-গণনা দিয়ে derive করতে পারবেন
  • Direct-mapped cache-এর গঠন ও conflict সমস্যা concrete সংখ্যা দিয়ে ব্যাখ্যা করতে পারবেন
  • Fully-associative cache কীভাবে conflict এড়ায়, আর কেন সেটা বড় cache-এ ব্যবহারিক নয় সেটা বুঝবেন
  • N-way set-associative cache-কে direct-mapped আর fully-associative-এর মধ্যবর্তী একটা স্পেকট্রাম হিসেবে দেখতে পারবেন, নিজে একটা 4-way উদাহরণের bit-split হিসাব করতে পারবেন
  • একই cache size-এ associativity বাড়ালে/কমালে tag/index/offset বিটের সংখ্যা কীভাবে বদলায় সেটা predict করতে পারবেন
  • একটা বাস্তব cache miss কীভাবে ট্রেস করে দেখাবেন — address থেকে hit/miss সিদ্ধান্ত পর্যন্ত

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

গত লেসনে আমরা দেখেছি cache কেন আছে — locality of reference exploit করে গড় access time কমানোর জন্য। কিন্তু একটা প্রশ্ন এড়িয়ে গিয়েছিলাম: cache যখন একটা memory ঠিকানা পায়, সেটা কীভাবে জানে ডেটাটা তার কাছে আছে কি না — আর যদি থাকে, ঠিক কোথায়?

সংখ্যাটা একটু ভাবুন। একটা 64-বিট ঠিকানা মানে 2^64 সম্ভাব্য location — কার্যত অসীম। অথচ একটা L1 cache মাত্র 32 KB, যেটা 64-বাইট line হিসেবে গুনলে মাত্র 512-টা “slot”। এই বিশাল space-কে এই ক্ষুদ্র array-তে map করতে হবে, আর সেটা করতে হবে প্রতি cycle-এ — cache-এর পুরো সুবিধাটাই নষ্ট হয়ে যাবে যদি “এটা cache-এ আছে কি না” যাচাই করতেই কয়েকশ cycle লাগে।

সমাধানটা লেসন ৭-এর (digital-logic module-এর) দুইটা পুরনো পরিচিত hardware building block-এর মধ্যেই আছে — decoder আর multiplexer, ঠিক যেভাবে DRAM row/column addressing-এ ব্যবহৃত হয়েছিল। কিন্তু cache-এর সমস্যাটা DRAM addressing-এর চেয়ে একটু আলাদা: DRAM-এ প্রতিটা bit-এর একটা নির্দিষ্ট, স্থায়ী ঠিকানা আছে (row+column সরাসরি সেই bit-কে নির্দেশ করে)। Cache-এ উল্টো — cache-এর প্রতিটা slot সময়ের সাথে বদলে যেতে পারে কোন memory ঠিকানার ডেটা ধরে রাখছে। এই লেসনের মূল বিষয়: কীভাবে একটা বিশাল, পরিবর্তনশীল mapping-কে দ্রুত hardware দিয়ে বাস্তবায়ন করা যায়।

মূল ধারণা

Address-কে তিন ভাগে ভাঙা — tag, index, offset

প্রতিটা cache design একটা memory address-কে তিনটা অংশে ভাগ করে দেখে:

  • Offset — cache line-এর ভেতরে কোন বাইট, সেটা নির্দেশ করে।
  • Index — cache-এর কোন slot (বা slot-এর group) এই ঠিকানা ব্যবহার করবে, সেটা নির্দেশ করে।
  • Tag — সেই slot-এ ঠিক কোন memory ঠিকানার ডেটা আছে, সেটা নিশ্চিত করার জন্য বাকি সব বিট।

একটা concrete উদাহরণ দিয়ে derive করা

ধরুন 32-বিট address, cache আকার 4 KB (4096 বাইট), line আকার 64 বাইট, direct-mapped (প্রতি address-এর জন্য ঠিক একটা সম্ভাব্য line — এই লেসনের পরের অংশে বিস্তারিত)।

ধাপ ১ — Offset বিট: একটা line-এ 64 বাইট আছে, তাই সেই 64টা বাইটের মধ্যে একটাকে চিহ্নিত করতে লাগবে: log2(64)=6 বিট\log_2(64) = 6 \text{ বিট}

ধাপ ২ — মোট line সংখ্যা, তাই index বিট: cache-এ মোট line সংখ্যা: cache আকারline আকার=409664=64টা line\frac{\text{cache আকার}}{\text{line আকার}} = \frac{4096}{64} = 64 \text{টা line}

64-টা line-এর একটাকে চিহ্নিত করতে লাগবে: log2(64)=6 বিট\log_2(64) = 6 \text{ বিট}

ধাপ ৩ — বাকি সব বিট tag: 326(offset)6(index)=20 বিট32 - 6 (\text{offset}) - 6 (\text{index}) = 20 \text{ বিট}

যাচাই: 20 + 6 + 6 = 32 — ঠিক address-এর প্রস্থের সমান। ✓

   বিট:  31                      12  11        6  5         0
        ┌──────────────────────────┬────────────┬────────────┐
        │      tag (20 বিট)         │ index (6)  │ offset (6) │
        └──────────────────────────┴────────────┴────────────┘
         "কোন memory location"      "কোন line"    "line-এর কোন বাইট"
32-বিট address-এর bit split — 4KB direct-mapped cache, 64-বাইট line।

Direct-mapped cache — প্রতি ঠিকানার একটাই সম্ভাব্য বাড়ি

Direct-mapped cache-এ প্রতিটা memory address-এর জন্য ঠিক একটা cache line সম্ভব — index বিট সরাসরি বলে দেয় কোনটা। Lookup প্রক্রিয়া:

  1. Address-কে tag/index/offset-এ ভাঙা হয়।
  2. Index দিয়ে সরাসরি (কোনো তুলনা ছাড়াই) সেই একটা line select করা হয় — এটাই decoder-এর কাজ, ঠিক DRAM row-select-এর মতো।
  3. সেই line-এ সংরক্ষিত tag-এর সাথে address-এর tag তুলনা করা হয় — একটা মাত্র comparator
  4. যদি মেলে এবং সেই line “valid” (একটা valid bit থাকে, cold-start-এ সব line invalid) — তাহলে hit, offset দিয়ে সঠিক বাইট বের করা হয়।
  5. না মিললে — miss। নিচের memory স্তর থেকে পুরো line আনতে হয়, cache-এ বসাতে হয় (পুরনো যা ছিল তা মুছে), তারপর data দেওয়া হয়।
                 address (32 বিট)
        ┌──────────┬──────────┬──────────┐
        │   tag     │  index   │  offset  │
        │ (20 বিট)  │ (6 বিট)  │ (6 বিট)  │
        └────┬─────┘└────┬─────┘└────┬────┘
             │            │            │
             │      ┌─────▼─────┐      │
             │      │  index দিয়ে │     │
             │      │  ঠিক ১টা    │     │
             │      │  line select│     │
             │      └─────┬─────┘      │
             │            │            │
             │     ┌──────▼──────┐     │
             │     │ line-এ সংরক্ষিত │  │
             │     │ tag + valid বিট │  │
             │     └──────┬──────┘     │
             │            │            │
             └───►[ = ? ]◄┘            │
                    │ comparator (১টা)  │
              ┌─────▼─────┐            │
              │ match+valid│           │
              │  → HIT     │           │
              │  না মিললে   │           │
              │  → MISS    │           │
              └────────────┘           │
                                   line-এর ভেতরে
                                   offset দিয়ে বাইট বাছাই
Direct-mapped lookup — index সরাসরি একটা line বেছে নেয়, tag শুধু যাচাই করে।

Conflict-এর সমস্যা — concrete উদাহরণ

Direct-mapped-এর সমস্যা তার নামেই লুকানো — প্রতিটা address-এর জন্য একটাই সম্ভাব্য line। দুইটা ভিন্ন, ঘনঘন-ব্যবহৃত ঠিকানা যদি একই index-এ পড়ে (কিন্তু আলাদা tag), তারা একে অপরকে বারবার উৎখাত (evict) করতে থাকবে — এটাকে বলে thrashing

মনে করুন উপরের 4 KB, 64-বাইট-line cache-এ দুইটা int array আছে, প্রতিটাতে 1024 element (4096 বাইট করে):

int a[1024];  /* ঠিকানা শুরু হয় 0x10000 থেকে ধরুন */
int b[1024];  /* ঠিকানা শুরু হয় 0x11000 থেকে — a-এর ঠিক পরে */

0x11000 - 0x10000 = 0x1000 = 4096 — অর্থাৎ b-এর base ঠিকানা a-এর base ঠিকানা থেকে ঠিক cache আকারের সমান দূরত্বে। যেহেতু 4096 = 2^12 আর index+offset মিলিয়ে মাত্র 12 বিট (6+6), 4096 যোগ করলে সেই 12 বিটের কোনোটাই বদলায় না — শুধু tag বদলায়:

a[i]-এর ঠিকানা:  ... tag=T1 ... index=I ... offset=O
b[i]-এর ঠিকানা:  ... tag=T2 ... index=I ... offset=O   ← একই index, শুধু tag ভিন্ন!

এখন কল্পনা করুন একটা loop যা পালাক্রমে a[i] আর b[i] access করছে:

for (int i = 0; i \< 1024; i++) {
    sum += a[i] + b[i];
}

a[i] access করলে সেই index-এ a-এর line বসে (tag=T1)। পরের মুহূর্তেই b[i] access — একই index, কিন্তু tag=T2, মিলছে না — miss, a-এর line উৎখাত হয়ে b-এর line বসে। পরের iteration-এ a[i+1] — যদিও সেটা নতুন offset-এর জন্য হয়তো একই line-এই থাকত (spatial locality থাকলে), কিন্তু b ইতিমধ্যে সেই line দখল করে ফেলেছে — আবার miss। ফলাফল: প্রতিটা access-ই miss, যদিও প্রতিটা array আলাদা করে পুরোপুরি cache-এ আঁটত (4096 বাইট, cache-এর সমান আকার), আর দুটো মিলিয়েও (8192 বাইট) মাত্র cache আকার — তুলনায় নগণ্য working set।

Fully-associative cache — যেকোনো জায়গায়, কিন্তু সবখানে খুঁজতে হয়

বিপরীত চরম ডিজাইন: fully-associative cache-এ কোনো index-ই নেই — একটা address যেকোনো line-এ থাকতে পারে। Address ভাগ হয় শুধু tag + offset-এ (উপরের উদাহরণে: tag 26 বিট, offset 6 বিট, 26+6=32 ✓ — index বিট শূন্য)।

সুবিধা: উপরের conflict সমস্যা সম্পূর্ণ অদৃশ্য — a আর b একই “index”-এর জন্য প্রতিদ্বন্দ্বিতা করছে না, কারণ কোনো নির্দিষ্ট index নেই যেখানে তাদের যেতেই হবে। যেকোনো দুটো ঠিকানা cache-এর যেকোনো দুইটা (বা একই সময়ে বেশি) ভিন্ন line-এ থাকতে পারে, যতক্ষণ পুরো cache না ভরে।

কিন্তু দাম: index না থাকায় hardware জানে না কোথায় খুঁজবে — তাই প্রতিটা line-এর tag-এর সাথে address-এর tag তুলনা করতে হয়, একযোগে (parallel), একটা cycle-এই। এর মানে N-টা line-এর cache-এ N-টা comparator লাগবে।

              address → tag (26 বিট) + offset (6 বিট)

        ┌──────────┬──────────┬──────────┬───┬──────────┐
        │  line 0  │  line 1  │  line 2  │...│  line 63 │
        │  tag=?   │  tag=?   │  tag=?   │   │  tag=?   │
        └────┬─────┴────┬─────┴────┬─────┴───┴────┬─────┘
             │           │           │              │
          [=?]        [=?]        [=?]     ...   [=?]     ← ৬৪টা comparator, একসাথে
             └───────────┴─────OR───┴──────────────┘

                    কোনো একটাতে match+valid → HIT
Fully-associative lookup — সব line-এর tag একসাথে, সমান্তরালে তুলনা করা হয়।

Set-associative cache — বাস্তবসম্মত মাঝামাঝি

এখানেই আসল ইঞ্জিনিয়ারিং সমাধান — N-way set-associative। ধারণাটা direct-mapped আর fully-associative-এর মধ্যে একটা spectrum তৈরি করে:

  • Cache-এর line-গুলোকে ছোট ছোট group বা set-এ ভাগ করা হয়, প্রতিটা set-এ ঠিক N-টা line (N-কে বলে “way”)।
  • একটা address-এর index বিট বলে দেয় সে কোন set-এ যাবে (direct-mapped-এর মতো — নির্দিষ্ট)।
  • কিন্তু সেই set-এর ভেতরে address N-টা line-এর যেকোনো একটাতে থাকতে পারে (fully-associative-এর মতো — নমনীয়, তবে শুধু সেই set-এর মধ্যে)।
  • Lookup-এ শুধু সেই একটা set-এর N-টা line-এর tag তুলনা করতে হয় — N-টা comparator, পুরো cache-এর সবগুলো না।

একটা concrete 4-way উদাহরণ

একই 4 KB cache, 64-বাইট line, কিন্তু এবার 4-way set-associative

ধাপ ১ — মোট line সংখ্যা অপরিবর্তিত: 409664=64টা line\frac{4096}{64} = 64 \text{টা line}

ধাপ ২ — set সংখ্যা: প্রতিটা set-এ 4-টা line থাকলে: 64 line4 way=16টা set\frac{64 \text{ line}}{4 \text{ way}} = 16 \text{টা set}

ধাপ ৩ — index বিট: 16-টা set-এর একটা চিহ্নিত করতে: log2(16)=4 বিট\log_2(16) = 4 \text{ বিট}

ধাপ ৪ — offset অপরিবর্তিত: 6 বিট (line আকার একই)।

ধাপ ৫ — tag বাকি সব বিট: 324(index)6(offset)=22 বিট32 - 4 (\text{index}) - 6 (\text{offset}) = 22 \text{ বিট}

যাচাই: 22 + 4 + 6 = 32 ✓। আর সঞ্চয় ক্ষমতাও মেলে: 16 set × 4 way × 64 বাইট = 4096 বাইট = 4 KB ✓।

Direct-mapped (1-way):    64 line,  64 set  → tag=20  index=6  offset=6
4-way set-associative:    64 line,  16 set  → tag=22  index=4  offset=6
Fully-associative (64-way): 64 line, 1 set  → tag=26  index=0  offset=6

                     tag বিট বাড়ছে ──────────►
   ◄────────── index বিট কমছে (associativity বাড়ার সাথে)
একই 4KB cache, তিন ভিন্ন associativity — index বিট কমে, tag বিট বাড়ে, সঞ্চয় ক্ষমতা অপরিবর্তিত।

একই conflict উদাহরণ, এবার 4-way-তে

আগের a[i]/b[i] thrashing উদাহরণটা 4-way cache-এ ফিরিয়ে আনি। a-এর আর b-এর ঠিকানা এখনও 4096 বাইট দূরত্বে, তাই তারা এখনও একই set-এ পড়বে (কারণ 4096 = 2^12, আর index+offset মিলে এখনও 10 বিট (4+6), যা 2^10=1024-এর সীমার মধ্যে repeat করে — 4096-এর প্যাটার্ন এখনও index অপরিবর্তিত রাখে)। কিন্তু এবার সেই set-এ 4-টা line আছে — a[i] একটা way দখল করে, b[i] আরেকটা way দখল করে, দুটোই একই সাথে সেই set-এ থাকতে পারে, একে অপরকে উৎখাত না করেই। Thrashing সম্পূর্ণ চলে গেছে — শুধু associativity বাড়ানোর কারণে, কোনো code পরিবর্তন ছাড়াই।

Skew-associative — associativity-র সুবিধা, কম হার্ডওয়্যার খরচে

ভেতরে কী ঘটছে

Lookup hardware ভেতর থেকে — set-associative-এর পূর্ণ পথ

একটা 4-way set-associative cache-এ একটা address আসার পর ঠিক কী ঘটে, ধাপে ধাপে:

                       address (32 বিট)
        ┌──────────────┬──────────┬──────────┐
        │  tag (22 বিট)  │ index(4) │ offset(6)│
        └──────┬───────┘└────┬─────┘└────┬────┘
               │              │            │
               │       ┌──────▼──────┐     │
               │       │ index দিয়ে   │     │
               │       │ ১টা SET বাছাই │     │
               │       │ (16 সেটের    │     │
               │       │  একটা)       │     │
               │       └──────┬──────┘     │
               │              │            │
               │    ┌─────────▼─────────┐  │
               │    │  সেই set-এর ৪টা line │ │
               │    │  way0 way1 way2 way3│ │
               │    │  tag তুলনা (৪টা      │ │
               │    │  comparator, সমান্তরাল)│ │
               │    └─────────┬─────────┘  │
               └──────────────┤             │
                        ┌──────▼──────┐     │
                        │ কোনো way-তে  │     │
                        │ match+valid  │     │
                        │  → HIT       │     │
                        │  না হলে       │     │
                        │  → MISS      │     │
                        └──────────────┘     │
                                         hit হলে offset
                                         দিয়ে বাইট বাছাই
4-way set-associative lookup — index দিয়ে ১টা set বাছাই, তারপর সেই set-এর ৪টা line-এর tag সমান্তরাল তুলনা।

ধাপে ধাপে:

  1. Address আসে, তিন ভাগে বিভক্ত হয় (হার্ডওয়্যার শুধু বিট-পজিশন অনুযায়ী wire আলাদা করে — কোনো গণনা লাগে না, এটা প্রায় বিনামূল্যে)।
  2. Index বিট একটা ছোট decoder-এ যায় (4-to-16) — ঠিক একটা set select হয়। এটা DRAM row decoder-এরই ছোট সংস্করণ।
  3. সেই set-এর 4-টা line-এর সংরক্ষিত tag একসাথে পড়া হয়।
  4. 4-টা comparator সমান্তরালে address-এর tag-এর সাথে তুলনা করে — প্রতিটার সাথে সেই line-এর valid bit-ও AND করা হয় (invalid line কখনো match হতে পারবে না, এমনকি tag কাকতালীয়ভাবে মিললেও)।
  5. 4-টা তুলনার ফলাফল একটা OR-এ যায় — যেকোনো একটা true হলে hit।
  6. Hit হলে, যেই way-তে match হয়েছে তার ডেটা একটা mux দিয়ে বাছাই করা হয়, তারপর offset দিয়ে ভেতরের সঠিক বাইট।

এই পুরো প্রক্রিয়া — decode, parallel compare, OR, mux — একটা একক, পাইপলাইনড hardware ধাপে ঘটে, সাধারণত 1-4 cycle-এর মধ্যে (এই কারণেই L1 latency ~4 cycle)। কোনো “খোঁজা” (searching) নেই, লেসন ৭-এর কোনো sequential loop নেই — সবকিছু সমান্তরাল combinational logic।

Miss হলে কী হয় — replacement-এর প্রথম আভাস

যদি miss হয়, নতুন ডেটা নিচের memory স্তর থেকে আনতে হবে। কিন্তু cache (বা সেই set) যদি ইতিমধ্যে ভরা থাকে, একটা পুরনো line-কে জায়গা করে দিতে উৎখাত (evict) করতে হবে। Direct-mapped-এ এই সিদ্ধান্ত তুচ্ছ — শুধু একটাই সম্ভাব্য line, সেটাই যাবে। কিন্তু set-associative-এ (N > 1 way) — সেই set-এর N-টা line-এর মধ্যে কোনটা উৎখাত হবে, সেই সিদ্ধান্তটাই একটা স্বতন্ত্র প্রশ্ন — এটাই replacement policy, যেটা পরের লেসনের প্রধান বিষয়।

Way prediction — associativity-র latency খরচ আংশিক এড়ানো

Set-associative lookup-এ একটা সূক্ষ্ম দাম আছে — parallel tag compare-এর ফলাফল (কোন way-তে hit) জানার পরেই সঠিক way-র ডেটা mux দিয়ে বাছাই করা যায়। এই “compare তারপর mux” ক্রম একটা ছোট বাড়তি delay যোগ করে, direct-mapped-এর তুলনায় (যেখানে কোনো mux লাগে না, ডেটা সরাসরি একটা line থেকে)।

কিছু উচ্চ-performance CPU design way prediction ব্যবহার করে এই delay কমাতে — একটা ছোট predictor আগেভাগেই অনুমান করে কোন way-তে hit হতে পারে (আগের access pattern দেখে), আর সেই অনুমানের ডেটা speculatively আগেই পড়া শুরু করে, tag compare সম্পূর্ণ হওয়ার আগেই। যদি অনুমান সঠিক হয় (বেশিরভাগ সময়), latency প্রায় direct-mapped-এর কাছাকাছি নেমে আসে। ভুল হলে, একটা ছোট penalty দিয়ে সঠিক way থেকে আবার পড়তে হয়। এটা এই module-এর পরবর্তী একটা বড় থিমের (branch prediction — ভুল হতে পারে এমন একটা দ্রুত অনুমান, ভুল হলে সংশোধনের penalty) একটা ছোট, cache-স্তরের পূর্বাভাস।

Cache line কেন এত গুরুত্বপূর্ণ একটা একক

লক্ষ্য করুন cache কখনো একটা মাত্র বাইট বা word আনে না — সবসময় পুরো line (এই উদাহরণে 64 বাইট) আনে, ব্যবহার হোক বা না হোক বাকি বাইটগুলো। এটা ঠিক আগের লেসনের DRAM word-oriented organization-এর একই যুক্তি — যেহেতু row/line access করার “fixed cost” আছে (address decode, sense amplify), একসাথে বেশি ডেটা আনা প্রায় বিনামূল্যে, আর spatial locality নিশ্চিত করে সেই বাড়তি বাইটগুলো শীঘ্রই কাজে লাগবে। Cache line-এর আকার তাই একটা design trade-off: বড় line spatial locality বেশি কাজে লাগায় কিন্তু miss-এ বেশি বাইট আনতে হয় (বেশি bandwidth খরচ) আর কম useful ডেটার সাথে বেশি অপ্রয়োজনীয় বাইট আসার ঝুঁকি (false sharing-এর মূল, যেটা advanced-architecture module-এ multi-core প্রসঙ্গে ফিরে আসবে)।

উদাহরণ

সংখ্যায় সম্পূর্ণ ট্রেস — একটা L1 cache, বাস্তব স্পেসিফিকেশন

ধরুন একটা বাস্তবসম্মত L1 data cache: 32 KB, 8-way set-associative, 64-বাইট line, 48-বিট virtual address (আধুনিক x86-64-এ ব্যবহৃত প্রকৃত address প্রস্থ, যদিও পুরো 64-বিট register ব্যবহার হয়)।

হিসাব:

মোট line=32,768 বাইট64 বাইট=512টা line\text{মোট line} = \frac{32{,}768 \text{ বাইট}}{64 \text{ বাইট}} = 512 \text{টা line}

set সংখ্যা=5128=64টা set\text{set সংখ্যা} = \frac{512}{8} = 64 \text{টা set}

offset বিট=log2(64)=6\text{offset বিট} = \log_2(64) = 6 index বিট=log2(64)=6\text{index বিট} = \log_2(64) = 6 tag বিট=4866=36\text{tag বিট} = 48 - 6 - 6 = 36

যাচাই: 36 + 6 + 6 = 48 ✓। সঞ্চয় ক্ষমতা: 64 \text{ set} \times 8 \text{ way} \times 64 \text{ বাইট} = 32{,}768 বাইট = 32 KB ✓।

বিট পরিসরঅংশমান
বিট 4712tag36 বিট
বিট 116index6 বিট (64 সম্ভাব্য set)
বিট 50offset6 বিট (64 সম্ভাব্য বাইট)

একটা নির্দিষ্ট ঠিকানার সম্পূর্ণ ট্রেস

ধরুন CPU ঠিকানা 0x00007FFE_2A48_1064 access করছে (একটা সাধারণ ৪৮-বিট virtual address, উদাহরণস্বরূপ)।

Hexadecimal-কে binary-তে দেখলে শেষ কয়েকটা byte grouped:

ঠিকানা (hex):  ... 2A48 1064

       নিচের ১২ বিট (৩ hex digit) বের করি: 0x064 = 0000 0110 0100 (বাইনারি)

       offset (নিচের ৬ বিট):  100100  = 0x24 = 36 (দশমিক)
       index  (তার পরের ৬ বিট): 000001 = 0x01 = 1 (দশমিক)
       tag    (বাকি উপরের ৩৬ বিট): ঠিকানার বাকি অংশ

অর্থ: এই access set নাম্বার 1-এ যাবে। সেই set-এর 8-টা way-র tag-এর সাথে ঠিকানার tag-অংশ তুলনা হবে। যদি কোনো way-তে match+valid পাওয়া যায়, hit — সেই line-এর অফসেট 36-তম বাইট থেকে ডেটা পড়া হবে (36-তম বাইট থেকে শুরু করে, যত বাইট instruction চায় — 1, 2, 4, বা 8 বাইট)।

একটা cache lookup — address থেকে hit/miss পর্যন্ত
  1. address আসে0x00007FFE2A481064, 48 বিট
  2. বিভাজনtag(36) | index(6)=1 | offset(6)=36
  3. set selectindex=1 → set #1, ৬৪টার মধ্যে একটা
  4. parallel tag compareset #1-এর ৮টা way, ৮টা comparator একসাথে
  5. hit/miss সিদ্ধান্তকোনো way-তে match+valid থাকলে hit, নাহলে miss → নিচের স্তরে যাও
  6. data বাছাইhit হলে: offset=36 দিয়ে সেই line-এর ভেতরের বাইট বাছাই

নিজে চালিয়ে দেখুন

EXPERIMENT

Conflict miss হাতেকলমে তৈরি করুন — stride দিয়ে thrash

Python 3 বা C (C-তে পার্থক্য অনেক তীক্ষ্ণ)· ২০ মিনিট

ধারণা: দুইটা array বানাই যাদের মধ্যে দূরত্ব ইচ্ছাকৃতভাবে L1 cache আকারের একটা গুণিতক করে রাখি, তারপর তাদের পালাক্রমে access করে conflict miss আদায় করি — বনাম দূরত্ব সামান্য বদলে (যাতে তারা ভিন্ন set-এ পড়ে) একই experiment আবার চালাই।

#include \<stdio.h\>
#include \<stdlib.h\>
#include \<time.h\>

#define L1_SIZE (32 * 1024)   /* আপনার মেশিনের প্রকৃত L1 আকার lscpu দিয়ে যাচাই করুন */
#define N 100000

double time_pair(char *a, char *b, long stride_a, long stride_b) {
    struct timespec t0, t1;
    volatile long sum = 0;
    clock_gettime(CLOCK_MONOTONIC, &t0);
    for (int i = 0; i \< N; i++) {
        sum += a[(i * stride_a) % L1_SIZE];
        sum += b[(i * stride_b) % L1_SIZE];
    }
    clock_gettime(CLOCK_MONOTONIC, &t1);
    return (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1e9;
}

int main(void) {
    /* conflicting: b ঠিক L1_SIZE দূরে বসানো, দুই array-ই একটা বড় buffer-এর অংশ */
    char *buf_conflict = malloc(L1_SIZE * 2);
    char *a1 = buf_conflict;
    char *b1 = buf_conflict + L1_SIZE;      /* ঠিক L1 আকারের দূরত্বে — একই set-এ পড়বে */

    /* non-conflicting: b সামান্য offset করে বসানো (অর্ধেক cache line, ভিন্ন set) */
    char *buf_ok = malloc(L1_SIZE * 2 + 32);
    char *a2 = buf_ok;
    char *b2 = buf_ok + L1_SIZE + 32;       /* L1_SIZE + অর্ধেক line — ভিন্ন index */

    double t_conflict = time_pair(a1, b1, 64, 64);
    double t_ok = time_pair(a2, b2, 64, 64);

    printf("conflicting stride:     %.4f সেকেন্ড\\n", t_conflict);
    printf("non-conflicting stride: %.4f সেকেন্ড\\n", t_ok);
    printf("অনুপাত: %.2fx\\n", t_conflict / t_ok);

    free(buf_conflict);
    free(buf_ok);
    return 0;
}

প্রত্যাশিত ফলাফলের ধরন (মেশিনভেদে বদলাবে):

conflicting stride:     0.0850 সেকেন্ড
non-conflicting stride: 0.0210 সেকেন্ড
অনুপাত: 4.05x

Direct-mapped-এর মতো L1 বাস্তব CPU-তে বিরল (বেশিরভাগ 8-way বা তার বেশি), তাই পার্থক্যটা তাত্ত্বিক direct-mapped উদাহরণের মতো নাটকীয় (কার্যত ০% hit) হবে না — associativity কিছুটা রক্ষা করবে। কিন্তু যদি একাধিক array একসাথে একই set-এ ভিড় করে (way সংখ্যার চেয়ে বেশি প্রতিযোগী), তখনও পরিমাপযোগ্য পার্থক্য দেখা যাবে। perf stat -e cache-misses ./a.out দিয়ে সরাসরি miss সংখ্যা মিলিয়ে দেখুন।

এটা কী প্রমাণ করে

দুইটা array যাদের base ঠিকানা cache আকারের গুণিতক দূরত্বে, তাদের পালাক্রমে access করলে conflict miss তৈরি হয় — এমনকি মোট working set cache-এর চেয়ে ছোট হলেও।

EXPERIMENT

Set distribution visualizer — address-এর index কীভাবে ছড়ায়

Python 3· ১৫ মিনিট

এই ছোট simulation address-এর একটা তালিকা নিয়ে (কোনো real hardware লাগবে না) দেখায় সেগুলো 64-টা set-এ কীভাবে বিতরণ (distribute) হয় — একটা সুস্থ ব্যবহারে হিস্টোগ্রাম প্রায় সমান হওয়া উচিত।

def index_of(addr, num_sets=64, offset_bits=6):
    return (addr >> offset_bits) % num_sets


# ভালো case: সাধারণ sequential heap allocation, বিভিন্ন আকারের বস্তু
import random
random.seed(0)
good_addrs = []
cur = 0x10000
for _ in range(2000):
    good_addrs.append(cur)
    cur += random.choice([16, 24, 32, 48, 64])   # বাস্তবসম্মত allocation আকার

# খারাপ case: ইচ্ছাকৃতভাবে সব address ঠিক 4096 বাইট (= cache size) ব্যবধানে
bad_addrs = [0x10000 + i * 4096 for i in range(2000)]

def histogram(addrs, num_sets=64):
    counts = [0] * num_sets
    for a in addrs:
        counts[index_of(a, num_sets)] += 1
    return counts

good_hist = histogram(good_addrs)
bad_hist = histogram(bad_addrs)

print(f"ভালো case  — সবচেয়ে বেশি ব্যবহৃত set: {max(good_hist)} বার, সবচেয়ে কম: {min(good_hist)} বার")
print(f"খারাপ case — সবচেয়ে বেশি ব্যবহৃত set: {max(bad_hist)} বার, সবচেয়ে কম: {min(bad_hist)} বার")

প্রত্যাশিত ফলাফলের ধরন:

ভালো case  — সবচেয়ে বেশি ব্যবহৃত set: 38 বার, সবচেয়ে কম: 24 বার
খারাপ case — সবচেয়ে বেশি ব্যবহৃত set: 2000 বার, সবচেয়ে কম: 0 বার

খারাপ case-এ একটা মাত্র set সবগুলো 2000-টা address-ই পেয়েছে (বাকি 63-টা set সম্পূর্ণ অব্যবহৃত থেকেছে) — এটা এই লেসনের conflict miss উদাহরণেরই একটা সাধারণীকৃত (generalized), পরিসংখ্যানগত সংস্করণ। ভালো case-এ বিতরণ প্রায় সমান, কারণ allocation আকার বৈচিত্র্যময় হওয়ায় address-এর নিচের বিটগুলো (যেগুলো index নির্ধারণ করে) কার্যত এলোমেলো আচরণ করে। এটাই দেখায় কেন বাস্তব cache design-এ শুধু associativity যথেষ্ট না — address-থেকে-index mapping-টাও (এই ক্ষেত্রে সহজ modulo) access pattern-এর সাথে “ভালোভাবে মেশে” এটা নিশ্চিত করা জরুরি।

এটা কী প্রমাণ করে

'ভালো' access pattern (স্বাভাবিক sequential বা random malloc address) set-গুলোর মধ্যে মোটামুটি সমানভাবে ছড়িয়ে পড়ে; কিন্তু ইচ্ছাকৃতভাবে power-of-2 stride ব্যবহার করলে সবকিছু মাত্র কয়েকটা set-এ জমা হয়ে যায়।

নিজে বানান

BUILD IT

Address Splitter — যেকোনো cache config-এর জন্য tag/index/offset হিসাব

Python · ●●○○○
  1. Cache আকার, line আকার, associativity (way সংখ্যা), আর address প্রস্থ ইনপুট নিন
  2. Offset, index, tag বিট সংখ্যা গণনা করুন এবং assert দিয়ে যাচাই করুন যোগফল address প্রস্থের সমান কিনা
  3. একটা নির্দিষ্ট hex address দিয়ে তার tag/index/offset মান বের করুন
  4. একই cache আকারে associativity 1 থেকে পূর্ণ (fully-associative) পর্যন্ত বাড়িয়ে index/tag বিট কীভাবে বদলায় তার একটা টেবিল ছাপান
import math

def split_address(cache_size, line_size, ways, addr_width):
    total_lines = cache_size // line_size
    assert cache_size % line_size == 0, "cache size line size দিয়ে ভাগযোগ্য হতে হবে"
    assert total_lines % ways == 0, "line সংখ্যা way সংখ্যা দিয়ে ভাগযোগ্য হতে হবে"

    num_sets = total_lines // ways
    offset_bits = int(math.log2(line_size))
    index_bits = int(math.log2(num_sets)) if num_sets > 1 else 0
    tag_bits = addr_width - offset_bits - index_bits

    assert tag_bits + index_bits + offset_bits == addr_width, "বিট যোগফল মিলছে না!"
    return {
        "total_lines": total_lines,
        "num_sets": num_sets,
        "offset_bits": offset_bits,
        "index_bits": index_bits,
        "tag_bits": tag_bits,
    }


def decode_address(addr_hex, cache_size, line_size, ways, addr_width):
    info = split_address(cache_size, line_size, ways, addr_width)
    addr = int(addr_hex, 16)
    offset = addr & ((1 \<\< info["offset_bits"]) - 1)
    index = (addr \>\> info["offset_bits"]) & ((1 \<\< info["index_bits"]) - 1)
    tag = addr \>\> (info["offset_bits"] + info["index_bits"])
    return tag, index, offset


# ── উদাহরণ ১: 32KB, 8-way, 64B line, 48-বিট address ──
info = split_address(32 * 1024, 64, 8, 48)
print(f"32KB/8-way/64B: {info}")

tag, index, offset = decode_address("7FFE2A481064", 32 * 1024, 64, 8, 48)
print(f"address 0x7FFE2A481064 → tag={hex(tag)} index={index} offset={offset}")

# ── উদাহরণ ২: একই cache আকারে associativity spectrum ──
print(f"\\n{'way সংখ্যা':\<12}{'set সংখ্যা':\<12}{'index বিট':\<12}{'tag বিট':\<10}")
for ways in [1, 2, 4, 8, 16, 32, 64]:
    info = split_address(4096, 64, ways, 32)
    print(f"{ways:\<12}{info['num_sets']:\<12}{info['index_bits']:\<12}{info['tag_bits']:\<10}")

প্রত্যাশিত আউটপুট (উদাহরণ ২-এর টেবিল, 4 KB / 64B line / 32-বিট address):

way সংখ্যা    set সংখ্যা     index বিট     tag বিট
1             64            6             20
2             32            5             21
4             16            4             22
8             8             3             23
16            4             2             24
32            2             1             25
64            1             0             26

লক্ষ্য করুন — way × set = ৬৪ (মোট line সংখ্যা) প্রতিটা সারিতে অপরিবর্তিত, আর প্রতিবার way দ্বিগুণ হলে index বিট ঠিক কমছে, tag বিট বাড়ছে — এটাই এই লেসনের spectrum দাবির সংখ্যাগত প্রমাণ, নিজের কোডে যাচাই করা।

বাস্তব সিস্টেমে

বাস্তব সিস্টেমে cache organization

১. বাস্তব CPU-র associativity সংখ্যা

আধুনিক CPU-তে L1 সাধারণত --way, L2 -১৬-way, L3 প্রায়ই ১৬-way বা তার বেশি। Associativity সাধারণত hierarchy-তে নিচের দিকে বাড়ে — কারণ নিচের স্তর বড়, বেশি set প্রতিযোগী সহ্য করতে হয়, আর latency budget-ও একটু শিথিল (L3-এর ~40 cycle বাজেটে বেশি comparator চালানো সাশ্রয়ী)।

২. TLB — fully-associative-এর একটা বাস্তব উদাহরণ

operating-systems module-এ (Level 4) TLB (Translation Lookaside Buffer) নিয়ে বিস্তারিত পড়বেন — এটা virtual-to-physical address translation cache করে। TLB সাধারণত ছোট (কয়েক ডজন থেকে কয়েকশ entry) এবং প্রায়ই fully-associative বা উচ্চ-associativity — কারণ ছোট entry সংখ্যায় fully-associative-এর comparator খরচ সহনীয়, আর TLB miss-এর দাম (একটা page table walk) এত বেশি যে conflict miss এড়ানো অগ্রাধিকার পায়।

৩. Victim cache — একটা সৃজনশীল হাইব্রিড

কিছু CPU design একটা ছোট, fully-associative “victim cache” ব্যবহার করে যা সদ্য L1 থেকে উৎখাত হওয়া line-গুলো সাময়িকভাবে ধরে রাখে। এটা direct-mapped বা low-associativity L1-এর conflict miss সমস্যা কমায় fully-associative-এর পূর্ণ খরচ ছাড়াই — শুধু সাম্প্রতিক উৎখাত হওয়া কয়েকটা line-এর জন্য।

৪. Cache line size-এর বাস্তব মান

প্রায় সব আধুনিক x86-64 এবং ARM64 CPU ৬৪-বাইট cache line ব্যবহার করে (কিছু পুরনো বা বিশেষায়িত ডিজাইনে ৩২ বা ১২৮ বাইট)। এই সংখ্যাটা sysconf বা /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size (Linux) দিয়ে সরাসরি দেখা যায় — এই লেসনের উদাহরণে ব্যবহৃত ৬৪ বাইট তাই একটা বাস্তব, প্রায়-সার্বজনীন মান, নির্বিচার সংখ্যা না।

৫. Direct-mapped-এর বাস্তব ব্যবহার — খুব ছোট, বিশেষায়িত cache

যদিও L1/L2/L3-এ direct-mapped বিরল, ছোট, বিশেষায়িত cache-এ (যেমন কিছু embedded system-এর instruction cache, বা branch prediction-এর branch target buffer) direct-mapped এখনও ব্যবহৃত হয় — কারণ hardware সরলতা আর গতি (index থেকে সরাসরি line, কোনো tag-comparison delay ছাড়াই প্রাথমিক prediction) কখনো কখনো conflict miss-এর ঝুঁকির চেয়ে বেশি গুরুত্বপূর্ণ।

৬. Cache size বাছাইয়ে associativity-র প্রভাব — hit rate গবেষণা

Hennessy-Patterson-এর classic গবেষণায় দেখানো হয়েছে associativity 1 থেকে 2-এ যাওয়া (direct-mapped থেকে 2-way) hit rate-এ প্রায়ই cache আকার দ্বিগুণ করার সমান উন্নতি আনে (একটা rule-of-thumb, workload-নির্ভর) — এই কারণেই বাস্তব design-এ প্রায় কখনোই বড় cache-এ direct-mapped দেখা যায় না, associativity-র হার্ডওয়্যার খরচ এই সুবিধার তুলনায় সাশ্রয়ী প্রমাণিত হয়েছে।

৭. GPU cache — ভিন্ন trade-off

GPU-র L1/L2 cache সাধারণত CPU-র তুলনায় ছোট associativity রাখে, কারণ GPU workload (massively parallel, প্রায়ই streaming-ধরনের access) conflict miss-এর প্রতি কম সংবেদনশীল আর bandwidth অগ্রাধিকার পায় latency-র চেয়ে — advanced-architecture module-এ (Level 11) GPU architecture নিয়ে বিস্তারিত আসবে।

৮. Cache simulator — এই লেসনের ধারণাগুলো নিজে যাচাই করার টুল

dinero, cachegrind (Valgrind-এর অংশ), বা এই curriculum-এর নিজস্ব “Cache Simulator” প্রজেক্ট (curriculum-এ তালিকাভুক্ত) একটা memory trace নিয়ে বিভিন্ন associativity-তে hit/miss rate সিমুলেট করে — এই লেসনের tag/index/offset arithmetic হুবহু সেই simulator-এর ভেতরের কোর যুক্তি।

যে ভুলগুলো সবাই করে

“Associativity বাড়ানো সবসময় বিনামূল্যে একটা win — তাই সব cache fully-associative হওয়া উচিত”

Associativity বাড়ানো মানে প্রতি lookup-এ বেশি সমান্তরাল comparator চালানো — hardware area, power খরচ, আর প্রায়ই সামান্য বাড়তি latency (বেশি comparator-এর ফলাফল একত্র করতে বেশি logic লাগে)। এই কারণেই L1 (যেখানে latency সবচেয়ে সংবেদনশীল, ~4 cycle বাজেট) associativity মাঝারি রাখে (4-8-way), যেখানে fully-associative শুধু ছোট, বিশেষায়িত cache-এ (TLB-এর মতো) ব্যবহারিক। এটা একটা trade-off, না কোনো একমুখী “যত বেশি তত ভালো” নিয়ম।

“Index বিট address-এর কোনো নির্দিষ্ট, বিশেষ বিট — সেগুলো আলাদাভাবে সংরক্ষিত বা চিহ্নিত থাকে”

Index (এবং offset, tag) কোনো বিশেষ, আলাদাভাবে চিহ্নিত বিট না — এরা শুধু address-এর সাধারণ বিট, যাদের hardware কীভাবে ব্যাখ্যা করে তার ভিত্তিতে বিভক্ত করা হয়। একই address, ভিন্ন cache configuration-এ (ভিন্ন আকার/associativity) সম্পূর্ণ ভিন্নভাবে বিভক্ত হবে — এই লেসনের “Address Splitter” build-এ ঠিক এটাই দেখা গেছে, একই address-এর index/tag বিট সংখ্যা associativity বদলানোর সাথে সাথে বদলে যাচ্ছে।

“Cache miss মানেই ভুল বা অদক্ষ প্রোগ্রাম”

প্রতিটা প্রোগ্রামের প্রথম access (cold start), যেকোনো নতুন ডেটা structure-এর প্রথম access — এগুলো অনিবার্যভাবে miss হবে, প্রোগ্রামিং কতই ভালো হোক না কেন (এই ধরনের miss-কে বলা হয় compulsory miss)। এই লেসনের conflict miss (associativity-জনিত) আরেকটা ভিন্ন প্রকার, যেটা এড়ানো সম্ভব design/access-pattern পরিবর্তন দিয়ে। বাস্তবে miss-এর তিনটা শ্রেণীবিভাগ প্রচলিত — compulsory, capacity (working set cache-এর চেয়ে বড়), আর conflict (এই লেসনে দেখা) — একে বলা হয় “3C” model। সব miss সমান “দোষ” না — কিছু অনিবার্য, কিছু design-নির্ভর।

বুঝেছেন কি না দেখুন

1

একটা 16 KB direct-mapped cache, 32-বাইট line। Offset, index, আর tag বিট সংখ্যা কত (ধরুন 32-বিট address)?

স্মরণ

Offset: log2(32) = 5 বিট। মোট line = 16384/32 = 512, তাই index: log2(512) = 9 বিট। Tag: 32 - 5 - 9 = 18 বিট। যাচাই: 18+9+5=32 ✓।

2

একই 16 KB cache, 32-বাইট line, কিন্তু এখন 4-way set-associative। নতুন offset, index, tag বিট সংখ্যা কত? আগেরটার সাথে তুলনা করে কী প্যাটার্ন দেখছেন?

প্রয়োগ

মোট line অপরিবর্তিত: 512। Set সংখ্যা = 512/4 = 128, index = log2(128) = 7 বিট (আগে ছিল 9)। Offset অপরিবর্তিত 5 বিট। Tag = 32-7-5=20 বিট (আগে ছিল 18)। প্যাটার্ন: associativity 4× বাড়ায় (1-way থেকে 4-way) index ঠিক 2 বিট কমেছে (log2(4)=2), আর tag ঠিক 2 বিট বেড়েছে — এটাই এই লেসনের spectrum নীতির সরাসরি সংখ্যাগত প্রকাশ।

3

দুইটা ঠিকানা যাদের মধ্যে দূরত্ব ঠিক cache-এর মোট আকারের সমান (যেমন 4 KB cache-এ 4096 বাইট দূরত্ব), direct-mapped cache-এ কেন সবসময় একই line-এ সংঘর্ষ করবে?

যুক্তি

কারণ index+offset বিট একসাথে ঠিক log2(\text{cache size}) বিট নির্ধারণ করে (এই উদাহরণে log2(4096)=12 বিট)। যদি দুইটা ঠিকানা ঠিক 4096 বাইট (= 2^12) দূরত্বে থাকে, তাদের বিয়োগফলের নিচের 12 বিট শূন্য — অর্থাৎ index+offset অংশ হুবহু অভিন্ন, শুধু tag (উপরের বিট) ভিন্ন হয়। যেহেতু direct-mapped-এ index-ই একমাত্র নির্ধারক কোন line ব্যবহার হবে, index অভিন্ন মানে line-ও অভিন্ন — অনিবার্য conflict।

4

Fully-associative cache-এ কোনো “index” নেই, তবু hardware কীভাবে জানে ডেটা cache-এ আছে কি না? Direct-mapped-এর তুলনায় এই পদ্ধতির hardware খরচ কেমন?

যুক্তি

Fully-associative-এ প্রতিটা line-এর সংরক্ষিত tag একসাথে, সমান্তরালে address-এর tag-এর সাথে তুলনা করা হয় — কোনো index দিয়ে আগেই একটামাত্র line বেছে নেওয়া হয় না। এর মানে N-line cache-এ N-টা পূর্ণ-প্রস্থ comparator লাগে, প্রতিটা একসাথে সক্রিয়। Direct-mapped-এ মাত্র -টা comparator লাগে (index দিয়ে আগেই একটা line নির্ধারিত)। তাই fully-associative hardware খরচে (comparator সংখ্যা, area, power) N-এর সমানুপাতিক, direct-mapped-এ constant — এই পার্থক্যই বড় cache-এ fully-associative অব্যবহারিক করে তোলে।

5

একটা 8 MB L3 cache, 64-বাইট line, 16-way set-associative। Set সংখ্যা আর (ধরে 48-বিট address) tag বিট সংখ্যা বের করুন।

প্রয়োগ

মোট line = 8{,}388{,}608 / 64 = 131{,}072। Set সংখ্যা = 131{,}072 / 16 = 8{,}192। Index বিট = log2(8192) = 13। Offset বিট = log2(64) = 6। Tag বিট = 48 - 13 - 6 = 29। যাচাই: 29+13+6=48 ✓, আর সঞ্চয় ক্ষমতা 8192 × 16 × 64 = 8{,}388{,}608 বাইট = 8 MB ✓।

6

একটা চিপ ডিজাইনার প্রস্তাব দিলেন একটা নতুন L1 cache, একই আকার আর line size বজায় রেখে, কিন্তু direct-mapped-এর বদলে fully-associative করার — যুক্তি: “conflict miss পুরোপুরি বাদ যাবে, hit rate বাড়বে।” এই প্রস্তাবের সম্ভাব্য সমস্যা কী, আর একটা বিকল্প কী প্রস্তাব করবেন?

ডিজাইন

সমস্যা: L1-এর জন্য latency budget সবচেয়ে কড়া (~4 cycle) — fully-associative-এর জন্য প্রয়োজনীয় শত শত সমান্তরাল comparator L1-এর ছোট, দ্রুত আকারের সাথে বেমানান; এত বেশি comparator চালাতে গিয়ে হয় latency বেড়ে যাবে (আর L1 আর “L1-গতির” থাকবে না), অথবা area/power বাজেট বহুগুণ বেড়ে যাবে। বিকল্প: hit rate বাড়ানোর জন্য conflict miss কমাতে associativity মাঝারি মাত্রায় বাড়ানো (যেমন direct-mapped থেকে - বা -way set-associative) — এতে conflict miss উল্লেখযোগ্যভাবে কমবে, কিন্তু per-set comparator সংখ্যা মাত্র বা (পুরো cache-এর সব line না), যা L1-এর latency বাজেটের মধ্যেই সহনীয়। এটাই বাস্তবে প্রায় সব CPU-র L1 design-এর পছন্দ।

এরপর কী

আমরা এখন জানি একটা cache কীভাবে একটা address-কে দ্রুত hit/miss-এ পরিণত করে, আর কেন associativity একটা spectrum, কোনো তিনটা আলাদা প্রযুক্তি না। কিন্তু দুইটা প্রশ্ন এখনও বাকি: set-associative cache-এ যখন একটা set পূর্ণ থাকে আর নতুন line আনতে হয়, কোন line উৎখাত হবে সেই সিদ্ধান্ত কে নেয় আর কীভাবে? আর যখন CPU cache-এ লেখে (write করে), সেই পরিবর্তন কখন, কীভাবে নিচের memory স্তরে পৌঁছায়?

পরের লেসনে (Cache Policies) আমরা replacement policy (LRU, FIFO, random) আর write policy (write-through, write-back, write-allocate) নিয়ে বিস্তারিত আলোচনা করব, আর সবশেষে একটা একক সূত্রে (AMAT) দেখব কীভাবে hit rate-এর একটা ছোট পরিবর্তনও পুরো সিস্টেমের গড় গতিতে নাটকীয় প্রভাব ফেলতে পারে।

আরও পড়ুন

  • Computer Organization and Design (RISC-V Edition), Chapter 5 — David A. Patterson, John L. Hennessy · Cache mapping, associativity, আর address split-এর প্রামাণ্য আলোচনা
  • Computer Architecture: A Quantitative Approach, Chapter 2 (Appendix B) — John L. Hennessy, David A. Patterson · Cache design স্পেসের বিস্তারিত quantitative বিশ্লেষণ
  • What Every Programmer Should Know About Memory, Section 3 — Ulrich Drepper · Associativity আর real cache geometry-র ব্যবহারিক আলোচনা