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

Cache-এর মৌলিক কথা — direct-mapped cache আর address-এর ভাগ

Cache Basics — Blocks, Direct-Mapped Placement, the Tag | Index | Offset Split, Valid Bits, Total Storage Bits, Block Size and the 3 C's

Taught in Bangla, this lesson covers cache basics: blocks and lines, hits and misses, direct-mapped placement, splitting an address into tag, index and offset, valid bits and the tag-compare hit circuit, the classic total-storage-bits numerical, a full textbook trace by hand and in a simulator, block-size trade-offs, the three C's of misses, handling misses in a pipeline, split versus unified caches, and quick tricks for hex addresses.

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

  • Cache কী, block বা line কেন, আর hit, miss, hit rate, miss penalty-র মানে
  • Direct-mapped cache-এ কোন block কোন ঘরে যায়, আর address থেকে tag, index, offset-এর বিট-সংখ্যা বের করার নিয়ম ও তার যুক্তি
  • Valid bit, tag তুলনা আর offset দিয়ে word বাছাইয়ের circuit, আর পরীক্ষার “মোট কত bit” numerical
  • পাঠ্যবইয়ের ৮-block trace হাতে আর simulator-এ, block size বাড়ালে কী লাভ-ক্ষতি, আর প্রতিটি miss-কে তিন C-এর কোনটিতে ফেলবেন
  • Pipeline-এ instruction আর data miss কীভাবে সামলানো হয়, split বনাম unified cache, আর hex address থেকে দ্রুত ভাগ পড়ার কৌশল

১ · Cache কী — ডেস্কের খাতা আর আলমারি

পর্ব ৪৪-এ (মেমরি হায়ারার্কি আর locality) দেখেছেন, কেন মেমরি স্তরে স্তরে সাজানো: register খুব দ্রুত কিন্তু খুব কম, main memory অনেক বড় কিন্তু CPU-র তুলনায় বহু গুণ ধীর। মাঝখানের সেতুটি হলো cache — CPU-র কাছে রাখা ছোট, দ্রুত SRAM, যাতে main memory-র সেই অংশের কপি থাকে, যা এখন কাজে লাগছে। Cache-এ আলাদা কোনো ঠিকানা নেই; প্রোগ্রাম সাধারণ memory address-ই দেয়, আর hardware নিজে খুঁজে দেখে কপিটি কাছে আছে কি না। Programmer-এর চোখে cache অদৃশ্য — শুধু গতিতে তার উপস্থিতি টের পাওয়া যায়।

একজন হিসাবরক্ষকের কথা ভাবুন। অফিসের আলমারিতে কয়েকশো খাতা, কিন্তু টেবিলে জায়গা মাত্র আটটি খাতার। প্রতিবার একটি সংখ্যার জন্য উঠে আলমারি পর্যন্ত গেলে সারাদিন হাঁটতেই কেটে যেত। তাই তিনি যে খাতাগুলো এখন লাগছে, সেগুলো টেবিলে এনে রাখেন। খেয়াল করুন, তিনি একটি পাতা ছিঁড়ে আনেন না — পুরো খাতা আনেন, কারণ একটি পাতা লাগলে পাশের পাতাগুলোও শিগগিরই লাগবে। টেবিলে খাতা পেলে কাজ সেকেন্ডে (hit); না পেলে আলমারি পর্যন্ত হাঁটা (miss), আর টেবিল ভরা থাকলে একটি খাতা ফেরত পাঠিয়ে জায়গা করতে হয়। Cache ঠিক এই টেবিল, খাতা হলো block, আলমারি main memory.

Block বা line — কপির একক

Cache কখনো একটি বাইট আলাদা করে আনে না। Memory-কে সমান মাপের টুকরোয় ভাগ করা হয় — একেকটি block বা line — আর cache আনে-রাখে পুরো block. আজকের সাধারণ x86-64 আর ARM প্রসেসরে L1-এর line সাধারণত ৬৪ বাইট (কিছু নকশায় ১২৮)। পাঠ্যবইয়ের ছোট উদাহরণে block হতে পারে ১ বা ৪ word. Block ধরে আনার কারণ পর্ব ৪৪-এর spatial locality: একটি ঠিকানা লাগলে পাশের ঠিকানাও শিগগিরই লাগার সম্ভাবনা বেশি (array-র পরের element, পরের instruction). আর একবার আনা block কিছুক্ষণ রেখে দেওয়ার কারণ temporal locality: যা এইমাত্র লেগেছে, তা আবার লাগার সম্ভাবনা বেশি (loop-এর counter, loop-এর instruction নিজেই)।

কয়েকটি শব্দ পুরো Module ৮-এ বারবার আসবে। যা খোঁজা হচ্ছে তা cache-এ থাকলে hit, না থাকলে miss. মোট access-এর মধ্যে hit-এর ভগ্নাংশ hit rate, আর miss rate = ১ − hit rate. Hit হলে উত্তর দিতে যে সময় লাগে, তা hit time; miss হলে পরের স্তর থেকে block এনে বসাতে যে বাড়তি সময় লাগে, তা miss penalty. এগুলো জুড়ে গড় সময়ের সূত্র (AMAT) আর CPI-এর হিসাব পর্ব ৪৭-এ (Cache-এর পারফরম্যান্স)।

যেকোনো cache-এর চারটি প্রশ্ন

যেকোনো cache-এর নকশা আসলে চারটি প্রশ্নের উত্তর। ১. Placement: একটি block cache-এর কোথায় বসতে পারে? ২. Identification: বসার পর তাকে চিনব কীভাবে? ৩. Replacement: জায়গা না থাকলে কাকে সরাব? ৪. Write: লেখা হলে memory-কে কখন জানাব? এই পর্ব প্রথম দুটি প্রশ্নের সবচেয়ে সরল উত্তর দেয় — direct-mapped cache. তৃতীয় প্রশ্ন, আর একাধিক ঘরের সুযোগ, পর্ব ৪৬-এ (Set-associative আর fully associative cache); চতুর্থটি পর্ব ৪৭-এ।

মূল কথা

Cache একটি অনুমান-নির্ভর ব্যবস্থা: hardware ধরে নেয়, যা লেগেছে আর যা তার পাশে আছে, তা আবার লাগবে। Locality থাকলে hit rate ৯৫% বা তারও বেশি হতে পারে, আর গড় memory access প্রায় cache-এর গতিতে চলে। Locality না থাকলে cache শুধু খরচ বাড়ায়।

২ · Direct-mapped cache — প্রতিটি block-এর একটিই ঘর

Placement-এর সবচেয়ে সরল নিয়ম: প্রতিটি memory block-এর জন্য cache-এ ঠিক একটি নির্দিষ্ট ঘর। কোন ঘর? Block-এর নম্বরকে cache-এর মোট block-সংখ্যা দিয়ে ভাগ করে ভাগশেষ:

index = (block address) mod (cache-এ block-এর সংখ্যা)

এখানে block address = byte address ÷ block size (ভাগফলের পূর্ণ অংশ) — অর্থাৎ memory-র কত নম্বর block. ধরুন cache-এ ৮টি block আর block size ১ word. Word address ২২-এর block ২২, আর ২২ mod ৮ = ৬, তাই সে বসবে ঘর ৬-এ। Word ১৪, ৩০, ৩৮ — সবারই ভাগশেষ ৬, তাই তারাও ঘর ৬-এর দাবিদার। একসাথে একজনই বসতে পারে।

মহল্লার গলিতে বাড়ির নম্বরের কথা ভাবুন। ধরুন একটি ছোট মহল্লায় ৮টি পার্কিংয়ের জায়গা, নম্বর ০ থেকে ৭, আর নিয়ম: যার গাড়ির নম্বরের শেষ অঙ্ক (৮-ভিত্তিক) যত, সে তত নম্বর জায়গায় রাখবে। খুঁজতে খুব সহজ — নম্বর দেখেই জায়গা বলা যায়, সব জায়গা ঘুরে দেখতে হয় না। কিন্তু একই শেষ অঙ্কের দুটি গাড়ি এলে একটিকে বের করে দিতে হয়, অন্য জায়গাগুলো খালি থাকলেও। Direct-mapped cache-এর সুবিধা আর দুর্বলতা দুটোই এই একটি নিয়মে।

কেন mod মানে শুধু বিট তুলে নেওয়া

Cache-এর block-সংখ্যা আর block size দুটোই ২-এর ঘাত রাখা হয়। পর্ব ০৪-এর (সংখ্যা পদ্ধতি) একটি সরল সত্য এখানে কাজে লাগে: কোনো binary সংখ্যাকে ২k দিয়ে ভাগ করলে ভাগশেষ হলো তার নিচের k বিট, আর ভাগফল হলো বাকি ওপরের বিট। যেমন ২২ = 10110; ৮ = ২৩ দিয়ে ভাগ করলে ভাগশেষ নিচের তিন বিট 110 = ৬, ভাগফল ওপরের দুই বিট 10 = ২। তাই hardware-এ কোনো ভাগের circuit লাগে না — address-এর তার থেকে কয়েকটি তার সরাসরি নিয়ে নিলেই index.

একই যুক্তি দুবার খাটালে পুরো byte address তিন ভাগ হয়ে যায়। ধরুন address n বিটের, block size ২b বাইট, cache-এ ২iটি block:

  • Block offset (নিচের b বিট): address ÷ ২b-এর ভাগশেষ — block-এর ভেতরে কত নম্বর বাইট। offset = log2(block size).
  • Index (পরের i বিট): block address ÷ ২i-এর ভাগশেষ — cache-এর কোন ঘর। index = log2(block-সংখ্যা) = log2(cache size ÷ block size).
  • Tag (ওপরের n − i − b বিট): block address ÷ ২i-এর ভাগফল। একই ঘরের দাবিদার অনেক block, তাদের আলাদা করে চেনার জন্য এটুকু ঘরের পাশে লিখে রাখা হয় — এটিই tag.

| tag : n − i − b | index : i | block offset : b |

কেন এটুকুই যথেষ্ট (ছোট প্রমাণ): যেকোনো address A-কে অনন্যভাবে লেখা যায় A = (tag × ২i + index) × ২b + offset, যেখানে ০ ≤ index < ২i আর ০ ≤ offset < ২b — এটি দুবার ভাগ-ভাগশেষের উপপাদ্য। ঘর নম্বর (index) জানা থাকায় সেটি লিখে রাখার দরকার নেই, offset তো block-এর ভেতরের অবস্থান মাত্র। তাই একটি ঘরে কোন block বসে আছে, তা নিশ্চিত বলতে শুধু tag জমা রাখাই যথেষ্ট — দুটি ভিন্ন block একই index-এ এলে তাদের tag অবশ্যই ভিন্ন। আবার এর চেয়ে কম বিটে চলবে না: একই index-এর ২n−i−bটি সম্ভাব্য block আলাদা করতে অন্তত n − i − b বিট লাগে।

সাধারণ ভুল: index-এর বিট হিসাব করতে cache size-কে সরাসরি log করা — যেমন ১৬ KiB = ২১৪, তাই “index ১৪ বিট”. না: index গোনে block, বাইট নয়। ১৬ KiB ÷ ১৬ B = ১,০২৪ = ২১০টি block, তাই index ১০ বিট, আর বাকি ৪ বিট offset. একটি ভালো যাচাই: index + offset = log2(cache size) — direct-mapped cache-এ সবসময়।

নিচের stepper-এ একটি বাস্তবধর্মী cache-এ একটি hex address খোঁজা হচ্ছে: ৩২-বিট address, ১৬ KiB data, ৬৪ B block. হিসাব: offset = log2৬৪ = ৬, block-সংখ্যা = ১৬,৩৮৪ ÷ ৬৪ = ২৫৬ = ২৮, তাই index = ৮, tag = ৩২ − ৮ − ৬ = ১৮ বিট।

Address-এর ভাগ আর cache-এ খোঁজা
ধাপ
Address (binary)
Offset (৬ বিট)
Index (৮ বিট)
Tag (১৮ বিট)
ঘর 0x59-এর অবস্থা
ফল
  1. Hex থেকে binary. প্রতিটি hex অঙ্ক চারটি বিট: 1→0001, 2→0010, … 8→1000. মোট ৩২ বিট, বাঁ দিকে বিট ৩১, ডান দিকে বিট ০।
  2. Offset = নিচের ৬ বিট। ডান থেকে ছয়টি বিট 111000 = ৫৬: block-এর ভেতরে ৫৬ নম্বর বাইট। ৪-বাইটের word ধরলে ৫৬ ÷ ৪ = ১৪ নম্বর word.
  3. Index = পরের ৮ বিট। বিট ১৩ থেকে ৬: 0101 1001 = 0x59 = ৮৯. এই address-এর block শুধু ঘর ৮৯-এ বসতে পারে — ২৫৬টি ঘরের মধ্যে আর কোথাও নয়।
  4. Tag = বাকি ওপরের ১৮ বিট। 00 0100 1000 1101 0001 = 0x48D1 = ১৮,৬৪১. ঘর ৮৯-এর দাবিদার ২১৮টি block-এর মধ্যে এটি কোনটি, তা এই সংখ্যাটিই বলে।
  5. ঘর ৮৯ দেখা। প্রোগ্রাম সবে শুরু, ঘরটির valid bit ০ — ভেতরে যা আছে তা অর্থহীন। Tag মিলুক বা না মিলুক, V = ০ মানেই miss. এই block আগে কখনো আসেনি, তাই এটি compulsory miss.
  6. Block আনা। পরের স্তর থেকে পুরো ৬৪ বাইট — ঠিকানা 0x12345640 থেকে 0x1234567F — এনে ঘর ৮৯-এ বসানো হয়, tag-এ 0x48D1 লেখা হয়, V = ১। তারপর offset ৫৬-এর বাইট (বা word) CPU-কে দেওয়া হয়।
  7. পাশের ঠিকানা। এরপর 0x12345640 চাইলে: index আবার 0x59, tag আবার 0x48D1, V = ১ — hit, শুধু offset ভিন্ন (০)। এটিই spatial locality-র লাভ: একটি miss-এর খরচে পুরো ৬৪ বাইট কাছে চলে এসেছে।

৩ · Hardware-এর ভেতরে — valid bit, tag তুলনা আর hit-এর circuit

প্রতিটি cache line আসলে তিনটি জিনিসের একটি সারি: data (পুরো block), tag, আর একটি valid bit. Valid bit কেন লাগে? Computer চালু হওয়ার মুহূর্তে cache-এর SRAM-এ এলোমেলো বিট থাকে; কোনো tag ঘটনাক্রমে মিলে গেলে CPU আবর্জনা পড়ত। তাই চালু হওয়ার সময় (বা OS যখন cache খালি করতে বলে) সব valid bit ০ করা হয়, আর block বসানোর সময় ১। নিচের ছবিটি পাঠ্যবইয়ের ক্লাসিক diagram-এর ধাঁচে: ৩২-বিট address, ১,০২৪টি line, প্রতিটি line-এ ৪ word (১৬ বাইট) — ভাগ ১৮ | ১০ | ৪.

31141343 21 0 Tag (18 bit) Index (10) word byte Address IndexVTagData (4 words = 128 bit) 01⋮i⋮10221023 word 0word 1word 2word 3 1stored tag = address tag AND Hit 4-to-1 MUX word offset (bit 3–2) Data (32 bit) সারি i
ছবি ৪৫.১ — Direct-mapped cache-এর lookup. Index (কমলা) একটি সারি বাছে; সেই সারির stored tag address-এর tag-এর (নীল) সাথে তুলনা হয়; তুলনার ফল AND valid = Hit (লাল); আর একই সময়ে চারটি word-এর মধ্যে word offset (সবুজ) একটি বেছে নেয়। Byte offset (বিট ১-০) word-এর ভেতরে বাইট বাছতে লাগে, word load-এ এটি সাধারণত ০।

ছবির তিনটি অংশ পর্ব ১২-এর (Multiplexer, decoder আর encoder) পুরোনো বন্ধু। Index একটি ১০-থেকে-১,০২৪ decoder-এর মতো কাজ করে — SRAM-এর ঠিক একটি সারি সক্রিয় হয়। Tag-এর তুলনা একটি equality comparator: ১৮ জোড়া বিটের প্রতিটিতে একটি XNOR (মিললে ১), তারপর সব ফল একটি বড় AND-এ। শেষে:

Hit = V · (stored tag = address tag)

আর data-র পথে একটি ৪-থেকে-১ multiplexer, যার select লাইন word offset-এর দুই বিট। লক্ষ করুন, tag তুলনা আর data পড়া একই সময়ে চলে — direct-mapped নকশায় একটি মাত্র সম্ভাব্য জায়গা, তাই hit হবে ধরে নিয়ে data আগেভাগেই multiplexer দিয়ে বেরিয়ে আসে; Hit = ০ হলে CPU সেটি ফেলে দেয়। এজন্যই direct-mapped cache-এর hit time সবচেয়ে কম। পর্ব ৪৬-এর set-associative cache-এ একাধিক tag-এর তুলনার পর কোন data নেওয়া হবে, সেই বাড়তি multiplexer hit time-এ কিছুটা যোগ করে।

Dirty bit — এক নজর আগে: store instruction cache-এর data বদলালে memory-র কপি পুরোনো হয়ে যায়। Write-back নীতিতে (পর্ব ৪৭) memory-তে তখনই লেখা হয় না; বরং line-এ একটি dirty bit ১ করে রাখা হয়, আর line সরানোর সময় dirty হলে আগে memory-তে লিখে দিতে হয়। তাই write-back cache-এ প্রতি line-এ আরও এক বিট। এই পর্বের হিসাবে আমরা দুই রকমই দেখাব।

৪ · বিটের হিসাব — tag, index, offset আর “মোট কত bit”

পরীক্ষায় cache-এর সবচেয়ে প্রচলিত numerical দুটি: (১) একটি configuration দেওয়া আছে, tag, index আর offset কত বিট? (২) পুরো cache বানাতে মোট কত bit SRAM লাগবে? দ্বিতীয়টির ফাঁদ হলো, “cache size” শুধু data গোনে (Module ৮-এর নিয়ম), অথচ প্রতিটি line-এ tag আর valid bit-ও জমা রাখতে হয়।

তিন ধাপের নিয়ম

  1. offset = log2(block size বাইটে)
  2. line-সংখ্যা = cache size ÷ block size; index = log2(line-সংখ্যা)
  3. tag = address-এর বিট − index − offset

আর মোট bit = line-সংখ্যা × (৮ × block size + tag + ১ valid [+ ১ dirty]).

AddressCache (data)BlockLine-সংখ্যাtag | index | offsetপ্রতি line-এ bitমোট
৩২ বিট১৬ KiB১৬ B (৪ word)১,০২৪১৮ | ১০ | ৪১২৮ + ১৮ + ১ = ১৪৭১৪৭ Kibit ≈ ১৮.৩৮ KiB
৩২ বিট১৬ KiB৬৪ B২৫৬১৮ | ৮ | ৬৫১২ + ১৮ + ১ = ৫৩১১৩২.৭৫ Kibit ≈ ১৬.৫৯ KiB
৩২ বিট৬৪ KiB৩২ B২,০৪৮১৬ | ১১ | ৫২৫৬ + ১৬ + ১ = ২৭৩৫৪৬ Kibit = ৬৮.২৫ KiB
৩২ বিট৪ KiB৪ B (১ word)১,০২৪২০ | ১০ | ২৩২ + ২০ + ১ = ৫৩৫৩ Kibit ≈ ৬.৬৩ KiB

পূর্ণ সমাধান — পাঠ্যবইয়ের “মোট কত bit” সমস্যা

প্রশ্ন: একটি direct-mapped cache-এ ১৬ KiB data, block ৪ word (১ word = ৪ বাইট), address ৩২ বিট। মোট কত bit লাগবে?

  • Block size = ৪ × ৪ = ১৬ বাইট → offset = log2১৬ = ৪ বিট (২ বিট word বাছাই + ২ বিট বাইট বাছাই)।
  • Line-সংখ্যা = ১৬,৩৮৪ ÷ ১৬ = ১,০২৪ = ২১০ → index = ১০ বিট.
  • Tag = ৩২ − ১০ − ৪ = ১৮ বিট.
  • প্রতি line: data ৪ × ৩২ = ১২৮ বিট, tag ১৮, valid ১ → ১৪৭ বিট।
  • মোট = ১,০২৪ × ১৪৭ = ১,৫০,৫২৮ বিট = ১৪৭ Kibit ≈ ১৮.৪ KiB — অর্থাৎ ১৬ KiB data-র জন্য প্রায় ১.১৫ গুণ SRAM.

টেবিলের শেষ সারিটি লক্ষ করুন: ১ word-এর block-এ প্রতি ৩২ বিট data-র জন্য ২১ বিট বাড়তি — প্রায় ৬৬% overhead. Block বড় হলে প্রতি line-এ tag একটিই থাকে কিন্তু data বাড়ে, তাই overhead দ্রুত কমে: ৬৪ B block-এ মাত্র ৩.৭%. বাস্তব cache-এর block কেন বড়, তার এটি একটি কারণ। নিচের calculator একই নিয়ম পাঁচটি configuration-এ চালায় — শেষ দুটিতে dirty bit, আর শেষটিতে ৬৪-বিট address.

// Direct-mapped cache: address split and total storage bits.
// "Cache size" counts data only; every line also stores a tag, a valid
// bit and (for a write-back cache, see lesson 47) a dirty bit.
const KiB = 1024;
function split(addrBits, cacheBytes, blockBytes, dirty) {
  const lines = cacheBytes / blockBytes;
  const off = Math.log2(blockBytes), idx = Math.log2(lines);
  const tag = addrBits - idx - off;
  const perLine = 8 * blockBytes + tag + 1 + (dirty ? 1 : 0);
  const total = lines * perLine, data = 8 * cacheBytes;
  console.log(`${addrBits}-bit, ${cacheBytes / KiB} KiB, ${blockBytes} B blocks` +
    (dirty ? " (+dirty)" : ""));
  console.log(`   lines = ${lines} = 2^${idx}   tag|index|offset = ${tag}|${idx}|${off}`);
  console.log(`   bits/line = ${8 * blockBytes} data + ${tag} tag + 1 valid` +
    (dirty ? " + 1 dirty" : "") + ` = ${perLine}`);
  console.log(`   total = ${lines} x ${perLine} = ${total} bits = ${total / KiB} Kibit` +
    ` = ${(total / 8 / KiB).toFixed(2)} KiB   (overhead ${(100 * (total - data) / data).toFixed(1)}%)`);
}
split(32, 16 * KiB, 16, false);   // textbook: 16 KiB data, 4-word blocks
split(32, 16 * KiB, 64, false);
split(32, 64 * KiB, 32, false);
split(32, 64 * KiB, 32, true);    // same cache, write-back
split(64, 32 * KiB, 64, true);    // a 64-bit address, typical L1 line size

আউটপুট:

32-bit, 16 KiB, 16 B blocks
   lines = 1024 = 2^10   tag|index|offset = 18|10|4
   bits/line = 128 data + 18 tag + 1 valid = 147
   total = 1024 x 147 = 150528 bits = 147 Kibit = 18.38 KiB   (overhead 14.8%)
32-bit, 16 KiB, 64 B blocks
   lines = 256 = 2^8   tag|index|offset = 18|8|6
   bits/line = 512 data + 18 tag + 1 valid = 531
   total = 256 x 531 = 135936 bits = 132.75 Kibit = 16.59 KiB   (overhead 3.7%)
32-bit, 64 KiB, 32 B blocks
   lines = 2048 = 2^11   tag|index|offset = 16|11|5
   bits/line = 256 data + 16 tag + 1 valid = 273
   total = 2048 x 273 = 559104 bits = 546 Kibit = 68.25 KiB   (overhead 6.6%)
32-bit, 64 KiB, 32 B blocks (+dirty)
   lines = 2048 = 2^11   tag|index|offset = 16|11|5
   bits/line = 256 data + 16 tag + 1 valid + 1 dirty = 274
   total = 2048 x 274 = 561152 bits = 548 Kibit = 68.50 KiB   (overhead 7.0%)
64-bit, 32 KiB, 64 B blocks (+dirty)
   lines = 512 = 2^9   tag|index|offset = 49|9|6
   bits/line = 512 data + 49 tag + 1 valid + 1 dirty = 563
   total = 512 x 563 = 288256 bits = 281.5 Kibit = 35.19 KiB   (overhead 10.0%)

প্রথম সারি পাঠ্যবইয়ের উত্তর: ১৪৭ Kibit, overhead ১৪.৮%. দ্বিতীয় সারিতে একই ১৬ KiB, কিন্তু ৬৪ B block — tag একই ১৮ বিট থাকে (কারণ index + offset = ১৪ বদলায় না), কিন্তু line-সংখ্যা চার ভাগের এক, তাই মোট tag-বিট কমে overhead নেমে আসে ৩.৭%-এ। Dirty bit যোগ করলে প্রতি line-এ ২৭৩ থেকে ২৭৪, মোট ৫৪৬ থেকে ৫৪৮ Kibit. শেষ সারির ৬৪-বিট address-এ tag ৪৯ বিটে লাফিয়ে ওঠে আর overhead ১০% — তবে সাবধান: বাস্তব CPU-র cache সাধারণত পূর্ণ ৬৪-বিট virtual address দিয়ে tag রাখে না; physical address প্রায়ই ৪০ থেকে ৫২ বিটের মতো, আর L1-এ virtual আর physical address-এর মিশ্র কৌশল থাকে (পর্ব ৫০)। Calculator-এ split(...) লাইন বদলে নিজের configuration দিয়ে দেখুন।

৫ · একটি পূর্ণ trace — পাঠ্যবইয়ের ৮-block cache

এবার সবচেয়ে পরিচিত হাতে-কলমে উদাহরণ (Patterson–Hennessy-র ধাঁচে)। Cache-এ ৮টি block, প্রতিটি ১ word; address দেওয়া word-এ, ৫ বিটের। তাই offset ০ বিট, index = log2৮ = ৩ বিট (নিচের তিন), tag = ২ বিট (ওপরের দুই)। প্রোগ্রাম পরপর এই word address-গুলো চায়:

২২, ২৬, ২২, ২৬, ১৬, ৩, ১৬, ১৮

প্রতিটি address binary-তে লিখে নিচের তিন বিট দেখুন — সেটিই ঘর। ২২ = 10 110 → ঘর ১১০ (৬), tag ১০। ২৬ = 11 010 → ঘর ০১০ (২), tag ১১। ১৬ = 10 000 → ঘর ০০০, tag ১০। ৩ = 00 011 → ঘর ০১১, tag ০০। ১৮ = 10 010 → ঘর ০১০, tag ১০ — ২৬-এর ঘরেই! নিচের stepper-এ প্রতিটি access-এর পর পুরো cache-এর ছবি।

প্রতিটি access-এর পরে cache (V · tag · data)
Access
ঘর · tag
ফল
000
001
010
011
100
101
110
111
Hit এ পর্যন্ত
  1. ২২. ঘর ১১০ খালি (N), তাই miss. Memory থেকে word ২২ এনে ঘর ১১০-এ বসানো হলো, tag ১০, V = Y. প্রথম ছোঁয়া, তাই compulsory.
  2. ২৬. ঘর ০১০ খালি — miss, compulsory. Tag ১১ লেখা হলো।
  3. ২২ আবার. ঘর ১১০-এ V = Y আর tag ১০ = address-এর tag ১০ — hit. এটি temporal locality.
  4. ২৬ আবার. ঘর ০১০, tag ১১ মেলে — hit. চারটি access-এ দুটি hit.
  5. ১৬. ঘর ০০০ খালি — miss, compulsory. এখন আটটির মধ্যে তিনটি ঘর ভরা।
  6. ৩. ঘর ০১১ খালি — miss, compulsory. Tag ০০।
  7. ১৬ আবার. ঘর ০০০, tag ১০ মেলে — hit.
  8. ১৮. ঘর ০১০-এ V = Y, কিন্তু সেখানে tag ১১ (word ২৬), আর চাই tag ১০ — miss. পুরোনো ২৬ সরিয়ে ১৮ বসল। মোট ৩টি hit, ৫টি miss: hit rate ৩/৮ = ৩৭.৫%. ১৮ আগে কখনো আসেনি, তাই এই miss-ও compulsory — তবে ২৬ এখন সরে গেছে; ২৬ আবার এলে সেটি হতো conflict miss (অংশ ৭)।

এই হিসাব হাতে করতে করতে ভুল হওয়া সহজ, তাই একটি simulator বানানো যাক। নিচের প্রোগ্রাম যেকোনো cache size, block size আর address-এর বিট-সংখ্যা নেয়, trace দশমিক বা hex-এ, আর প্রতিটি access-এর tag, index, offset, hit বা miss, miss-এর তিন C-এর শ্রেণি (অংশ ৭-এর নিয়ম), শেষে cache-এর ভেতরে কী আছে আর hit rate ছাপে। পাঁচটি পরীক্ষা চলে: পাঠ্যবইয়ের trace; একই trace ৪-word block-এ (একই ৮ word ধারণক্ষমতা); ৪-word block-এ টানা একটি scan; একটি ping-pong; আর ১৬ KiB, ৬৪ B block-এর একটি hex trace.

// Direct-mapped cache simulator. Sizes are in addressable units (bytes,
// or words for the textbook trace). Miss class (3 C's): first touch of the
// block = compulsory; else a fully associative LRU cache with the same
// number of blocks would also miss = capacity; else = conflict.
function simulate(title, cacheSize, blockSize, addrBits, trace, hex) {
  const nBlocks = cacheSize / blockSize, offBits = Math.log2(blockSize);
  const idxBits = Math.log2(nBlocks), tagBits = addrBits - idxBits - offBits;
  const line = Array.from({length: nBlocks}, () => ({v: 0, tag: 0, blk: 0}));
  const seen = new Set(), fa = [];        // fa: fully associative LRU list
  let hits = 0;
  const f = (n, w) => (hex ? "0x" + n.toString(16).toUpperCase() : String(n)).padStart(w);
  console.log(`== ${title}: ${nBlocks} blocks x ${blockSize}, tag|index|offset = ${tagBits}|${idxBits}|${offBits} bits`);
  for (const a of trace) {
    const blk = Math.floor(a / blockSize), idx = blk % nBlocks;
    const tag = Math.floor(blk / nBlocks), off = a % blockSize, L = line[idx];
    const hit = L.v === 1 && L.tag === tag, p = fa.indexOf(blk);
    if (p >= 0) fa.splice(p, 1); else if (fa.length === nBlocks) fa.shift();
    fa.push(blk);
    const kind = hit ? "" : !seen.has(blk) ? "compulsory" : p >= 0 ? "conflict" : "capacity";
    seen.add(blk);
    if (hit) hits++; else Object.assign(L, {v: 1, tag, blk});
    console.log(`${f(a, 11)}  tag ${f(tag, 7)}  idx ${f(idx, 5)}  off ${f(off, 4)}  ${hit ? "HIT " : "MISS"} ${kind}`);
  }
  const live = line.map((L, i) => L.v ? `[${f(i, 1)}] tag ${f(L.tag, 1)} = block ${f(L.blk, 1)}` : "").filter(s => s);
  console.log("cache now: " + live.join(", "));
  console.log(`hits ${hits}/${trace.length}  hit rate ${(100 * hits / trace.length).toFixed(1)}%\n`);
}
const book = [22, 26, 22, 26, 16, 3, 16, 18];          // word addresses
simulate("textbook, 1-word blocks", 8, 1, 5, book);
simulate("same 8 words, 4-word blocks", 8, 4, 5, book);
simulate("scan 16..23, 4-word blocks", 8, 4, 5, [16, 17, 18, 19, 20, 21, 22, 23]);
simulate("ping-pong 0,8,0,8", 8, 1, 5, [0, 8, 0, 8, 0, 8]);
simulate("16 KiB, 64 B blocks, 32-bit", 16384, 64, 32,
  [0x00012340, 0x0001237C, 0x00016340, 0x00012344, 0x12345678, 0x12345640], true);

আউটপুটের প্রথম অংশ — পাঠ্যবইয়ের trace আর ৪-word block-এ একই trace:

== textbook, 1-word blocks: 8 blocks x 1, tag|index|offset = 2|3|0 bits
         22  tag       2  idx     6  off    0  MISS compulsory
         26  tag       3  idx     2  off    0  MISS compulsory
         22  tag       2  idx     6  off    0  HIT
         26  tag       3  idx     2  off    0  HIT
         16  tag       2  idx     0  off    0  MISS compulsory
          3  tag       0  idx     3  off    0  MISS compulsory
         16  tag       2  idx     0  off    0  HIT
         18  tag       2  idx     2  off    0  MISS compulsory
cache now: [0] tag 2 = block 16, [2] tag 2 = block 18, [3] tag 0 = block 3, [6] tag 2 = block 22
hits 3/8  hit rate 37.5%

== same 8 words, 4-word blocks: 2 blocks x 4, tag|index|offset = 2|1|2 bits
         22  tag       2  idx     1  off    2  MISS compulsory
         26  tag       3  idx     0  off    2  MISS compulsory
         22  tag       2  idx     1  off    2  HIT
         26  tag       3  idx     0  off    2  HIT
         16  tag       2  idx     0  off    0  MISS compulsory
          3  tag       0  idx     0  off    3  MISS compulsory
         16  tag       2  idx     0  off    0  MISS conflict
         18  tag       2  idx     0  off    2  HIT
cache now: [0] tag 2 = block 4, [1] tag 2 = block 5
hits 3/8  hit rate 37.5%

প্রথম টেবিল stepper-এর সাথে ঘর-ঘর মেলে: tag ২ মানে binary 10, idx ৬ মানে 110, আর শেষে চারটি ঘর ভরা — ০-তে ১৬, ২-তে ১৮ (২৬ সরে গেছে), ৩-এ ৩, ৬-এ ২২. দ্বিতীয় টেবিলে block ৪ word, কিন্তু মোট ধারণক্ষমতা একই ৮ word, তাই মাত্র ২টি block: offset ২ বিট, index ১ বিট। এখানে দুটি উল্টো ঘটনা একসাথে ঘটেছে। লাভ: ১৮ এখন hit — কারণ ১৬-র miss-এ পুরো block ৪ (word ১৬–১৯) চলে এসেছিল; এটি spatial locality. ক্ষতি: দ্বিতীয় ১৬ এখন miss — word ৩-এর block ০ আর ১৬-র block ৪ একই ঘর ০-তে পড়ে (দুটি ঘর মাত্র!), তাই ৩ এসে ১৬-কে সরিয়ে দিয়েছে; simulator একে বলছে conflict. ফল সমান-সমান, ৩/৮.

বাকি তিন অংশ:

== scan 16..23, 4-word blocks: 2 blocks x 4, tag|index|offset = 2|1|2 bits
         16  tag       2  idx     0  off    0  MISS compulsory
         17  tag       2  idx     0  off    1  HIT
         18  tag       2  idx     0  off    2  HIT
         19  tag       2  idx     0  off    3  HIT
         20  tag       2  idx     1  off    0  MISS compulsory
         21  tag       2  idx     1  off    1  HIT
         22  tag       2  idx     1  off    2  HIT
         23  tag       2  idx     1  off    3  HIT
cache now: [0] tag 2 = block 4, [1] tag 2 = block 5
hits 6/8  hit rate 75.0%

== ping-pong 0,8,0,8: 8 blocks x 1, tag|index|offset = 2|3|0 bits
          0  tag       0  idx     0  off    0  MISS compulsory
          8  tag       1  idx     0  off    0  MISS compulsory
          0  tag       0  idx     0  off    0  MISS conflict
          8  tag       1  idx     0  off    0  MISS conflict
          0  tag       0  idx     0  off    0  MISS conflict
          8  tag       1  idx     0  off    0  MISS conflict
cache now: [0] tag 1 = block 8
hits 0/6  hit rate 0.0%

== 16 KiB, 64 B blocks, 32-bit: 256 blocks x 64, tag|index|offset = 18|8|6 bits
    0x12340  tag     0x4  idx  0x8D  off  0x0  MISS compulsory
    0x1237C  tag     0x4  idx  0x8D  off 0x3C  HIT
    0x16340  tag     0x5  idx  0x8D  off  0x0  MISS compulsory
    0x12344  tag     0x4  idx  0x8D  off  0x4  MISS conflict
 0x12345678  tag  0x48D1  idx  0x59  off 0x38  MISS compulsory
 0x12345640  tag  0x48D1  idx  0x59  off  0x0  HIT
cache now: [0x59] tag 0x48D1 = block 0x48D159, [0x8D] tag 0x4 = block 0x48D
hits 2/6  hit rate 33.3%

টানা scan-এ ৪-word block চমৎকার: প্রতি ৪টি access-এ একটি miss, hit rate ৭৫%. ১-word block-এ এই আটটি word-এর প্রতিটিই নতুন block, তাই আটটিই compulsory miss হতো। Ping-pong-এ ০ আর ৮ দুজনেরই ঘর ০ (৮ mod ৮ = ০), তাই তারা পালা করে একে অপরকে সরায় — ছয়টি access-এ একটিও hit নেই, অথচ বাকি সাতটি ঘর পুরো সময় খালি। শেষ অংশে 0x12340 আর 0x16340-এর পার্থক্য 0x4000 = ১৬ KiB, অর্থাৎ ঠিক cache size — তাই একই index 0x8D, ভিন্ন tag (৪ ও ৫); 0x16340 এসে 0x12340-এর block সরিয়ে দেয়, ফলে পরের 0x12344 conflict miss. 0x12345678-এর ভাগ (0x48D1, 0x59, 0x38) অংশ ২-এর stepper-এর সাথে মেলে। নিজের trace বসিয়ে দেখুন — যেমন book-এর শেষে আবার ২৬ যোগ করলে কী হয়।

৬ · Block কত বড় হবে — লাভ, ক্ষতি আর miss rate-এর বাঁক

ওপরের দুটি পরীক্ষা আসলে পুরো নকশা-প্রশ্নটির ক্ষুদ্র সংস্করণ। Cache-এর মোট আকার স্থির রেখে block বড় করলে তিনটি জিনিস ঘটে:

লাভ: spatial locality

একটি miss-এ পাশের অনেক বাইট আসে। টানা চলা data (array, instruction-এর ক্রম) আর আলাদা miss করে না, তাই compulsory miss কমে। Tag-এর overhead-ও কমে (অংশ ৪)।

ক্ষতি: block-এর সংখ্যা কমে

৮ word-এর cache-এ ১-word block মানে ৮টি ঘর, ৪-word block মানে ২টি। দূরের ঠিকানাগুলো কম ঘরে ঠেলাঠেলি করে — conflict বাড়ে, আর বড় block-এর অনেক বাইট কাজে লাগার আগেই সরে যায়।

ক্ষতি: miss penalty বাড়ে

Memory থেকে প্রথম বাইট আসতে একটি স্থির দেরি, তারপর প্রতিটি বাড়তি বাইট আনতে আরও সময়। ১২৮ বাইট আনতে ৬৪ বাইটের চেয়ে বেশি সময় লাগে, আর অব্যবহৃত বাইটের জন্য bandwidth-ও খরচ হয়।

ফল: একটি নির্দিষ্ট cache size-এ block size বাড়ালে miss rate প্রথমে দ্রুত কমে, তারপর সমতল হয়, আর block যখন cache-এর তুলনায় বড় হয়ে block-এর সংখ্যা খুব কমে যায়, তখন আবার বাড়তে পারে। পাঠ্যবইয়ে benchmark-এর মাপা miss rate-এর যে বক্ররেখা দেখানো হয়, তাতে ছোট cache-এ এই “U” আকার স্পষ্ট, আর বড় cache-এ বাঁকটি অনেক পরে আসে — কারণ বড় cache-এ block বড় করলেও যথেষ্ট ঘর থাকে। সঠিক সংখ্যাগুলো workload আর মাপার পদ্ধতিভেদে বদলায়, তাই এখানে আকারটিই মনে রাখুন। আরও একটি সূক্ষ্মতা: miss rate সর্বনিম্ন যেখানে, গড় সময় সর্বনিম্ন সেখানে না-ও হতে পারে, কারণ বড় block-এ miss penalty-ও বাড়ে; তাই নকশাকাররা গড় memory access time (AMAT, পর্ব ৪৭) দেখে সিদ্ধান্ত নেন। বাস্তবে সাধারণ CPU-র L1-এ ৬৪ বাইট বহুদিন ধরে প্রচলিত।

নিচের C প্রোগ্রাম JS simulator-এর একটি স্বাধীন cross-check: একই direct-mapped নিয়মে একটি ১ KiB cache-এর model, যা একটি ৪ KiB int array-র তিন রকম ব্যবহারে ১৬ B আর ৬৪ B block-এর miss rate ছাপে। দ্বিতীয় আর তৃতীয় trace-এ দুটি array a[] আর b[] পাশাপাশি পড়া হয়; b-এর শুরুর ঠিকানা কেবল ৬৪ বাইট সরানো হয়েছে।

// Direct-mapped cache model (1 KiB data) fed with array-access traces.
// int a[1024] starts at address 0; b[] starts right after it (aliased)
// or one 64-byte line later (padded). Prints miss rate per block size.
#include <stdio.h>
#include <string.h>
#define CACHE 1024u
static unsigned tagv[CACHE / 4];
static unsigned char valid[CACHE / 4];
static unsigned long accesses, misses;
static void access_addr(unsigned addr, unsigned block) {
    unsigned lines = CACHE / block, blk = addr / block;
    unsigned idx = blk % lines, tag = blk / lines;
    accesses++;
    if (!valid[idx] || tagv[idx] != tag) {      /* miss: fetch the block */
        misses++; valid[idx] = 1; tagv[idx] = tag;
    }
}
static double miss_rate(int trace, unsigned block) {
    memset(valid, 0, sizeof valid);
    accesses = misses = 0;
    unsigned b = trace == 1 ? 4096u : 4096u + 64u;   /* address of b[0] */
    for (unsigned i = 0; i < 1024; i++) {
        access_addr(4 * i, block);                    /* read a[i] */
        if (trace) access_addr(b + 4 * i, block);     /* read b[i] */
    }
    return 100.0 * (double)misses / (double)accesses;
}
int main(void) {
    const char *name[] = {"scan a[i]", "a[i]+b[i], b = a+4096", "a[i]+b[i], b = a+4096+64"};
    printf("%-26s %10s %10s\n", "trace (1 KiB cache)", "16 B", "64 B");
    for (int t = 0; t < 3; t++)
        printf("%-26s %9.2f%% %9.2f%%\n", name[t], miss_rate(t, 16), miss_rate(t, 64));
    return 0;
}

আউটপুট:

trace (1 KiB cache)              16 B       64 B
scan a[i]                      25.00%      6.25%
a[i]+b[i], b = a+4096         100.00%    100.00%
a[i]+b[i], b = a+4096+64       25.00%      6.25%

প্রথম সারি spatial locality-র নিখুঁত হিসাব: ৪ বাইটের int, তাই ১৬ B block-এ প্রতি ৪টি পড়ায় একটি miss (২৫%), ৬৪ B block-এ প্রতি ১৬টিতে একটি (৬.২৫%) — block চার গুণ, miss চার ভাগের এক। দ্বিতীয় সারিতে b ঠিক ৪,০৯৬ বাইট পরে শুরু, যা cache size ১,০২৪-এর গুণিতক; তাই a[i] আর b[i] সবসময় একই ঘরে পড়ে আর পালা করে একে অপরকে সরায় — block যত বড়ই হোক, ১০০% miss. এটি ওপরের ping-pong-এরই বড় সংস্করণ, আর block বড় করে এর কোনো সমাধান নেই। তৃতীয় সারিতে b-কে মাত্র ৬৪ বাইট সরানো হয়েছে, তাতে দুই array ভিন্ন ঘরে পড়ে, আর miss rate আবার প্রথম সারির মতো। এই “padding” কৌশল বাস্তব প্রোগ্রামে কীভাবে কাজে লাগে, তা পর্ব ৪৮-এ (Cache-বান্ধব প্রোগ্রাম)। সংখ্যাগুলো JS simulator-এর একই নিয়মের সাথে মিলিয়ে দেখা হয়েছে।

৭ · তিন C — miss কেন হলো, তার শ্রেণিভাগ

সব miss এক জাতের নয়, আর প্রতিকারও আলাদা। Mark Hill-এর প্রবর্তিত বলে পরিচিত “three C's” মডেল প্রতিটি miss-কে তিনটি শ্রেণির একটিতে ফেলে:

  • Compulsory miss (cold-start): block-টি এই প্রথম চাওয়া হলো — যত বড় cache-ই হোক, এটি এড়ানো যেত না (তবে বড় block আর prefetching এর সংখ্যা কমায়)।
  • Capacity miss: প্রোগ্রাম এই মুহূর্তে যত block ঘুরিয়ে ফিরিয়ে ব্যবহার করছে, তা cache-এ একসাথে আঁটে না — একই আকারের, যেকোনো ঘরে বসতে পারে এমন cache-ও এখানে miss করত। প্রতিকার: বড় cache.
  • Conflict miss (collision): cache-এ জায়গা ছিল, কিন্তু placement-এর নিয়মে দুটি block একই ঘরের দাবিদার হওয়ায় একটি সরে গেছে। প্রতিকার: বেশি associativity (পর্ব ৪৬), বা data-র ঠিকানা সরানো।
শ্রেণিভাগের নিয়ম

একটি miss হলে প্রশ্ন করুন: (১) এই block কি আগে কখনো এসেছে? না → compulsory. (২) হ্যাঁ হলে: একই সংখ্যক block-এর একটি fully associative cache (যেকোনো block যেকোনো ঘরে, LRU দিয়ে সরানো) কি এখানে hit করত? না → capacity; হ্যাঁ → conflict. ওপরের simulator ঠিক এই নিয়মটি চালায় — পাশাপাশি একটি fully associative LRU তালিকা রেখে।

এখন trace-গুলোর miss শ্রেণিভাগ করা যাক। পাঠ্যবইয়ের ১-word trace-এ পাঁচটি miss-এর পাঁচটিই compulsory — প্রতিটি miss-এ এমন word এসেছে যা আগে কখনো আসেনি। ৪-word সংস্করণে দ্বিতীয় ১৬-র miss conflict: ৩ আর ১৬-র block একই ঘরে, অথচ ২-block-এর একটি fully associative cache-এ তখন block ৪ (১৬–১৯) আর block ০ (০–৩) দুটোই থাকত। Ping-pong-এর প্রথম দুটি compulsory, বাকি চারটি conflict — ৮টি ঘরের মধ্যে মাত্র দুটি block, কাজেই capacity-র প্রশ্নই নেই।

Capacity miss দেখতে একটু বড় trace লাগে। অনুশীলন ২-এ একটি ৪-block cache-এ পাঁচটি ভিন্ন block ঘুরিয়ে চাওয়ার পর প্রথম block ফিরে এলে simulator যে “capacity” দেখায়, তা হাতে মিলিয়ে নিতে পারবেন। মনে রাখার মতো আরও একটি কথা: এই শ্রেণিভাগ একটি মডেল, প্রকৃতির নিয়ম নয়। Fully associative cache-এর replacement নীতি (LRU, না অন্য কিছু) বদলালে শ্রেণির সংখ্যাও বদলায়, আর মাঝে মাঝে direct-mapped cache এমন জায়গায় hit করে যেখানে fully associative LRU miss করত। Multicore-এ একটি চতুর্থ C-ও আছে — coherence miss — যা পর্ব ৫৩-এ (Multicore আর cache coherence).

বাস্তব যন্ত্রে ping-pong — দেখা যায় কি?

বাস্তব CPU-র L1 data cache direct-mapped নয়; সাধারণত ৮ থেকে ১২-way set-associative (পর্ব ৪৬)। তাই দুটি ঠিকানা একই set-এ পড়লেও তারা পাশাপাশি থাকতে পারে, আর ওপরের দুই-array ping-pong বাস্তবে প্রায় দেখাই যায় না। কিন্তু একই set-এ way-এর চেয়ে বেশি line পড়লে conflict ফিরে আসে। সাধারণ x86-64 L1-এ ৬৪ বাইটের line আর ৬৪টি set থাকলে ৪,০৯৬ বাইট দূরের ঠিকানাগুলো একই set-এ পড়ে। নিচের প্রোগ্রাম ৩২টি এমন ঠিকানা বারবার পড়ে, তারপর একই কাজ করে ৪,০৯৬ + ৬৪ বাইট দূরত্বে, যেখানে ৩২টি line ৩২টি ভিন্ন set-এ ছড়িয়ে পড়ে।

// Real machine: K lines 4096 B apart (all map to the same L1 set) versus
// 4096+64 B apart (spread over K sets). Timings vary per run and machine.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define K 32
#define ROUNDS 200000
static double now_ns(void) {
    struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t);
    return t.tv_sec * 1e9 + t.tv_nsec;
}
static double ns_per_access(const volatile char *base, size_t stride) {
    const volatile char *p[K];
    for (int k = 0; k < K; k++) p[k] = base + (size_t)k * stride;
    unsigned sum = 0; double best = 1e30;
    for (int trial = 0; trial < 3; trial++) {         /* keep the best of 3 */
        double t0 = now_ns();
        for (int r = 0; r < ROUNDS; r++)
            for (int k = 0; k < K; k++) sum += p[k][0];
        double t = (now_ns() - t0) / ((double)ROUNDS * K);
        if (t < best) best = t;
    }
    if (sum == 1) puts("");                           /* keep the loads alive */
    return best;
}
int main(void) {
    char *buf = calloc((size_t)K * (4096 + 64) + 4096, 1);
    if (!buf) return 1;
    double alias = ns_per_access(buf, 4096), pad = ns_per_access(buf, 4096 + 64);
    printf("%d lines, stride 4096    : %.2f ns per access\n", K, alias);
    printf("%d lines, stride 4096+64 : %.2f ns per access\n", K, pad);
    printf("ratio aliased/padded = %.2f\n", alias / pad);
    free(buf);
    return 0;
}

যাচাইয়ের সময় একটি shared virtual machine-এ আউটপুট (সময় প্রতিবার আর প্রতিটি যন্ত্রে বদলায়):

32 lines, stride 4096    : 5.37 ns per access
32 lines, stride 4096+64 : 2.03 ns per access
ratio aliased/padded = 2.64

এই run-এ ৪,০৯৬ বাইট দূরত্বে প্রতিটি পড়া গড়ে ৫.৩৭ ns, আর মাত্র ৬৪ বাইট বাড়তি দূরত্বে ২.০৩ ns — প্রায় ২.৬ গুণ পার্থক্য, যদিও দুই ক্ষেত্রেই একই ৩২টি line, মোট মাত্র ২ KiB data, যা L1-এ অনায়াসে আঁটার কথা। ব্যাখ্যা: ৪,০৯৬ বাইট দূরত্বে (ধরে নেওয়া ৬৪টি set আর ৬৪ বাইটের line-এ) ৩২টি line-এর সবাই একটি set-এর দাবিদার, অথচ সেই set-এ জায়গা মাত্র ৮ বা ১২টি way-এর। Line-গুলো পালাক্রমে ঘুরে ঘুরে পড়া হচ্ছে, তাই প্রতিটি line আবার লাগার আগেই সরে যায় — কার্যত প্রায় প্রতিটি পড়াই L1 miss, উত্তর আসে L2 থেকে। এটি conflict miss-এর বাস্তব রূপ: cache খালি পড়ে আছে, শুধু একটি set-এ ভিড়। ৪,১৬০ বাইট দূরত্বে ৩২টি line ৩২টি ভিন্ন set-এ ছড়িয়ে পড়ে, আর সব পড়া L1 hit. সংখ্যাগুলো প্রতিটি run-এ আর যন্ত্রে বদলাবে — set-এর সংখ্যা, way, prefetcher আর -O0-এর loop overhead সবই প্রভাব ফেলে; কোনো যন্ত্রে পার্থক্য কম দেখালেও অবাক হবেন না। দুটি array-র ping-pong (মাত্র ২টি line এক set-এ) এখানে কিছুই দেখাত না, কারণ বাস্তব L1 set-associative — তাই K = ৩২ নেওয়া হয়েছে, way-সংখ্যার চেয়ে বেশি।

৮ · Pipeline-এ miss সামলানো — থামা, আনা, আবার চলা

পর্ব ৩৫-এর (Pipelining) পাঁচ ধাপের pipeline ধরে নিয়েছিল, IF আর MEM ধাপে memory এক cycle-এ উত্তর দেয়। Hit হলে তা সত্যি। Miss হলে উত্তর আসতে কয়েক ডজন থেকে শত cycle লাগতে পারে (কোন স্তর থেকে আসছে তার ওপর নির্ভর করে, আর যন্ত্রভেদে বদলায়)। Cache-কে নিয়ন্ত্রণ করে একটি ছোট FSM — cache controller (পর্ব ১৭-এর finite state machine-এর একটি বাস্তব প্রয়োগ)। সরল in-order pipeline-এ miss-এর জবাব হলো পুরো processor-কে stall করা — পর্ব ৩৬-এর (Pipeline hazard) data hazard-এর stall-এর মতো, তবে অনেক লম্বা, আর সব ধাপ একসাথে জমে থাকে (register-এর মান বদলায় না)।

Instruction miss-এর ধাপ (পাঠ্যবইয়ের নিয়মে):

  1. যে instruction miss করেছে, তার PC-টি পরের স্তরে পাঠানো। IF ধাপে PC + ৪ ইতিমধ্যে হিসাব হয়ে থাকতে পারে, তাই পাঠ্যবই বলে “current PC − ৪” পাঠাতে — মূল কথা, miss-করা instruction-এর ঠিকানাই চাই।
  2. পরের স্তরকে (L2 বা main memory) পুরো block পড়তে বলা, আর উত্তর আসা পর্যন্ত অপেক্ষা।
  3. Cache line লেখা: data অংশে block, tag অংশে address-এর ওপরের বিট, valid bit = ১।
  4. Instruction-টি আবার শুরু থেকে আনা — এবার সেটি hit.

Data miss একই রকম: lw MEM ধাপে miss করলে processor থেমে থাকে যতক্ষণ না block এসে cache-এ বসে, তারপর load শেষ হয় আর pipeline আবার এগোয়। নিচের stepper-এ একটি instruction miss-এর পুরো যাত্রা — I-cache ১৬ KiB, ১৬ B block (১৮ | ১০ | ৪), PC = 0x00400010.

  1. IF: খোঁজাindex 1tag মেলে না
  2. StallPC স্থিরpipeline জমে
  3. Block চাওয়া0x00400010পরের স্তরে
  4. অপেক্ষাmiss penaltyধরুন ১০০ cycle
  5. Line লেখাtag 0x100, V=1১৬ বাইট
  6. আবার IFHITpipeline চলে
ধাপ ১Address ভাগ আর তুলনা
PC = 0x00400010
   tag    = PC >> 14          = 0x100
   index  = (PC >> 4) & 0x3FF = 0x001
   offset = PC & 0xF          = 0x0
line 1:  V = 1, stored tag = 0x0FF   (some older code)
0x0FF != 0x100  ->  MISS
ধাপ ২পুরো pipeline থামে
IF   : the missing instruction, waiting
ID..WB: frozen - no register or memory is written
PC   : held at 0x00400010 (not advanced to the next one)
cost : every stalled cycle adds 1 to the total cycle count
ধাপ ৩Cache controller block চায়
block address = 0x00400010 >> 4 = 0x0040001
request: read 16 bytes, 0x00400010 .. 0x0040001F
         from the next level (L2 or main memory)
ধাপ ৪অপেক্ষা — miss penalty
cycle   1 .. 100 : waiting for the block   (assumed penalty)
a hit would have taken about 1 cycle in this simple pipeline
-> one miss costs as much time as roughly 100 hits
ধাপ ৫Line 1 নতুন করে লেখা
line 1:  data  = Mem[0x00400010 .. 0x0040001F]
         tag   = 0x100
         V     = 1
(old block with tag 0x0FF is gone; this is a read-only
 I-cache, so nothing has to be written back)
ধাপ ৬আবার fetch — এবার hit
IF again with PC = 0x00400010
line 1: V = 1, tag 0x100 == 0x100  ->  HIT
the pipeline resumes; the next three instructions
(0x00400014, 0x00400018, 0x0040001C) also hit
  1. খোঁজা। PC-কে ভাগ করলে tag 0x100, index ১, offset ০. Line ১-এ valid একটি block আছে, কিন্তু তার tag 0x0FF — অন্য কোনো code. Tag মেলে না, তাই miss.
  2. Stall. Cache controller processor-কে থামিয়ে রাখে। PC সামনে এগোয় না, আর পেছনের ধাপগুলোর কেউ কোনো register বা memory লেখে না — যেন সময় থেমে গেছে।
  3. Block চাওয়া। Miss-করা instruction-এর ঠিকানা থেকে পুরো ১৬ বাইটের block (0x00400010 থেকে 0x0040001F) পরের স্তরে চাওয়া হয়।
  4. অপেক্ষা। এই উদাহরণে ধরে নেওয়া হয়েছে ১০০ cycle — বাস্তবে L2-তে পেলে অনেক কম, DRAM পর্যন্ত গেলে বেশি। এই পুরো সময় pipeline কোনো কাজ করে না।
  5. Line লেখা। Data, নতুন tag 0x100, আর V = ১ লেখা হয়। পুরোনো block হারিয়ে যায়; I-cache-এ store হয় না বলে কিছু ফেরত লেখার দরকার নেই।
  6. আবার চলা। একই PC আবার fetch — এবার hit, আর pipeline চলতে শুরু করে। Block ১৬ বাইটের বলে পরের তিনটি instruction-ও hit: একটি miss-এর খরচে চারটি instruction.

এই “সব থামিয়ে অপেক্ষা” নকশা সরল, কিন্তু ব্যয়বহুল — পর্ব ৪৭-এ দেখবেন, miss rate সামান্য হলেও memory stall CPI প্রায় দ্বিগুণ করে দিতে পারে। বড় out-of-order প্রসেসর (পর্ব ৪০) একটি load miss করলেও পরের স্বাধীন instruction-গুলো চালিয়ে যায়, আর cache একসাথে একাধিক miss সামলাতে পারে (non-blocking cache) — তবু instruction miss হলে ফ্রন্ট-এন্ড কার্যত শুকিয়ে যায়।

Split বনাম unified cache

Pipeline-এ IF আর MEM একই cycle-এ memory চায় — পর্ব ৩৫-এর structural hazard. তাই প্রায় সব আধুনিক CPU-তে L1 স্তরে দুটি আলাদা cache: instruction cache (I-cache) আর data cache (D-cache) — একে বলে split cache. দুটির নিজস্ব port, নিজস্ব tag আর নিজস্ব index; ওপরের stepper-এর I-cache ঠিক এমন একটি। L2 আর L3 সাধারণত unified — instruction আর data একসাথে। একই মোট আকারে unified cache-এর miss rate সাধারণত একটু কম হতে পারে, কারণ instruction আর data নিজেদের প্রয়োজন অনুযায়ী জায়গা ভাগাভাগি করে নেয়; কিন্তু L1-এ প্রতি cycle-এ দুটি access-এর bandwidth আর ছোট-দ্রুত cache-এর hit time বেশি জরুরি — তাই L1-এ split-ই জেতে।

৯ · পরীক্ষার কৌশল — hex address থেকে দ্রুত tag, index, offset

পরীক্ষায় address প্রায়ই hex-এ দেওয়া থাকে। পুরো ৩২ বিট লিখতে সময় নষ্ট; কয়েকটি কৌশল জানলে কাজ অর্ধেক।

  • সীমানা ৪-এর গুণিতকে পড়লে সরাসরি পড়ুন। প্রতিটি hex অঙ্ক ঠিক ৪ বিট। ৩২-বিট, ৬৪ KiB, ৩২ B (১৬ | ১১ | ৫)-এ tag ঠিক ওপরের চারটি অঙ্ক: 0x12345678-এর tag = 0x1234. বাকি 0x5678 = 0101 0110 0111 1000 থেকে নিচের ৫ বিট 11000 = ২৪ (offset), ওপরের ১১ বিট 010 1011 0011 = 0x2B3 = ৬৯১ (index).
  • শুধু নিচের অংশ binary করুন। ১৬ KiB, ৬৪ B (১৮ | ৮ | ৬)-এ index + offset = ১৪ বিট, যা নিচের চার hex অঙ্কে (১৬ বিট) আঁটে। 0x5678-এর নিচের ১৪ বিট 01 0110 0111 1000 → offset 111000 = 0x38, index 0101 1001 = 0x59. Tag = (ওপরের অঙ্কগুলো 0x1234) × ৪ + বাদ পড়া দুই বিট (01) = 0x48D0 + ১ = 0x48D1.
  • Offset ৪ বিট হলে শেষ অঙ্কটিই offset. ১৬ KiB, ১৬ B (১৮ | ১০ | ৪)-এ 0x12345678-এর offset = 0x8, index = 0x567-এর নিচের ১০ বিট = 0x167 = ৩৫৯, tag = 0x48D1.
  • Cache size-এর গুণিতক দূরত্ব = একই index. দুটি address-এর পার্থক্য cache size-এর গুণিতক হলে (direct-mapped-এ) তারা একই ঘরে পড়ে, শুধু tag আলাদা। যেমন ১৬ KiB = 0x4000: 0x12345678 আর 0x12349678 দুটিরই index ৩৫৯ (১৬ B block-এ), tag 0x48D1 বনাম 0x48D2. Conflict-এর প্রশ্নে এটি এক নজরে ধরা যায়।
  • উত্তর যাচাই: tag + index + offset = address-এর বিট; index + offset = log2(cache size); আর tag-কে ২index+offset দিয়ে গুণ করে বাকি দুই অংশ বসালে মূল address ফিরে আসে।

আরও গভীরে: আধুনিক x86-64 আর অনেক ARM core-এর L1 data cache-এর আকার প্রায়ই way-সংখ্যা × ৪ KiB (যেমন ৮-way × ৪ KiB = ৩২ KiB, বা ১২-way × ৪ KiB = ৪৮ KiB). কারণটি সুন্দর: index + offset বিটগুলো ৪ KiB page-এর ১২-বিট offset-এর ভেতরে রাখলে virtual address থেকেই index নেওয়া যায় — TLB-তে অনুবাদ চলার একই সময়ে cache-এর সারি পড়া শুরু হয়, আর tag তুলনা হয় physical address দিয়ে (virtually indexed, physically tagged; পর্ব ৫০)। তাই L1 বড় করতে নকশাকাররা প্রায়ই index না বাড়িয়ে way বাড়ান। আরেকটি কৌশল: miss-এ পুরো block আসার আগেই CPU-র চাওয়া word-টি প্রথমে পাঠানো (critical word first) আর তা পেয়েই আবার চলা (early restart), যা বড় block-এর miss penalty-র একটি অংশ লুকায়।

১০ · AI-এর যুগে cache-এর হিসাব

AI টুল cache-এর ধারণা গুছিয়ে বলতে পারে, কিন্তু বিটের হিসাব আর trace-এ নিয়মিত ভুল করে — বিশেষ করে দীর্ঘ hex-binary রূপান্তরে আর “cache size” কী গোনে তাতে। প্রচলিত ভুল আর ধরার উপায়:

AI-এর প্রচলিত ভুলসঠিক কথাকীভাবে ধরবেন
index = log2(cache size বাইটে)index = log2(block-এর সংখ্যা)index + offset = log2(cache size) কি না দেখুন
মোট bit-এ tag বা valid বাদ, বা “১৬ KiB = মোট SRAM”Cache size শুধু data; প্রতি line-এ tag + valid (+ dirty)প্রতি line-এর bit আলাদা লিখে গুণ করুন
Word address আর byte address গুলিয়ে ফেলাপ্রশ্নে কোনটি দেওয়া, তা ঠিক করে offset-এ byte বিট যোগ বা বাদBlock size word না বাইটে, আবার পড়ুন
Hex থেকে binary-তে একটি অঙ্ক ভুল, ফলে index ভুলপ্রতিটি অঙ্ক = ঠিক ৪ বিটSimulator বা (a >>> off) & mask দিয়ে মিলিয়ে নিন
সব দ্বিতীয় miss-কে “conflict” বলাFully associative একই আকারেও miss হলে capacityFA-LRU পাশাপাশি চালিয়ে দেখুন
“Intel-এর L1 direct-mapped” বা নির্দিষ্ট chip-এর বানানো সংখ্যাআধুনিক L1 সাধারণত ৮–১২ way; সংখ্যা model-ভেদে বদলায়নির্মাতার manual বা /sys/devices/system/cpu দেখুন

AI-কে এমনভাবে প্রশ্ন করুন যাতে প্রতিটি ধরে নেওয়া বিষয় আর ধাপ লেখা থাকে:

আমি computer architecture-এ direct-mapped cache শিখছি। Byte-addressed memory, 32-বিট address. Cache-এ 32 KiB data, block 32 বাইট, write-back (প্রতি line-এ valid আর dirty bit). ১. offset, index আর tag কত বিট — প্রতিটি ধাপ log2 সহ দেখাও, আর index + offset = log2(cache size) যাচাই করো। ২. মোট SRAM bit বের করো (data + tag + valid + dirty), আর data-র তুলনায় overhead শতাংশে। ৩. hex address 0x0040A7E4, 0x0041A7E0, 0x0040A7F8 — প্রতিটির tag, index, offset hex-এ লেখো; খালি cache থেকে শুরু করে প্রতিটি hit না miss, আর miss হলে তিন C-এর কোনটি, কারণসহ বলো। ৪. শেষে একটি ছোট JavaScript স্ক্রিপ্ট দাও যা এই সব সংখ্যা ছাপে — আমি নিজে চালিয়ে মিলিয়ে দেখব।

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

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

  • ৩২-বিট address, ৬৪ KiB direct-mapped, ৩২ B block — tag, index, offset? — offset ৫, line ২,০৪৮ → index ১১, tag ১৬.
  • Valid bit কেন লাগে? — চালু হওয়ার পর বা খালি করার পর line-এর tag আর data অর্থহীন; V = ০ থাকলে tag মিললেও miss.
  • Direct-mapped cache-এর সুবিধা আর অসুবিধা? — সুবিধা: একটি tag তুলনা, data আগেভাগে পড়া, সবচেয়ে কম hit time আর hardware; অসুবিধা: একই index-এর block-এর মধ্যে conflict miss.
  • Block size বাড়ালে miss rate সবসময় কমে? — না; প্রথমে কমে, কিন্তু নির্দিষ্ট cache size-এ block-এর সংখ্যা কমে conflict বাড়ে আর miss penalty বাড়ে।
  • তিন C কী, আর capacity বনাম conflict আলাদা করবেন কীভাবে? — Compulsory, capacity, conflict; একই আকারের fully associative cache-ও miss করলে capacity, না করলে conflict.

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

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

প্র ০১Cache-এর ধারণা কোথা থেকে এল, আর “cache” নামটি কীভাবে এল?

ধারণাটির সবচেয়ে উদ্ধৃত সূচনা ১৯৬৫ সালে Cambridge-এর Maurice Wilkes-এর একটি ছোট প্রবন্ধ, “Slave Memories and Dynamic Storage Allocation”. তিনি প্রস্তাব করেন, একটি বড় ধীর main memory-র সামনে একটি ছোট দ্রুত “slave” memory থাকুক, যা স্বয়ংক্রিয়ভাবে সম্প্রতি ব্যবহৃত word-গুলোর কপি রাখবে — programmer-কে কিছু জানতে হবে না। লক্ষ করুন, এটি ঠিক এই পর্বের মূল কথা: কপি, অদৃশ্যতা, আর locality-র ওপর ভরসা।

বাণিজ্যিক যন্ত্রে প্রথম বড় প্রয়োগ হিসেবে সাধারণত IBM System/360 Model 85-এর নাম বলা হয়, যা ১৯৬০-এর দশকের শেষভাগে আসে। এর সম্পর্কে IBM Systems Journal-এ ১৯৬৮ সালে প্রকাশিত প্রবন্ধে এই দ্রুত buffer-এর জন্য “cache” শব্দটি ব্যবহার হয়; প্রচলিত গল্প অনুযায়ী জার্নালের একজন সম্পাদক “high-speed buffer”-এর চেয়ে ছোট আর সুন্দর একটি শব্দ খুঁজছিলেন, আর ফরাসি “cacher” (লুকানো) থেকে আসা শব্দটি বেছে নেন — লুকানো ভান্ডার, যা ব্যবহারকারী দেখেন না। গল্পটির খুঁটিনাটি নিয়ে উৎসভেদে কিছু পার্থক্য আছে, তবে নামটি যে ওই সময় থেকে চালু হয়, তা নিয়ে সংশয় কম।

এরপর ১৯৭০ আর ৮০-র দশকে minicomputer আর তারপর microprocessor-এ cache আসে। প্রথমে cache থাকত chip-এর বাইরে, আলাদা SRAM chip-এ; ১৯৮০-র দশকের শেষে transistor যথেষ্ট সস্তা হলে cache CPU-র chip-এর ভেতরে ঢুকে পড়ে — Intel 80486 (১৯৮৯)-এ on-chip cache ছিল বলে জানা যায়। আজ একটি সাধারণ laptop CPU-র chip-এর একটি বড় অংশ জুড়ে শুধু cache.

কেন এত গুরুত্ব? কারণ পর্ব ৪৪-এর memory wall: CPU-র গতি বহু বছর ধরে DRAM-এর latency-র চেয়ে অনেক দ্রুত বেড়েছে। Cache না থাকলে আধুনিক CPU-র বেশিরভাগ সময় শুধু memory-র অপেক্ষায় কাটত।

মূল উপলব্ধি: Cache-এর মূল ভাবনা — locality-নির্ভর স্বয়ংক্রিয় কপি — প্রায় ছয় দশক পুরোনো আর প্রায় অপরিবর্তিত; বদলেছে শুধু মাত্রা, স্তরের সংখ্যা আর placement-এর কৌশল।

প্র ০২Conflict miss থাকা সত্ত্বেও direct-mapped cache কেন কখনো কখনো সেরা পছন্দ?

Miss rate একমাত্র মাপ নয়। গড় সময় = hit time + miss rate × miss penalty (পর্ব ৪৭) — প্রথম পদটি প্রতিটি access-এ দিতে হয়, দ্বিতীয়টি শুধু miss-এ। L1-এ প্রায় সব access hit, তাই hit time-এর সামান্য সাশ্রয়ও বিরাট লাভ।

Direct-mapped নকশায় hit-এর পথ সবচেয়ে ছোট। একটিমাত্র সম্ভাব্য line, তাই data পড়া আর tag তুলনা একসাথে শুরু হয়, আর data সরাসরি CPU-র দিকে বেরিয়ে আসে — hit হবে ধরে নিয়ে (অংশ ৩)। Set-associative cache-এ কয়েকটি tag একসাথে তুলনা করে, তারপর কোন way-এর data নেওয়া হবে তা একটি multiplexer-এ বাছতে হয় — এই বাড়তি ধাপ critical path-এ পড়ে (পর্ব ১৮-এর critical path-এর কথা মনে করুন)। একই সাথে কম comparator মানে কম power আর কম জায়গা।

১৯৮৮ সালে Mark Hill-এর “A Case for Direct-Mapped Caches” (IEEE Computer) প্রবন্ধটি ঠিক এই যুক্তি দিয়েছিল: বড় cache-এ associativity থেকে miss rate-এর লাভ কমে আসে, অথচ hit time-এর খরচ থাকে — তাই বেশ কিছু ক্ষেত্রে direct-mapped-ই দ্রুততর। সেই সময়ের কিছু উচ্চগতির প্রসেসরে, যেমন DEC Alpha 21064-এ, L1 cache direct-mapped ছিল বলে জানা যায়। কম-power-এর embedded নকশায় আর কিছু বড় off-chip বা DRAM-ভিত্তিক cache-এও direct-mapped বা কম-way নকশা দেখা যায়, কারণ সেখানে tag পড়ার খরচই বেশি।

তবে সময় বদলেছে। আজকের L1-এ ৮ বা তার বেশি way সাধারণ, কারণ transistor সস্তা, আর অংশ ৯-এর “আরও গভীরে”-র কৌশলে (index page offset-এর ভেতরে) বড় L1 পেতে way বাড়ানোই সহজ পথ। Prediction দিয়েও hit time বাঁচানো হয়: কোন way-এ data আছে আগে অনুমান করে সেটি পড়া (way prediction) — direct-mapped-এর গতি আর associative-এর কম miss rate মেলানোর চেষ্টা।

মূল উপলব্ধি: নকশার লক্ষ্য গড় সময় কমানো, miss rate নয়। Direct-mapped cache miss কিছু বেশি করে, কিন্তু প্রতিটি hit-এ সময় বাঁচায় — কোনটি জিতবে তা নির্ভর করে cache-এর আকার, clock আর workload-এর ওপর।

প্র ০৩Index নিতে সবসময় address-এর “মাঝের বিট” কেন? ওপরের বিট বা hash নিলে কী হতো?

ধরুন উল্টোটা করা হলো — index নেওয়া হলো address-এর ওপরের বিট থেকে, আর tag নিচের দিক থেকে। তাহলে পাশাপাশি থাকা block-গুলো (যাদের ওপরের বিট একই) সবাই একই ঘরে পড়ত! একটি array টানা পড়লে প্রতিটি নতুন block আগেরটিকে সরাত — spatial locality-র পুরো লাভ উল্টো হয়ে conflict-এর ঝড় হতো। মাঝের বিট (offset-এর ঠিক ওপরের) নেওয়ায় পরপর block-গুলো পরপর ঘরে পড়ে, আর cache size-এর সমান একটি টানা অংশ পুরোটাই একসাথে আঁটে। এটিই প্রচলিত নির্বাচনের মূল কারণ।

তবু এই সরল নিয়মের একটি দুর্বলতা আছে, যা এই পর্বে বারবার দেখেছেন: ২-এর ঘাতের দূরত্বে থাকা data একই ঘরে পড়ে। আর প্রোগ্রামে ২-এর ঘাত সর্বত্র — ১০২৪ × ১০২৪ matrix-এর একটি কলাম ধরে হাঁটলে প্রতিটি element ঠিক ৪,০৯৬ বাইট (float হলে) দূরে, ফলে সবাই একই কয়েকটি set-এ ভিড় করে। এজন্য গবেষকরা বিকল্প index ফাংশন প্রস্তাব করেছেন: কয়েকটি ওপরের বিট XOR করে মিশিয়ে দেওয়া (XOR-based বা skewed indexing), এমনকি একটি মৌলিক সংখ্যা দিয়ে mod নেওয়া (prime-modulo cache) — যাতে নিয়মিত stride-এর ঠিকানা ছড়িয়ে পড়ে।

বাস্তবে এর একটি রূপ দেখা যায় বড় শেয়ার করা L3-এ: কিছু multicore chip-এ L3 কয়েকটি টুকরোয় (slice) ভাগ করা, আর কোন address কোন slice-এ যাবে তা address-এর অনেক বিটের একটি hash দিয়ে ঠিক হয় বলে গবেষণায় বর্ণনা করা হয়েছে — নির্মাতারা সাধারণত পুরো ফাংশন প্রকাশ করেন না। L1-এ সরল বিট-বাছাই-ই থাকে, কারণ সেখানে প্রতিটি gate delay মূল্যবান, আর hash-এর জন্য XOR-এর একটি স্তরও hit time-এ যোগ হয়।

Software-এর দিক থেকে সমাধান সহজ: array-র মাপ ২-এর ঘাত থেকে সামান্য সরিয়ে দেওয়া (padding), যেমন অংশ ৬-এর C প্রোগ্রামে ৬৪ বাইট সরিয়ে ১০০% থেকে ৬.২৫% miss. পর্ব ৪৮-এ এর বিস্তারিত।

মূল উপলব্ধি: মাঝের বিটে index সবচেয়ে সস্তা আর spatial locality-বান্ধব; এর দাম হলো ২-এর ঘাতের stride-এ conflict — যা hardware-এ hash দিয়ে বা software-এ padding দিয়ে সামলানো হয়।

প্র ০৪তিন C মডেল কতটা নির্ভরযোগ্য — কোথায় এটি ভেঙে পড়ে?

Three C's মডেলটি সাধারণত Mark Hill-এর ১৯৮০-র দশকের শেষভাগের Berkeley-র গবেষণার সাথে যুক্ত বলে উল্লেখ করা হয়, আর Hennessy–Patterson-এর বইয়ের মাধ্যমে এটি প্রায় সব কোর্সে পৌঁছেছে। এর শক্তি হলো প্রতিকারের দিকনির্দেশ: compulsory কমাতে বড় block বা prefetch, capacity কমাতে বড় cache, conflict কমাতে বেশি associativity বা padding. একটি numerical হাতে নিয়ে কোন miss কোন শ্রেণির, তা বলতে পারা নকশার আলোচনায় খুব কাজে লাগে।

কিন্তু মডেলটি সংজ্ঞা দিয়ে বানানো, প্রকৃতি দিয়ে নয়। প্রথমত, capacity আর conflict-এর সীমা নির্ভর করে তুলনার fully associative cache কোন replacement নীতি মানে তার ওপর। LRU-র বদলে আদর্শ (ভবিষ্যৎ-জানা, Belady-র OPT) নীতি ধরলে কিছু “capacity” miss অদৃশ্য হয়ে যায়। দ্বিতীয়ত, অদ্ভুত হলেও সত্য: কিছু trace-এ direct-mapped cache এমন জায়গায় hit করে যেখানে একই আকারের fully associative LRU miss করে — যেমন একটি loop যা cache-এর চেয়ে সামান্য বড়, LRU সেখানে সবসময় ঠিক পরের প্রয়োজনীয় block-টিকেই সরায়, অথচ direct-mapped-এ কিছু block নিজের ঘরে টিকে যায়। ফলে মোট conflict miss-এর হিসাব ঋণাত্মক পর্যন্ত হতে পারে, যা মডেলের একটি পরিচিত সীমাবদ্ধতা।

তৃতীয়ত, block size বদলালে “compulsory” কী, তাও বদলায় — বড় block-এ প্রথম ছোঁয়া কম। চতুর্থত, multicore-এ অন্য core-এর লেখার কারণে line বাতিল হলে যে miss, তা কোনো তিন শ্রেণিতেই পড়ে না — তাই চতুর্থ C, coherence (পর্ব ৫৩)। আর OS-এর context switch-এর পর অন্য প্রোগ্রামের ফেলে যাওয়া cache-এ যে miss, সেগুলোকে অনেকে আলাদা করে দেখেন।

তবু মডেলটি টিকে আছে কারণ এটি সরল, পরিমাপযোগ্য (simulator-এ সহজেই গোনা যায় — এই পর্বের JS প্রোগ্রামেই গোনা হয়েছে), আর দিকনির্দেশে মোটামুটি নির্ভুল।

মূল উপলব্ধি: তিন C একটি হিসাবের কাঠামো — তুলনার মানদণ্ড (fully associative LRU) বদলালে সংখ্যাও বদলায়। একে নকশার দিকনির্দেশ হিসেবে ব্যবহার করুন, চূড়ান্ত সত্য হিসেবে নয়।

প্র ০৫আজকের CPU-তে cache line প্রায় সবসময় ৬৪ বাইট কেন — ৩২ বা ২৫৬ নয়?

Line size একটি বহুমুখী আপস, আর ৬৪ বাইট অনেক দিক থেকে মাঝামাঝি জায়গা। ১৯৯০-এর দশকের অনেক প্রসেসরে ৩২ বাইটের line ছিল; ২০০০-এর দশক থেকে সাধারণ x86 প্রসেসরে ৬৪ বাইট প্রচলিত হয়ে ওঠে বলে জানা যায়, আর বহু ARM core-ও একই পথে। কিছু নকশা ভিন্ন পথ নিয়েছে — যেমন Apple-এর M-সিরিজ chip-এ ১২৮ বাইটের line-এর কথা প্রকাশিত মাপে উল্লেখ আছে।

প্রথম কারণ DRAM-এর গঠন। আধুনিক DDR memory একটি “burst”-এ পরপর কয়েকটি transfer দেয়; ৬৪-বিট চওড়া একটি channel-এ DDR4-এর আটটি transfer-এর burst ঠিক ৬৪ বাইট আনে। ফলে একটি line একটি স্বাভাবিক burst-এ আসে — বেশি ছোট line নিলে burst-এর একাংশ অপচয়, বেশি বড় নিলে একাধিক burst আর বেশি miss penalty (পর্ব ৪৯-এ DRAM-এর বিস্তারিত)।

দ্বিতীয় কারণ অংশ ৪ আর ৬-এর হিসাব: ৬৪ বাইটে tag-এর overhead কয়েক শতাংশে নেমে আসে, আর সাধারণ প্রোগ্রামের spatial locality-র বেশিরভাগ লাভ ততক্ষণে পাওয়া হয়ে যায়; এর পরে লাভ সমতল, কিন্তু অপচয় বাড়ে। তৃতীয় কারণ multicore: পর্ব ৫৩-এর false sharing — দুটি core একই line-এর ভিন্ন অংশে লিখলে line-টি তাদের মাঝে লাফালাফি করে। Line যত বড়, এই সমস্যার সম্ভাবনা তত বেশি।

আর একটি বড় কারণ ঐতিহাসিক জড়তা: অসংখ্য software ৬৪ বাইট ধরে নিয়ে লেখা — data structure-কে ৬৪ বাইটে align করা, false sharing এড়াতে ৬৪ বাইট padding. Line size বদলালে এসব অনুমান ভাঙে। এজন্য কোনো কোনো নকশায় L2-তে দুটি পাশাপাশি ৬৪-বাইট line জোড়ায় আনা হয় (adjacent-line prefetch), যা কার্যত বড় block-এর লাভ দেয় কিন্তু coherence-এর একক ৬৪ বাইটই থাকে। আপনার নিজের যন্ত্রে Linux-এ getconf LEVEL1_DCACHE_LINESIZE দিয়ে দেখে নিতে পারেন।

মূল উপলব্ধি: ৬৪ বাইট কোনো জাদুর সংখ্যা নয় — DRAM-এর burst, tag overhead, spatial locality-র সমতল হয়ে আসা আর false sharing-এর ঝুঁকির একটি ভারসাম্য, যা software-এর অভ্যাসে এখন শক্ত করে বসে গেছে।

অনুশীলন

পরীক্ষার ধাঁচে লেখা — আগে খাতায় সমাধান করুন, তারপর উত্তর মিলিয়ে নিন।

  1. বিট আর মোট bit (numerical): ৩২-বিট byte address, ১২৮ KiB data-র direct-mapped write-back cache, block ৩২ বাইট, প্রতি line-এ valid আর dirty bit. (ক) offset, index, tag কত বিট? (খ) মোট কত bit SRAM? (গ) data-র তুলনায় overhead কত শতাংশ? (ঘ) address 0x0001F2A4-এর tag, index, offset hex-এ কত?
    • (ক) offset = log2৩২ = ৫; line = ১,৩১,০৭২ ÷ ৩২ = ৪,০৯৬ = ২১২ → index ১২; tag = ৩২ − ১২ − ৫ = ১৫. যাচাই: index + offset = ১৭ = log2(১২৮ KiB).
    • (খ) প্রতি line: ২৫৬ data + ১৫ tag + ১ valid + ১ dirty = ২৭৩ বিট; মোট ৪,০৯৬ × ২৭৩ = ১১,১৮,২০৮ বিট = ১,০৯২ Kibit = ১৩৬.৫ KiB.
    • (গ) (২৭৩ − ২৫৬) / ২৫৬ ≈ ৬.৬%.
    • (ঘ) 0x0001F2A4: offset = নিচের ৫ বিট = 0x04 (0xA4 = 1010 0100 → 00100); index = (0x1F2A4 >> ৫) AND 0xFFF = 0xF95; tag = 0x1F2A4 >> ১৭ = 0x0. অর্থাৎ index 0xF95 (৩,৯৮৯), offset 0x04.
  2. Hit/miss টেবিল আর তিন C (trace by hand): ৮-বিট byte address, ৩২ বাইটের direct-mapped cache, block ৮ বাইট (৪টি line), শুরুতে খালি। ক্রমে এই address-গুলো পড়া হয় (hex): 00, 04, 08, 10, 18, 20, 00, 24, 08, 40, 0C. প্রতিটির tag, index, offset, hit বা miss আর miss-এর শ্রেণি (compulsory / capacity / conflict) লিখুন। Hit rate কত?

    ভাগ: offset ৩, index ২, tag ৩ বিট। এই পর্বের simulator-এ (simulate("practice", 32, 8, 8, [...], true)) চালিয়ে মেলানো:

    addr  tag idx off  result
    0x00   0   0   0   MISS compulsory
    0x04   0   0   4   HIT
    0x08   0   1   0   MISS compulsory
    0x10   0   2   0   MISS compulsory
    0x18   0   3   0   MISS compulsory
    0x20   1   0   0   MISS compulsory   (evicts block 0)
    0x00   0   0   0   MISS capacity
    0x24   1   0   4   MISS conflict
    0x08   0   1   0   HIT
    0x40   2   0   0   MISS compulsory
    0x0C   0   1   4   HIT
    hits 3/11 = 27.3%

    কেন 0x00-র দ্বিতীয় miss capacity: ততক্ষণে পাঁচটি ভিন্ন block (০, ১, ২, ৩, ৪) এসেছে, cache-এ ধরে মাত্র ৪টি; ৪-block-এর fully associative LRU cache-এও block ৪ এসে সবচেয়ে পুরোনো block ০-কেই সরাত — তাই সেখানেও miss. কেন 0x24 conflict: block ৪ (0x20–0x27) সরে গেছে কারণ block ০ একই ঘর ০-তে ফিরে এসেছে; fully associative cache-এ তখন {২, ৩, ৪, ০} থাকত, block ৪ hit করত। 0x08 আর 0x0C একই block ১, তাই দুটিই hit (spatial locality).

  3. আউটপুট অনুমান করুন: অংশ ৪-এর calculator-এ নিচের লাইনটি যোগ করলে কোন চারটি লাইন ছাপবে? Run না করে হিসাব করুন।
    split(32, 8 * KiB, 32, true);
    32-bit, 8 KiB, 32 B blocks (+dirty)
       lines = 256 = 2^8   tag|index|offset = 19|8|5
       bits/line = 256 data + 19 tag + 1 valid + 1 dirty = 277
       total = 256 x 277 = 70912 bits = 69.25 Kibit = 8.66 KiB   (overhead 8.2%)

    Line = ৮,১৯২ ÷ ৩২ = ২৫৬ = ২৮; offset ৫, index ৮, tag ৩২ − ১৩ = ১৯. প্রতি line ২৫৬ + ১৯ + ২ = ২৭৭; মোট ২৫৬ × ২৭৭ = ৭০,৯১২ বিট; ÷ ১,০২৪ = ৬৯.২৫ Kibit; ÷ ৮ = ৮.৬৬ KiB; overhead (২৭৭ − ২৫৬) / ২৫৬ ≈ ৮.২%. লক্ষ করুন, cache ছোট হলে tag বড় হয় — তাই overhead ১২৮ KiB-এর ৬.৬%-এর চেয়ে বেশি।

  4. Hit-এর circuit লিখুন: ওপরের ১৬ KiB, ১৬ B block-এর cache-এর জন্য (১৮ | ১০ | ৪) একটি Verilog module লিখুন, যা একটি line-এর stored tag, valid bit আর ১২৮-বিট data এবং ৩২-বিট address নিয়ে hit আর ৩২-বিট rdata দেয় (word offset দিয়ে বাছাই)। কয়টি XNOR লাগবে?

    উদাহরণ (synthesizable combinational Verilog; SRAM-এর সারি পড়াটি বাইরে ধরে নেওয়া হয়েছে):

    module dm_hit (
        input  wire [31:0]  addr,
        input  wire         valid,       // V of the line chosen by addr[13:4]
        input  wire [17:0]  stored_tag,  // tag of that line
        input  wire [127:0] line_data,   // 4 words of that line
        output wire         hit,
        output reg  [31:0]  rdata
    );
        assign hit = valid && (stored_tag == addr[31:14]);   // 18-bit equality
        always @(*) begin
            case (addr[3:2])                                // word offset
                2'd0: rdata = line_data[31:0];
                2'd1: rdata = line_data[63:32];
                2'd2: rdata = line_data[95:64];
                default: rdata = line_data[127:96];
            endcase
        end
    endmodule

    Tag তুলনায় ১৮টি XNOR (প্রতি বিট-জোড়ায় একটি), তারপর ১৮টি ফল আর valid মিলিয়ে একটি ১৯-ইনপুট AND (বাস্তবে AND-এর একটি গাছ)। Data-র দিকে প্রতি আউটপুট বিটে একটি ৪-থেকে-১ multiplexer — মোট ৩২টি। লক্ষ করুন, rdata hit-এর অপেক্ষা করে না; CPU hit = ০ দেখলে মানটি ফেলে দেয় — ঠিক অংশ ৩-এর মতো।

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

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

ক্রমDirect-mapped cache-এ একটি load-এর কাজগুলো যে ক্রমে ঘটে, সেই ক্রমে সাজান — ধরুন access-টি miss (▲▼ চেপে সরান)

  1. Address-কে tag, index আর offset-এ ভাগ করা
  2. Index দিয়ে cache-এর একটি সারি বাছাই করা
  3. সেই সারির valid bit আর stored tag-কে address-এর tag-এর সাথে মেলানো
  4. না মেলায় processor থামিয়ে পরের স্তর থেকে পুরো block আনা
  5. সারিতে data, নতুন tag আর valid = ১ লেখা
  6. Offset দিয়ে block থেকে চাওয়া word বেছে CPU-কে দেওয়া

৩২-বিট byte address, ৬৪ KiB data-র একটি direct-mapped cache, block ৩২ বাইট। Tag, index আর offset যথাক্রমে কত বিট?

  • ১১ | ১৬ | ৫
  • ১৬ | ১১ | ৫
  • ১৮ | ১১ | ৩
  • ১৭ | ১০ | ৫

একটি direct-mapped cache-এ ১৬ KiB data, block ১৬ বাইট, address ৩২ বিট, আর প্রতি line-এ শুধু একটি valid bit (dirty নেই)। Cache বানাতে মোট কত bit লাগবে?

  • ১২৮ Kibit
  • ১৪৬ Kibit
  • ১৪৭ Kibit
  • ১৫১ Kibit

৮টি block-এর একটি direct-mapped cache, block ১ word, শুরুতে খালি। Word address-এর ক্রম: ২২, ২৬, ২২, ২৬, ১৬, ৩, ১৬, ১৮. কয়টি hit হবে?

  • ৩
  • ৪
  • ২
  • ৫

একটি ৪-block direct-mapped cache-এ একটি block আগে একবার এসেছিল, পরে অন্য block এসে সরিয়ে দিয়েছে। এখন আবার চাওয়ায় miss হলো। একই ৪-block-এর fully associative LRU cache হলে এই access hit হতো। এই miss কোন শ্রেণির?

  • Compulsory
  • Capacity
  • Coherence
  • Conflict

একটি direct-mapped cache-এর মোট data-ধারণক্ষমতা স্থির রেখে block size অনেক বাড়িয়ে দেওয়া হলো। সাধারণভাবে কোনটি সবচেয়ে সঠিক?

  • Miss rate সবসময় কমতে থাকে
  • Miss rate প্রথমে কমে, পরে বাড়তে পারে; আর miss penalty বাড়ে
  • Miss penalty কমে, কারণ miss কম হয়
  • প্রতিটি tag-এর বিট-সংখ্যা বাড়ে

একটি সরল পাঁচ ধাপের in-order pipeline-এ instruction cache miss হলে সাধারণত কী করা হয়?

  • Processor থামিয়ে block এনে line লেখা, তারপর একই instruction আবার fetch করা
  • Miss-করা instruction বাদ দিয়ে পরেরটি চালানো
  • Tag না মিললেও line-এর data দিয়েই চালিয়ে যাওয়া
  • একটি exception তুলে OS-কে block আনতে বলা

শব্দকোষ

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

মনে রাখুন

  1. Cache = block-এ ভাগ করা ছোট দ্রুত কপি: hit হলে দ্রুত, miss হলে পরের স্তর থেকে পুরো block; locality-ই একে কার্যকর করে।
  2. Direct-mapped: index = block address mod block-সংখ্যা; ২-এর ঘাত বলে mod মানে বিট তুলে নেওয়া। | tag | index | offset |: offset = log2(block), index = log2(line), tag = বাকি; index + offset = log2(cache size).
  3. Hit = V · (tag মেলে): data আর tag একসাথে পড়া, offset দিয়ে multiplexer — তাই hit time কম। মোট bit = line × (data + tag + valid [+ dirty]); ১৬ KiB, ১৬ B → ১৪৭ Kibit.
  4. Trace আর block size: পাঠ্যবইয়ের ২২, ২৬, ২২, ২৬, ১৬, ৩, ১৬, ১৮ → ৩ hit. বড় block spatial locality দেয়, কিন্তু block কমে conflict আর miss penalty বাড়ে — miss rate-এর বাঁক “U”-এর মতো হতে পারে।
  5. তিন C আর pipeline: প্রথম ছোঁয়া = compulsory; একই আকারের fully associative-ও miss করত = capacity; নইলে conflict. Miss হলে সরল pipeline stall করে, block এনে line লিখে, আবার চলে; L1 split (I/D), নিচের স্তর unified.

পরের পর্বে: Ping-pong-এর সমাধান — একটি block-কে একাধিক ঘরের সুযোগ দেওয়া। Set-associative আর fully associative cache, ঠিকানার নতুন ভাগ, একাধিক comparator, আর জায়গা না থাকলে কাকে সরাবেন: LRU, FIFO আর random replacement — trace ধরে ধরে।

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

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