সরাসরি পর্বে যান
পর্ব ৪৬ · ৬২-এর মধ্যে · মডিউল ৮

Set-associative আর fully associative cache — replacement policy

Set-Associative and Fully Associative Caches — Conflict Misses, Address Split, Parallel Tag Lookup, CAM, LRU, Pseudo-LRU, FIFO, Random and Belady's OPT

Taught in Bangla, this lesson covers set-associative and fully associative caches: conflict misses, the placement spectrum from direct-mapped to n-way to fully associative, how the address split changes with associativity, parallel tag comparators, n:1 muxes and CAM, replacement with LRU, pseudo-LRU trees, FIFO, random, NRU and Belady's OPT, LRU's cyclic trap, way prediction, victim caches and VIPT L1 caches, with a simulator and worked comparisons.

এই পর্বে যা শিখবেন

  • Conflict miss কেন হয়, আর direct-mapped থেকে n-way set-associative হয়ে fully associative পর্যন্ত placement-এর পুরো বর্ণালি
  • একই আকারের cache-এ 1, 2, 4, 8-way আর fully associative-এ tag-index-offset-এর বিট, মোট tag বিট আর comparator-এর সংখ্যা
  • 4-way lookup-এর hardware — parallel tag তুলনা, valid বিট, one-hot hit আর n:1 mux; CAM আর তার দাম
  • পাঠ্যবইয়ের তুলনা: 0, 8, 0, 6, 8 ক্রমে direct-mapped, 2-way আর fully associative-এ ৫, ৪ আর ৩টি miss — হাতে আর simulator-এ
  • LRU, pseudo-LRU tree, FIFO, random, NRU, Belady-র OPT, LRU-র চক্রাকার ফাঁদ, victim cache, way prediction, VIPT L1 — আর একটি সত্যিকার L1-এর way গোনা Wandbox-এ

১ · Conflict miss — direct-mapped cache-এর দুর্বলতা

