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

মেমরি হায়ারার্কি আর locality — কেন cache কাজ করে

The Memory Hierarchy and Locality — Memory Wall, Technology Trade-offs, Temporal and Spatial Locality, Hit/Miss, a First Look at AMAT, and the Cache Staircase

Taught in Bangla, this lesson explains the memory hierarchy: the processor–memory gap and memory wall, latency, capacity and cost trade-offs from registers through SRAM caches, DRAM, SSD and disk, temporal and spatial locality measured with reuse distance, hits, misses, hit rate and miss penalty, a first AMAT calculation with proof, inclusion, who manages each level, a live C program that reveals the cache staircase, and scratchpads, HBM and CXL.

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

  • Processor–memory gap কী, কেন তৈরি হলো, আর কেন শুধু দ্রুত DRAM বানিয়ে এর সমাধান হয় না
  • প্রতিটি মেমরি-প্রযুক্তির latency, ধারণক্ষমতা আর দামের মোটামুটি মাত্রা; “এক cycle = এক সেকেন্ড” ধরলে কোনটি কত দূরে
  • Temporal আর spatial locality — কোডে চেনা, reuse distance দিয়ে মাপা, আর working set-এর ধারণা
  • Hit, miss, hit rate, miss rate, hit time, miss penalty, block-এর সংজ্ঞা; প্রত্যাশিত access সময়ের সূত্র আর তার প্রমাণ
  • কোন স্তর কে সামলায় (hardware, OS, software), কোন সীমানায় কত বড় টুকরো যায়; আর scratchpad, HBM, CXL, persistent memory-র মতো আধুনিক মোড়

১ · প্রসেসর আর মেমরির ফারাক — memory wall

মডিউল ৬ আর ৭-এ আমরা CPU-কে দ্রুত করার অনেক কৌশল দেখেছি: পর্ব ৩৫-এর pipeline, পর্ব ৩৯-এর superscalar, পর্ব ৪০-এর out-of-order execution, পর্ব ৪৩-এর multithreading. প্রতিবারই একটি শত্রু ফিরে এসেছে — ডেটা না পাওয়া load.

প্রতিটি instruction মেমরি ছোঁয়। Instruction নিজেই মেমরিতে থাকে, তাই প্রতিটি instruction-এর জন্য অন্তত একটি instruction fetch. তার ওপর load আর store — সাধারণ প্রোগ্রামে প্রতি তিন-চারটি instruction-এ একটি, মোটামুটি। একটি ৩ GHz CPU-র এক clock cycle মাত্র ০.৩৩ ন্যানোসেকেন্ড (ns). অথচ main memory বা DRAM থেকে একটি ডেটা আনতে মোটামুটি ৫০ থেকে ১০০ ns লাগে — অর্থাৎ ১৫০ থেকে ৩০০ cycle. প্রতিটি access-এ যদি এতক্ষণ অপেক্ষা করতে হতো, পর্ব ৩-এর CPI-র সূত্রে CPI শত শত হয়ে যেত।

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

ফারাকটা কীভাবে তৈরি হলো

১৯৭০-এর দশকের শেষ আর ১৯৮০-এর দশকের শুরুতে microprocessor-এর clock আর DRAM-এর গতি কাছাকাছি ছিল। তখনকার অনেক ছোট কম্পিউটারে cache ছাড়াই CPU সরাসরি মেমরি পড়ত, আর খুব একটা অপেক্ষা করতে হতো না। তারপর দুটি রেখা আলাদা হয়ে যায়। পর্ব ২-এ দেখেছেন, Moore-এর সূত্র আর Dennard scaling-এর যুগে একটি processor-এর কর্মক্ষমতা ১৯৮০-এর দশকের মাঝামাঝি থেকে ২০০০-এর দশকের শুরু পর্যন্ত বছরে মোটামুটি ৫০%-এর মতো হারে বেড়েছে (Hennessy–Patterson-এর বইয়ের হিসাবে)। DRAM-এর ধারণক্ষমতা দ্রুত বেড়েছে, কিন্তু তার latency কমেছে বছরে মাত্র কয়েক শতাংশ। তাঁদের বইয়ের একটি log-scale ছবিতে ফারাকটা কয়েক দশকে কয়েকশো গুণ ছাড়িয়ে যায় (সংস্করণভেদে সংখ্যা কিছুটা আলাদা)।

১৯৯৫ সালে William Wulf আর Sally McKee একটি ছোট প্রবন্ধে এই অবস্থার নাম দেন memory wall: যদি ফারাক এভাবে বাড়তেই থাকে, তাহলে একদিন CPU-র গতি আর কোনো কাজেই আসবে না — সব সময় যাবে মেমরির অপেক্ষায়। ২০০০-এর দশকের মাঝামাঝি থেকে একটি core-এর clock আর তেমন বাড়েনি (পর্ব ২-এর power wall), তাই ফারাক বাড়ার গতি কমেছে। কিন্তু একই chip-এ অনেক core এসেছে, আর সবাই একই মেমরি ভাগ করে — চাপ কমেনি, শুধু রূপ বদলেছে। আজও একটি DRAM access-এ মোটামুটি কয়েকশো cycle লাগে।

মূল ধারণা

সমস্যাটি দুই ভাগে। Latency — একটি ডেটা চাওয়া থেকে পাওয়া পর্যন্ত সময়; DRAM-এ এটি পদার্থবিজ্ঞান আর বৈদ্যুতিক নকশায় বাঁধা, তাই খুব ধীরে কমে। Bandwidth — প্রতি সেকেন্ডে কত byte আনা যায়; এটি বেশি চ্যানেল আর চওড়া bus দিয়ে বাড়ানো সহজ। মেমরি হায়ারার্কির প্রধান কাজ latency লুকানো: বেশিরভাগ access এমন জায়গা থেকে মেটানো, যেটি CPU-র খুব কাছে।

১৯৬৫ সালে Cambridge-এর Maurice Wilkes একটি ছোট প্রবন্ধে “slave memory”-র প্রস্তাব দেন — একটি ছোট দ্রুত মেমরি, যা বড় ধীর মেমরির সাম্প্রতিক অংশ ধরে রাখবে। এটাই আজকের cache-এর ধারণা। বাণিজ্যিক কম্পিউটারে প্রথম cache সাধারণত ধরা হয় ১৯৬৮ সালের IBM System/360 Model 85-কে। আর x86 পরিবারে chip-এর ভেতরে cache প্রথম আসে ১৯৮৯ সালের Intel 80486-এ (৮ KB).

২ · আদর্শ মেমরি কেন অসম্ভব — প্রযুক্তির trade-off

একজন প্রোগ্রামার যা চান: অসীম বড়, CPU-র সমান দ্রুত, আর প্রায় বিনামূল্যে পাওয়া মেমরি। কোনো একটি প্রযুক্তি তিনটি একসাথে দিতে পারে না। কারণগুলো ভৌত:

  • বড় মানে দূরে। মেমরি যত বড়, তার wire তত লম্বা, ঠিকানা বাছাইয়ের decoder তত গভীর। পর্ব ১৮-এ দেখেছেন, লম্বা wire-এ সংকেত পৌঁছাতে সময় লাগে। বড় মেমরি তাই chip-এর বাইরে, কয়েক সেন্টিমিটার দূরে বসে।
  • দ্রুত cell মানে বড় cell. SRAM-এর একটি bit রাখতে সাধারণত ৬টি transistor লাগে (দুটি inverter-এর loop আর দুটি access transistor — পর্ব ১৫-এর latch-এর আত্মীয়). DRAM-এর একটি bit = ১টি transistor আর ১টি ছোট capacitor. তাই একই জায়গায় DRAM অনেক বেশি bit রাখে, কিন্তু capacitor-এর চার্জ পড়া ধীর, আর চার্জ চুইয়ে যায় বলে বারবার refresh করতে হয় (পর্ব ৪৯).
  • স্থায়িত্ব মানে আরও ধীর। Flash-এ (SSD-এর ভেতরে) চার্জ একটি অন্তরিত গেটে আটকে রাখা হয়, তাই বিদ্যুৎ গেলেও ডেটা থাকে; কিন্তু লেখা আর পড়া microsecond-এর মাপে। HDD-তে ঘুরন্ত চাকতি আর নড়াচড়া করা মাথা — milliseconds.

নিচের টেবিলে প্রতিটি স্তরের মোটামুটি মাত্রা দেওয়া হলো, ২০২০-এর দশকের একটি সাধারণ desktop বা server-এর জন্য। এগুলো নির্দিষ্ট chip-এর মাপ নয় — নির্মাতা, প্রজন্ম আর মাপার পদ্ধতিভেদে কয়েক গুণ পর্যন্ত আলাদা হতে পারে। দামও বাজারভেদে ওঠানামা করে। মনে রাখার বিষয় সংখ্যাগুলো নয়, তাদের মাত্রার ফারাক: প্রতিটি ধাপে latency প্রায় ১০ থেকে ১০০ গুণ বাড়ে।

স্তরপ্রযুক্তিমোটামুটি latencyমোটামুটি ধারণক্ষমতামোটামুটি দাম (প্রতি GB)
Registerflip-flop / SRAM-ধরনের, core-এর ভেতরে~১ cycle (< ১ ns)কয়েকশো byte থেকে কয়েক KB (RV32I-এ ৩২ × ৪ B = ১২৮ B architectural)আলাদা বিক্রি হয় না
L1 cacheSRAM, প্রতি core-এ~৪–৫ cycle (~১ ns)প্রতি core-এ ৩২–৬৪ KiB (instruction আর data আলাদা)আলাদা বিক্রি হয় না; প্রতি bit-এ DRAM-এর চেয়ে বহু গুণ দামি
L2 cacheSRAM~১০–২০ cycle (~৩–৫ ns)প্রতি core-এ ২৫৬ KiB থেকে কয়েক MiBওই
L3 cacheSRAM, সব core-এর ভাগে~৩০–৭০ cycle (~১০–২০ ns)কয়েক MiB থেকে ১০০ MiB-এর বেশিওই
Main memoryDRAM (DDR4/DDR5, LPDDR)~৫০–১০০ ns (কয়েকশো cycle)৮ GiB থেকে কয়েকশো GiB; বড় server-এ TBমোটামুটি কয়েক মার্কিন ডলার
SSDNAND flash~১০–১০০ µs (random read)২৫৬ GB থেকে কয়েক TBমোটামুটি ০.০৫–০.১৫ ডলার
HDDচৌম্বক চাকতি~৩–১০ ms (seek + ঘোরা)কয়েক TB থেকে ২০ TB-এর বেশিমোটামুটি ০.০২–০.০৩ ডলার
Network / cloud storageঅন্য যন্ত্রের মেমরি বা diskএকই data center-এ ~০.১–১ ms; মহাদেশ পেরিয়ে ~১০০ ms বা বেশিকার্যত সীমাহীনসেবাভেদে, মাসিক ভাড়া

যদি এক cycle হতো এক সেকেন্ড

ন্যানোসেকেন্ড আর মাইক্রোসেকেন্ড মানুষের অনুভূতিতে ধরা পড়ে না। তাই একটি মানব-মাপের তুলনা করা যাক। ধরুন CPU-র clock ৩ GHz, অর্থাৎ এক cycle ≈ ০.৩৩৩ ns. এখন কল্পনা করুন সময় এমনভাবে টেনে লম্বা করা হলো যে এক cycle = এক সেকেন্ড; তাহলে গুণক = ১ s ÷ ০.৩৩৩ ns = ৩ × ১০৯. প্রতিটি latency-কে এই গুণক দিয়ে গুণ করলে (হিসাবটি একটি ছোট script-এ করা হয়েছে, latency-গুলো উপরের টেবিলের মাঝামাঝি ধরে নেওয়া মান):

ঘটনাধরে নেওয়া latencycycle (৩ GHz)এক cycle = ১ সেকেন্ড হলেদৈনন্দিন তুলনা
Register পড়া০.৩৩ ns১১ সেকেন্ডহাতে ধরা কলম
L1 hit~১.৩ ns৪৪ সেকেন্ডডেস্কের খাতা
L2 hit~৪ ns১২১২ সেকেন্ডপাশের তাক
L3 hit~১৩ ns৪০৪০ সেকেন্ডপাশের ঘরের আলমারি
DRAM~১০০ ns৩০০৫ মিনিটমোড়ের দোকান থেকে কিছু আনা
SSD random read~৫০ µs১,৫০,০০০প্রায় ৪১.৭ ঘণ্টা ≈ ১.৭ দিনঢাকা থেকে চট্টগ্রাম গিয়ে বই এনে ফেরা
একই data center-এ network round trip~০.৫ ms১৫,০০,০০০প্রায় ১৭.৪ দিনদুই সপ্তাহের বেশি
HDD-তে একটি random read~৫ ms১,৫০,০০,০০০প্রায় ১৭৩.৬ দিন ≈ ৬ মাসবর্ষা থেকে শীত পর্যন্ত
ঢাকা থেকে আমেরিকার একটি server-এ round trip~২০০ ms৬০,০০,০০,০০০প্রায় ১৯ বছরএকটি শিশুর বড় হয়ে ওঠা

L1 থেকে পাওয়া যায় ৪ সেকেন্ডে, DRAM-এ ৫ মিনিট, আর disk-এ মাস। তাই একটি cache miss-কে “মাত্র ১০০ ns” বলে উড়িয়ে দেওয়া যায় না — CPU-র চোখে সেটা পাঁচ মিনিট হাত গুটিয়ে বসে থাকা, যখন প্রতি সেকেন্ডে কয়েকটি instruction শেষ করার ক্ষমতা ছিল।

৩ · হায়ারার্কির পিরামিড — প্রতিটি স্তর পরের স্তরের cache

সমাধান হলো স্তর সাজানো। ওপরে ছোট, দ্রুত, দামি; নিচে বড়, ধীর, সস্তা। CPU সব সময় ওপরের স্তরে খোঁজে। যা দরকার তা ওপরে না পেলে নিচ থেকে আনা হয়, আর আনার সময় সেটির একটি কপি ওপরে রেখে দেওয়া হয় — যাতে পরের বার কাছেই পাওয়া যায়। এই ব্যবস্থার নাম memory hierarchy.

মেমরি হায়ারার্কির পিরামিড

Smaller, faster, costlier per byte at the top; larger, slower, cheaper at the bottom

Register L1 (SRAM) L2 (SRAM) L3 (SRAM) DRAM — main memory SSD (flash) HDD · network · cloud ~1 cycle · ~KB ~4 cycle · 32–64 KiB ~12 cycle · ~1 MiB ~40 cycle · ~10s MiB ~100 ns · GiB ~50 µs · TB ms+ · TB+ ছোট · দ্রুত বড় · ধীর · সস্তা প্রতিটি স্তর নিচের স্তরের cache
ছবি ৪৪.১ — ওপরের স্তরে যা আছে, তা নিচের স্তরের কোনো অংশের কপি। সংখ্যাগুলো মোটামুটি মাত্রা, যন্ত্রভেদে আলাদা।
নীতি

প্রতিটি স্তর তার নিচের স্তরের cache. L1 হলো L2-এর cache, L2 হলো L3-এর, L3 হলো DRAM-এর, DRAM হলো SSD-এর (virtual memory আর OS-এর file cache-এর মাধ্যমে), আর আপনার ফোনের storage হলো cloud-এর ছবির cache. লক্ষ্য: খরচ প্রায় নিচের স্তরের মতো কম, গতি প্রায় ওপরের স্তরের মতো বেশি।

একটি load যখন কোনো স্তরে ডেটা পায় না, কী ঘটে? নিচের stepper-এ একটি RISC-V load ধাপে ধাপে নিচে নামে আর ডেটা নিয়ে ওপরে ওঠে। সময়গুলো ধরে নেওয়া গোল সংখ্যা (L1 ৪, L2 ১২, L3 ৪০, DRAM ২০০ cycle, প্রতিটি স্তর একের পর এক দেখা হয়); বাস্তব chip-এ অনেক কাজ সমান্তরালে হয় আর সংখ্যা আলাদা।

CPU corelw x10, 8(x11) · x11 = 0x2A40
L1 data cache৪ cycle
L2 cache+১২ cycle
L3 cache+৪০ cycle
DRAM+২০০ cycle · পুরো ৬৪ B block
অবস্থা (ঠিকানা 0x2A48, block = ৬৪ B)
কোথায় খোঁজা হচ্ছে
ফল
block-এর ঠিকানা
এই access-এর মোট সময়
  1. L1-এ খোঁজা। Instruction lw x10, 8(x11), x11 = 0x2A40, তাই ঠিকানা 0x2A40 + 8 = 0x2A48. Cache কখনো একা একটি byte রাখে না, রাখে ৬৪ byte-এর একটি block. ঠিকানার নিচের ৬টি bit বাদ দিলে block-এর শুরু: 0x2A48 & ~0x3F = 0x2A40, আর block-এর ভেতরে offset ৮। L1 ৪ cycle পরে জানাল — এই block তার কাছে নেই: miss.
  2. L2-তে খোঁজা। অনুরোধ নিচে নামল। L2 আরও ১২ cycle পরে জানাল — নেই। মোট ১৬ cycle গেল, কিন্তু ডেটা এখনো আসেনি।
  3. L3-তে খোঁজা। L3 সব core-এর ভাগে, বড়, তাই ধীর: আরও ৪০ cycle. এখানেও miss. মোট ৫৬ cycle — তিনটি cache-এই miss, তাই এবার main memory.
  4. DRAM থেকে পুরো block. Memory controller DRAM-কে অনুরোধ পাঠাল। DRAM শুধু ৪ byte নয়, পুরো ৬৪ byte block (0x2A40 থেকে 0x2A7F) পাঠায়, কারণ DRAM থেকে একসাথে অনেক byte আনা প্রায় একটি byte আনার সমান খরচের (পর্ব ৪৯). আরও ২০০ cycle; মোট ২৫৬ cycle.
  5. Block ওপরে ফেরে। Block-এর কপি পথের cache-গুলোতে বসানো হয় (ঠিক কোন কোন স্তরে, তা chip-এর inclusion নীতির ওপর নির্ভর করে — অংশ ৬). Offset ৮-এর ৪ byte x10-এ গেল। এই প্রথম access-এর দাম ২৫৬ cycle — এটাই miss penalty-র বাস্তব চেহারা।
  6. পাশের ঠিকানা: spatial locality-র লাভ। পরের instruction lw x12, 12(x11) চাইল ঠিকানা 0x2A4C — একই block-এর offset ১২। Block এখন L1-এ, তাই hit, মাত্র ৪ cycle. আগের miss-এর সময় আনা বাকি ৬০ byte বিনা খরচে কাজে লাগল।
  7. একই ঠিকানা আবার: temporal locality-র লাভ। এই কোড একটি loop-এর ভেতরে থাকলে পরের পাকে আবার 0x2A48 চাওয়া হবে — block তখনো L1-এ, আবার ৪ cycle. প্রথম access ২৫৬ cycle, পরের প্রতিটি ৪ — যত বেশি বার ব্যবহার, গড় খরচ তত কম।

কে কোন স্তর সামলায়, আর সীমানায় কত বড় টুকরো যায়

প্রতিটি সীমানায় ডেটা আসে-যায় একটি নির্দিষ্ট মাপের টুকরোয়। আর সিদ্ধান্ত নেয় আলাদা আলাদা “ব্যবস্থাপক”:

সীমানাকে সিদ্ধান্ত নেয়টুকরোর মাপ (সাধারণত)বিস্তারিত
Register ↔ L1Compiler (কোন মান register-এ থাকবে) আর প্রোগ্রামের load/storeএকটি word: ১, ২, ৪ বা ৮ byteপর্ব ২৬, ৩১
L1 ↔ L2 ↔ L3 ↔ DRAMপুরোপুরি hardware; প্রোগ্রাম আর OS প্রায় টেরই পায় নাCache block বা cache line: x86-64 আর বেশিরভাগ ARM chip-এ ৬৪ B (কিছু chip-এ ১২৮ B)পর্ব ৪৫–৪৭
DRAM ↔ SSD/HDD (virtual memory)Operating system, hardware-এর সাহায্যে (page table, TLB)Page: সাধারণত ৪ KiB (বড় page ২ MiB বা ১ GiB)পর্ব ৫০; OS কোর্সের paging
DRAM ↔ file (file cache)OS-এর file system আর প্রোগ্রাম নিজেFile system block: প্রায়ই ৪ KiBOS কোর্সের file system পর্ব
Local disk ↔ network/cloudপ্রোগ্রাম, browser, sync softwareপুরো file, object বা chunk—

লক্ষ করুন, নিচে নামলে টুকরো বড় হয়। কারণ নিচের স্তরে একটি access শুরু করাই দামি (latency বেশি), কিন্তু শুরু হয়ে গেলে একসাথে অনেক byte আনা তুলনায় সস্তা। তাই বড় টুকরো আনা হয়, এই আশায় যে পাশের ডেটাও কাজে লাগবে — অর্থাৎ spatial locality-র ওপর বাজি।

Volatile আর non-volatile

আরেকটি বড় বিভাজন: বিদ্যুৎ চলে গেলে ডেটা থাকে কি না। Register, SRAM আর DRAM হলো volatile — লোডশেডিংয়ে কম্পিউটার বন্ধ হলে সব মুছে যায়। Flash (SSD, ফোনের storage, pen drive) আর HDD হলো non-volatile. তাই হায়ারার্কির একটি সীমারেখা হলো “কাজের জায়গা” আর “সংরক্ষণের জায়গা”: Word-এ লেখা ফাইল “Save” না করা পর্যন্ত DRAM-এই থাকে। অংশ ৮-এ দেখবেন, persistent memory এই রেখাটি মুছে ফেলার চেষ্টা করেছিল।

৪ · Locality — temporal আর spatial

হায়ারার্কি কাজ করবে কেবল তখনই, যদি বেশিরভাগ access ওপরের ছোট স্তরে মেটে। এটা কেন হয়? কারণ প্রোগ্রাম এলোমেলোভাবে মেমরি ছোঁয় না। প্রোগ্রামের এই স্বভাবের নাম locality of reference, আর এর দুটি রূপ:

Temporal locality (সময়ের)

যে ডেটা বা instruction এইমাত্র ব্যবহার হলো, খুব শিগগির আবার ব্যবহার হওয়ার সম্ভাবনা বেশি। তাই সদ্য ব্যবহৃতটি কাছে রাখুন।

loop counter, sum, loop-এর instruction, stack-এর শীর্ষ

Spatial locality (স্থানের)

যে ঠিকানা এইমাত্র ব্যবহার হলো, তার পাশের ঠিকানাগুলো শিগগির ব্যবহার হওয়ার সম্ভাবনা বেশি। তাই একটি চাইলে আশপাশেরগুলোও নিয়ে আসুন।

array-র পরপর উপাদান, পরপর instruction, struct-এর field

মহল্লার মুদি দোকানের কথা ভাবুন। দোকানি জানেন, চাল-ডাল-তেল-লবণ প্রতিদিন বিক্রি হয় — তাই এগুলো হাতের কাছের তাকে (temporal: যা বারবার লাগে, তা কাছে)। আর কেউ চা-পাতা কিনলে প্রায়ই চিনিও কেনেন, তাই দুটো পাশাপাশি রাখা; পাইকারি বাজার থেকে চা-পাতা আনার সময় একটি নয়, পুরো এক কার্টন আনা হয় (spatial: পাশের জিনিসও শিগগির লাগবে, আর একবারে বেশি আনা সস্তা)। গুদামে সব আছে, কিন্তু দিনের বেশিরভাগ বিক্রি সামনের দুটি তাক থেকেই হয়।

সাধারণ কোডে locality কোথায় লুকিয়ে থাকে

int sum = 0;
for (int i = 0; i < n; i++)     /* i, n, sum: প্রতি পাকে ব্যবহার -> temporal       */
    sum += a[i];                /* a[0], a[1], ... পরপর ঠিকানা -> spatial          */
                                /* loop-এর ৪-৫টি instruction বারবার -> temporal    */
                                /* instruction-গুলো পরপর ঠিকানায় -> spatial         */
  • Loop counter আর accumulator (i, sum): প্রতি পাকে পড়া আর লেখা — সবচেয়ে শক্তিশালী temporal locality. Compiler সাধারণত এগুলো register-এই রাখে।
  • Array পরপর পড়া (a[i]): ৪ byte-এর int হলে একটি ৬৪ B block-এ ১৬টি উপাদান। প্রথমটিতে miss, পরের ১৫টিতে hit — miss rate ১ ÷ ১৬ = ৬.২৫%.
  • Instruction fetch: loop-এর শরীর কয়েকটি instruction, প্রতি পাকে একই ঠিকানা থেকে আনা হয় (temporal), আর loop-এর বাইরেও instruction সাধারণত পরপর ঠিকানায় চলে, branch ছাড়া (spatial).
  • Stack: পর্ব ৩০-এ দেখেছেন, function call-এ stack-এর শীর্ষে frame তৈরি হয় আর ফেরার সময় মোছে। পরপর call একই কয়েকশো byte বারবার ব্যবহার করে — temporal আর spatial দুটোই।

এবার একটি ছোট loop-কে cache-এর চোখে দেখা যাক। বোঝার সুবিধার জন্য এখানে block-কে ছোট ধরা হলো — ১৬ byte, অর্থাৎ ৪টি int (বাস্তবে সাধারণত ৬৪ B). Array a শুরু হয় ঠিকানা 0x1000-এ, আর শুরুতে cache খালি।

1int sum = 0;2for (int i = 0; i < 8; i++)3    sum += a[i];
a শুরু 0x1000 · block ১৬ B = ৪টি int
i
ঠিকানা
block
a[i]-এর ফল
access / miss
  1. i = ০: প্রথম ছোঁয়ায় miss. a[0]-এর ঠিকানা 0x1000. Cache খালি, তাই miss — এটা এড়ানো যায় না, কারণ এই block আগে কখনো আসেনি (পর্ব ৪৫-এ এর নাম compulsory miss). কিন্তু আনা হলো পুরো block 0x1000–0x100F, অর্থাৎ a[0] থেকে a[3]. একই সময়ে loop-এর instruction-গুলোও প্রথমবার আনা হলো।
  2. i = ১: spatial hit. a[1] = 0x1004, একই block-এ — hit. আগের miss-এর সময় যে বাড়তি ১২ byte আনা হয়েছিল, তা এখন কাজে লাগল। Loop-এর instruction এবার instruction cache-এ hit: একই ঠিকানা, এইমাত্র ব্যবহৃত — temporal.
  3. i = ২, ৩: আরও দুটি hit. 0x1008 আর 0x100C একই block-এ। এখন পর্যন্ত ৪টি access-এ ১টি miss. i আর sum প্রতি পাকে পড়া-লেখা হচ্ছে — register-এ থাকলে মেমরিতে যায়ই না, stack-এ থাকলে প্রতিবার L1 hit.
  4. i = ৪: নতুন block, miss. a[4] = 0x1010, যা পরের block (0x1010–0x101F). এটি আগে আসেনি, তাই miss, আর আনা হলো a[4] থেকে a[7]. ৫টি access-এ ২টি miss.
  5. i = ৫, ৬, ৭: spatial hit. তিনটিই 0x1010 block-এ। ৮টি access-এ মাত্র ২টি miss — miss rate ২ ÷ ৮ = ২৫%, hit rate ৭৫%. Block ৪টি int ধরে, তাই প্রতি ৪টিতে ১টি miss.
  6. হিসাব আর সাধারণ নিয়ম। Block যদি B byte আর উপাদান e byte হয়, তাহলে পরপর পড়ায় miss rate ≈ e ÷ B. বাস্তবের ৬৪ B block আর ৪ B int-এ এটি ১ ÷ ১৬ = ৬.২৫%, আর হাজার হাজার পাক চলা loop-এর instruction-গুলোতে miss প্রায় শূন্য। লক্ষ করুন, এই array-র কোনো উপাদান দুবার পড়া হয়নি — এখানে ডেটার hit-গুলো সব spatial; temporal লাভ এসেছে i, sum আর instruction থেকে।

কোডে locality চেনার উপায়

  • ঠিকানা কীভাবে বদলায় দেখুন। a[i], i++ — stride ১ উপাদান: ভালো spatial. a[i * 16] (৪ B int-এ ৬৪ B লাফ) — প্রতি access নতুন block: spatial প্রায় শূন্য। a[rand()] — কোনো ধারা নেই।
  • 2D array-র loop-এর ক্রম দেখুন। C-তে m[i][j] সারি ধরে (row-major) পাশাপাশি থাকে। ভেতরের loop যদি j হয়, পরপর ঠিকানা; যদি i হয়, প্রতিবার এক সারি সমান লাফ (পর্ব ৪৮-এ বিস্তারিত; C কোর্সের performance আর cache পর্বে হাতে-কলমে)।
  • পুনর্ব্যবহারের দূরত্ব ভাবুন। একটি মান দুবার ব্যবহারের মাঝে আর কতগুলো আলাদা ডেটা ছোঁয়া হয়? দূরত্ব কম হলে temporal লাভ, বেশি হলে মাঝখানে ডেটা cache থেকে বেরিয়ে যায়। অংশ ৫-এ এটা মাপব।
  • Pointer-এর পেছনে দৌড় সন্দেহ করুন। Linked list, tree, hash table-এ পরের ঠিকানা আগের ডেটার ভেতরে লেখা থাকে, আর node-গুলো মেমরির যেখানে-সেখানে — spatial locality দুর্বল।

Working set: একটি সময়-জানালায় (ধরুন শেষ কয়েক লাখ access) প্রোগ্রাম যে আলাদা আলাদা block বা page ছুঁয়েছে, তাদের সমষ্টি। ১৯৬৮ সালে Peter Denning OS-এর page ব্যবস্থাপনার জন্য ধারণাটি দেন। Working set যদি একটি স্তরে এঁটে যায়, তাহলে প্রায় সব access সেখানেই মেটে; না আঁটলে miss বাড়তে থাকে — page-এর ক্ষেত্রে চরম রূপের নাম thrashing (OS কোর্সের thrashing আর working set পর্ব). অংশ ৭-এর staircase ঠিক এটাই দেখাবে।

৫ · Locality মাপা — reuse distance আর LRU stack

“এই কোডে locality ভালো” বলা সহজ, কিন্তু সংখ্যায় মাপা যায় কীভাবে? একটি সুন্দর মাপ হলো reuse distance (stack distance-ও বলা হয়): একটি block দ্বিতীয়বার চাওয়ার সময়, মাঝখানে আর কতগুলো আলাদা block ছোঁয়া হয়েছে। একই block পরপর দুবার চাইলে দূরত্ব ০; কখনো আগে না চাওয়া হলে দূরত্ব অসীম (cold).

LRU stack-এর ধর্ম

ধরুন একটি cache C-টি block রাখে, যেকোনো block যেকোনো জায়গায় বসতে পারে (fully associative, পর্ব ৪৬), আর জায়গা লাগলে সবচেয়ে আগে ব্যবহৃতটি বাদ যায় (LRU). তাহলে একটি access hit হবে ঠিক তখনই, যখন তার reuse distance d < C.

যুক্তি। সব block-কে শেষ ব্যবহারের ক্রমে একটি তালিকায় (stack) সাজান — সবচেয়ে সাম্প্রতিকটি ওপরে, অবস্থান ০। প্রতিটি access-এ block-টি ওপরে উঠে আসে, আর তার ওপরে যারা ছিল তারা এক ধাপ নামে। LRU cache সব সময় ঠিক এই তালিকার ওপরের C-টি block ধরে রাখে। আরোহ পদ্ধতিতে এটি দেখানো যায়: শুরুতে দুটোই খালি; প্রতি access-এ ব্যবহৃত block দুই জায়গাতেই সবার ওপরে ওঠে, আর জায়গা লাগলে cache থেকে যে block বাদ পড়ে (সবচেয়ে আগে ব্যবহৃত), সেটিই তালিকার ওপরের C-টির ভেতর থেকে নিচে নেমে যায়। এখন block X-এর আগের access-এর পরে d-টি আলাদা block ছোঁয়া হলে X ঠিক অবস্থান d-তে থাকবে, কারণ শুধু ওই d-টি block তার ওপরে উঠেছে। অবস্থান d < C মানে X ওপরের C-টির মধ্যে, অর্থাৎ cache-এ: hit. d ≥ C হলে নেই: miss. একটি মজার ফল: একটি trace-এর reuse distance-এর histogram একবার বের করলেই সব মাপের LRU cache-এর hit rate একসাথে জানা যায়।

নিচের simulator পাঁচটি address trace তৈরি করে — প্রতিটি ৪ byte-এর int-এর ৬৪ KiB array (১০২৪টি block) নিয়ে — আর প্রতিটি access-এর reuse distance মেপে histogram ছাপে। শেষে তিন মাপের fully associative LRU cache-এ hit rate: ৮টি block (৫১২ B), ৬৪টি (৪ KiB) আর ৫১২টি (৩২ KiB).

// Locality analyser: reuse distance of 64-byte blocks in five address traces (4-byte ints)
const B = 64, N = 16384;                        // array of 16384 ints = 64 KiB = 1024 blocks
let seed = 12345; const rnd = n => Math.floor((seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0) / 2 ** 32 * n);
const traces = {
  "sequential x2": [...Array(2 * N).keys()].map(i => (i % N) * 4),
  "stride 64 B x2": [...Array(2 * N / 16).keys()].map(i => (i * 16 % N) * 4),
  "random":        Array.from({ length: N }, () => rnd(N) * 4),
  "matrix rows":   [...Array(N).keys()].map(k => k * 4),                          // a[128][128], i outer
  "matrix cols":   [...Array(N).keys()].map(k => ((k % 128) * 128 + (k >> 7)) * 4) // j outer
};
const buckets = ["0", "1-7", "8-63", "64-511", "512+", "cold"];
const bucket = d => d < 0 ? 5 : d === 0 ? 0 : d < 8 ? 1 : d < 64 ? 2 : d < 512 ? 3 : 4;
console.log("trace           " + buckets.map(b => b.padStart(7)).join("") + "   hit% if LRU holds 8 / 64 / 512 blocks");
for (const [name, addrs] of Object.entries(traces)) {
  const stack = [], hist = [0, 0, 0, 0, 0, 0], hits = [0, 0, 0];   // stack[0] = most recently used block
  for (const a of addrs) {
    const blk = Math.floor(a / B), d = stack.indexOf(blk);       // d = distinct blocks used since last use
    hist[bucket(d)]++;
    [8, 64, 512].forEach((c, i) => { if (d >= 0 && d < c) hits[i]++; });
    if (d >= 0) stack.splice(d, 1);
    stack.unshift(blk);
  }
  const pct = x => (100 * x / addrs.length).toFixed(1);
  console.log(name.padEnd(16) + hist.map(h => String(h).padStart(7)).join("") + "   " + hits.map(pct).join(" / "));
}

আসল আউটপুট:

trace                 0    1-7   8-63 64-511   512+   cold   hit% if LRU holds 8 / 64 / 512 blocks
sequential x2     30720      0      0      0   1024   1024   93.8 / 93.8 / 93.8
stride 64 B x2        0      0      0      0   1024   1024   0.0 / 0.0 / 0.0
random               14    112    878   7011   7345   1024   0.8 / 6.1 / 48.9
matrix rows       15360      0      0      0      0   1024   93.8 / 93.8 / 93.8
matrix cols           0      0      0  15360      0   1024   0.0 / 0.0 / 93.8

sequential x2 (পুরো array দুবার পরপর): প্রথম পাসে প্রতি block-এ ১টি cold miss আর ১৫টি দূরত্ব-০ access — ১০২৪টি cold, ৩০,৭২০টি দূরত্ব ০। এই দূরত্ব-০ access-গুলোই spatial locality: একটি ৮-block-এর ছোট্ট cache-ও এগুলো ধরে, তাই তিন মাপেই ৯৩.৮% (৩০,৭২০ ÷ ৩২,৭৬৮). দ্বিতীয় পাসে প্রতিটি block-এর প্রথম access-এর দূরত্ব ১০২৩ (মাঝখানে বাকি সব block ছোঁয়া হয়েছে) — temporal reuse আছে, কিন্তু তা ধরতে অন্তত ১০২৪ block (৬৪ KiB) লাগত।

stride 64 B x2: প্রতি access নতুন block-এ, তাই দূরত্ব-০ একটিও নেই; দ্বিতীয় পাসের সব দূরত্ব ১০২৩। ৩২ KiB পর্যন্ত কোনো cache-এ একটিও hit নেই — ০.০%. শুধু ক্রম বদলেই spatial locality উধাও। random: দূরত্বগুলো ছড়ানো, প্রায় সব বড়; ৫১২-block cache array-র অর্ধেক ধরে বলে hit rate প্রায় অর্ধেক — ৪৮.৯%, আর ছোট cache-এ প্রায় শূন্য (০.৮%, ৬.১%).

matrix rows বনাম matrix cols — ১২৮ × ১২৮ int matrix, প্রতি সারি ৫১২ B = ৮ block. সারি ধরে পড়লে ৯৩.৮% সব মাপে। কলাম ধরে পড়লে পরপর দুটি access ৫১২ B দূরে, আলাদা block-এ; একই block আবার লাগে পরের কলামে, ১২৮টি সারি পরে — দূরত্ব ১২৭, তাই সব ১৫,৩৬০টি পড়েছে “64-511” ঘরে। ৮ বা ৬৪ block-এর cache-এ ০.০%, ৫১২ block-এ হঠাৎ ৯৩.৮%. শিক্ষা: “locality ভালো না খারাপ” প্রশ্নের উত্তর cache-এর মাপের ওপরও নির্ভর করে — working set (এখানে ১২৮ block = ৮ KiB) cache-এ আঁটলে খারাপ ক্রমও বেঁচে যায়।

৬ · Hit, miss আর প্রত্যাশিত সময় — AMAT-এর প্রথম স্বাদ

এবার শব্দগুলো ঠিকঠাক সংজ্ঞায়িত করা যাক। পর্ব ৪৫ থেকে ৪৭ পর্যন্ত এই শব্দগুলোই বারবার আসবে।

শব্দঅর্থ
Block / lineদুই স্তরের মধ্যে আনা-নেওয়ার সবচেয়ে ছোট একক; cache-এ সাধারণত ৬৪ B. Cache-এ যে খোপে একটি block বসে, তাকেও অনেক সময় line বলা হয়।
Hitচাওয়া ডেটা এই স্তরেই পাওয়া গেল।
Missপাওয়া গেল না; নিচের স্তর থেকে block আনতে হবে।
Hit rate (h)এই স্তরে আসা access-এর মধ্যে hit-এর ভগ্নাংশ।
Miss rate (m)১ − h. দ্বিতীয় স্তরের জন্য “local” miss rate মানে যত access L2-তে এসেছে তার মধ্যে miss; “global” মানে মোট access-এর মধ্যে (পর্ব ৪৭).
Hit timeএই স্তরে খুঁজে hit হলে ডেটা পেতে যে সময় — খোঁজার সময়সহ।
Miss penaltyMiss হলে নিচের স্তর থেকে block এনে বসাতে যে বাড়তি সময় লাগে।

দুই স্তরের প্রথম হিসাব

ধরুন একটি cache-এর hit time ৪ cycle, miss rate ৫%, আর miss হলে main memory থেকে block আনতে বাড়তি ১০০ cycle. প্রতি ১০০টি access-এর ৯৫টি ৪ cycle-এ শেষ। বাকি ৫টি-তে ৪ cycle খোঁজা (miss), তারপর ১০০ cycle — মোট ১০৪. গড় = (৯৫ × ৪ + ৫ × ১০৪) ÷ ১০০ = (৩৮০ + ৫২০) ÷ ১০০ = ৯ cycle. সংক্ষেপে:

AMAT = hit time + miss rate × miss penalty
= ৪ + ০.০৫ × ১০০ = ৯ cycle

Cache না থাকলে প্রতিটি access-এ ১০০ cycle-এর বেশি; cache থাকায় গড় ৯ — দশ গুণের বেশি লাভ। লক্ষ করুন, ৯ cycle-এর মধ্যে ৫ cycle এসেছে মাত্র ৫% miss থেকে। Miss rate ৫% থেকে ২% হলে AMAT = ৪ + ২ = ৬; অর্থাৎ hit rate-এ সামান্য উন্নতিও গড় সময়ে বড় পার্থক্য আনে।

অনেক স্তরের প্রত্যাশিত সময় — সূত্র আর প্রমাণ

এখন k-টি স্তর ধরুন: স্তর i-এর hit time ti, আর local hit rate hi (যত access স্তর i পর্যন্ত পৌঁছায়, তার মধ্যে কতগুলো সেখানে মেটে); শেষ স্তরে hk = ১ — সব কিছু সেখানে আছে। ধরা যাক স্তরগুলো একের পর এক দেখা হয়।

সূত্র

স্তর i-তে পৌঁছানোর সম্ভাবনা ri = (১ − h১)(১ − h২)…(১ − hi−১), সেখানে মেটার সম্ভাবনা fi = ri · hi, আর তখন মোট সময় Ti = t১ + t২ + … + ti. প্রত্যাশিত সময় E[T] = Σ fi · Ti = t১ + m১(t২ + m২(t৩ + … + mk−১ tk)), যেখানে mi = ১ − hi.

প্রমাণ। প্রথম রূপটি প্রত্যাশিত মানের সংজ্ঞা: access-টি ঠিক একটি স্তরে মেটে, স্তর i-তে মেটার সম্ভাবনা fi, আর তখন সে t১ থেকে ti সব খোঁজার সময় দিয়েছে। দ্বিতীয় রূপের জন্য যোগফলটি অন্যভাবে সাজান: tj খরচটি দিতে হয় ঠিক তখনই, যখন access স্তর j পর্যন্ত পৌঁছায় — যার সম্ভাবনা rj. তাই E[T] = Σj rj tj = t১ + m১t২ + m১m২t৩ + …, আর m১, তারপর m২ ইত্যাদি সাধারণ গুণনীয়ক হিসেবে বাইরে আনলেই nested রূপ। দুই স্তরে এটি t১ + m১t২ — ঠিক AMAT, যেখানে miss penalty = t২. তিন স্তরে AMAT = HTL1 + MRL1 × (HTL2 + MRL2 × MPL2) — পর্ব ৪৭-এর multi-level সূত্র।

নিচের calculator তিনটি পরিস্থিতিতে এই হিসাব করে, স্তর ধরে ধরে: কোন স্তরে কত ভগ্নাংশ access মেটে, আর গড়ে কতটা সময় যোগ করে। সব সংখ্যা ধরে নেওয়া গোল মান (৩ GHz-এর cycle); বাস্তব chip-এ L2 আর L3-এর খোঁজা আংশিক সমান্তরাল হয়, আর DRAM-এর সময় অবস্থাভেদে বদলায়।

// Expected access time of a memory hierarchy (ASSUMED round numbers, cycles at 3 GHz; real chips differ)
function expected(name, levels) {
  console.log(`== ${name}`);
  console.log("level  hit time  local hit   served here   time to here   contribution");
  let reach = 1, path = 0, total = 0;            // reach = fraction of accesses that get this far
  for (const [lv, ht, hit] of levels) {
    path += ht;                                  // lookups are tried one after another
    const served = reach * hit, part = served * path;
    total += part;
    console.log(lv.padEnd(6) + String(ht).padStart(9) + (+(100 * hit).toFixed(4) + "%").padStart(11) +
      ((100 * served).toPrecision(4) + "%").padStart(14) + String(path).padStart(15) + part.toFixed(3).padStart(15));
    reach *= 1 - hit;
  }
  // the same answer written as nested AMAT: HT1 + MR1 x (HT2 + MR2 x (...))
  const nested = levels.reduceRight((acc, [, ht, hit]) => ht + (1 - hit) * acc, 0);
  console.log(`expected time = ${total.toFixed(3)} cycles (nested AMAT ${nested.toFixed(3)}) = ${(total / 3).toFixed(2)} ns`);
}
const base = [["L1", 4, 0.95], ["L2", 12, 0.6], ["L3", 40, 0.5], ["DRAM", 200, 1]];
expected("A: typical-looking program", base);
expected("B: L1 hit rate 98% instead of 95%", [["L1", 4, 0.98], ...base.slice(1)]);
expected("C: like A, but 1 access in 100,000 needs a page from SSD (50 us)",
  [...base.slice(0, 3), ["DRAM", 200, 1 - 1e-5 / 0.01], ["SSD", 150000, 1]]);

আসল আউটপুট:

== A: typical-looking program
level  hit time  local hit   served here   time to here   contribution
L1            4        95%        95.00%              4          3.800
L2           12        60%        3.000%             16          0.480
L3           40        50%        1.000%             56          0.560
DRAM        200       100%        1.000%            256          2.560
expected time = 7.400 cycles (nested AMAT 7.400) = 2.47 ns
== B: L1 hit rate 98% instead of 95%
level  hit time  local hit   served here   time to here   contribution
L1            4        98%        98.00%              4          3.920
L2           12        60%        1.200%             16          0.192
L3           40        50%       0.4000%             56          0.224
DRAM        200       100%       0.4000%            256          1.024
expected time = 5.360 cycles (nested AMAT 5.360) = 1.79 ns
== C: like A, but 1 access in 100,000 needs a page from SSD (50 us)
level  hit time  local hit   served here   time to here   contribution
L1            4        95%        95.00%              4          3.800
L2           12        60%        3.000%             16          0.480
L3           40        50%        1.000%             56          0.560
DRAM        200      99.9%       0.9990%            256          2.557
SSD      150000       100%     0.001000%         150256          1.503
expected time = 8.900 cycles (nested AMAT 8.900) = 2.97 ns

A-তে ৯৫% access L1-এ মেটে, আর বাকি ৫%-এর মধ্যে L2 নেয় ৬০% (মোটের ৩%), L3 বাকির অর্ধেক (১%), আর DRAM শেষ ১%. প্রত্যাশিত সময় ৭.৪ cycle — DRAM-এর ২৫৬ cycle-এর চেয়ে প্রায় ৩৫ গুণ কম। কিন্তু খেয়াল করুন: মাত্র ১% access DRAM-এ যায়, অথচ ৭.৪-এর ২.৫৬ cycle (প্রায় এক-তৃতীয়াংশ) তাদেরই অবদান। দুই হিসাব-পদ্ধতি (ভগ্নাংশ × সময়ের যোগফল আর nested AMAT) একই সংখ্যা দিয়েছে — উপরের প্রমাণের সংখ্যাগত যাচাই।

B-তে শুধু L1-এর hit rate ৯৫% থেকে ৯৮%. প্রত্যাশিত সময় ৭.৪ থেকে নেমে ৫.৩৬ — প্রায় ২৮% কম, কারণ নিচের সব স্তরে যাওয়া access আড়াই গুণ কমেছে (৫% থেকে ২%). C-তে A-র সাথে একটিই পার্থক্য: প্রতি এক লাখ access-এ একটি (০.০০১%) ডেটা DRAM-এ নেই, SSD থেকে page আনতে হয় (~৫০ µs = ১,৫০,০০০ cycle ধরে). এই অতি বিরল ঘটনাই ১.৫ cycle যোগ করে, গড় ৭.৪ থেকে ৮.৯ — প্রায় ২০% ধীর। বাস্তবে OS-এর কাজও যোগ হয়। নিচের স্তর যত ধীর, তার ঠিক ওপরের স্তরের miss rate তত অবিশ্বাস্য রকম ছোট রাখতে হয়।

Inclusion — ওপরের স্তরের সব কিছু কি নিচেও থাকে?

“প্রতিটি স্তর পরের স্তরের cache” কথাটি শুনে মনে হতে পারে, L1-এর প্রতিটি block L2-তেও আছে। এটি নকশার একটি সিদ্ধান্ত, প্রাকৃতিক নিয়ম নয়। Inclusive নীতিতে নিচের cache ওপরের সবের কপি রাখতে বাধ্য: L3 থেকে কিছু বাদ দিলে L1/L2 থেকেও বাদ দিতে হয়। সুবিধা — অন্য core জানতে চাইলে শুধু L3 দেখলেই চলে (পর্ব ৫৩-এর coherence-এ কাজে লাগে); অসুবিধা — একই ডেটা দুই জায়গায়, কার্যকর ধারণক্ষমতা কমে। Exclusive নীতিতে একটি block একসময়ে একটিই স্তরে; L1 থেকে বাদ পড়া block নিচে নামে (victim cache ধরনের), ধারণক্ষমতা পুরোটা কাজে লাগে, কিন্তু ব্যবস্থাপনা জটিল। মাঝামাঝি non-inclusive নীতিতে কোনো বাধ্যবাধকতা নেই। বিভিন্ন নির্মাতা আর প্রজন্ম ভিন্ন পথ নিয়েছে — যেমন Intel-এর অনেক পুরোনো desktop chip-এর L3 inclusive ছিল বলে জানা যায়, আর পরের কিছু server chip ও AMD-এর Zen-এর L3 non-inclusive বা victim-ধরনের; নির্দিষ্ট chip-এর জন্য নির্মাতার optimization manual দেখাই নিরাপদ।

৭ · নিজের চোখে দেখা — runner-এর cache আর staircase

এই কোর্সের C আর bash কোড চলে একটি shared virtual machine-এ। তার cache কত বড়? নিচের script শুধু cache-এর স্তর, ধরন, মাপ, line-এর মাপ আর associativity ছাপে — Linux-এর /sys থেকে আর getconf থেকে। CPU-র নাম, নির্মাতা বা যন্ত্রের পরিচয় ছাপা হয় না (মানগুলো থেকে অক্ষর-সংখ্যা ছাড়া সব বাদ দেওয়া হয়)।

# Cache levels, sizes and line size ONLY -- no CPU name, vendor, flags or host info
d=/sys/devices/system/cpu/cpu0/cache
if [ -d "$d" ]; then
    printf '%-7s %-12s %-6s %-9s %-5s\n' level type size line_B ways
    for i in "$d"/index*; do
        [ -r "$i/level" ] || continue
        v() { tr -cd 'A-Za-z0-9' < "$i/$1" 2>/dev/null || echo '?'; }
        printf 'L%-6s %-12s %-6s %-9s %-5s\n' "$(v level)" "$(v type)" "$(v size)" \
            "$(v coherency_line_size)" "$(v ways_of_associativity)"
    done
else
    echo "sysfs cache তথ্য নেই"
fi
echo "--- getconf ---"
if command -v getconf >/dev/null; then
    getconf -a 2>/dev/null | grep -E '^LEVEL[1-4]_(I|D)?CACHE_(SIZE|LINESIZE|ASSOC) ' \
        | awk '{ printf "%-24s %s\n", $1, ($2 == "" ? "?" : $2) }'
else
    echo "getconf নেই"
fi

যাচাইয়ের সময় আউটপুট:

level   type         size   line_B    ways 
L1      Data         32K    64        8    
L1      Instruction  32K    64        8    
L2      Unified      512K   64        8    
L3      Unified      16384K 64        16   
--- getconf ---
LEVEL1_ICACHE_SIZE       32768
LEVEL1_ICACHE_ASSOC      ?
LEVEL1_ICACHE_LINESIZE   64
LEVEL1_DCACHE_SIZE       32768
LEVEL1_DCACHE_ASSOC      8
LEVEL1_DCACHE_LINESIZE   64
LEVEL2_CACHE_SIZE        524288
LEVEL2_CACHE_ASSOC       8
LEVEL2_CACHE_LINESIZE    64
LEVEL3_CACHE_SIZE        16777216
LEVEL3_CACHE_ASSOC       16
LEVEL3_CACHE_LINESIZE    64
LEVEL4_CACHE_SIZE        0
LEVEL4_CACHE_ASSOC       ?
LEVEL4_CACHE_LINESIZE    ?

এই যন্ত্রে প্রতি core-এ L1 data ৩২ KiB আর L1 instruction ৩২ KiB (আলাদা — পর্ব ৩৬-এ দেখা split cache), L2 ৫১২ KiB, আর L3 ১৬ MiB (১৬৩৮৪K), যা সাধারণত কয়েকটি core-এর ভাগে। প্রতিটি স্তরে line ৬৪ byte — x86-64-এর প্রচলিত মাপ। ways হলো associativity, পর্ব ৪৬-এর বিষয়। getconf-এর সংখ্যা byte-এ, আর sysfs-এর সাথে মেলে; যেখানে ? দেখাচ্ছে, সেখানে এই সিস্টেম মান জানায়নি, আর L4 নেই (০). এটি একটি cloud সার্ভারের x86-64 CPU-র vCPU-র চোখে দেখা ছবি; আপনার নিজের যন্ত্রে একই script ভিন্ন সংখ্যা দেখাবে।

এবার মাপা যাক, array-র মাপ বাড়লে একটি access-এর সময় কীভাবে বদলায়। নিচের প্রোগ্রাম ১৬ KiB থেকে ৬৪ MiB পর্যন্ত array-তে একটি pointer chase চালায়: প্রতিটি ৬৪ B line-এ পরের line-এর ঠিকানা লেখা, তাই প্রতিটি load আগেরটির ফলের ওপর নির্ভরশীল — CPU একসাথে অনেকগুলো load পাঠিয়ে latency লুকাতে পারে না, আর আমরা সত্যিকারের latency দেখি। তিনটি ক্রম: seq — line-এর পর line; stride4K — প্রতিবার ৪ KiB লাফ (প্রতি access নতুন page); random — এলোমেলো একটি চক্র। Wandbox-এ প্রোগ্রাম -O0-তে compile হয়, তাই প্রতিটি পাকে loop-এর নিজের কয়েক ns খরচ যোগ হয়। সময় প্রতি run-এ আর যন্ত্রভেদে বদলায়।

// Pointer chase over arrays of growing size: ns per access (varies per run and machine)
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>
#define LINE 16                       /* uint32_t per 64-byte line */
static double now(void) { struct timespec t; timespec_get(&t, TIME_UTC); return t.tv_sec * 1e9 + t.tv_nsec; }
static uint64_t rng = 88172645463325252u;
static uint32_t rnd(uint32_t n) { rng ^= rng << 13; rng ^= rng >> 7; rng ^= rng << 17; return (uint32_t)(rng % n); }
static double chase(uint32_t *a, uint32_t *ord, uint32_t n, long steps) {
    for (uint32_t k = 0; k < n; k++) a[ord[k] * LINE] = ord[(k + 1) % n] * LINE;   /* build the cycle */
    uint32_t p = 0;
    for (uint32_t k = 0; k < n; k++) p = a[p];                                    /* warm-up lap */
    double t0 = now();
    for (long s = 0; s < steps; s++) p = a[p];               /* each load needs the previous one */
    double t = now() - t0;
    return p == 1 ? -1 : t / steps;                          /* use p so it is not dropped */
}
int main(void) {
    uint32_t maxn = (64u << 20) / 64;                        /* lines in 64 MiB */
    uint32_t *a = malloc((size_t)maxn * 64), *ord = malloc(maxn * sizeof *ord);
    if (!a || !ord) { puts("out of memory"); return 1; }
    printf("%9s %8s %8s %8s   (ns per access)\n", "size", "seq", "stride4K", "random");
    for (uint32_t kib = 16; kib <= 65536; kib *= 4) {
        uint32_t n = kib * 1024 / 64, cols = n / 64; double r[3];
        for (int pat = 0; pat < 3; pat++) {
            for (uint32_t k = 0; k < n; k++)                 /* order in which lines are visited */
                ord[k] = pat == 0 ? k : pat == 1 ? (k % cols) * 64 + k / cols : k;
            if (pat == 2) for (uint32_t k = n - 1; k > 0; k--) { uint32_t j = rnd(k), t = ord[k]; ord[k] = ord[j]; ord[j] = t; }
            r[pat] = chase(a, ord, n, 3000000);
        }
        printf("%6u KiB %8.2f %8.2f %8.2f\n", kib, r[0], r[1], r[2]);
    }
    free(a); free(ord); return 0;
}

একটি run-এর আসল আউটপুট (আপনি চালালে সংখ্যা কিছুটা আলাদা আসবে):

     size      seq stride4K   random   (ns per access)
    16 KiB     3.11     3.14     3.17
    64 KiB     3.46     6.25     6.08
   256 KiB     3.23     7.73     6.62
  1024 KiB     3.17     9.96    21.14
  4096 KiB     3.07    17.18    34.96
 16384 KiB     5.21    41.22   183.09
 65536 KiB     6.13    42.65   204.16

random কলামটি পড়ুন ওপর থেকে নিচে: ১৬ KiB-এ ৩.১৭ ns, ৬৪–২৫৬ KiB-এ ৬.০৮–৬.৬২ ns, ১ MiB-এ ২১.১৪ ns, ৪ MiB-এ ৩৪.৯৬ ns, আর ১৬–৬৪ MiB-এ ১৮৩.০৯–২০৪.১৬ ns. Array যতক্ষণ কোনো cache স্তরে আঁটে, সময় প্রায় সমান; আঁটা বন্ধ হলেই লাফ। পুরো ছবিটি নিচের stepper-এ ধাপে ধাপে দেখুন। seq কলাম প্রায় সমতল, কারণ hardware prefetcher পরপর line চাওয়ার ধারা চিনে ফেলে আর CPU চাওয়ার আগেই পরের line নিয়ে আসে — spatial ধারা থাকলে hardware ভবিষ্যৎ আন্দাজ করতে পারে। stride4K মাঝামাঝি: ধারা নিয়মিত, কিন্তু প্রতি access নতুন ৪ KiB page-এ, আর অনেক prefetcher page-এর সীমা পেরোয় না বলে জানা যায়; তার ওপর প্রতিটি নতুন page-এর ঠিকানা-অনুবাদ লাগে (TLB, পর্ব ৫০).

random pointer chaseএই run-এর সংখ্যা

            
Runner: L1d 32 KiB · L2 512 KiB · L3 16 MiB · line 64 B
array
কোথায় আঁটে
প্রতি access
১৬ KiB-এর তুলনায়
  1. ১৬ KiB — মেঝে। পুরো array L1 data cache-এ (৩২ KiB) আঁটে, তাই প্রায় প্রতিটি load L1 hit: random-এ ৩.১৭ ns. এর বড় অংশ আসলে -O0 loop-এর নিজের খরচ (counter বাড়ানো, তুলনা, stack-এ লেখা); L1-এর latency নিজে কয়েক cycle, ~১ ns-এর মাত্রায়। এই সংখ্যাকেই মেঝে ধরে বাকিগুলো তুলনা করব।
  2. ৬৪ KiB — প্রথম ধাপ। Array এখন L1-এর দ্বিগুণ, তাই বেশিরভাগ access L1-এ miss, কিন্তু L2-তে (৫১২ KiB) hit. সময় ৬.০৮ ns — মেঝের প্রায় ১.৯ গুণ। একটি স্তর নিচে নামলেই দাম বাড়ে।
  3. ২৫৬ KiB — এখনো L2. Working set L2-এর অর্ধেক; সময় ৬.৬২ ns. বাস্তব ধাপের কিনারা সব সময় খাড়া নয়: cache-এর associativity, replacement আর ঠিকানা-অনুবাদের (TLB) খরচ মিলে সংখ্যা ধীরে ধীরে বাড়তে পারে।
  4. ১ MiB — দ্বিতীয় ধাপ। L2 ছাড়িয়ে গেল, কিন্তু L3-তে (১৬ MiB) আঁটে: ২১.১৪ ns. L3 সব core-এর ভাগে, বড় আর দূরে — তাই L2-এর চেয়ে কয়েক গুণ ধীর, তবু DRAM-এর চেয়ে অনেক দ্রুত।
  5. ৪ MiB — কাগজের মাপ বনাম বাস্তব। এখনো L3-এর কাগুজে মাপের (১৬ MiB) অনেক নিচে; সময় ৩৪.৯৬ ns — ১ MiB-এর চেয়ে বেশি, কিন্তু DRAM-এর অনেক নিচে। L3 কয়েকটি core-এর ভাগে, আর shared virtual machine-এ অন্যদের ডেটাও সেখানে জায়গা নেয়, তাই আমাদের ভাগে পুরো ১৬ MiB থাকে না; সাথে ১০২৪টি page-এর ঠিকানা-অনুবাদে TLB-র চাপও বাড়ে। এই মাপের সংখ্যাই run-ভেদে সবচেয়ে বেশি ওঠানামা করে — যাচাইয়ের অন্য কয়েকটি run-এ এখানে ১০০ ns-এর কাছাকাছি বা বেশিও এসেছে।
  6. ১৬ আর ৬৪ MiB — পুরোপুরি DRAM. ১৮৩.০৯ আর ২০৪.১৬ ns — ৬৪ MiB-এ মেঝের প্রায় ৬৪ গুণ। ৩ GHz-এ এটি কয়েকশো cycle: অংশ ২-এর টেবিলের “৫ মিনিট”. Memory wall এখন আর বইয়ের ছবি নয়, নিজের মাপা সংখ্যা।
  7. একই ৬৪ MiB, তিন ক্রম। seq মাত্র ৬.১৩ ns, stride4K ৪২.৬৫ ns, random ২০৪.১৬ ns. তিনটিতেই প্রতিটি load একটি নতুন line ছোঁয় আর আগের load-এর ফলের অপেক্ষা করে — তবু seq এত দ্রুত, কারণ prefetcher ধারাটি চিনে line-গুলো আগেই এনে রাখে। শিক্ষা: সময় নির্ভর করে working set-এর মাপ আর access-এর ধারা — দুটোর ওপরই।

৮ · আধুনিক মোড় — scratchpad, HBM, CXL আর persistent memory

ক্লাসিক পিরামিড এখনো সত্য, কিন্তু কয়েকটি নতুন স্তর আর নতুন ধরনের ব্যবস্থাপনা যোগ হয়েছে:

  • Scratchpad memory: chip-এর ভেতরের SRAM, কিন্তু cache নয় — hardware নিজে কিছু রাখে না বা বাদ দেয় না; প্রোগ্রাম (বা compiler) স্পষ্টভাবে ডেটা কপি করে আনে-নেয়। সুবিধা: সময় সম্পূর্ণ অনুমানযোগ্য (hit-miss-এর অনিশ্চয়তা নেই), tag রাখার জায়গা ও শক্তি লাগে না। তাই অনেক microcontroller, DSP আর real-time সিস্টেমে (পর্ব ৫৬) এটি প্রচলিত। GPU-তেও একই ভাবনা: NVIDIA-র CUDA-তে “shared memory” হলো প্রতি multiprocessor-এ প্রোগ্রামার-নিয়ন্ত্রিত দ্রুত মেমরি (পর্ব ৫৫).
  • HBM (High Bandwidth Memory): কয়েকটি DRAM die একটির ওপর আরেকটি সাজিয়ে (stacked) processor-এর ঠিক পাশে, একটি silicon interposer-এ বসানো, হাজারের বেশি bit চওড়া সংযোগে। Latency খুব বেশি কমে না, কিন্তু bandwidth বিশাল — তাই আধুনিক GPU আর AI accelerator-এ প্রচলিত।
  • CXL (Compute Express Link): PCIe-এর ভৌত সংযোগের ওপর চলা একটি মান (পর্ব ৫১), যার মাধ্যমে CPU বাইরের একটি card বা বাক্সের মেমরিকে প্রায় নিজের মেমরির মতো load/store দিয়ে ব্যবহার করতে পারে। এটি DRAM-এর নিচে একটি নতুন স্তর তৈরি করে — স্থানীয় DRAM-এর চেয়ে ধীর (অতিরিক্ত latency, নকশাভেদে মোটামুটি কয়েকশো ns-এর মাত্রায় বলে উল্লেখ পাওয়া যায়), কিন্তু অনেক বড় আর কয়েকটি server ভাগ করে নিতে পারে। প্রযুক্তিটি তুলনামূলক নতুন, ব্যবহার এখনো ছড়াচ্ছে।
  • Persistent memory: এমন মেমরি যা DRAM-এর মতো byte ধরে load/store করা যায়, কিন্তু বিদ্যুৎ গেলেও ডেটা থাকে। সবচেয়ে পরিচিত উদাহরণ Intel Optane persistent memory (3D XPoint প্রযুক্তি), যা DRAM-এর slot-এ বসত; latency DRAM-এর চেয়ে বেশি, flash-এর চেয়ে অনেক কম। ২০২২ সালে Intel Optane ব্যবসা গুটিয়ে নেওয়ার ঘোষণা দেয়। ধারণাটি অবশ্য বেঁচে আছে — CXL-এর ওপর বা নতুন ধরনের মেমরি-প্রযুক্তিতে।

আরও গভীরে: আধুনিক chip-এ “L1 থেকে DRAM” ছাড়াও আরও অনেক ছোট cache আছে, যাদের কাজ একই locality-র ওপর দাঁড়িয়ে: TLB (ঠিকানা-অনুবাদের cache), micro-op cache, branch target buffer, page walk-এর cache. বড় server chip-এ L3 একটি অখণ্ড জিনিস নয়, বরং core-গুলোর পাশে ছড়ানো slice, একটি on-chip network দিয়ে যুক্ত — তাই L3-এর latency নির্ভর করে ডেটা কোন slice-এ আছে তার ওপর (NUCA, non-uniform cache access). গবেষণায় processing-in-memory — হিসাবকেই ডেটার কাছে নিয়ে যাওয়া — memory wall-এর আরেকটি উত্তর হিসেবে ব্যাপকভাবে পরীক্ষিত হচ্ছে।

শেষে একটি সতর্কতা। Cache সাধারণত “অদৃশ্য” — প্রোগ্রাম ঠিক থাকে, শুধু গতি বদলায়। কিন্তু সময়ের পার্থক্য দেখা যায়, আর ঠিক এই পার্থক্যই side channel হতে পারে: একটি প্রোগ্রাম মেপে বুঝতে পারে অন্যটি কোন ডেটা ছুঁয়েছে। প্রতিরক্ষার কথা পর্ব ৪১-এ (Spectre, Meltdown) দেখেছেন।

৯ · AI-এর যুগে — মেমরি হায়ারার্কির প্রশ্নে AI, আর যাচাইয়ের অভ্যাস

মেমরি নিয়ে AI-এর উত্তর প্রায়ই আত্মবিশ্বাসী আর সংখ্যায় ভরা। সমস্যা হলো, সেই সংখ্যাগুলো প্রায়ই নির্দিষ্ট কোনো যন্ত্রের নয়, আর কখনো একেবারে বানানো:

AI-এর সাধারণ ভুলকীভাবে ধরবেন
“L1 latency ঠিক ১ ns, DRAM ঠিক ১০০ ns” — যেন সব যন্ত্রে একমাত্রা হিসেবে নিন; নিজের যন্ত্রে staircase মেপে দেখুন, নির্মাতার optimization manual দেখুন
Local আর global miss rate গুলিয়ে ফেলা; AMAT-এ miss penalty-তে hit time দুবার যোগসংজ্ঞা লিখে নিন, তারপর ভগ্নাংশ × সময়ের যোগফল আর nested সূত্র — দুই পথে হিসাব মিলিয়ে দেখুন
“Sequential access-এ temporal locality” — temporal আর spatial উল্টো বলাপ্রশ্ন করুন: একই ঠিকানা আবার, নাকি পাশের ঠিকানা? reuse distance বের করুন
নির্দিষ্ট chip-এর cache-মাপ, inclusion নীতি বা সাল বানিয়ে বলানিজের যন্ত্রে /sys/devices/system/cpu/cpu0/cache বা lscpu; নির্মাতার datasheet
Benchmark কোড যেখানে compiler পুরো loop মুছে দেয়, বা prefetcher-এর কারণে সমতল ফল দেখে “cache নেই” সিদ্ধান্তPointer chase-এর মতো নির্ভরশীল load ব্যবহার করুন, ফল ব্যবহার করুন, কয়েকবার চালান

AI-কে হায়ারার্কির হিসাব করতে দিলে অনুমানগুলো স্পষ্ট লিখে দিন আর যাচাইযোগ্য টেবিল চান:

আমি একটি মেমরি হায়ারার্কির প্রত্যাশিত access সময় হিসাব করছি। সব সংখ্যা cycle-এ। অনুমান: L1 hit time 4, local hit rate 95%. L2 hit time 12, local hit rate 60%. L3 hit time 40, local hit rate 50%. DRAM-এ বাড়তি 200, সব access এখানে মেটে। স্তরগুলো একের পর এক দেখা হয়। ১. প্রতিটি স্তরে মোট access-এর কত ভগ্নাংশ মেটে আর তখন মোট সময় কত — একটি টেবিলে দাও। ২. প্রত্যাশিত সময় দুইভাবে হিসাব করো: (ক) ভগ্নাংশ × সময়ের যোগফল, (খ) nested AMAT = HT1 + MR1 × (HT2 + MR2 × (HT3 + MR3 × MP)). দুটো না মিললে কোথায় ভুল, খুঁজে বলো। ৩. L1 hit rate 98% হলে নতুন মান কত? ৪. local আর global miss rate-এর পার্থক্য এক লাইনে বলো। কোনো বাস্তব chip-এর latency উল্লেখ করলে উৎস দাও, নিশ্চিত না হলে "নিশ্চিত নই" লেখো।

AI-এর দেওয়া মেমরি-ব্যাখ্যা হাতে পেলে — চেকলিস্ট

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

  • Temporal আর spatial locality-র পার্থক্য, উদাহরণসহ? — একই ঠিকানা শিগগির আবার (loop counter, sum) বনাম পাশের ঠিকানা শিগগির (array-র পরপর উপাদান, পরপর instruction).
  • Memory hierarchy কেন দরকার? — একই সাথে বড়, দ্রুত আর সস্তা মেমরি হয় না; locality থাকায় ছোট দ্রুত স্তরেই বেশিরভাগ access মেটে, গড় গতি প্রায় ওপরের মতো, দাম প্রায় নিচের মতো।
  • Hit time ২ cycle, miss rate ৪%, miss penalty ১০০ cycle হলে AMAT? — ২ + ০.০৪ × ১০০ = ৬ cycle.
  • Cache আর main memory-র মাঝে, আর main memory ও disk-এর মাঝে ডেটা কোন এককে যায়, কে সামলায়? — block (সাধারণত ৬৪ B), hardware; page (সাধারণত ৪ KiB), OS.
  • Array বড় হলে random access-এর সময় সিঁড়ির মতো বাড়ে কেন? — Working set একেকটি cache স্তর ছাড়িয়ে গেলে access-গুলো পরের ধীর স্তরে চলে যায়।

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

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

প্র ০১Wilkes-এর “slave memory” থেকে 80486-এর on-chip cache — cache-এর ধারণা কেন এত আগে এসেও microprocessor-এ এত দেরিতে এল?

১৯৬৫ সালে Maurice Wilkes যখন একটি ছোট দ্রুত “slave” মেমরির কথা লেখেন, তখন বড় কম্পিউটারের মূল মেমরি ছিল magnetic core — ছোট ছোট চৌম্বক আংটি, যাদের পড়া-লেখা microsecond-এর মাপে, আর processor-এর logic তার চেয়ে দ্রুত হয়ে উঠছিল। ফারাক ছিল, আর দামি mainframe-এ সেই ফারাক মেটানোর জন্য বাড়তি hardware-এর খরচ মেনে নেওয়া যেত। তাই ১৯৬৮ সালে IBM System/360 Model 85-এ cache আসে, আর পরের দশকে বড় কম্পিউটারে তা প্রায় স্বাভাবিক হয়ে যায়।

১৯৭০-এর দশকের microprocessor-এর গল্প আলাদা। Intel 8080 বা 8086-এর মতো chip-এর clock ছিল কয়েক MHz, আর তখনকার DRAM সেই গতির সাথে মোটামুটি তাল রাখতে পারত — একটি bus cycle-এ মেমরি সাড়া দিত, বড়জোর একটি-দুটি wait state লাগত। এক chip-এ মাত্র কয়েক হাজার থেকে কয়েক দশ হাজার transistor; তার মধ্যে cache রাখার জায়গাই ছিল না, আর দরকারও কম ছিল। ১৯৮০-এর দশকে দুই রেখা আলাদা হয়ে যায়। ১৯৮৯ সালের 80486 দশ লাখের বেশি (প্রায় ১২ লাখ) transistor পেয়ে প্রথমবার ৮ KB cache chip-এর ভেতরেই রাখে — কারণ ততদিনে বাইরের মেমরির অপেক্ষা এত দামি হয়ে গেছে যে transistor-এর একটি অংশ cache-কে দেওয়াই সবচেয়ে লাভজনক।

এর পরের ইতিহাস সেই একই হিসাবের পুনরাবৃত্তি: transistor সস্তা হয়েছে, মেমরির latency প্রায় একই থেকেছে, তাই প্রতিটি প্রজন্মে cache বড় হয়েছে আর স্তর বেড়েছে — L2 chip-এর ভেতরে এসেছে, তারপর L3, আর আজ কিছু chip-এ বাড়তি stacked L3. অর্থাৎ cache কোনো একটি উজ্জ্বল উদ্ভাবনের ফল নয়; এটি দুই প্রযুক্তির অসম অগ্রগতির স্বাভাবিক অর্থনৈতিক উত্তর।

মূল উপলব্ধি: নকশার সিদ্ধান্ত নির্ভর করে প্রযুক্তিগুলোর আপেক্ষিক গতি আর দামের ওপর; যখন একটি অংশ আরেকটির চেয়ে অনেক দ্রুত এগোয়, তখনই হায়ারার্কির নতুন স্তর জন্মায়।

প্র ০২পুরো main memory-কেই SRAM দিয়ে বানালে কী হতো? তাহলে তো আর cache miss-এর ঝামেলা থাকত না।

প্রথম বাধা জায়গা। SRAM-এর একটি bit-এ সাধারণত ছয়টি transistor, DRAM-এ একটি transistor আর একটি capacitor. প্রক্রিয়া-প্রযুক্তিভেদে সংখ্যা বদলায়, তবে একই মাপের silicon-এ DRAM কয়েক গুণ বেশি bit রাখে, আর DRAM-এর জন্য আলাদা বিশেষ উৎপাদন-প্রক্রিয়া আছে যা ঘনত্বের জন্যই সাজানো। ১৬ GiB SRAM বানাতে যে silicon লাগত, তার দাম একটি সাধারণ কম্পিউটারের দামের বহু গুণ হতো।

দ্বিতীয় বাধা শক্তি। SRAM cell-এ সব সময় কিছু leakage current বয়ে যায়; কোটি কোটি cell-এ সেটা উল্লেখযোগ্য। তৃতীয় আর সবচেয়ে সূক্ষ্ম বাধা হলো দূরত্ব। SRAM দ্রুত কারণ সেটি ছোট আর processor-এর পাশে। ১৬ GiB SRAM যদি বানানোও যেত, তা chip-এর বাইরে বা বিশাল এক chip-এ ছড়িয়ে থাকত, আর সংকেতকে লম্বা wire পেরোতে হতো, বিশাল decoder পেরোতে হতো। পর্ব ১৮-এর wire delay তখন SRAM cell-এর গতির সুবিধা খেয়ে ফেলত। অর্থাৎ “বড় SRAM” আর “দ্রুত SRAM” আসলে পরস্পরবিরোধী — L3 যে L1-এর চেয়ে ধীর, তার মূল কারণ মাপ আর দূরত্ব, প্রযুক্তি একই হওয়া সত্ত্বেও।

বাস্তবে উল্টো দিকের পরীক্ষাও হয়েছে: কিছু chip অনেক বেশি SRAM পাশে রাখে। AMD-এর 3D V-Cache (প্রথম desktop পণ্য ২০২২ সালের দিকে) core-এর ওপর আলাদা SRAM die বসিয়ে L3 কয়েক গুণ বাড়ায়; কিছু AI chip-এ বিশাল on-chip SRAM থাকে, যাতে মডেলের অংশ DRAM-এ না যায়। এসব নকশা নির্দিষ্ট কাজে চমৎকার, কিন্তু প্রতি GB-এর দাম তবু DRAM-এর ধারেকাছে নয়।

মূল উপলব্ধি: দ্রুততা আসে ছোট মাপ আর নৈকট্য থেকে, শুধু cell-এর প্রযুক্তি থেকে নয়; তাই যেকোনো প্রযুক্তিতেই বড় মেমরি ধীর হয়, আর হায়ারার্কি অনিবার্য।

প্র ০৩Locality কি প্রকৃতির নিয়ম, নাকি শুধু প্রোগ্রামারদের অভ্যাস? যেসব কাজে locality নেই, সেখানে আর্কিটেকচার কী করে?

Locality কোনো গাণিতিক নিশ্চয়তা নয়; এটি মানুষের লেখা প্রোগ্রামের একটি অভিজ্ঞতালব্ধ বৈশিষ্ট্য। আমরা কাজকে loop-এ ভাগ করি, সম্পর্কিত ডেটা একসাথে রাখি, function বারবার ডাকি — তাই locality স্বাভাবিকভাবে আসে। ১৯৬০-এর দশকের শেষে virtual memory নিয়ে কাজ করতে গিয়ে Peter Denning দেখান যে প্রোগ্রাম একেকটি পর্যায়ে মেমরির একটি ছোট অংশেই ঘোরাফেরা করে, আর তা থেকেই working set-এর ধারণা।

কিন্তু ব্যতিক্রমও বাস্তব। বিশাল graph-এ (যেমন সামাজিক নেটওয়ার্কের বন্ধুত্বের জাল) একটি node থেকে পরের node মেমরির যেকোনো জায়গায়। বড় hash table-এ key-র hash ইচ্ছা করেই এলোমেলো। Linked structure আর sparse matrix-এও working set বিশাল, reuse distance বড়। অংশ ৫-এর “random” trace-এ দেখেছেন, এমন কাজে cache-এর আকারের অনুপাতের বেশি কিছু পাওয়া যায় না।

আর্কিটেকচার তখন অন্য অস্ত্র নেয়। এক, latency লুকানো: পর্ব ৪৩-এর multithreading, বিশেষত Tera MTA-র মতো নকশা যেখানে cache প্রায় ছিলই না — অনেক thread-এর মধ্যে পালা করে, একটির অপেক্ষার সময় অন্যটির কাজ। GPU একই পথ নেয় হাজার হাজার thread দিয়ে (পর্ব ৫৫). দুই, একসাথে অনেক miss চালানো (memory-level parallelism): out-of-order core একসাথে কয়েক ডজন miss চলমান রাখতে পারে। তিন, bandwidth বাড়ানো — HBM. চার, software-এর দিক থেকে locality তৈরি করা: ডেটা নতুন করে সাজানো, graph-এর node-গুলোর নম্বর বদলানো, blocking (পর্ব ৪৮). অনেক সময় এটাই সবচেয়ে বড় লাভ দেয়।

মূল উপলব্ধি: Locality প্রোগ্রামের ধর্ম, আর তাই প্রোগ্রামার তা বাড়াতে বা নষ্ট করতে পারেন; যেখানে locality সত্যিই নেই, সেখানে hardware cache-এর বদলে সমান্তরালতা দিয়ে latency লুকায়।

প্র ০৪Cache hardware নিজে সামলায়, scratchpad সামলায় software — কোনটি ভালো? PlayStation 3-এর Cell processor থেকে কী শেখা যায়?

Hardware-চালিত cache-এর সবচেয়ে বড় গুণ স্বচ্ছতা: প্রোগ্রাম কিছু না জেনেই দ্রুত চলে। দাম হলো tag রাখার জায়গা, প্রতিটি access-এ tag মেলানোর শক্তি, আর অনিশ্চয়তা — একই কোড কখনো ৪ cycle, কখনো ২০০ cycle নেয়, যা real-time সিস্টেমে (গাড়ির ব্রেক, পেসমেকার) সবচেয়ে খারাপ সময়ের হিসাবকে কঠিন করে।

Scratchpad উল্টো: প্রোগ্রাম স্পষ্টভাবে বলে কী কখন আসবে, সাধারণত DMA দিয়ে (পর্ব ৫২). সময় পুরো অনুমানযোগ্য, শক্তি কম, জায়গা বেশি কাজে লাগে। ২০০৬ সালের দিকে PlayStation 3-এ ব্যবহৃত Cell Broadband Engine (Sony, Toshiba আর IBM-এর যৌথ নকশা) এর বিখ্যাত উদাহরণ: তার আটটি SPE core-এর প্রতিটির ছিল ২৫৬ KB local store, আর কোনো data cache ছিল না। ঠিকঠাক লেখা কোডে Cell অসাধারণ গতি দিত; কিন্তু প্রোগ্রামারদের DMA-র double-buffering, ডেটা টুকরো করা, alignment — সব হাতে সামলাতে হতো। পরের প্রজন্মের console-গুলো প্রচলিত cache-ওয়ালা x86 core-এ ফিরে যায়।

আজকের উত্তর হলো দুটোর মিশ্রণ। GPU-তে hardware cache আছে, আবার প্রোগ্রামার-নিয়ন্ত্রিত shared memory-ও আছে; যে অংশে প্রোগ্রামার ডেটার ধরন জানেন (যেমন matrix গুণের tile), সেখানে scratchpad, বাকিটা cache.

মূল উপলব্ধি: স্বচ্ছতা আর অনুমানযোগ্যতার মধ্যে একটি trade-off আছে; সাধারণ কাজের জন্য hardware cache জেতে, আর যেখানে ডেটার ধরন জানা বা সময়ের নিশ্চয়তা দরকার, সেখানে software-চালিত মেমরি টিকে থাকে।

প্র ০৫Gray-এর “পাঁচ মিনিটের নিয়ম” থেকে Optane আর CXL — কোন ডেটা মেমরিতে রাখবেন আর কোনটা disk-এ, এই সিদ্ধান্ত কি বদলে যাচ্ছে?

১৯৮৭ সালে Jim Gray আর Franco Putzolu একটি সরল কিন্তু প্রভাবশালী প্রশ্ন তোলেন: একটি ডেটার টুকরো (তখন একটি disk page) মেমরিতে রেখে দেওয়া সস্তা, নাকি প্রতিবার disk থেকে পড়া? মেমরির দাম প্রতি byte, আর disk-এর খরচ প্রতি access ধরে। দুটো মিলিয়ে তাঁরা পান: যে page মোটামুটি প্রতি পাঁচ মিনিটে অন্তত একবার লাগে, তা মেমরিতে রাখাই সস্তা। এই “পাঁচ মিনিটের নিয়ম” পরে কয়েকবার নতুন দামে আবার হিসাব করা হয়েছে, আর প্রযুক্তি বদলালে সময়টাও বদলেছে — নিয়মের সংখ্যা নয়, পদ্ধতিটাই আসল শিক্ষা: কোন স্তরে কী রাখবেন, তা দাম আর access-এর ঘনত্বের অর্থনীতি।

Flash SSD এসে এই হিসাবে একটি নতুন স্তর ঢোকায়: disk-এর চেয়ে প্রতি access বহু গুণ সস্তা, DRAM-এর চেয়ে প্রতি byte সস্তা। তারপর আসে persistent memory-র প্রতিশ্রুতি। Intel আর Micron ২০১৫ সালে 3D XPoint ঘোষণা করে, আর Intel পরে Optane নামে SSD আর DIMM-আকারের persistent memory বিক্রি করে। এটি byte ধরে ব্যবহার করা যেত, বিদ্যুৎ গেলেও ডেটা থাকত — অর্থাৎ “মেমরি” আর “storage”-এর রেখা মুছে যাওয়ার কথা। কিন্তু software-কে নতুন করে ভাবতে হতো (কোন store কখন সত্যিই স্থায়ী হলো, cache থেকে কখন flush), দাম DRAM আর flash-এর মাঝামাঝি আটকে থাকল, আর ২০২২ সালে Intel ব্যবসাটি গুটিয়ে নেওয়ার ঘোষণা দেয়।

এখন আলোচনার কেন্দ্রে CXL: স্থায়িত্ব নয়, বরং DRAM-কে CPU-র socket-এর বাইরে নিয়ে যাওয়া, কয়েকটি server-এর মধ্যে মেমরি ভাগ করা (memory pooling), আর পুরোনো বা সস্তা DRAM-কে একটি ধীর কিন্তু বড় স্তর হিসেবে ব্যবহার। এতে OS-কে আবার সিদ্ধান্ত নিতে হয় কোন page দ্রুত স্থানীয় DRAM-এ আর কোনটা ধীর CXL-মেমরিতে — Linux-এ এ ধরনের “memory tiering” নিয়ে সক্রিয় কাজ চলছে বলে জানা যায়।

মূল উপলব্ধি: স্তরের নাম বদলায়, কিন্তু প্রশ্ন একই থাকে — প্রতি byte-এর দাম আর প্রতি access-এর দামের মধ্যে ভারসাম্য; নতুন প্রযুক্তি হায়ারার্কি ভাঙে না, নতুন স্তর যোগ করে।

অনুশীলন

পরীক্ষার ধাঁচে: AMAT ও প্রত্যাশিত সময়, কোডে locality চেনা, মানব-মাপের latency আর আউটপুট অনুমান। আগে নিজে চেষ্টা করুন।

  1. AMAT-এর হিসাব: একটি CPU-র L1 cache-এর hit time ১ cycle, miss rate ৪%, আর L1 miss হলে main memory থেকে আনতে বাড়তি ১২০ cycle. (ক) AMAT কত? (খ) এবার L1 আর মেমরির মাঝে একটি L2 বসানো হলো: hit time ১০ cycle, local miss rate ২৫%, L2 miss হলে মেমরি থেকে বাড়তি ১২০ cycle. নতুন AMAT কত, আর কত গুণ দ্রুত? (গ) L2-এর global miss rate কত?

    (ক) AMAT = ১ + ০.০৪ × ১২০ = ১ + ৪.৮ = ৫.৮ cycle.

    (খ) L1-এর miss penalty এখন L2-এর গড় সময়: ১০ + ০.২৫ × ১২০ = ১০ + ৩০ = ৪০ cycle. তাই AMAT = ১ + ০.০৪ × ৪০ = ১ + ১.৬ = ২.৬ cycle. লাভ ৫.৮ ÷ ২.৬ ≈ ২.২৩ গুণ. ভগ্নাংশ দিয়ে যাচাই: ৯৬% access ১ cycle-এ; ৪% × ৭৫% = ৩% access ১ + ১০ = ১১ cycle-এ; ৪% × ২৫% = ১% access ১ + ১০ + ১২০ = ১৩১ cycle-এ। গড় = ০.৯৬ × ১ + ০.০৩ × ১১ + ০.০১ × ১৩১ = ০.৯৬ + ০.৩৩ + ১.৩১ = ২.৬ — মিলে গেল।

    (গ) Global miss rate = ০.০৪ × ০.২৫ = ০.০১ = ১% — মোট access-এর ১% মেমরি পর্যন্ত যায়। Local miss rate ২৫% শুনতে খারাপ, কিন্তু L2-তে শুধু L1-এর “কঠিন” access-গুলোই আসে, তাই local সংখ্যা স্বাভাবিকভাবেই বড় দেখায়।

  2. কোডে locality চিনুন: নিচের loop-এ প্রতিটি মেমরি-ব্যবহার কোন ধরনের locality দেখায় (temporal, spatial, দুটোই, বা দুর্বল), লিখুন। price, qty আর score হলো n উপাদানের int array, hist হলো ১০১ উপাদানের int array (score ০ থেকে ১০০), আর next একটি linked list-এর pointer.
    for (i = 0; i < n; i++) {
        total += price[i] * qty[i];
        hist[score[i]]++;
    }
    for (p = head; p != NULL; p = p->next)
        count++;

    i, n, total, count: প্রতি পাকে একই জিনিস — temporal (সাধারণত register-এ). price[i], qty[i], score[i]: পরপর ঠিকানা — spatial; প্রতিটি উপাদান একবারই পড়া, তাই ডেটার temporal reuse নেই। ৬৪ B block-এ প্রতিটি array-তে প্রতি ১৬টিতে ১টি miss.

    hist[score[i]]: ঠিকানা ডেটার ওপর নির্ভর করে, তাই spatial ধারা অনিয়মিত। কিন্তু hist মাত্র ১০১ × ৪ = ৪০৪ B, অর্থাৎ ৭টি block — সব সময় L1-এ থাকে, তাই প্রায় সব access hit: শক্তিশালী temporal (ছোট working set). hist যদি কয়েক কোটি উপাদানের হতো, একই কোড খারাপ locality দেখাত।

    p->next: node-গুলো মেমরির কোথায় আছে তার ওপর নির্ভর। যদি এলোমেলো জায়গায় malloc করা হয়, প্রতিটি node সম্ভবত আলাদা block-এ — spatial দুর্বল, আর প্রতিটি load আগেরটির ফলের ওপর নির্ভরশীল বলে prefetcher-ও সাহায্য করতে পারে না (অংশ ৭-এর random pointer chase-এর মতো)। আর loop-এর instruction-গুলো দুটি loop-এই temporal আর spatial.

  3. মানব-মাপের latency: একটি CPU-র clock ৪ GHz. ধরুন DRAM access ৮০ ns, SSD-র একটি random read ২০ µs, আর HDD-র একটি random read ৪ ms. প্রতিটি কত cycle? এক cycle = এক সেকেন্ড ধরলে প্রতিটি মানুষের সময়ে কত?

    ৪ GHz-এ এক cycle = ১ ÷ (৪ × ১০৯) s = ০.২৫ ns. তাই cycle-সংখ্যা = latency ÷ ০.২৫ ns, আর “এক cycle = এক সেকেন্ড” হলে সেই সংখ্যাই সেকেন্ড।

    • DRAM: ৮০ ÷ ০.২৫ = ৩২০ cycle → ৩২০ সেকেন্ড = ৫ মিনিট ২০ সেকেন্ড.
    • SSD: ২০,০০০ ns ÷ ০.২৫ = ৮০,০০০ cycle → ৮০,০০০ ÷ ৩৬০০ ≈ ২২.২ ঘণ্টা, প্রায় এক দিন।
    • HDD: ৪০,০০,০০০ ns ÷ ০.২৫ = ১,৬০,০০,০০০ cycle → ১,৬০,০০,০০০ ÷ ৮৬,৪০০ ≈ ১৮৫.২ দিন, প্রায় ছয় মাস।

    লক্ষ করুন, clock ৩ থেকে ৪ GHz হলে একই DRAM-এর জন্য cycle আরও বাড়ে — দ্রুত CPU মানে মেমরির অপেক্ষা cycle-এর হিসাবে আরও দামি। Memory wall ঠিক এটাই।

  4. আউটপুট অনুমান: অংশ ৫-এর locality analyser-এ matrix-কে ১২৮ × ১২৮-এর বদলে ৬৪ × ৬৪ int করা হলো (মোট ৪০৯৬টি access, প্রতিটি ৪ B, block ৬৪ B), বাকি সব একই। “matrix rows” আর “matrix cols” দুই লাইনে histogram-এর কোন ঘরে কত সংখ্যা আসবে, আর ৮ / ৬৪ / ৫১২ block-এর LRU-তে hit% কত, অনুমান করুন।

    Matrix = ৬৪ × ৬৪ × ৪ B = ১৬ KiB = ২৫৬ block; প্রতি সারি ২৫৬ B = ৪ block. প্রতিটি block প্রথমবার cold: ২৫৬টি cold, বাকি ৪০৯৬ − ২৫৬ = ৩৮৪০টি reuse.

    rows: পরপর ঠিকানা, প্রতি block-এ ১৬টি access-এর প্রথমটি cold, বাকি ১৫টি দূরত্ব ০ → ঘর “0”-এ ৩৮৪০. সব মাপে hit = ৩৮৪০ ÷ ৪০৯৬ = ৯৩.৮ / ৯৩.৮ / ৯৩.৮.

    cols: একটি কলামের ৬৪টি access ৬৪টি আলাদা block-এ (প্রতি সারিতে একটি)। পরের কলামে একই ৬৪টি block, তাই প্রতিটি reuse-এর আগে বাকি ৬৩টি block ছোঁয়া হয়েছে: দূরত্ব ৬৩ → ঘর “8-63”-এ ৩৮৪০. ৮ block-এর cache-এ d < ৮ নয়, তাই ০.০%; ৬৪ block-এ ৬৩ < ৬৪, তাই সব reuse hit — ৯৩.৮%; ৫১২-তেও ৯৩.৮%. সিমুলেটর চালিয়ে মিলিয়ে দেখা আউটপুট:

    matrix rows        3840      0      0      0      0    256   93.8 / 93.8 / 93.8
    matrix cols           0      0   3840      0      0    256   0.0 / 93.8 / 93.8

    ১২৮ × ১২৮-এ কলামের দূরত্ব ছিল ১২৭, তাই ৬৪-block cache-এ ০%; ৬৪ × ৬৪-এ দূরত্ব ৬৩, তাই একই cache-এ ৯৩.৮%. Matrix ছোট হওয়ায় কলাম-ক্রমের working set (৬৪ block = ৪ KiB) cache-এ এঁটে গেছে।

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

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

ক্রমL1, L2 আর L3 তিনটিতেই miss করা একটি load-এর ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🔎 CPU ঠিকানা হিসাব করে L1-এ block খোঁজে — miss
  2. ⬇️ অনুরোধ L2, তারপর L3-তে যায় — দুই জায়গাতেই miss
  3. 🏭 Memory controller DRAM থেকে পুরো ৬৪ byte block আনে
  4. ⬆️ Block-এর কপি পথের cache-গুলোতে বসে, চাওয়া word register-এ যায়
  5. ⚡ একই block-এর পাশের ঠিকানায় পরের load L1-এই hit করে

একটি প্রোগ্রাম ৪ byte-এর int-এর একটি বড় array-র উপাদানগুলো a[0], a[1], a[2] … ক্রমে একবার করে পড়ে। Cache-এর ৬৪ byte block থেকে এটি মূলত কোন ধরনের locality-র সুবিধা পায়?

  • Temporal locality
  • Spatial locality
  • Write locality
  • কোনো locality নেই

একটি cache-এর hit time ২ cycle, miss rate ৫%, আর miss penalty ১০০ cycle. AMAT (average memory access time) কত?

  • ৫ cycle
  • ১০২ cycle
  • ৭ cycle
  • ১.৯ cycle

Virtual memory-তে DRAM আর SSD/disk-এর মধ্যে কোন page কখন আনা হবে বা বাদ পড়বে, এই সিদ্ধান্ত মূলত কে নেয়?

  • L1 cache controller
  • Compiler
  • DRAM chip নিজে
  • Operating system, hardware-এর সাহায্যে

নিচের কোন মেমরি volatile — বিদ্যুৎ চলে গেলে তার ডেটা মুছে যায়?

  • DRAM main memory
  • NAND flash SSD
  • HDD
  • ROM

একটি fully associative LRU cache ৪টি block রাখতে পারে, শুরুতে খালি। Block-এর ক্রম A, B, C, D, A, E, A চাওয়া হলো। মোট কয়টি hit?

  • ১টি
  • ২টি
  • ৩টি
  • ৭টি

একটি benchmark-এ array-র মাপ ১৬ KiB থেকে ৬৪ MiB পর্যন্ত বাড়িয়ে random pointer chase-এ প্রতি access-এর সময় মাপা হলো। সময় কয়েকটি মাপে হঠাৎ লাফ দিয়ে বাড়ে। এর সবচেয়ে ভালো ব্যাখ্যা কী?

  • বড় array-তে CPU-র clock কমে যায়
  • Compiler বড় array-র জন্য ধীর কোড তৈরি করে
  • Working set একেকটি cache স্তরের মাপ ছাড়িয়ে যায়
  • Hardware prefetcher বড় array-তে বেশি কাজ করে

শব্দকোষ

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

মনে রাখুন

  1. Memory wall: CPU-র গতি DRAM-এর latency-র চেয়ে বহু গুণ দ্রুত বেড়েছে; আজ একটি DRAM access মোটামুটি কয়েকশো cycle — ৩ GHz-এ এক cycle = এক সেকেন্ড হলে প্রায় ৫ মিনিট।
  2. হায়ারার্কি: register → L1 → L2 → L3 → DRAM → SSD/HDD → network; প্রতি ধাপে latency প্রায় ১০–১০০ গুণ বাড়ে; প্রতিটি স্তর নিচেরটির cache; টুকরো word → ৬৪ B block → ৪ KiB page; cache সামলায় hardware, page সামলায় OS.
  3. Locality: temporal = একই ঠিকানা শিগগির আবার, spatial = পাশের ঠিকানা শিগগির; reuse distance d < C হলে C-block-এর LRU cache-এ hit.
  4. AMAT = hit time + miss rate × miss penalty; অনেক স্তরে E[T] = t১ + m১(t২ + m২(…)); বিরল কিন্তু খুব ধীর access (page fault) গড়কে চোখে পড়ার মতো বাড়ায়।
  5. Staircase: working set cache-এ আঁটলে সময় সমতল, ছাড়ালে লাফ; sequential ধারায় prefetcher latency লুকায়, random-এ পারে না।

পরের পর্বে: হায়ারার্কির ভেতরে ঢুকব — একটি cache আসলে কীভাবে জানে কোন block তার কাছে আছে? Direct-mapped cache, ঠিকানাকে tag, index আর block offset-এ ভাগ করা, আর valid bit.

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

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