পর্ব ৪৫-এ (Cache-এর মৌলিক কথা) দেখেছেন, একটি direct-mapped cache-এ মেমরির প্রতিটি block-এর জন্য ঘর মাত্র একটি: ঘর = block address mod (ঘরের সংখ্যা)। Byte address তিন ভাগে ভাঙে — | tag | index | block offset | — index ঘর বাছে, ঘরে রাখা tag মিললে আর valid bit ১ হলে hit. Miss তিন রকম (3 C's): compulsory (প্রথমবার ছোঁয়া), capacity (cache-এ এত ডেটা ধরেই না) আর conflict (জায়গা ছিল, কিন্তু ভুল ঘরে ভিড়)। এই পর্ব তৃতীয়টি নিয়ে — আর তার সমাধান থেকে জন্ম নেওয়া প্রশ্ন: ঘর ভরা থাকলে কাকে বাদ দেবেন?

একটি ছোট উদাহরণ ভাবুন। ৪টি ঘরের direct-mapped cache, আর একটি লুপ পালা করে block 0 আর block 8 পড়ছে (যেমন দুটি array-র একই অবস্থানের উপাদান, যাদের দূরত্ব cache-এর আকারের গুণিতক)। 0 mod 4 = 0, 8 mod 4 = 0 — দুজনেরই ঘর ০. প্রতিবার একজন অন্যজনকে বের করে দেয়, প্রতিটি প্রবেশ miss, অথচ ঘর ১, ২, ৩ খালি পড়ে আছে। Cache-এর আকার দোষী নয় — দোষী নিয়মটি, যা প্রতিটি block-কে একটিমাত্র ঘরে বেঁধে রেখেছে। একে বলে thrashing (দুই বা ততোধিক block-এর পালা করে একে অপরকে তাড়ানো)।

একটি মহল্লার গলিতে ৪টি পার্কিংয়ের জায়গা, আর নিয়ম: গাড়ির নম্বরের শেষ অঙ্ক দেখে জায়গা ঠিক — নম্বর ÷ ৪-এর ভাগশেষ ০ হলে জায়গা ০. দুই প্রতিবেশীর গাড়ির নম্বর ১২০ আর ২০৮ — দুজনেরই জায়গা ০. তাঁরা প্রতিদিন একে অপরকে সরাতে বলেন, যদিও বাকি তিনটি জায়গা খালি। সহজ সমাধান: নিয়ম শিথিল করা — “ভাগশেষ জোড় হলে বাঁ দিকের দুটি জায়গার যেকোনোটিতে” (2-way), বা “যেখানে খুশি” (fully associative). শিথিল নিয়মের দাম: গাড়ি খুঁজতে এখন একটির বদলে দুটি বা সবগুলো জায়গা দেখতে হয় — আর সব ভরা থাকলে কাকে সরাতে বলবেন, সেই নিয়মও লাগে।

মূল ধারণা

Associativity = একটি block কতগুলো ঘরের যেকোনোটিতে বসতে পারে। বেশি বিকল্প মানে conflict miss কম, কিন্তু খোঁজার সময় বেশি ঘর দেখা (বেশি hardware, বেশি শক্তি) আর একটি replacement policy-র দরকার।

২ · Placement-এর বর্ণালি — direct-mapped, n-way আর fully associative

একটি cache-এ মোট B টি block-এর ঘর (cache line). এগুলোকে S টি set-এ ভাগ করা হয়, প্রতিটি set-এ n টি ঘর — প্রতিটি ঘরকে বলে একটি way. তাই B = S × n. নিয়ম:

set = (block address) mod S,   S = B ÷ n

Block address মানে byte address ÷ block size (offset বাদ দেওয়া অংশ)। Block টি সেই set-এর যেকোনো way-তে বসতে পারে। এই এক সূত্রেই পুরো বর্ণালি আসে — এমন cache-কে বলে n-way set-associative cache, আর n-কে বলে associativity:

সংগঠন (৪টি block-এর cache)n (way)S (set)block 8 কোথায় বসতে পারেখুঁজতে কতগুলো tag দেখা
Direct-mapped১৪set 8 mod 4 = 0-এর একমাত্র ঘরে১
2-way set-associative২২set 8 mod 2 = 0-এর দুটি ঘরের যেকোনোটিতে২
4-way = fully associative৪১যেকোনো ঘরে (একটিই set)৪

অর্থাৎ direct-mapped হলো “১-way set-associative” আর fully associative cache হলো “B-way, ১টি set”. বাস্তবের প্রায় সব cache মাঝখানে — 2, 4, 8, 12 বা 16-way.

পাঠ্যবইয়ের তুলনা: 0, 8, 0, 6, 8

Patterson–Hennessy-র বইয়ের বিখ্যাত উদাহরণটি এখানে হাতে করা যাক। ৪টি block-এর cache, শুরুতে খালি, block address-এর ক্রম 0, 8, 0, 6, 8; set-associative অংশে replacement LRU (যে block সবচেয়ে বেশি সময় অব্যবহৃত, তাকে বাদ)। Set নির্ণয়: direct-mapped-এ mod 4 — 0 → 0, 8 → 0, 6 → 2; 2-way-তে mod 2 — তিনটিই set 0; fully associative-এ set একটিই। “পরে” চেপে তিনটি cache পাশাপাশি দেখুন:

1# 4-block cache, empty at start, LRU2read block 03read block 84read block 05read block 66read block 87# misses: 5 / 4 / 3
তিনটি cache, একই ক্রম
Block
DM: set (mod 4)
DM: ফল
DM: ঘরগুলো
2-way: set (mod 2)
2-way: ফল
2-way: set 0
FA: ফল
FA: ঘরগুলো
Miss (DM / 2 / FA)
  1. Block 0. তিনটি cache-ই খালি — তিনটিতেই compulsory miss. DM-এ set 0, 2-way-তে set 0-এর প্রথম way, FA-তে প্রথম খালি ঘর।
  2. Block 8. আবার তিনটিতেই miss (প্রথম ছোঁয়া)। কিন্তু DM-এ 8-এর একমাত্র ঘর set 0, যেখানে 0 বসে আছে — 0-কে বের করে দিতে হলো। 2-way আর FA-তে খালি ঘর ছিল, তাই 0 আর 8 দুজনেই রইল।
  3. Block 0 আবার। DM-এ 0 নেই (8 তাকে তাড়িয়েছে) — miss, আর এবার 8 বের হলো। এটি খাঁটি conflict miss: cache-এ ৩টি ঘর খালি। 2-way আর FA-তে hit.
  4. Block 6. তিনটিতেই প্রথম ছোঁয়া — miss. DM-এ 6 যায় set 2-এ, কাউকে তাড়ায় না। 2-way-তে 6 mod 2 = 0 — set 0 ভরা [0, 8]; সবচেয়ে বেশি সময় অব্যবহৃত 8 (শেষ ব্যবহার দ্বিতীয় প্রবেশে), তাই LRU 8-কে বাদ দিল। FA-তে খালি ঘর আছে।
  5. Block 8 আবার। DM-এ set 0-তে 0 আছে — miss, 0 বাদ। 2-way-তে 8 আগেই বাদ গেছে — miss; এখন set 0-তে [0, 6], LRU 0 (শেষ ব্যবহার তৃতীয় প্রবেশে) — 0 বাদ। FA-তে 8 এখনো আছে — hit.
  6. হিসাব। Miss: direct-mapped ৫, 2-way ৪, fully associative ৩. ৩টি block আলাদা, তাই ৩টি compulsory miss অনিবার্য — FA ঠিক সেটুকুই করেছে। DM-এর বাড়তি ২টি আর 2-way-এর বাড়তি ১টি conflict miss.

লক্ষ করুন, 2-way-এর ৫ নম্বর প্রবেশে miss হলো কারণ তিনটি block-ই একই set-এ পড়েছে — set-এ ঘর মাত্র ২টি। Associativity বাড়ালে conflict কমে, কিন্তু set-এর ভেতরে ঘর ভরা থাকলে কাকে বাদ দেওয়া হবে, তা-ও ফলকে বদলে দেয়। এই দুটি প্রশ্নই বাকি পর্বের বিষয়।

৩ · Associativity বাড়লে address-এর ভাগ কীভাবে বদলায়

Byte-addressed মেমরি, ৩২-বিট address (Module 8-এর সব উদাহরণের নিয়ম)। Block size 2b byte হলে offset = b বিট। Set-এর সংখ্যা S = 2s হলে index = s বিট। বাকি সব tag:

tag = 32 − index − offset,   index = log2(cache size ÷ (block size × n))

যুক্তি: একই set-এ যে block-গুলো পড়ে, তাদের block address-এর নিচের s বিট এক (কারণ mod 2s মানে নিচের s বিট)। তাই set-এর ভেতরে আলাদা করে চেনার জন্য বাকি উঁচু বিটগুলো রাখতে হয় — সেটাই tag. মোট আকার আর block size স্থির রেখে n দ্বিগুণ করলে S অর্ধেক, s এক কমে; যে বিটটি index থেকে বেরোল, সেটি এখন set চেনাতে পারে না — তাই tag-এ যোগ হয়। Fully associative-এ S = 1, index = 0 — offset ছাড়া পুরো address-ই tag.

একটি সাধারণ মাপ ধরুন: ৩২ KiB data, ৬৪ B block — মোট ৩২৭৬৮ ÷ ৬৪ = ৫১২টি ঘর, offset = ৬ বিট। পাঁচটি সংগঠনে (সংখ্যাগুলো scratch script-এ মেলানো):

সংগঠনSet (S)tag │ index │ offsetমোট tag বিট (512 × tag)Tag comparator
1-way (direct-mapped)51217 │ 9 │ 68704১টি
2-way25618 │ 8 │ 69216২টি
4-way12819 │ 7 │ 69728৪টি
8-way6420 │ 6 │ 610240৮টি
Fully associative126 │ 0 │ 613312৫১২টি

তিনটি জিনিস লক্ষ করার মতো। এক, way দ্বিগুণ হলে প্রতিটি tag ১ বিট লম্বা, তাই মোট tag storage প্রতি ধাপে ৫১২ বিট বাড়ে। দুই, comparator-এর সংখ্যা n-এর সমান, আর প্রতিটি আরও চওড়া। তিন, 8-way-এর ঘরে index + offset = ৬ + ৬ = ১২ বিট — ঠিক একটি ৪ KiB page-এর offset-এর সমান। এই কাকতালীয়-মনে-হওয়া মিলটি আসলে নকশার ইচ্ছাকৃত সিদ্ধান্ত; অংশ ৮-এ কারণ দেখবেন।

পরীক্ষার সাধারণ ভুল: “8-way মানে index ৩ বিট” — না। Way-এর সংখ্যা address-এর কোনো ক্ষেত্র নয়; way বাছাই হয় tag মিলিয়ে, address-এর বিট দিয়ে নয়। log2(n) বিট কমে index থেকে। আরেকটি ভুল: cache size-এ tag আর valid বিট গোনা। এই কোর্সে “cache size” মানে শুধু data; tag-সহ হিসাব চাইলে প্রশ্নে “মোট বিট” লেখা থাকবে।

৪ · Hardware — n টি tag একসাথে তুলনা, n:1 mux আর CAM

Direct-mapped cache-এ index দিয়ে একটি ঘর পড়া হয়, একটি tag comparator বলে মিলল কি না। n-way-তে index দিয়ে একটি পুরো set পড়া হয় — n টি tag, n টি valid বিট আর (দ্রুত নকশায়) n টি data block, সবগুলো একই সময়ে। তারপর n টি comparator একসাথে কাজ করে। প্রতিটি comparator আসলে বিট ধরে ধরে XNOR আর সবগুলোর AND — অর্থাৎ ৩২-বিটের তুলনা নয়, শুধু tag-এর বিটগুলো। মিল আর valid-এর AND প্রতিটি way-এর জন্য একটি বিট দেয়; সঠিক নকশায় বড়জোর একটি ১ হয় (one-hot). এই n বিটের OR = hit, আর এরাই n:1 multiplexer-এর select — কোন way-এর data বেরোবে।

tag (19) index (7) offset (6) address set বাছাই set i: way0: V │ tag │ data way1: V │ tag │ data way2: V │ tag │ data way3: V │ tag │ data = = = = tag তুলনা &V &V &V &V h0 h1 h2 h3 (one-hot) OR hit 4:1 mux select data block → offset দিয়ে byte/word বাছাই
ছবি ৪৬.১ — 4-way lookup: index একটি set বাছে, চারটি tag একসাথে তুলনা হয়, মিল AND valid থেকে one-hot সংকেত — তার OR হলো hit, আর সেটিই 4:1 mux-কে বলে কোন way-এর data বেরোবে।

নিচের stepper-এ একটি সুনির্দিষ্ট উদাহরণ: ৩২ KiB, 4-way, ৬৪ B block (ভাগ ১৯ │ ৭ │ ৬). প্রথম load-এর address 0x00402A48: 0x00402A48 = 0x201 × 213 + 41 × 26 + 8, তাই tag 0x201, set 41, offset 8. দ্বিতীয় load 0x00752A70 একই set-এ পড়ে কিন্তু tag ভিন্ন:

1# 32 KiB, 4-way, 64 B: tag 19 │ index 7 │ offset 62lw x5, 0(x10)    # x10 = 0x00402A483set 41  way0: V=1 tag 0x1F34        way1: V=0 tag 0x201  (stale)5        way2: V=1 tag 0x2016        way3: V=1 tag 0x0C47lw x6, 0(x11)    # x11 = 0x00752A70
Lookup-এর সংকেত
Address
tag │ index │ offset
V (w0 w1 w2 w3)
tag মিলল?
h = মিল AND V
ফল
  1. Address ভাগ। 0x00402A48-এর নিচের ৬ বিট offset = 8, পরের ৭ বিট index = 41, ওপরের ১৯ বিট tag = 0x201. কোনো hardware এখানে “হিসাব” করে না — তার কেটে আলাদা করা মাত্র।
  2. Set পড়া। Index 41 দিয়ে tag array আর data array থেকে set 41-এর চারটি way একসাথে পড়া হলো: চারটি valid বিট (1 0 1 1), চারটি tag আর চারটি ৬৪-byte block. এখনো জানা নেই কোনটি দরকার — তাই সবগুলো পড়া, আর এখানেই শক্তির খরচ।
  3. চারটি তুলনা একসাথে। ঠিকানার tag 0x201 চারটি comparator-এ যায়। way0 (0x1F3) ≠, way1 (0x201) =, way2 (0x201) =, way3 (0x0C4) ≠. দুটি মিল! কিন্তু way1-এর valid বিট ০ — সেটি একটি পুরোনো, বাতিল ঘর, যার tag বিটগুলো মুছে ফেলা হয়নি।
  4. Valid দিয়ে ছাঁকা। hi = (tag মিলল) AND Vi: way1-এর মিল V = 0-এ বাতিল, থাকে শুধু way2 — h = 0 0 1 0, one-hot. OR = 1, তাই hit. Valid বিট না দেখলে দুটি way একসাথে “hit” বলত — ভুল data-র ঝুঁকি।
  5. Mux আর offset. h সরাসরি 4:1 mux-এর select: way2-এর block বেরিয়ে আসে, offset 8 থেকে ৪ byte (byte 8–11) যায় x5-এ। Mux টি comparator-এর পরে বসে — তাই direct-mapped-এর চেয়ে hit time বেশি।
  6. দ্বিতীয় load — miss. 0x00752A70: tag 0x3A9, index আবার 41, offset 48. চারটি comparator-এর কোনোটিই মেলে না — h = 0 0 0 0, OR = 0: miss. Cache controller পরের স্তর (L2) থেকে block আনতে অনুরোধ পাঠায়।
  7. কোথায় বসবে। Set 41-এ way1 invalid — খালি ঘর থাকলে কাউকে বাদ দিতে হয় না, সেখানেই নতুন block: V = 1, tag = 0x3A9. সব way valid থাকলে replacement policy বলত কোনটি যাবে (অংশ ৫)। তারপর replacement-এর অবস্থাও হালনাগাদ হয়।

দামের হিসাব — hit time, জায়গা আর শক্তি

  • Hit time: direct-mapped-এ data পড়া আর tag তুলনা সমান্তরালে চলে, ফলের data সরাসরি বেরোয় (ভুল হলে পরে ফেলে দেওয়া হয়)। n-way-তে data বেরোনোর আগে তুলনা শেষ হয়ে mux-এর select তৈরি হতে হয় — critical path-এ একটি comparator আর একটি n:1 mux যোগ হয়। n বাড়লে mux-এর স্তরও বাড়ে (মোটামুটি log2n স্তর)।
  • জায়গা: n টি comparator, আরও লম্বা tag (অংশ ৩-এর টেবিল), replacement-এর বিট, আর চওড়া mux.
  • শক্তি: প্রতিটি প্রবেশে n টি tag আর (দ্রুত নকশায়) n টি data block পড়া হয়, অথচ কাজে লাগে একটি। এই কারণে বড়, ধীর cache (L2, L3) প্রায়ই আগে শুধু tag পড়ে, মিল জানার পরে একটিমাত্র way-এর data পড়ে — এক cycle বেশি লাগে, কিন্তু শক্তি অনেক কম। L1 সময় বাঁচাতে দুটি একসাথে পড়ে।

Fully associative আর CAM

Fully associative-এ index নেই — প্রতিটি ঘরের tag-কে খোঁজা tag-এর সাথে তুলনা করতে হয়, অর্থাৎ প্রতিটি ঘরে একটি comparator. এটি বানানো হয় CAM (content-addressable memory) দিয়ে: সাধারণ SRAM-এ ঠিকানা দিলে data আসে; CAM-এ data (tag) দিলে বলে কোন সারিতে সেটি আছে। প্রতিটি CAM cell-এ রাখার বিটের পাশে তুলনার transistor থাকে, তাই সাধারণ ৬-transistor SRAM cell-এর চেয়ে বড় (প্রায় ৯–১০ transistor-এর নকশা প্রচলিত), আর প্রতিটি খোঁজে সব সারির match line একসাথে চার্জ-ডিসচার্জ হয় — শক্তিখেকো। তাই CAM শুধু ছোট কাঠামোয়: TLB (L1 TLB প্রায়ই কয়েক ডজন entry-র, অনেক নকশায় fully associative — পর্ব ৫০), victim cache (অংশ ৭), branch target buffer-এর কিছু রূপ, আর নেটওয়ার্ক router-এর lookup table. ৫১২ ঘরের fully associative L1 data cache প্রায় দেখা যায় না।

৫ · Replacement policy — ঘর ভরা থাকলে কাকে বাদ দেবেন

Direct-mapped-এ প্রশ্নটিই ওঠে না — ঘর একটিই। n-way-তে miss হলে, আর set-এর সব way valid থাকলে, একজনকে বের করতে হয় — যাকে বের করা হয় সে victim. এই নিয়মই replacement policy. আদর্শ নিয়ম জানা: যে block সবচেয়ে দেরিতে আবার লাগবে, তাকে বাদ দেওয়া। কিন্তু hardware ভবিষ্যৎ জানে না, তাই অতীত দেখে অনুমান করে।

LRU — সবচেয়ে বেশি সময় অব্যবহৃতকে বাদ

LRU (least recently used) temporal locality-র ওপর বাজি ধরে: যা অনেকক্ষণ ছোঁয়া হয়নি, তা সম্ভবত শিগগির লাগবে না। নিখুঁত LRU-র জন্য set-এর n টি block-এর পুরো “সাম্প্রতিকতার ক্রম” মনে রাখতে হয়। n টি block-এর সম্ভাব্য ক্রম n! টি, তাই অন্তত ⌈log2(n!)⌉ বিট লাগে — এর কমে সব ক্রম আলাদা করা অসম্ভব (pigeonhole নীতি)।

n (way)log2(n!) — নিখুঁত LRU-র ন্যূনতমপ্রতি way-এ বয়স-counter (n·log2n)জোড়া-তুলনার matrix (n(n−1)/2)PLRU tree (n − 1)
21.00 → ১ বিট211
44.58 → ৫ বিট863
815.30 → ১৬ বিট24287
1644.25 → ৪৫ বিট6412015

বিটের সংখ্যাই একমাত্র সমস্যা নয়। প্রতিটি hit-এও অবস্থা বদলাতে হয় (ক্রম নতুন করে সাজানো), আর ৫ বিটের ঘন encoding থেকে “কে সবচেয়ে পুরোনো” বের করার logic জটিল। তাই 2-way-তে নিখুঁত LRU সস্তা (১ বিট: “কোন way শেষে ছোঁয়া”), 4-way পর্যন্ত চলে, কিন্তু 8-way বা বেশিতে প্রায় সবাই অনুমিত LRU ব্যবহার করে।

Pseudo-LRU tree — n − 1 বিটে প্রায় LRU

Tree pseudo-LRU (PLRU) way-গুলোকে একটি binary tree-এর পাতা হিসেবে সাজায়। 4-way-তে ৩টি বিট: b1 (root: বাঁ জোড়া way0–1 নাকি ডান জোড়া way2–3), b2 (way0 নাকি way1), b3 (way2 নাকি way3). এই পর্বের নিয়ম: বিট ০ মানে victim বাঁ দিকে, ১ মানে ডানে। দুটি কাজ:

  • Victim খোঁজা: root থেকে বিট অনুযায়ী নিচে নামা। যেমন b1 b2 b3 = 1 0 0: root 1 → ডান জোড়া, b3 = 0 → বাঁ — victim way2.
  • প্রবেশে হালনাগাদ (hit বা নতুন block বসানো): ছোঁয়া way-এর পথের প্রতিটি বিটকে সেই way থেকে উল্টো দিকে ঘোরানো। way1 ছোঁয়া হলে: সে বাঁ জোড়ায়, তাই b1 = 1 (ডান দিক নির্দেশ); জোড়ার ভেতরে সে ডানে, তাই b2 = 0. b3 অপরিবর্তিত।

প্রতিটি প্রবেশে শুধু log2n টি বিট বদলায় — সরল, দ্রুত। কিন্তু এটি নিখুঁত LRU নয়: tree শুধু বলে “কোন অর্ধেক কম সাম্প্রতিক”, পুরো ক্রম নয়। নিচের stepper-এ একটি set-এ A B C D B E A F ক্রম (শুরুতে সব বিট ০, খালি way আগে ক্রমানুসারে ভরা হয়) — আর কোথায় PLRU নিখুঁত LRU-র থেকে আলাদা সিদ্ধান্ত নেয়:

1# one 4-way set; b1 = root, b2 = ways 0/1, b3 = ways 2/32A B C D   # fill ways 0, 1, 2, 33B         # hit in way 14E         # miss: PLRU victim?5A         # hit?6F         # miss: PLRU victim?
PLRU-র অবস্থা
প্রবেশ
ফল, way
b1 b2 b3
way0–3
এখন victim হতো
নিখুঁত LRU-র victim
  1. A → way0. খালি way থাকলে victim খোঁজা হয় না, প্রথম খালিটিতে বসে। তারপর way0-এর পথ ঘোরানো: b1 = 1 (ডান দিক নির্দেশ), b2 = 1 (জোড়ার ভেতরে ডানে, way1-এর দিকে)। বিট 1 1 0.
  2. B → way1. way1 বাঁ জোড়ায়, তাই b1 = 1 থাকে; জোড়ার ভেতরে সে ডানে, তাই b2 = 0. বিট 1 0 0.
  3. C → way2. ডান জোড়া: b1 = 0 (এবার বাঁ দিক নির্দেশ), জোড়ার ভেতরে বাঁয়ে, তাই b3 = 1. বিট 0 0 1.
  4. D → way3; set ভরা। b1 = 0, b3 = 0. বিট 0 0 0 — নামলে বাঁ, বাঁ: victim way0 (A). নিখুঁত LRU-ও A-কেই বলত (সবচেয়ে আগে ছোঁয়া)। এখানে দুজন একমত।
  5. B — hit. way1-এর পথ ঘোরানো: b1 = 1, b2 = 0. বিট 1 0 0 — এখন victim: root 1 → ডান জোড়া, b3 = 0 → way2 (C). কিন্তু সাম্প্রতিকতার আসল ক্রম (পুরোনো থেকে নতুন) A, C, D, B — নিখুঁত LRU-র victim A. PLRU-র tree “বাঁ জোড়ায় সদ্য ছোঁয়া হয়েছে” মনে রেখেছে, কিন্তু সেই জোড়ার A যে সবচেয়ে পুরোনো, তা ভুলে গেছে।
  6. E — miss. PLRU victim way2: C বেরোল, E বসল। নিখুঁত LRU হলে A বেরোত। E-কে বসানো মানে way2 ছোঁয়া: b1 = 0, b3 = 1 → বিট 0 0 1, পরের victim way0 (A).
  7. A — hit! PLRU-তে A টিকে ছিল, তাই hit; নিখুঁত LRU এখানে miss করত (A আগেই বাদ)। অর্থাৎ PLRU-র “ভুল” সিদ্ধান্ত এই ক্রমে আসলে লাভ দিল — অনুমিত নিয়ম কখনো ভালো, কখনো খারাপ। way0 ছোঁয়া: b1 = 1, b2 = 1 → 1 1 1, victim way3 (D).
  8. F — miss. Victim way3 (D) — এবার নিখুঁত LRU-ও D বলত (শেষ ব্যবহার সবচেয়ে আগে)। way3 ছোঁয়া: b1 = 0, b3 = 0 → 0 1 0, পরের victim way1 (B). ৩ বিটে ৫ বিটের কাজ প্রায় হয়ে যায়।

FIFO, random আর NRU

  • FIFO (first in, first out): যে সবচেয়ে আগে ঢুকেছে সে যাবে — hit-এ কিছু বদলায় না, তাই একটি ঘুরন্ত pointer-ই যথেষ্ট (log2n বিট)। দুর্বলতা: খুব ব্যস্ত block-ও তার পালা এলে বের হয়।
  • Random: একটি সস্তা pseudo-random generator (যেমন LFSR — কয়েকটি flip-flop আর XOR) দিয়ে victim বাছাই। কোনো অবস্থা মনে রাখতে হয় না। বড় cache-এ LRU-র সাথে পার্থক্য প্রায়ই ছোট বলে পাঠ্যবইয়ের মাপে দেখা যায়; কিছু ARM core-এর cache-এ pseudo-random replacement একটি বিকল্প হিসেবে ছিল বলে জানা যায়।
  • NRU (not recently used) বা clock-ধাঁচ: প্রতিটি way-এ একটি “ব্যবহৃত” বিট; ছোঁয়া হলে ১. Victim = প্রথম যে way-এর বিট ০; সবগুলো ১ হলে বাকিদের বিট মুছে আবার শুরু। n বিটে মোটা দাগের LRU — OS-এর page replacement-এর clock algorithm-এর হুবহু আত্মীয় (অপারেটিং সিস্টেম কোর্সের page replacement).

Belady-র OPT — যা কেউ বানাতে পারে না, কিন্তু সবাই মাপে

১৯৬৬ সালে IBM-এর Laszlo Belady virtual memory-র জন্য replacement নিয়ে একটি বিখ্যাত গবেষণা প্রকাশ করেন; সেখান থেকে পরিচিত নিয়মটি OPT বা MIN: যে block-এর পরের ব্যবহার সবচেয়ে দূরে (বা কখনো না), তাকে বাদ দেওয়া। এটি প্রমাণিতভাবে সর্বনিম্ন miss দেয়। যুক্তির সারকথা (exchange argument): ধরুন কোনো নিয়ম P প্রথমবার OPT-এর থেকে আলাদা সিদ্ধান্ত নেয় — Y-এর বদলে X-কে বাদ দেয়, যদিও X আবার লাগবে Y-এর আগে। এখন P′ নামের একটি নিয়ম কল্পনা করুন, যা এই মুহূর্তে OPT-এর মতো Y-কে বাদ দেয় আর তারপর P-কে নকল করে, শুধু X আর Y-এর ভূমিকা অদলবদল করে। X-এর পরের ব্যবহারে P miss করে কিন্তু P′ hit পায়; আর Y আবার লাগার আগেই P′ নিজের cache-কে P-এর cache-এর সাথে মিলিয়ে নিতে পারে, কখনো P-এর চেয়ে বেশি miss না করে। তাই প্রতিটি পার্থক্য OPT-এর দিকে বদলালে miss বাড়ে না — বারবার এই বদল করলে OPT-এ পৌঁছানো যায়, তাই OPT-ই সর্বনিম্ন। OPT-এর জন্য পুরো ভবিষ্যৎ লাগে, তাই hardware-এ অসম্ভব; কিন্তু একটি trace রেকর্ড করে offline চালালে জানা যায় “কোনো নিয়ম এর চেয়ে ভালো পারবে না” — নতুন policy-র মাপকাঠি।

নিচের simulator চারটি policy একই কোডে চালায়: set = block mod sets, খালি way আগে ভরা, LRU আর FIFO একটি “বয়স” দিয়ে (LRU প্রতিটি hit-এ বয়স নতুন করে, FIFO শুধু ঢোকার সময়), PLRU উপরের tree-নিয়মে, আর RAND একটি নির্দিষ্ট seed-এর generator-এ — তাই প্রতিবার একই ফল। প্রথম অংশ 0, 8, 0, 6, 8 ক্রম তিন সংগঠনে চালিয়ে miss-গুলোকে 3 C's-এ ভাগ করে; দ্বিতীয় অংশ একটি চক্রাকার ক্রম:

// Set-associative cache simulator. Input: block addresses. set = block mod sets. Policies: LRU, FIFO, RAND, PLRU.
function sim(trace, sets, ways, policy, show) {
  const S = Array.from({ length: sets }, () => ({ blk: [], age: [], bits: [] }));
  let seed = 7, t = 0, miss = 0;
  const rnd = n => (seed = (seed * 1103515245 + 12345) % 2147483648) % n;       // seeded: same "random" every run
  const victimPLRU = b => { let n = 1; while (n < ways) n = 2 * n + (b[n] || 0); return n - ways; };
  const touchPLRU = (b, w) => { for (let n = w + ways; n > 1; n >>= 1) b[n >> 1] = n & 1 ? 0 : 1; }; // point away from w
  for (const blk of trace) {
    const set = blk % sets, s = S[set]; t++;
    let w = s.blk.indexOf(blk); const hit = w >= 0;
    if (!hit) {
      miss++;
      if (s.blk.length < ways) w = s.blk.length;                                 // an empty way: just fill it
      else if (policy === "RAND") w = rnd(ways);
      else if (policy === "PLRU") w = victimPLRU(s.bits);
      else w = s.age.indexOf(Math.min(...s.age));                                // LRU and FIFO: smallest age
      s.blk[w] = blk; s.age[w] = t;                                              // FIFO keeps the fill time
    } else if (policy === "LRU") s.age[w] = t;                                   // LRU refreshes on every hit
    if (policy === "PLRU") touchPLRU(s.bits, w);
    if (show) console.log(`  block ${String(blk).padStart(2)} -> set ${set}  ${hit ? "hit " : "MISS"}  set ${set} = [${s.blk.join(", ")}]`);
  }
  return miss;
}
const classic = [0, 8, 0, 6, 8], compulsory = new Set(classic).size;
const fa = sim(classic, 1, 4, "LRU", false);
for (const [name, sets, ways] of [["direct-mapped (4 sets x 1 way)", 4, 1], ["2-way (2 sets x 2 ways)", 2, 2], ["fully associative (1 set x 4 ways)", 1, 4]]) {
  console.log(name);
  const m = sim(classic, sets, ways, "LRU", true);
  console.log(`  misses = ${m}: compulsory ${compulsory}, capacity ${fa - compulsory}, conflict ${m - fa}`);
}
const cyclic = []; for (let r = 0; r < 4; r++) cyclic.push(0, 1, 2, 3, 4);      // 5 blocks cycling through 4 ways
console.log("cyclic 0..4 x 4 (20 accesses), fully associative, 4 ways:");
console.log("  " + ["LRU", "FIFO", "PLRU", "RAND"].map(p => `${p} ${sim(cyclic, 1, 4, p, false)} misses`).join(" | "));

আউটপুট (নির্ধারিত — seed স্থির):

direct-mapped (4 sets x 1 way)
  block  0 -> set 0  MISS  set 0 = [0]
  block  8 -> set 0  MISS  set 0 = [8]
  block  0 -> set 0  MISS  set 0 = [0]
  block  6 -> set 2  MISS  set 2 = [6]
  block  8 -> set 0  MISS  set 0 = [8]
  misses = 5: compulsory 3, capacity 0, conflict 2
2-way (2 sets x 2 ways)
  block  0 -> set 0  MISS  set 0 = [0]
  block  8 -> set 0  MISS  set 0 = [0, 8]
  block  0 -> set 0  hit   set 0 = [0, 8]
  block  6 -> set 0  MISS  set 0 = [0, 6]
  block  8 -> set 0  MISS  set 0 = [8, 6]
  misses = 4: compulsory 3, capacity 0, conflict 1
fully associative (1 set x 4 ways)
  block  0 -> set 0  MISS  set 0 = [0]
  block  8 -> set 0  MISS  set 0 = [0, 8]
  block  0 -> set 0  hit   set 0 = [0, 8]
  block  6 -> set 0  MISS  set 0 = [0, 8, 6]
  block  8 -> set 0  hit   set 0 = [0, 8, 6]
  misses = 3: compulsory 3, capacity 0, conflict 0
cyclic 0..4 x 4 (20 accesses), fully associative, 4 ways:
  LRU 20 misses | FIFO 20 misses | PLRU 19 misses | RAND 11 misses

প্রথম তিনটি অংশ stepper-এর সাথে হুবহু মেলে: direct-mapped ৫, 2-way ৪, fully associative ৩টি miss. শ্রেণিভাগের নিয়ম (অংশ ৭-এ বিস্তারিত): compulsory = আলাদা block-এর সংখ্যা (৩); capacity = একই আকারের fully associative LRU cache-এর miss − compulsory (3 − 3 = 0); conflict = আসল miss − fully associative-এর miss — DM-এ ২, 2-way-তে ১. 2-way-এর set 0-এর লাইনগুলোতে দেখুন, block 6 ঢোকার সময় [0, 6] — 8 বাদ গেছে (LRU), তারপর 8 এসে 0-কে সরিয়েছে, [8, 6]. শেষ লাইন অংশ ৬-এর বিষয়: পাঁচটি block চক্রাকারে চারটি way-এ — LRU আর FIFO ২০টির মধ্যে ২০টিই miss, PLRU ১৯, আর random মাত্র ১১. Random-এর সংখ্যাটি এই seed-এর; অন্য seed-এ কিছুটা আলাদা হবে, কিন্তু LRU-র ২০ কখনো বদলাবে না।

OPT-কে আলাদা করে দেখা যাক। নিচের simulator প্রতিটি miss-এ ভবিষ্যতের trace দেখে সবচেয়ে দূরের পরের-ব্যবহারওয়ালা block বাদ দেয়, আর কারণ ছাপে। ক্রমটি virtual memory-র বইয়ের পরিচিত 1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5 — ৩টি ঘরের fully associative cache-এ (বা ৩-way-এর একটি set-এ):

// Belady's OPT (MIN): on a miss, evict the block whose next use is farthest in the future (or never).
// Needs the whole future trace, so real hardware cannot build it; it is the lower bound for any policy.
function run(trace, cap, policy, show) {
  let cache = [], miss = 0;
  trace.forEach((b, i) => {
    const hit = cache.includes(b);
    let note = "";
    if (hit) { if (policy === "LRU") cache = cache.filter(x => x !== b).concat(b); }   // LRU order: oldest first
    else {
      miss++;
      if (cache.length === cap) {
        let v;
        if (policy === "LRU") v = cache[0];
        else {
          const next = x => { const j = trace.indexOf(x, i + 1); return j < 0 ? Infinity : j; };
          v = cache.reduce((a, x) => (next(x) > next(a) ? x : a));
          note = `evict ${v} (next use ${next(v) === Infinity ? "never" : "at #" + next(v)})`;
        }
        if (policy === "LRU") note = `evict ${v} (least recently used)`;
        cache = cache.filter(x => x !== v);
      }
      cache.push(b);
    }
    if (show) console.log(`#${String(i).padStart(2)} ${b}: ${hit ? "hit " : "MISS"} [${[...cache].sort().join(" ")}] ${note}`);
  });
  return miss;
}
const T = [1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5];
console.log("trace " + T.join(",") + ", 3 blocks, OPT:");
run(T, 3, "OPT", true);
console.log(`3 blocks: OPT ${run(T, 3, "OPT")} misses, LRU ${run(T, 3, "LRU")} misses`);
const cyc = []; for (let r = 0; r < 4; r++) cyc.push(0, 1, 2, 3, 4);
console.log(`cyclic 0..4 x 4, 4 blocks: OPT ${run(cyc, 4, "OPT")} misses, LRU ${run(cyc, 4, "LRU")} misses`);

আউটপুট (নির্ধারিত):

trace 1,2,3,4,1,2,5,1,2,3,4,5, 3 blocks, OPT:
# 0 1: MISS [1] 
# 1 2: MISS [1 2] 
# 2 3: MISS [1 2 3] 
# 3 4: MISS [1 2 4] evict 3 (next use at #9)
# 4 1: hit  [1 2 4] 
# 5 2: hit  [1 2 4] 
# 6 5: MISS [1 2 5] evict 4 (next use at #10)
# 7 1: hit  [1 2 5] 
# 8 2: hit  [1 2 5] 
# 9 3: MISS [2 3 5] evict 1 (next use never)
#10 4: MISS [3 4 5] evict 2 (next use never)
#11 5: hit  [3 4 5] 
3 blocks: OPT 7 misses, LRU 10 misses
cyclic 0..4 x 4, 4 blocks: OPT 8 misses, LRU 20 misses

#3-এ block 4 ঢোকার সময় cache-এ 1, 2, 3; তাদের পরের ব্যবহার #4, #5 আর #9 — সবচেয়ে দূরে 3, তাই 3 বাদ। #6-এ একই যুক্তিতে 4 (পরের ব্যবহার #10) বাদ, আর তাই #7, #8-এর 1, 2 hit. #9-এ 1 আর 2 আর কখনো লাগবে না — যেকোনোটি বাদ দেওয়া সমান ভালো (এখানে সমতায় প্রথমটি)। মোট OPT ৭, LRU ১০টি miss — কোনো বাস্তব নিয়ম ৭-এর নিচে যেতে পারবে না। চক্রাকার ক্রমে (৫টি block, ৪টি ঘর) OPT ৮, LRU ২০: LRU এখানে সম্ভাব্য সবচেয়ে খারাপ, আর OPT প্রতিবার “যে আসছে না, তাকে” রেখে দেয়।

৬ · LRU-র ফাঁদ — চক্রাকার প্রবেশ, আর আধুনিক policy

কেন চক্রাকার ক্রমে LRU প্রতিবার miss করে, তা হাতে দেখা সহজ। ৪টি way, ক্রম 0 1 2 3 4 0 1 2 3 4 … প্রথম চারটি ঢুকল। 4 এল — LRU সবচেয়ে পুরোনো 0-কে বাদ দিল। পরের প্রবেশ ঠিক 0! সে miss করে 1-কে বাদ দেয় — আর পরের প্রবেশ 1. প্রতিবার যাকে বাদ দেওয়া হয়, ঠিক তাকেই পরের মুহূর্তে লাগে। LRU-র যুক্তি (“পুরোনো জিনিস আর লাগবে না”) চক্রাকার প্রবেশে ঠিক উল্টো সত্য: সবচেয়ে পুরোনোটিই সবচেয়ে আগে আবার লাগবে। সাধারণভাবে, কাজের ডেটা (working set) চক্রাকারে k টি block আর set-এ n টি way হলে, k ≤ n-এ প্রথম ঘুরের পরে সব hit, কিন্তু k = n + 1 হলেই hit শূন্য — একটি block বেশি হলেই খাড়া পাহাড় থেকে পতন।

Random এখানে ভালো কারণ সে মাঝে মাঝে “ভুল” (সাম্প্রতিক) block-কে বাদ দেয়, আর পুরোনোগুলোর কিছু টিকে যায় — সেগুলো hit দেয়। OPT ভালোভাবে করে একই কাজ: কয়েকটি block স্থায়ীভাবে রেখে বাকিদের মধ্যে ঘোরানো। এই ধরনের প্রবেশ বাস্তবে খুব সাধারণ — একটি বড় array-র ওপর বারবার লুপ, যার আকার cache-এর চেয়ে একটু বড়; বা একবার পড়া “streaming” ডেটা (video frame, বড় ফাইল scan) যা দরকারি ছোট working set-কে ঠেলে বের করে দেয়।

আধুনিক ধারণা — কোথায় বসাবেন, শুধু কাকে বাদ দেবেন তা নয়

২০০০-এর দশকের শেষভাগ থেকে গবেষণায় একটি নতুন দৃষ্টিভঙ্গি জনপ্রিয় হয়: নতুন block-কে সবসময় “সবচেয়ে সাম্প্রতিক” অবস্থানে না বসিয়ে, কখনো “শিগগির বাদ যাবে” অবস্থানে বসানো। তাহলে একবার ব্যবহারের streaming ডেটা দ্রুত বেরিয়ে যায়, আর বারবার-লাগা block-গুলো টিকে থাকে। Adaptive insertion-এর ধারণা (Qureshi ও সহকর্মীদের ২০০৭-এর দিকের গবেষণায় পরিচিত, DIP নামে) কয়েকটি নমুনা set-এ দুই নিয়ম চালিয়ে দেখে কোনটি ভালো, আর বাকি set-এ সেটি প্রয়োগ করে (set dueling). RRIP (re-reference interval prediction; Jaleel ও সহকর্মীদের ২০১০-এর দিকের গবেষণা) প্রতিটি block-এ ২–৩ বিটের একটি “আবার কত দেরিতে লাগবে” অনুমান রাখে — NRU-র একটি বহু-বিট রূপ। কিছু নতুন Intel CPU-র শেষ স্তরের cache-এ এ ধরনের adaptive আচরণ দেখা যায় বলে স্বাধীন পরিমাপ-ভিত্তিক গবেষণা বলেছে; নির্মাতারা সাধারণত সঠিক policy প্রকাশ করে না। আরও সাম্প্রতিক গবেষণা (যেমন Hawkeye) অতীতের trace-এ OPT কী করত, তা থেকে শিখে অনুমান করার চেষ্টা করে।

কোন policy “সেরা”? কোনোটিই সব কাজে নয়। ছোট, দ্রুত L1-এ সরলতা আর গতি মুখ্য — PLRU বা NRU-ধাঁচ প্রচলিত বলে বর্ণনা পাওয়া যায়। বড় শেষ স্তরের cache-এ (যেখানে miss মানে DRAM-এ যাওয়া, শত cycle) জটিল, adaptive policy-র দাম পোষায়। Real-time ব্যবস্থায় (গাড়ির brake controller) গড় নয়, সবচেয়ে খারাপ সময়টি প্রমাণ করতে হয় — সেখানে LRU-র অনুমানযোগ্যতা random-এর চেয়ে মূল্যবান।

৭ · Miss rate বনাম associativity, way prediction আর victim cache

Associativity বাড়ালে conflict miss কমে, কিন্তু লাভ দ্রুত ফুরায়: 1 থেকে 2-way-তে লাফটি সাধারণত বড়, 2 থেকে 4-way-তে ছোট, তারপর আরও ছোট। Hennessy–Patterson-এর বইয়ে SPEC benchmark-এর মাপ থেকে দুটি rule of thumb প্রচলিত — নিয়ম নয়, অনেক কাজের চাপে দেখা প্রবণতা:

  • “2:1 cache rule” — N আকারের direct-mapped cache-এর miss rate মোটামুটি N/2 আকারের 2-way cache-এর সমান।
  • 8-way প্রায় fully associative-এর কাছাকাছি — বেশিরভাগ কাজে 8-way-এর পরে conflict miss প্রায় নগণ্য; বাকি miss-এর বেশিরভাগ capacity আর compulsory.

এর সাথে hit time-এর দাম যোগ করলে ছবি পূর্ণ হয়: miss rate-এ সামান্য লাভের জন্য যদি প্রতিটি hit-এ একটি বাড়তি cycle লাগে, তবে মোট গড় সময় বেড়েও যেতে পারে। কতটা, তার হিসাব (AMAT = hit time + miss rate × miss penalty) পরের পর্বে — পর্ব ৪৭ (Cache-এর পারফরম্যান্স)।

Miss-এর শ্রেণিভাগ আবার — fully associative-কে মাপকাঠি ধরে

3 C's একটি নির্দিষ্ট পদ্ধতিতে মাপা হয় (পর্ব ৪৫-এর সংজ্ঞার পরিমাপযোগ্য রূপ): একই trace তিনটি cache-এ চালানো —

  • Compulsory = অসীম বড় cache-এর miss (প্রতিটি আলাদা block-এর প্রথম ছোঁয়া)।
  • Capacity = একই আকারের fully associative (LRU) cache-এর miss − compulsory.
  • Conflict = আসল cache-এর miss − একই আকারের fully associative-এর miss.

অর্থাৎ conflict miss হলো সেগুলো, যা শুধু placement-এর বাঁধনের জন্য হয় — একই আকারে বাঁধন তুলে দিলে থাকত না। উপরের simulator ঠিক এভাবেই ৫ = ৩ + ০ + ২ ভাগ করেছে। একটি সূক্ষ্মতা: fully associative LRU নিজেই নিখুঁত নয় (চক্রাকার ক্রমে সে খুব খারাপ), তাই কখনো কোনো set-associative cache তার চেয়ে কম miss করে — তখন “conflict” সংখ্যা ঋণাত্মক আসে। এটি পদ্ধতির সীমাবদ্ধতা, ভুল নয়।

Way prediction — n-way-এর miss rate, direct-mapped-এর গতি

Way prediction-এ প্রতিটি set-এর সাথে কয়েকটি বিট অনুমান রাখে “পরের প্রবেশ সম্ভবত কোন way-তে”। শুধু সেই way-এর data আর tag আগে পড়া হয় — অনুমান ঠিক হলে direct-mapped-এর মতো দ্রুত আর কম শক্তির hit; ভুল হলে বাকি way দেখতে এক বা কয়েক cycle বেশি। পুরোনো Alpha 21264-এর instruction cache-এ এ ধরনের অনুমানের কথা পাঠ্যবইয়ে আসে; AMD-র কিছু নতুন core-এর L1 data cache-এ virtual address-এর একটি ছোট hash (μtag) দিয়ে way অনুমানের বর্ণনাও নির্মাতার নথিতে পাওয়া যায়। ভাবনাটি পর্ব ৩৭-এর branch prediction-এর আত্মীয় (Branch prediction).

Victim cache — বের হওয়া block-এর জন্য ছোট একটি আশ্রয়

Norman Jouppi ১৯৯০ সালের একটি পরিচিত গবেষণাপত্রে direct-mapped cache-এর পাশে মাত্র কয়েকটি (১ থেকে ৫টির মতো) ঘরের একটি ছোট fully associative victim cache-এর প্রস্তাব দেন: মূল cache থেকে বের হওয়া প্রতিটি block এখানে আসে। মূল cache-এ miss কিন্তু victim cache-এ hit হলে দুটি block অদলবদল — মেমরিতে যেতে হয় না। 0, 8, 0, 8 ধরনের thrashing-এ এটি প্রায় 2-way-এর কাজ করে, অথচ মূল cache direct-mapped-এর দ্রুততা রাখে। গবেষণাপত্রটিতে মাত্র কয়েকটি ঘরেই direct-mapped cache-এর conflict miss-এর বড় অংশ সরিয়ে দেওয়ার ফল দেখানো হয়েছিল। ধারণাটি পরে বহু রূপে বেঁচে আছে — যেমন কিছু নকশায় শেষ স্তরের cache-কে ওপরের স্তরের “victim” হিসেবে চালানো (L2 থেকে বের হওয়া block L3-এ যায়), যার আলোচনা পর্ব ৪৭-এ।

৮ · বাস্তবের L1 — কেন 8–12-way, VIPT আর একটি সত্যিকার cache-এর way গোনা

আজকের desktop আর server CPU-র L1 data cache সাধারণত ৩২–৪৮ KiB, 8 থেকে 12-way set-associative, ৬৪ B line বলে নির্মাতাদের প্রকাশিত বিবরণে পাওয়া যায় (নির্দিষ্ট মান প্রজন্ম আর core-ভেদে বদলায়)। L2 আর L3-এ way সাধারণত L1-এর সমান বা বেশি (প্রায়ই ৮–১৬ বা তার বেশি)। L1 কেন আরও বড় বা fully associative নয়? একটি বড় কারণ hit time আর শক্তি (অংশ ৪); আরেকটি কারণ virtual memory.

VIPT — virtual index, physical tag

প্রোগ্রাম virtual address দেয়; tag তুলনার জন্য physical address লাগে, যা TLB থেকে আসে (পর্ব ৫০, Virtual memory-র hardware). TLB শেষ হওয়া পর্যন্ত অপেক্ষা করলে L1-এর সময় বাড়ে। কৌশল: VIPT (virtually indexed, physically tagged) — set বাছাইয়ের index নেওয়া হয় virtual address থেকে, আর একই সময়ে TLB physical page number বের করে; set-এর tag-গুলো পড়া শেষ হতেই physical tag তুলনা। এটি নিরাপদ হয় একটি শর্তে: index আর offset-এর সব বিট page offset-এর ভেতরে পড়তে হবে, কারণ page offset-এর বিট virtual আর physical-এ একই। ৪ KiB page-এ page offset ১২ বিট, তাই index + offset ≤ ১২ —

VIPT L1-এর সর্বোচ্চ আকার = way-এর সংখ্যা × page size

8-way × ৪ KiB = ৩২ KiB; 12-way × ৪ KiB = ৪৮ KiB. তাই L1 বড় করতে হলে way বাড়াতে হয় — set বাড়ানো যায় না। এটিই ব্যাখ্যা করে কেন x86 L1 data cache বহু বছর ৩২ KiB 8-way-এ ছিল, আর বড় হতে গিয়ে 12-way হয়েছে। Apple-এর কিছু ARM chip ১৬ KiB page ব্যবহার করে, আর তাদের L1 data cache অনেক বড় (১২৮ KiB পর্যন্ত বলে বর্ণনা পাওয়া যায়) — একই সূত্রে ১৬ KiB × ৮ = ১২৮ KiB. শর্ত ভাঙলে একই physical block দুটি ভিন্ন set-এ বসতে পারে (synonym বা alias সমস্যা), যা সামলাতে বাড়তি hardware লাগে।

নিজের হাতে মাপা — একই set-এ K টি block

প্রথমে runner-এর (একটি shared virtual machine) Linux sysfs থেকে cache-এর সংগঠন — শুধু level, type, size, way, line আর set; অন্য কোনো তথ্য ছাপা হয় না:

# Cache organisation of CPU 0, read from Linux sysfs (only these fields are printed)
d=/sys/devices/system/cpu/cpu0/cache
if [ ! -d "$d" ]; then echo "sysfs cache info নেই"; exit 0; fi
printf "%-6s %-12s %8s %5s %5s %6s %10s\n" level type size ways line sets "way size"
for c in "$d"/index*; do
  lv=$(cat "$c/level" 2>/dev/null || echo "?")
  ty=$(cat "$c/type" 2>/dev/null || echo "?")
  sz=$(cat "$c/size" 2>/dev/null || echo "?")
  wy=$(cat "$c/ways_of_associativity" 2>/dev/null || echo "?")
  ln=$(cat "$c/coherency_line_size" 2>/dev/null || echo "?")
  st=$(cat "$c/number_of_sets" 2>/dev/null || echo "?")
  ws="?"
  if [[ "$st" =~ ^[0-9]+$ && "$ln" =~ ^[0-9]+$ ]]; then ws="$(( st * ln / 1024 ))K"; fi
  printf "L%-5s %-12s %8s %5s %5s %6s %10s\n" "$lv" "$ty" "$sz" "$wy" "$ln" "$st" "$ws"
done
command -v getconf >/dev/null && getconf -a 2>/dev/null | grep -E '^LEVEL1_DCACHE_(SIZE|ASSOC|LINESIZE)' || echo "getconf নেই"

আউটপুট (এই runner-এ; virtual machine-এ এই মান hypervisor যা দেখায় তাই, আর runner বদলালে বদলাতে পারে):

level  type             size  ways  line   sets   way size
L1     Data              32K     8    64     64         4K
L1     Instruction       32K     8    64     64         4K
L2     Unified          512K     8    64   1024        64K
L3     Unified        16384K    16    64  16384      1024K
LEVEL1_DCACHE_SIZE                 32768
LEVEL1_DCACHE_ASSOC                8
LEVEL1_DCACHE_LINESIZE             64

L1 data cache 32K, 8-way, ৬৪ B line, ৬৪টি set. “way size” কলাম = set × line = 64 × 64 = 4096 byte = 4K — ঠিক একটি page. অর্থাৎ VIPT-এর শর্ত টানটান মিলেছে: ৮ × ৪ KiB = ৩২ KiB. L2 512K-এর 1024 set-এর প্রতিটি way 64K, L3-এর 16-way. getconf-এর তিন লাইন একই কথা বলে (LEVEL1_DCACHE_ASSOC = 8). সংখ্যাগুলো আমাদের অংশ ৩-এর টেবিলের 8-way সারির সাথে হুবহু: index 6, offset 6.

এবার মেপে দেখা যাক। ৪০৯৬ byte দূরে দূরে রাখা address-এর index-এর বিট (৬–১১) একই, তাই তারা সবাই L1-এর একই set-এ পড়ে। প্রোগ্রামটি K টি এমন block একটি বৃত্তে জোড়ে (প্রতিটি পরেরটির address রাখে — pointer chasing, যাতে প্রতিটি load আগেরটির ফলের জন্য অপেক্ষা করে আর CPU একসাথে অনেক load চালিয়ে সময় লুকাতে না পারে), তারপর বৃত্তে ঘোরে। K ≤ way হলে সব block set-এ ধরে; K = way + 1 হলেই অংশ ৬-এর চক্রাকার ফাঁদ:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define STRIDE 4096                 /* 4 KiB apart: same L1 set if L1 has 64 sets of 64-byte lines */
#define MAXK 32
#define STEPS 10000000L
static double now(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec + t.tv_nsec * 1e-9; }
int main(void) {
    char *buf = aligned_alloc(4096, (size_t)MAXK * STRIDE);
    if (!buf) return 1;
    const int ks[] = {2, 4, 6, 7, 8, 9, 10, 12, 16, 24, 32};
    double base = 0;
    void *sink = 0;
    for (int n = 0; n < (int)(sizeof ks / sizeof ks[0]); n++) {
        int K = ks[n];
        for (int i = 0; i < K; i++)            /* K blocks in a ring: block i points to block i+1 */
            *(char **)(buf + (size_t)i * STRIDE) = buf + (size_t)((i + 1) % K) * STRIDE;
        double best = 1e9;
        for (int r = 0; r < 5; r++) {          /* best of 5 runs: less noise from the shared machine */
            register char **p = (char **)buf;
            double t = now();
            for (register long s = 0; s < STEPS; s++) p = (char **)*p;   /* each load needs the previous one */
            t = now() - t;
            if (t < best) best = t;
            sink = p;
        }
        double ns = best / STEPS * 1e9;
        if (n == 0) base = ns;
        printf("K = %2d blocks in one set: %5.2f ns/access  (x%.1f)\n", K, ns, ns / base);
    }
    printf("%s\n", sink ? "done" : "?");
    free(buf);
    return 0;
}

আউটপুট (Wandbox-এ gcc, warning চালু, শূন্য warning; একটি রানের ফল — ns-এর মান প্রতিবার আর প্রতিটি machine-এ বদলায়):

K =  2 blocks in one set:  1.64 ns/access  (x1.0)
K =  4 blocks in one set:  1.67 ns/access  (x1.0)
K =  6 blocks in one set:  1.60 ns/access  (x1.0)
K =  7 blocks in one set:  1.60 ns/access  (x1.0)
K =  8 blocks in one set:  1.60 ns/access  (x1.0)
K =  9 blocks in one set:  4.52 ns/access  (x2.8)
K = 10 blocks in one set:  5.21 ns/access  (x3.2)
K = 12 blocks in one set:  4.89 ns/access  (x3.0)
K = 16 blocks in one set:  4.78 ns/access  (x2.9)
K = 24 blocks in one set:  4.88 ns/access  (x3.0)
K = 32 blocks in one set:  5.14 ns/access  (x3.1)
done

K = 2 থেকে 8 পর্যন্ত প্রতি প্রবেশ প্রায় একই সময় — সব block L1-এর একটি set-এ ধরে, প্রতিটি load L1 hit (এই সময়ে -O0-এর লুপের বাড়তি কাজও আছে, তাই এটি খাঁটি L1 latency নয়)। K = 9-এ — way-এর সংখ্যার ঠিক একটি বেশি — সময় হঠাৎ কয়েক গুণ: set-এ ৮টি ঘর, ৯টি block চক্রাকারে, প্রায় প্রতিটি প্রবেশ L1 miss হয়ে L2 থেকে আসে। এর পরে K বাড়ালেও সময় প্রায় স্থির, কারণ L2-তে (1024 set) ৪০৯৬-byte দূরত্বের block-গুলো ১৬টি ভিন্ন set-এ ছড়িয়ে যায় — সেখানে জায়গার অভাব নেই। লক্ষ করুন, ডেটা মোট মাত্র ৯ × ৬৪ = ৫৭৬ byte — ৩২ KiB cache-এর ২%-এরও কম! এটি খাঁটি conflict miss. K = 9-এর অনুপাত কখনো K = 10-এর চেয়ে একটু কম আসে — একটি সম্ভাব্য ব্যাখ্যা, hardware-এর replacement নিখুঁত LRU নয় (simulator-এ PLRU ২০-এ ১৯টি miss করেছিল), তবে prefetcher, TLB আর virtual machine-এর গোলমালও ফলকে ঝাপসা করতে পারে — তাই সংখ্যা নয়, প্রবণতাটিই শিক্ষা।

আরও গভীরে। বড় শেষ স্তরের cache অনেক নকশায় কয়েকটি “slice”-এ ভাগ থাকে (core-প্রতি একটি), আর কোন address কোন slice-এ যাবে তা ঠিক করে address-এর অনেক বিটের একটি অপ্রকাশিত hash — যাতে power-of-two stride-এর ডেটাও ছড়িয়ে পড়ে। কিছু cache-এ set index নিজেই কয়েকটি উঁচু বিটের XOR দিয়ে বানানো হয় (skewed বা hashed indexing), যাতে 4096-এর মতো stride-এ সব block একই set-এ জমে না যায়। নিরাপত্তা-গবেষণায় set-এর conflict-কে side channel হিসেবে ব্যবহারের ঝুঁকি দেখানো হয়েছে, আর প্রতিরক্ষা হিসেবে randomized set mapping আর cache partitioning (যেমন Intel-এর Cache Allocation Technology) প্রস্তাবিত ও ব্যবহৃত — বিস্তারিত, প্রতিরক্ষার দিক থেকে, পর্ব ৪১-এ (Speculative execution আর নিরাপত্তা)।

৯ · AI-এর যুগে cache associativity

AI টুল cache-এর ধারণা ভালো বোঝায়, কিন্তু এই বিষয়ের প্রায় সব পরীক্ষার প্রশ্ন বিট গোনা আর trace চালানো — আর সেখানেই সবচেয়ে বেশি ভুল হয়। প্রচলিত ভুলগুলো:

AI-এর প্রচলিত ভুলঠিক কথাকীভাবে যাচাই
n-way-তে index-এর বিট না কমিয়ে way-এর জন্য address-এ আলাদা log2n বিট রাখাindex = log2(size ÷ (block × n)); way বাছাই tag মিলিয়েtag + index + offset = address-এর বিট — যোগ করে দেখা
set = block mod (way-এর সংখ্যা) লেখাset = block mod (set-এর সংখ্যা)৪ block, 2-way-এ 0, 8, 6 কোন set-এ — হাতে
Trace-এ LRU আর FIFO গুলিয়ে ফেলা (hit-এ ক্রম হালনাগাদ না করা)LRU প্রতিটি hit-এ ক্রম বদলায়, FIFO বদলায় নাএই পর্বের simulator-এ দুই policy চালিয়ে মেলানো
PLRU-র বিটের অর্থ (০ = বাঁ নাকি ডান) উল্লেখ না করে উত্তর দেওয়ানিয়ম বই-ভেদে উল্টো; আগে নিয়ম ঠিক করে তবে traceনিয়ম লিখে নিয়ে প্রতিটি ধাপ হাতে
“fully associative সবসময় সবচেয়ে কম miss” বলাLRU-তে চক্রাকার ক্রমে FA-ও সব miss করতে পারে; সর্বনিম্ন হলো OPT০..৪ চক্র ৪ ঘরে চালানো
নির্দিষ্ট chip-এর L1-এর way বা replacement policy বানিয়ে বলাঅনেক তথ্য প্রকাশিত নয়, প্রজন্মভেদে বদলায়নির্মাতার optimization manual, sysfs, lscpu, মেপে দেখা

AI-কে প্রশ্ন করার সময় মাপ, address-এর বিট আর নিয়মগুলো স্পষ্ট করে দিন, আর ধাপগুলো চেয়ে নিন:

আমি cache associativity শিখছি। Byte-addressed মেমরি, 32-বিট address, cache size মানে শুধু data. ১. 64 KiB, 8-way, 32 B block: tag, index, offset কত বিট? address 0x1234ABCD-এর tag, index, offset hex-এ দাও। যোগ করে মিলিয়ে দেখাও যে মোট 32 বিট। ২. ৩ ঘরের fully associative cache, block ক্রম 1 2 3 1 4 1 5 2 1 3 — LRU আর FIFO-তে প্রতিটি ধাপের cache-এর অবস্থা, hit/miss একটি টেবিলে দাও। ৩. 4-way PLRU tree, নিয়ম: বিট 0 মানে victim বাঁ দিকে। b1 b2 b3 = 1 0 1 হলে victim কোন way? সেখানে নতুন block বসানোর পরে বিট কী হবে? ৪. যে সংখ্যা বা তথ্য সম্পর্কে তুমি নিশ্চিত নও, স্পষ্ট করে চিহ্নিত করো। কোনো নির্দিষ্ট CPU-র cache-এর মাপ বানিয়ে বলবে না। আমি উত্তরগুলো নিজে একটি simulator-এ চালিয়ে মেলাব।

AI-এর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • Way দ্বিগুণ করলে address-এর ভাগে কী হয়? — set অর্ধেক, index ১ বিট কম, tag ১ বিট বেশি; offset অপরিবর্তিত।
  • Conflict miss কীভাবে মাপবেন? — একই আকারের fully associative cache-এর miss বাদ দিয়ে যা থাকে।
  • 8-way-এ নিখুঁত LRU কেন কম দেখা যায়? — ন্যূনতম ⌈log28!⌉ = ১৬ বিট আর প্রতিটি hit-এ জটিল হালনাগাদ; PLRU tree ৭ বিটে প্রায় একই কাজ করে।
  • LRU কখন random-এর চেয়ে খারাপ? — চক্রাকার প্রবেশে, যখন working set way-এর চেয়ে একটু বড়: LRU প্রতিবার miss করে।
  • VIPT L1-এর আকার কেন সীমিত? — index + offset page offset-এর ভেতরে থাকতে হয়, তাই আকার ≤ way × page size (৪ KiB page-এ 8-way = ৩২ KiB).

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

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

প্র ০১Fully associative cache-এ conflict miss নেই। তাহলে L1 data cache কেন কখনো fully associative বানানো হয় না, আর associativity কোথায় থামে?

প্রথম কারণ সময়। L1 data cache প্রায় প্রতিটি load-এর পথে, আর একটি দ্রুত core-এ তার hit সময় মাত্র কয়েক cycle (প্রায় ৪–৫ cycle বলে প্রায়ই বর্ণনা পাওয়া যায়)। সেই সময়ে set পড়া, tag তুলনা আর mux — সব শেষ করতে হয়। 8-way-তে ৮টি ২০-বিটের comparator আর একটি 8:1 mux; ৫১২-way-তে ৫১২টি ২৬-বিটের তুলনা আর ৫১২টি match line থেকে একটি বেছে নেওয়া — এই বিশাল OR-tree আর mux-ই কয়েক cycle খেয়ে ফেলবে। প্রতিটি load-এ এক cycle বাড়লে পুরো প্রোগ্রাম ধীর হয়, যা miss rate-এর সামান্য লাভ পুষিয়ে দিতে পারে না।

দ্বিতীয় কারণ শক্তি। প্রতিটি প্রবেশে সব ঘরের tag-এর সাথে তুলনা মানে CAM-এর সব match line চার্জ করা। একটি core প্রতি সেকেন্ডে কয়েকশো কোটি load করে; প্রতিটিতে ৫১২টি তুলনা মানে L1-ই chip-এর বড় শক্তিখেকো হয়ে যেত। মোবাইল ফোনে ব্যাটারি আর laptop-এ তাপ — দুটোই সীমা।

তৃতীয় কারণ লাভের অভাব। অংশ ৭-এর rule of thumb বলে, অনেক কাজে 8-way-এর পরে conflict miss প্রায় নগণ্য। বাকি miss capacity আর compulsory — সেগুলো associativity দিয়ে কমে না; কমে বড় cache, ভালো prefetching আর প্রোগ্রামের locality দিয়ে (পর্ব ৪৮)।

চতুর্থ কারণ VIPT. ৪ KiB page-এ VIPT L1-এর আকার way × ৪ KiB-এ বাঁধা। তাই L1 বড় করতে way বাড়াতে হয় — এজন্যই ৩২ KiB 8-way থেকে ৪৮ KiB 12-way. এখানে associativity আসলে আকারের দাস: ৬৪ KiB VIPT L1 চাইলে 16-way লাগবে, আর তখন সময় আর শক্তি আবার সমস্যা।

অন্যদিকে TLB-এর মতো ছোট কাঠামোয় (কয়েক ডজন entry) fully associative সহজেই চলে — ঘর কম, তাই CAM ছোট; আর TLB-এর একটি miss-এর দাম (page walk) এত বেশি যে প্রতিটি conflict বাঁচানো মূল্যবান। নিচের স্তরে, যেমন L2-L3-এ, way বেশি (16 বা তারও বেশি) রাখা যায় কারণ সেখানে কয়েক cycle বাড়তি সময় সহ্য করা যায় আর tag আগে, data পরে পড়ে শক্তি বাঁচানো হয়।

মূল উপলব্ধি: Associativity বিনামূল্যে নয় — প্রতিটি way hit time, শক্তি আর জায়গায় দাম নেয়। নকশাকার miss rate নয়, গড় প্রবেশ-সময় আর শক্তি কমান; তাই দ্রুত, ছোট স্তরে কম way আর ধীর, বড় স্তরে বেশি way — আর শুধু খুব ছোট কাঠামোয় fully associative.

প্র ০২Belady-র OPT কোনো hardware-এ বানানো যায় না। তবু প্রায় ছয় দশক ধরে এটি গবেষণার কেন্দ্রে কেন?

১৯৬০-এর দশকে IBM virtual memory নিয়ে কাজ করছিল — মেমরি তখন অত্যন্ত দামি, আর কোন page ডিস্কে পাঠানো হবে তার সিদ্ধান্ত গতির জন্য বিশাল। Laszlo Belady ১৯৬৬ সালের গবেষণায় বিভিন্ন replacement নিয়ম তুলনা করেন, আর দেখান যে ভবিষ্যৎ জানা থাকলে কোন নিয়ম সর্বনিম্ন page fault দেয় — সবচেয়ে দেরিতে আবার লাগবে যাকে, তাকে বাদ দেওয়া। একই সময়ের কাজ থেকে পরিচিত আরেকটি বিস্ময়, FIFO-তে বেশি frame দিলেও কখনো বেশি fault হয় — যা Belady's anomaly নামে পরিচিত (অপারেটিং সিস্টেম কোর্সের page replacement পর্বে দেখবেন)।

OPT-এর প্রথম মূল্য মাপকাঠি হিসেবে। কেউ নতুন policy বানিয়ে বলল “LRU-র চেয়ে ১০% কম miss” — কিন্তু কত ভালো সম্ভব? একই trace-এ OPT চালালে জানা যায় LRU আর আদর্শের মধ্যে ফাঁক কত। ফাঁক ছোট হলে নতুন policy-র পেছনে খাটুনি অর্থহীন; ফাঁক বড় হলে সুযোগ আছে। অংশ ৫-এর trace-এ LRU ১০, OPT ৭ — ফাঁক ৩টি miss; চক্রাকার ক্রমে LRU ২০, OPT ৮ — বিশাল ফাঁক, আর ঠিক এই ধরনের প্রবেশের জন্যই RRIP, DIP জাতীয় policy জন্মেছে।

দ্বিতীয় মূল্য শ্রেণিভাগে। কোনো miss “এড়ানো যেত কি না”, তা OPT দিয়ে বলা যায়: OPT-ও যে miss করে, তা কোনো replacement policy দিয়ে কমবে না — সেখানে বড় cache বা prefetching লাগবে।

তৃতীয়, আধুনিক গবেষণা OPT-কে শিক্ষক বানিয়েছে। Hawkeye (Jain ও Lin, ২০১৬-র দিকের গবেষণা) অতীতের প্রবেশ-ইতিহাসে OPT কী করত তা ছোট hardware টেবিলে অনুকরণ করে, আর প্রতিটি load instruction (PC) সাধারণত “OPT-বান্ধব” block আনে কি না শেখে — তারপর সেই শিক্ষায় নতুন block-কে বেশি বা কম সময় রাখার সিদ্ধান্ত নেয়। Machine learning-ভিত্তিক বহু প্রস্তাবও একইভাবে OPT-এর সিদ্ধান্তকে “সঠিক উত্তর” হিসেবে ব্যবহার করে।

OPT অন্য জায়গাতেও বাস্তব: যেখানে ভবিষ্যৎ জানা, যেমন compiler-এর register allocation-এ (একটি basic block-এর ভেতরে কোন মান কখন লাগবে তা compiler জানে) বা একটি GPU kernel-এর scratchpad মেমরি সাজানোয়, সেখানে OPT-ধাঁচের সিদ্ধান্ত সরাসরি নেওয়া যায়।

মূল উপলব্ধি: যা বানানো যায় না, তা-ও মাপা যায়। OPT বলে দেয় সীমা কোথায়, কোন miss-এর জন্য নীতি দায়ী আর কোনটির জন্য আকার — আর আজকের সেরা policy-গুলো আসলে OPT-কে যতটা সম্ভব নকল করার চেষ্টা।

প্র ০৩LRU-র যুক্তি temporal locality — অথচ চক্রাকার প্রবেশে সে সবচেয়ে খারাপ। বাস্তব ব্যবস্থায় (CPU cache, database, OS) এই দুর্বলতা কীভাবে সামলানো হয়েছে?

LRU ধরে নেয় “সদ্য ব্যবহৃত = শিগগির আবার লাগবে, অনেক আগে ব্যবহৃত = আর লাগবে না।” দুটি সাধারণ প্রবেশ-ধরন এটি ভাঙে। এক, চক্রাকার লুপ যার working set cache-এর চেয়ে একটু বড় — অংশ ৬-এ দেখেছেন, hit শূন্য। দুই, scan — একবার পড়া বড় ডেটা (একটি বড় ফাইলের checksum, database-এর পুরো table scan). LRU প্রতিটি নতুন block-কে “সবচেয়ে মূল্যবান” অবস্থানে বসায়, তাই scan দরকারি ছোট working set-কে পুরো ঠেলে বের করে দেয় — এর নাম cache pollution.

Database-এর জগৎ এটি আগে টের পেয়েছিল, কারণ buffer pool-এ table scan খুব সাধারণ। ১৯৯০-এর দশকের শুরুতে LRU-K-এর মতো প্রস্তাব (O'Neil ও সহকর্মীদের কাজ হিসেবে পরিচিত) শেষ একবার নয়, শেষ K বার ব্যবহারের সময় দেখে সিদ্ধান্ত নেয় — একবার-দেখা scan-এর page তাই দ্রুত বেরিয়ে যায়। পরে ARC-এর মতো adaptive নিয়ম সাম্প্রতিকতা আর ঘনত্বের (frequency) মধ্যে নিজে থেকে ভারসাম্য খোঁজে।

Operating system-ও একই সমস্যায়। Linux-এর page cache বহু বছর ধরে দুটি তালিকা রাখে — active আর inactive: নতুন page আগে inactive তালিকায় ঢোকে, আর দ্বিতীয়বার ছোঁয়া হলে তবেই active-এ ওঠে। ফলে একবার পড়া ফাইলের page বেশিদিন থাকে না। এটি CPU-র adaptive insertion-এর সাথে প্রায় একই ভাবনা (অপারেটিং সিস্টেম কোর্সের Linux memory পর্বে বিস্তারিত)।

CPU cache-এ hardware-এর সীমা কঠিন — কয়েক বিট আর কয়েক gate. তাই সমাধান ছোট: DIP-এর মতো নিয়ম নতুন block-কে বেশিরভাগ সময় LRU অবস্থানে বসায় (একবার hit হলে তবেই ওপরে ওঠে), আর set dueling দিয়ে চলার সময় ঠিক করে কোন নিয়ম চলবে। RRIP প্রতিটি block-এ ২–৩ বিটে “আবার লাগতে কত দেরি” অনুমান রাখে; নতুন block “দেরিতে লাগবে” মান নিয়ে ঢোকে। Scan-এর block দ্রুত বেরোয়, চক্রাকার ডেটার একটি অংশ টিকে যায়। প্রোগ্রামারের দিক থেকেও সাহায্য আছে: x86-এর non-temporal store বা prefetch-এর ইঙ্গিত দিয়ে বলা যায় “এই ডেটা cache-এ রাখার দরকার নেই”, আর পর্ব ৪৮-এর blocking চক্রকে ছোট টুকরোয় ভেঙে cache-এ ধরিয়ে দেয়।

মূল উপলব্ধি: কোনো replacement নীতি সব প্রবেশ-ধরনে ভালো নয়। সফল ব্যবস্থাগুলো একটি নিয়মে আটকে না থেকে প্রবেশের ধরন দেখে মানিয়ে নেয় — আর CPU, database ও OS তিন জগতে প্রায় একই সমাধান আলাদাভাবে আবিষ্কৃত হয়েছে।

প্র ০৪১৯৮০-র দশকের শেষে একটি পরিচিত যুক্তি ছিল যে direct-mapped cache-ই ভালো। তারপর victim cache এল, আর আজ প্রায় সব cache set-associative. এই বিতর্ক থেকে কী শেখা যায়?

১৯৮৮ সালে Mark Hill-এর “A Case for Direct-Mapped Caches” নামে একটি পরিচিত প্রবন্ধে যুক্তি দেওয়া হয়েছিল: miss rate-এ set-associative একটু ভালো, কিন্তু direct-mapped-এর hit time কম, কারণ data পড়া আর tag তুলনা সমান্তরালে চলে আর কোনো mux বা way বাছাই লাগে না। তখনকার বড়, chip-এর বাইরের cache-এ (বোর্ডে আলাদা SRAM chip দিয়ে বানানো) n-way মানে আরও chip, আরও তার আর আরও সময়। তাই সঠিক মাপকাঠি — গড় প্রবেশ-সময় — অনেক ক্ষেত্রে direct-mapped-এর পক্ষে যেত।

১৯৯০ সালে Norman Jouppi (তখন DEC-এর গবেষণাগারে) মাঝামাঝি পথ দেখালেন: মূল cache direct-mapped রেখে তার পাশে কয়েক ঘরের fully associative victim cache. hit-এর পথ দ্রুত থাকে, আর থ্র্যাশিং-এর সবচেয়ে খারাপ ক্ষেত্রগুলো — দুটি block একই ঘরে লড়াই — ছোট আশ্রয়ে ধরা পড়ে। একই গবেষণাপত্রে stream buffer (ক্রমানুসারে পরের block আগে আনা) প্রস্তাব করা হয়, যা আজকের hardware prefetcher-এর পূর্বসূরি হিসেবে প্রায়ই উল্লেখ করা হয়।

তারপর প্রযুক্তি বদলাল। Cache chip-এর ভেতরে এল; transistor সস্তা হলো; comparator আর mux-এর সময় clock-এর তুলনায় কম গুরুত্বপূর্ণ হলো, কারণ pipeline-এ L1-এর প্রবেশ কয়েক cycle-এ ভাগ করা যায়। একই সাথে CPU আর DRAM-এর গতির ফাঁক (memory wall) বাড়তে থাকল, তাই প্রতিটি miss-এর দাম বাড়ল — miss rate কমানো বেশি মূল্যবান হলো। Way prediction-এর মতো কৌশল set-associative-এর hit time-কে direct-mapped-এর কাছে নিয়ে এল। আর VIPT-এর শর্ত (অংশ ৮) L1 বড় করতে way বাড়াতে বাধ্য করল। ফলে আজ সাধারণ CPU-তে direct-mapped L1 দুর্লভ, যদিও খুব ছোট microcontroller-এ বা বিশেষ কাজের cache-এ এখনো দেখা যায়।

Victim cache-এর ধারণাও হারায়নি — কিছু নকশায় L3 আংশিক বা পুরোপুরি L2-এর victim হিসেবে কাজ করে বলে নির্মাতাদের বিবরণে পাওয়া যায়।

মূল উপলব্ধি: “কোনটি ভালো” প্রশ্নের উত্তর প্রযুক্তির দামের অনুপাতের ওপর নির্ভর করে — transistor, তার, শক্তি আর miss-এর দাম বদলালে সঠিক নকশাও বদলায়। ভালো প্রকৌশলী উত্তর নয়, মাপকাঠি মনে রাখেন: গড় প্রবেশ-সময় আর শক্তি।

প্র ০৫একই set-এ conflict সাধারণত কর্মক্ষমতার সমস্যা। নিরাপত্তার দিক থেকে এটি কেন সমস্যা, আর replacement বা placement নকশা কীভাবে প্রতিরক্ষায় সাহায্য করে?

শেষ স্তরের cache সাধারণত একাধিক core আর প্রোগ্রামের মধ্যে ভাগাভাগি করা। দুটি প্রোগ্রামের ডেটা একই set-এ পড়লে একজনের প্রবেশ অন্যজনের block বের করে দিতে পারে। এই বের হয়ে যাওয়া সময়ের পার্থক্য হিসেবে মাপা যায় — অর্থাৎ একটি প্রোগ্রাম অন্যটির সম্পর্কে কিছুটা তথ্য পেতে পারে, কোনো ডেটা সরাসরি না পড়েও। নিরাপত্তা-গবেষণায় একে cache side channel বলা হয়; ২০০০-এর দশক থেকে গবেষণাপত্রে দেখানো হয়েছে যে দুর্বলভাবে লেখা cryptographic কোডের টেবিল-প্রবেশ এভাবে ধরা পড়তে পারে, আর ২০১৮-র Spectre-জাতীয় সমস্যায় cache-ই ছিল তথ্য বের করার প্রধান পথ (পর্ব ৪১)। এখানে আমরা শুধু প্রতিরক্ষার দিক দেখব।

প্রথম প্রতিরক্ষা সফটওয়্যারে: constant-time কোড — গোপন তথ্যের ওপর নির্ভর করে কোনো টেবিলের ভিন্ন জায়গা পড়া বা শাখা নেওয়া হবে না। আধুনিক cryptographic library এবং AES-এর জন্য hardware instruction (যেমন x86-এর AES-NI) টেবিল-lookup-ই বাদ দেয়।

দ্বিতীয়, partitioning: cache-এর way-গুলো ভাগ করে দেওয়া, যাতে এক প্রোগ্রাম বা virtual machine অন্যজনের way ছুঁতে না পারে। Intel-এর Cache Allocation Technology মূলত কর্মক্ষমতার বিচ্ছিন্নতার জন্য (cloud-এ এক গ্রাহকের কাজ যেন অন্যজনের cache খেয়ে না ফেলে) এসেছিল, কিন্তু নিরাপত্তায়ও কাজে লাগে। এখানে associativity আবার মূল্য পায় — 16-way cache-কে কয়েক ভাগে ভাগ করা যায়, direct-mapped-কে যায় না।

তৃতীয়, placement এলোমেলো করা: গবেষণায় (CEASER, ScatterCache জাতীয় ২০১৮–২০১৯-এর দিকের প্রস্তাব) address থেকে set-এর মানচিত্রটি একটি গোপন চাবি দিয়ে encrypt বা hash করা হয়, আর মাঝে মাঝে চাবি বদলানো হয় — ফলে কোন address-গুলো একই set-এ পড়ে, তা বাইরে থেকে খুঁজে পাওয়া কঠিন। Random replacement-ও কিছুটা অনিশ্চয়তা যোগ করে, যদিও একা যথেষ্ট নয়।

লক্ষ করুন, এগুলোর প্রায় সবারই দাম আছে: partitioning প্রতিটি প্রোগ্রামের কার্যকর cache ছোট করে, এলোমেলো mapping-এ বাড়তি hash-এর সময় লাগে। নকশাকারকে কর্মক্ষমতা আর বিচ্ছিন্নতার মধ্যে ভারসাম্য খুঁজতে হয়।

মূল উপলব্ধি: Placement আর replacement শুধু গতির বিষয় নয় — ভাগাভাগি করা cache-এ তারা ঠিক করে কে কার ওপর প্রভাব ফেলতে পারে। তাই আধুনিক নকশায় associativity একই সাথে কর্মক্ষমতা আর বিচ্ছিন্নতার হাতিয়ার।

অনুশীলন

পরীক্ষার ধাঁচে লেখা — আগে খাতায় সমাধান করুন, তারপর উত্তর মিলিয়ে নিন। সব সংখ্যা এই পর্বের simulator-এ মেলানো।

  1. Address-এর ভাগ: ৬৪ KiB data, ৩২ B block, ৩২-বিট byte address. (ক) direct-mapped, (খ) 8-way, (গ) fully associative — প্রতিটিতে tag, index, offset কত বিট? (ঘ) 8-way-এ address 0x1234ABCD কোন set-এ যায়, tag কত?
    • মোট block = 65536 ÷ 32 = 2048; offset = log232 = ৫ বিট (সব ক্ষেত্রে)।
    • (ক) direct-mapped: set 2048, index ১১, tag = 32 − 11 − 5 = ১৬ │ ১১ │ ৫.
    • (খ) 8-way: set = 2048 ÷ 8 = 256, index ৮, tag ১৯ — ১৯ │ ৮ │ ৫. (index ৩ বিট কমেছে = log28.)
    • (গ) fully associative: index ০, tag ২৭ — ২৭ │ ০ │ ৫.
    • (ঘ) offset = 0x1234ABCD-এর নিচের ৫ বিট: 0xCD = 1100 1101 → 01101 = 13. ৫ বিট ডানে সরালে 0x91A55E; তার নিচের ৮ বিট 0x5E = set 94. আরও ৮ বিট সরালে tag = 0x91A5. মিলিয়ে দেখুন: 0x91A5 × 213 + 94 × 25 + 13 = 0x1234ABCD.
  2. LRU বনাম FIFO — trace হাতে: ৩ ঘরের fully associative cache (বা ৩-way-এর একটি set), শুরুতে খালি। Block ক্রম: 1, 2, 3, 1, 4, 1, 5, 2, 1, 3. LRU আর FIFO-তে প্রতিটি ধাপের অবস্থা লিখে মোট miss বের করুন। কোন ধাপে দুই নীতির পথ আলাদা হয়, আর কেন?

    অবস্থা লেখা হয়েছে পুরোনো থেকে নতুন ক্রমে (LRU-তে শেষ ব্যবহার, FIFO-তে ঢোকার সময়); বাঁয়েরটি পরের victim.

    ক্রম :  1    2    3    1    4    1    5    2    1    3
    LRU  :  M    M    M    h    M    h    M    M    h    M     = 7 miss
           [1]  [12] [123][231][314][341][415][152][521][213]
    FIFO :  M    M    M    h    M    M    M    M    h    M     = 8 miss
           [1]  [12] [123][123][234][341][415][152][152][523]
    • প্রথম পার্থক্য চতুর্থ ধাপে: 1-এর hit-এ LRU ক্রম বদলে 1-কে নতুন বানায় ([231]), FIFO কিছু বদলায় না ([123]). তাই পঞ্চম ধাপে 4 আসার সময় LRU বাদ দেয় 2, কিন্তু FIFO বাদ দেয় 1 — সবচেয়ে ব্যস্ত block!
    • ষষ্ঠ ধাপে 1: LRU-তে hit, FIFO-তে miss. মোট LRU ৭টি, FIFO ৮টি miss. FIFO “কে কত ব্যস্ত” দেখে না, শুধু কে আগে এসেছে। (এই পর্বের simulator-এ চালিয়ে মেলানো।)
  3. PLRU-তে victim বাছাই: 4-way set-এ tree-বিট b1 (root), b2 (way0/1), b3 (way2/3); নিয়ম: বিট ০ মানে victim বাঁ দিকে, প্রবেশে পথের বিট ছোঁয়া way থেকে উল্টো দিকে ঘোরে। এখন b1 b2 b3 = 1 0 1, সব way ভরা। (ক) একটি miss হলে victim কোন way? (খ) সেখানে নতুন block বসানোর পরে বিট কত, আর পরের victim কে? (গ) তারপর way1-এ একটি hit হলে বিট আর পরের victim?
    • (ক) b1 = 1 → ডান জোড়া (way2–3); b3 = 1 → ডান: victim way3.
    • (খ) way3 ছোঁয়া: সে ডান জোড়ায়, তাই b1 = 0 (বাঁ দিক নির্দেশ); জোড়ার ভেতরে ডানে, তাই b3 = 0. b2 অপরিবর্তিত (0). বিট 0 0 0 → পরের victim: বাঁ, বাঁ = way0.
    • (গ) way1 ছোঁয়া: বাঁ জোড়ায়, তাই b1 = 1; জোড়ার ভেতরে ডানে, তাই b2 = 0. বিট 1 0 0 → পরের victim: ডান জোড়া, b3 = 0 → way2. (simulator-এর PLRU কোডে মেলানো।)
  4. মোট বিট আর একটি কোড-প্রশ্ন: ১৬ KiB, 4-way, ৬৪ B block, ৩২-বিট address, write-back (প্রতি line-এ valid আর dirty, ১টি করে বিট), প্রতি set-এ 4-way PLRU. (ক) tag কত বিট আর মোট tag বিট? (খ) valid, dirty আর PLRU-সহ মোট বাড়তি বিট, আর data-র তুলনায় শতকরা কত? (গ) একই আকারে fully associative হলে tag + valid + dirty কত? (ঘ) এই পর্বের প্রথম simulator-এ 4-way PLRU-তে চক্রাকার 0..4 ক্রম চালাতে কী লিখবেন, আর ফল কী আসবে বলে অনুমান?
    • (ক) block = 16384 ÷ 64 = 256; set = 256 ÷ 4 = 64 → index ৬, offset ৬, tag = 32 − 12 = ২০ বিট; মোট tag = 256 × 20 = ৫১২০ বিট.
    • (খ) প্রতি line-এ ২০ + ২ = ২২ বিট → ২৫৬ × ২২ = ৫৬৩২; PLRU প্রতি set-এ ৩ বিট → ৬৪ × ৩ = ১৯২. মোট ৫৮২৪ বিট. Data = ১৬৩৮৪ × ৮ = ১৩১০৭২ বিট, তাই বাড়তি ≈ ৪.৪৪%.
    • (গ) fully associative: index ০, tag ২৬; 256 × (26 + 2) = ৭১৬৮ বিট — আর সাথে ২৫৬টি ২৬-বিটের comparator, 4-way-এর ৪টি ২০-বিটের বদলে।
    • (ঘ) sim(cyclic, 1, 4, "PLRU", false) — ১টি set, ৪টি way. এই পর্বের রানে ১৯টি miss (২০-এর মধ্যে); LRU-র মতো প্রায় সবই miss, কারণ PLRU-ও মোটামুটি LRU-র মতো আচরণ করে। Set-এর সংখ্যা ২ করলে (sim(cyclic, 2, 2, "LRU", false)) কী হবে, নিজে চালিয়ে দেখুন: 0, 2, 4 যায় set 0-এ, 1, 3 যায় set 1-এ।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রম4-way set-associative cache-এ একটি load-এর miss হলে ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Address-কে tag, index আর offset-এ ভাগ করা
  2. Index দিয়ে set বেছে চারটি way-এর tag আর valid বিট একসাথে পড়া
  3. চারটি comparator-এ tag তুলনা আর valid দিয়ে ছাঁকা — কোনো মিল নেই, miss
  4. Replacement policy দিয়ে victim way বাছাই (খালি way থাকলে সেটি)
  5. পরের স্তর থেকে block এনে victim-এর ঘরে বসানো, tag আর valid লেখা
  6. Replacement-এর অবস্থা হালনাগাদ আর offset দিয়ে দরকারি byte পাঠানো

৩২ KiB data-র একটি 8-way set-associative cache, block ৬৪ B, address ৩২ বিট। Index কত বিট?

  • ৯ বিট
  • ৩ বিট
  • ৬ বিট
  • ২০ বিট

৪টি block-এর cache, শুরুতে খালি, block address-এর ক্রম 0, 8, 0, 6, 8. 2-way set-associative আর LRU হলে কতগুলো miss?

  • ৫টি
  • ৪টি
  • ৩টি
  • ২টি

একটি 8-way set-এ tree pseudo-LRU-র জন্য প্রতি set-এ কত বিট লাগে, আর নিখুঁত LRU-র তাত্ত্বিক ন্যূনতম কত?

  • PLRU ৭ বিট, নিখুঁত LRU অন্তত ১৬ বিট
  • PLRU ৩ বিট, নিখুঁত LRU ৮ বিট
  • PLRU ৮ বিট, নিখুঁত LRU অন্তত ২৪ বিট
  • PLRU ২৮ বিট, নিখুঁত LRU ৭ বিট

৪টি ঘরের fully associative cache-এ block ক্রম 0, 1, 2, 3, 4 বারবার চক্রাকারে আসছে (৫টি block). LRU replacement-এ প্রথম ঘুরের পরে hit rate কত?

  • ৮০%
  • ৭৫%
  • প্রায় ৫০%
  • ০%

Page size ৪ KiB, আর L1 data cache VIPT (virtually indexed, physically tagged), যেখানে index ও offset page offset-এর ভেতরে থাকতে হবে। Cache 12-way হলে এর সর্বোচ্চ আকার কত?

  • ৩২ KiB
  • ৪৮ KiB
  • ৬৪ KiB
  • ৪ KiB

একটি direct-mapped cache একটি trace-এ ১২০টি miss করে। একই আকারের fully associative (LRU) cache একই trace-এ ৮০টি, আর অসীম বড় cache ৩০টি miss করে। Direct-mapped cache-এর conflict miss কতগুলো?

  • ৩০টি
  • ৫০টি
  • ৪০টি
  • ৯০টি

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. set = block address mod S, S = B ÷ n. ১-way = direct-mapped, ১টি set = fully associative. Associativity বাড়লে conflict miss কমে — 0, 8, 0, 6, 8-এ ৫ / ৪ / ৩.
  2. Address-এর ভাগ: আকার স্থির রেখে way দ্বিগুণ = index ১ বিট কম, tag ১ বিট বেশি। tag + index + offset = address-এর বিট; way-এর জন্য address-এ কোনো বিট নেই।
  3. Hardware: n টি tag একসাথে পড়া, n টি comparator, মিল AND valid = one-hot, তার OR = hit, আর n:1 mux. দাম hit time, জায়গা, শক্তি; fully associative মানে CAM — শুধু TLB-এর মতো ছোট কাঠামোয়।
  4. Replacement: নিখুঁত LRU ⌈log2(n!)⌉ বিট, PLRU tree n − 1 বিট, FIFO hit-এ কিছু বদলায় না, random অবস্থাহীন, NRU clock-ধাঁচ; OPT সর্বনিম্ন সীমা। চক্রাকারে n + 1 block-এ LRU-র hit শূন্য।
  5. বাস্তব: conflict = আসল − fully associative-এর miss; 8-way প্রায়ই FA-র কাছাকাছি (rule of thumb); victim cache আর way prediction; VIPT L1 ≤ way × page size — এই runner-এ ৮ × ৪ KiB = ৩২ KiB, আর একই set-এ ৯টি block-এই সময় কয়েক গুণ।

পরের পর্বে: miss-এর দাম কত, তা সময়ে মাপা — write-through বনাম write-back, write-allocate, AMAT-এর সূত্র, CPI-তে memory stall আর multi-level cache-এর local ও global miss rate.

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান