~/writing/memory-hierarchy $
    05/08
    intro ⏻
    LEVEL 2 — THE MACHINERYপর্ব ০৫/০৮~১৬ মিনিট

    মেমোরি হায়ারার্কি

    কেন এক memory দিয়ে হয় না, আর register থেকে disk পর্যন্ত যাত্রা

    আপনার laptop-এ কি একটাই memory আছে?

    স্বাভাবিক উত্তর — হ্যাঁ, RAM। ১৬ GB বা ৩২ GB, যা-ই হোক।

    কিন্তু আসলে laptop-এ এই মুহূর্তে কয়েক রকমের storage একসাথে কাজ করছে। কিছু এত ছোট যে সবমিলিয়ে সামান্য কয়েকটা value ধরে, কিন্তু CPU-র execution logic-এর একদম গায়ে লাগানো। কিছু এত বড় যে টেরাবাইট পর্যন্ত ধরে, কিন্তু সেখান থেকে data আনতে CPU-কে তুলনায় অনেক বেশি অপেক্ষা করতে হয়।

    কেন এই ব্যবস্থা? আগের আর্টিকেলে দেখেছি একটা instruction কীভাবে fetch, decode আর execute হয়। কিন্তু সেই instruction আর data-গুলো থাকে কোথায়, আর একটাই memory দিয়ে কেন কাজ চলে না? এটাই আজকের গল্প।

    01

    কেন একটাই memory দিয়ে হয় না?

    সোজা প্রশ্ন:

    একটাই বিশাল memory বানিয়ে সেটাকে CPU-র সবচেয়ে কাছের storage-এর মতো দ্রুত বানানো যায় না?

    সমস্যাটা engineering trade-off-এর।

    Memory design-এ আমরা একসাথে চাই:

    • কম latency — data খুব দ্রুত পাওয়া যাবে
    • বেশি capacity — অনেক data রাখা যাবে
    • কম cost — একই capacity-র জন্য খরচ নাগালের মধ্যে থাকবে

    কোনো একটা storage technology একসাথে এই সব দিকেই সেরা হতে পারে না।

    trade-off
    Very fast + very large
    → very expensive
    
    Very fast + inexpensive
    → small capacity
    
    Very large + inexpensive
    → higher latency

    তাই computer engineer-রা একটাই memory technology বেছে নেয়নি। বরং বিভিন্ন trade-off-এর storage-কে স্তরে স্তরে সাজানো হয়েছে — সবচেয়ে ছোট আর দ্রুত storage CPU-র সবচেয়ে কাছে, বড় আর ধীর storage আরও নিচে।

    এই layered বিন্যাসটাই Memory Hierarchy।

    এটা কোনো কঠিন rule নয় যে প্রতিটা computer-এ ঠিক একই সংখ্যক layer থাকবে। Architecture অনুযায়ী স্তরের সংখ্যা, মাপ, কোনটা shared আর কোনটা নয় — সব বদলাতে পারে। আমরা এখানে একটা simplified hierarchy নিয়ে কাজ করছি, যাতে মূল ধারণাটা পরিষ্কার হয়।
    02

    Layer-গুলো

    একটা simplified laptop বা desktop-এর memory hierarchy-কে এভাবে ভাবা যায়:

    • Register — CPU-র execution logic-এর সবচেয়ে কাছের storage। চলমান computation-এর জন্য দরকারি কিছু value, address বা state এখানে থাকে। Capacity খুব ছোট, কিন্তু access latency সবচেয়ে কম।
    • L1 Cache — প্রতিটা core-এর খুব কাছের ছোট cache। প্রায়ই instruction আর data-র জন্য আলাদা ভাগে থাকে — L1i আর L1d।
    • L2 Cache — L1-এর চেয়ে বড়, সাধারণত একটু বেশি latency-র। অনেক architecture-এ এটা core-এর সঙ্গে ঘনিষ্ঠভাবে যুক্ত থাকে, যদিও exact design processor ভেদে আলাদা।
    • L3 Cache — আরও বড় cache layer। অনেক modern multi-core processor-এ এটা একাধিক core-এর মধ্যে shared হতে পারে, তবে exact organization architecture অনুযায়ী বদলায়।
    • RAM — system-এর main memory। চলমান program আর তাদের data-র বড় অংশ এখানে থাকে। Cache বা register-এর তুলনায় ধারণক্ষমতা অনেক বেশি, latency-ও অনেক বেশি।
    • SSD / HDD — persistent storage। ধারণক্ষমতা অনেক বড়, আর power বন্ধ হলেও data থেকে যায়। কিন্তু CPU-র জন্য এদের access RAM-এর তুলনায়ও অনেক ধীর।
    hierarchy
    Registers
       ↓
    L1 Cache
       ↓
    L2 Cache
       ↓
    L3 Cache
       ↓
    RAM
       ↓
    SSD / HDD

    উপরের দিকে গেলে সাধারণত:

    ছোট → দ্রুত → প্রতি unit capacity-তে বেশি খরচসাপেক্ষ

    নিচের দিকে গেলে সাধারণত:

    বড় → ধীর → প্রতি unit capacity-তে সস্তা

    এটা একটা conceptual hierarchy। বাস্তব processor-এ কিছু layer আলাদা হতে পারে, কিছু shared হতে পারে, আবার কোনো architecture-এ নির্দিষ্ট কোনো layer না-ও থাকতে পারে। নিচের যন্ত্রে একেকটা layer-এ চাপ দিয়ে তার ভূমিকা দেখুন:

    THE PYRAMID
    প্রতিটি core-এর খুব কাছের ছোট cache · প্রায়ই instruction আর data-র জন্য আলাদা ভাগ
    ওপর থেকে নিচে — ছোট থেকে বড়, দ্রুত থেকে ধীর, প্রতি unit capacity-তে দামি থেকে সস্তা। এটা একটা conceptual hierarchy; বাস্তব processor-এ layer-এর সংখ্যা আর সাজানো আলাদা হতে পারে।

    Cache কি শুধু "আরও দ্রুত RAM"?

    না।

    Cache আর RAM — দুটোই memory, কিন্তু এরা একই technology-র storage নয়, একই ভূমিকারও নয়। CPU cache সাধারণত অনেক ছোট, কম latency-র, আর processor-এর execution logic-এর সঙ্গে ঘনিষ্ঠভাবে যুক্ত। Main memory অনেক বড়, আর system-এর অনেক বড় working set ধরে রাখে।

    Cache-এর উদ্দেশ্য RAM-এর বিকল্প হওয়া নয়। উদ্দেশ্য হলো বারবার কাজে লাগা data-কে CPU-র কাছাকাছি রাখা, যাতে প্রতিবার ধীর main memory পর্যন্ত যেতে না হয়।
    03

    Latency-র পার্থক্য কেন গুরুত্বপূর্ণ?

    "Register দ্রুত, RAM ধীর" — বলা সহজ।

    আসল কথাটা হলো, এই latency-গুলো একই scale-এর নয়। Hierarchy-র নিচের দিকে নামলে access time অনেক গুণ বেড়ে যেতে পারে।

    আর clock cycle-এর সংখ্যাও কোনো memory technology-র স্থায়ী বৈশিষ্ট্য নয়। একটা নির্দিষ্ট access কত cycle নেবে, সেটা processor-এর clock frequency, architecture আর সেই particular access-এর ওপর নির্ভর করে।

    তাই নির্দিষ্ট সংখ্যা মুখস্থ করার চেয়ে এই mental model-টা কাজে লাগবে:

    mental model
    Closer to the CPU
    → generally lower latency
    
    Farther down the hierarchy
    → generally higher latency
    IF THE NEAREST ACCESS = 1 SECOND
    system-এর main memory · cache-এর চেয়ে অনেক বেশি ধারণক্ষমতা, অনেক বেশি latency
    CPU-র সবচেয়ে কাছের access ১ সেকেন্ড ধরলে দূরের layer-গুলো কতটা দূরে, তার একটা অনুভব। bar-টা log scale-এ আঁকা। এগুলো কোনো hardware-এর মাপ নয় — শুধু order of magnitude বোঝানোর জন্য।

    CPU-কে যদি প্রতিটা data access-এর জন্য বারবার অনেক ধীর layer পর্যন্ত অপেক্ষা করতে হতো, তাহলে execution প্রায়ই data-র জন্য বসে থাকত। এই অবস্থাকে বলা হয় stall।

    তাই hierarchy-র মূল লক্ষ্যটা এক লাইনে:

    যে data এখন বা খুব শিগগির দরকার হতে পারে, তার অন্তত কিছু অংশ CPU-র কাছের দ্রুত layer-এ রাখা।

    কিন্তু cache তো ছোট। পুরো program-এর data সেখানে ধরবে না। তাহলে hardware বুঝবে কীভাবে কোন data কাছে রাখলে কাজে লাগবে? এখানেই আসে locality।

    04

    Locality: কেন hierarchy কাজ করে

    এত ছোট cache দিয়ে লাভটা হয়, কারণ অনেক program memory access করার সময় locality দেখায়।

    সব program-এর access pattern একরকম নয়, সব access predictable-ও নয়। কিন্তু অনেক common workload-এ দুটো pattern খুব বেশি দেখা যায়।

    Temporal locality

    এই মুহূর্তে যে data access হয়েছে, অল্প সময়ের মধ্যে সেটাই আবার access হওয়ার সম্ভাবনা থাকে — কোনো loop-এর counter, বারবার ব্যবহার করা variable, বা বারবার কল হওয়া function-এর data।

    Recently used data আবার লাগতে পারে।

    Spatial locality

    এখন যে memory address access হয়েছে, তার আশেপাশের address-গুলোও শিগগির লাগতে পারে। যেমন একটা array sequentially traverse করলে:

    sequential access
    arr[0]
    arr[1]
    arr[2]
    arr[3]
    ...

    একটা element-এর পরে সাধারণত তার কাছের element-ই আসে।

    কাছের data-ও শিগগির দরকার হতে পারে।

    Cache এই দুই ধরনের locality-ই কাজে লাগায়। এই কারণেই CPU যে byte চেয়েছে শুধু সেটা cache-এ আনা সবচেয়ে ভালো কৌশল নাও হতে পারে — আশেপাশের data সহ একটা ছোট block আনা লাভজনক হতে পারে।

    Cache line: cache আসলে কীভাবে data আনে?

    Cache সাধারণত একেকটা byte আলাদা করে নয়, বরং cache line নামের fixed-size block ধরে data manage করে। অনেক modern CPU-তে একটা cache line 64 byte, যদিও exact size architecture অনুযায়ী বদলায়।

    ধরুন CPU arr[0] access করল, আর সেই access-এ cache miss হলো। তখন সেই address-কে ধারণ করা cache line-টা hierarchy-র নিচের level থেকে আনা হতে পারে। সেই line-এর ভেতরে arr[1], arr[2] আর আরও কাছের element-ও থাকতে পারে — ফলে পরের access-গুলো cache থেকেই মিটে যেতে পারে।

    Cache line হলো cache-এর transfer বা storage-এর একক। "CPU সবসময় RAM থেকে ঠিক 64 byte চায়" — এমন কোনো universal নিয়ম এটা নয়।

    নিচের যন্ত্রে একটা address-এ চাপ দিয়ে দেখুন — line-এর বাকি অংশও কীভাবে সঙ্গে চলে আসে:

    ARRAY ACCESS & THE CACHE LINE
    ৪টা করে cell একসাথে ৮টা cache line গঠন করে।
    একটা address চাইলে তাকে ধারণ করা পুরো line-টাই cache-এ আসতে পারে (spatial locality)। সেই line-এর ভেতরের বাকি address পরে চাইলে সেটা HIT — নিচের layer পর্যন্ত যেতে হয় না। এখানে line-এর মাপ ৪ cell ধরা হয়েছে যাতে screen-এ ধরে; বাস্তব cache line-এর মাপ architecture অনুযায়ী বদলায়।

    কোড লেভেলে প্রভাব: row-major vs column-major

    Memory hierarchy শুধু hardware engineer-দের মাথাব্যথা নয় — সাধারণ code-এর performance-এও এর সরাসরি প্রভাব পড়ে।

    C-র built-in multidimensional array memory-তে row-major order-এ contiguousভাবে সাজানো থাকে — এক row-এর সব element পাশাপাশি, তারপরেই পরের row।

    নিচের দুটো loop একই matrix-এর সব element যোগ করে, কিন্তু access pattern আলাদা:

    #define SIZE 2048
    int arr[SIZE][SIZE];
    
    // Approach A: row-major traversal
    long long sumA = 0;
    for (int i = 0; i < SIZE; i++) {
        for (int j = 0; j < SIZE; j++) {
            sumA += arr[i][j];   // neighbouring addresses
        }
    }
    
    // Approach B: column-major traversal
    long long sumB = 0;
    for (int j = 0; j < SIZE; j++) {
        for (int i = 0; i < SIZE; i++) {
            sumB += arr[i][j];   // large-stride jumps
        }
    }

    Approach A-র inner loop-এ পাশাপাশি address পড়া হয়:

    approach a · row-major
    arr[0][0]
    arr[0][1]
    arr[0][2]
    arr[0][3]
    ...

    একটা cache line একবার চলে এলে সেই line-এর আরও অনেক element কাজে লাগানো যায়। Cache hit-এর সম্ভাবনা বাড়ে।

    Approach B-তে inner loop-এ i বাড়ে, তাই access-গুলো হয়:

    approach b · column-major
    arr[0][0]
    arr[1][0]
    arr[2][0]
    arr[3][0]
    ...

    SIZE = 2048 আর sizeof(int) = 4 হলে প্রতিটা access-এর মাঝে দূরত্ব:

    stride
    2048 × 4 = 8192 bytes

    অর্থাৎ পরের element পেতে memory-তে প্রায় ৮ KB লাফ দিতে হচ্ছে। এতে spatial locality খারাপ হয় — একই cache line আবার কাজে লাগার সুযোগ কমে যায়, cache miss-এর হার অনেক বেড়ে যেতে পারে।

    তাই Approach A সাধারণত Approach B-র চেয়ে অনেক বেশি cache-friendly। তবে ঠিক কত গুণ দ্রুত হবে, সেটা hardware, compiler আর workload-এর ওপর নির্ভর করে।

    একই algorithmic result-এর জন্যও memory access pattern performance বদলে দিতে পারে।

    এই একই locality principle-এর কারণেই data structure বা algorithm বাছার সময় শুধু Big-O complexity নয়, memory access pattern-ও গুরুত্বপূর্ণ হতে পারে। নিচের যন্ত্রে দুই mode toggle করে hit/miss গুনে দেখুন:

    ROW-MAJOR vs COLUMN-MAJOR
    hit: ০miss: ০
    Row-major-এ পাশাপাশি address পড়া হয়, তাই একই line বারবার কাজে লাগে — HIT বেশি। Column-major-এ প্রতিবার লাফ দিয়ে নতুন line-এ যেতে হয়, তাই miss অনেক বেড়ে যায়।
    05

    SRAM আর DRAM: ভেতরে পার্থক্য কী?

    Cache আর main RAM — দুটোই semiconductor memory, কিন্তু সাধারণত এদের storage cell একরকম নয়।

    SRAM — cache-এর দিকে

    SRAM (Static RAM) সাধারণত একাধিক transistor দিয়ে তৈরি একটা bistable cell ব্যবহার করে bit ধরে রাখে; common design-এ ৬-transistor cell দেখা যায়।

    সুবিধা: খুব দ্রুত access, আলাদা refresh cycle লাগে না, আর CPU-র কাছাকাছি ঘন ঘন access করা data-র জন্য উপযুক্ত।

    অসুবিধা: প্রতি bit-এ বেশি hardware লাগে, তাই storage density কম — একই capacity বানাতে বেশি silicon area লাগে।

    এই কারণেই SRAM তুলনামূলক ছোট আর দ্রুত cache-এর জন্য উপযোগী।

    DRAM — main memory-র দিকে

    DRAM (Dynamic RAM) সাধারণত একটা transistor আর একটা capacitor-ভিত্তিক cell দিয়ে bit রাখে। Capacitor-এ জমা charge সময়ের সঙ্গে leak করে, তাই DRAM-কে নিয়মিত refresh করতে হয়।

    এর বড় সুবিধা density — অনেক বেশি bit খুব ছোট physical area-তে রাখা যায়। তাই large-capacity main memory-র জন্য DRAM উপযোগী।

    কিন্তু এর cell structure আর read/write process SRAM-এর তুলনায় বেশি জটিল, তাই latency-ও বেশি।

    trade-off
    SRAM → faster, larger per-bit footprint, more expensive per bit
    DRAM → slower, denser, cheaper per bit

    এই trade-off-টাই বুঝিয়ে দেয় কেন cache আর main memory আলাদা technology-তে বানানো হয়।

    SRAM vs DRAM
    transistor + capacitor cell — জমা charge leak করে, তাই নিয়মিত refresh লাগে
    charge: 36% · refresh হয়েছে 0 বার
    প্রতি bit-এ: জায়গা কমব্যবহার: Main RAM
    SRAM-এর cell নিজে থেকেই state ধরে রাখে — দ্রুত, কিন্তু প্রতি bit-এ জায়গা বেশি লাগে। DRAM-এর capacitor leak করে, তাই refresh লাগে — dense আর সস্তা, কিন্তু latency বেশি।
    06

    Volatility: power গেলে কী হয়?

    Memory-কে আরেকভাবেও ভাগ করা যায়: power বন্ধ হওয়ার পর data কি নির্ভরযোগ্যভাবে থেকে যায়? এটাই volatile আর non-volatile-এর পার্থক্য।

    Register, cache আর DRAM-based main memory volatile। মানে এগুলো power ছাড়া stored state অনির্দিষ্টকাল ধরে রাখার জন্য designed নয় — power চলে গেলে আগের data আর নির্ভরযোগ্যভাবে থাকে না।

    SSD আর HDD non-volatile। power ছাড়াও এরা data ধরে রাখতে পারে, আর এদের physical technology আলাদা:

    • HDD: magnetic storage ব্যবহার করে।
    • SSD: NAND flash memory ব্যবহার করে, যেখানে electrical charge দিয়ে stored state বহুদিন ধরে রাখা যায়।
    state retention
    Volatile
    → requires power to retain state
    
    Non-volatile
    → can retain state without continuous power

    এখানে এই distinction-টুকুই যথেষ্ট। physical implementation-এর গভীর detail পরে দরকার হলে দেখা যাবে।

    07

    পুরো ছবিটা একবার

    ধরুন CPU-কে এমন একটা instruction execute করতে হবে, যার জন্য memory থেকে একটা value load করা দরকার। Simplifiedভাবে গল্পটা এমন:

    • CPU memory access-এর প্রয়োজন বুঝল। Instruction-এর control information থেকে CPU জানে যে একটা memory value লাগবে, আর কোন address থেকে সেটা নিতে হবে।
    • কাছের cache-এ খোঁজা হয়। L1 data cache-এ value থাকলে সেটা cache hit — CPU কম latency-তেই data পেয়ে যায়।
    • না থাকলে নিচের level-এ। Processor-এর design অনুযায়ী request L2, তারপর L3-এ যেতে পারে। যেখানে data পাওয়া যায়, সেখান থেকে সেটা CPU-র দিকে ফেরে, আর প্রয়োজন অনুযায়ী cache-এ রাখা হতে পারে। কোনো cache-এই না থাকলে request main memory পর্যন্ত যায়।
    • Data পেলে execution এগোয়। Data execution machinery-তে ফিরে এলে instruction-এর বাকি অংশ চলতে পারে, আর result শেষ পর্যন্ত কোনো register-এ বসতে পারে।
    lookup
    CPU
     ↓
    L1
     ↓
    L2
     ↓
    L3
     ↓
    RAM

    উপরের layer-এ data পাওয়া গেলে নিচ পর্যন্ত যেতেই হয় না। এই কারণেই cache hit মূল্যবান, আর cache miss expensive হতে পারে। নিচের যন্ত্রে data কোথায় পাওয়া গেল সেটা বেছে নিয়ে পুরো cascade দেখুন:

    THE FULL LOOKUP
    L1 Cache
    L2 Cache
    L3 Cache
    RAM
    যে layer-এ data পাওয়া যায়, তার আগের প্রতিটা layer একেকটা miss। উপরের layer-এ পাওয়া গেলে নিচ পর্যন্ত যেতেই হয় না — এজন্যই cache hit মূল্যবান।

    Page fault কোথায় আসে?

    এটা আলাদা একটা concept, যেটা আমরা এখানে বিস্তারিত করছি না। Virtual memory-তে CPU যে address ব্যবহার করছে, তার required page যদি RAM-এ এই মুহূর্তে না থাকে, তখন page fault ঘটতে পারে, আর operating system-কে জড়াতে হয়।

    Cache miss মানে relevant cache-এ data পাওয়া যায়নি। Page fault হলো virtual memory system-এর একটা আলাদা ঘটনা। দুটোকে গুলিয়ে না ফেলাই ভালো।
    08

    বাস্তবতার কোণা: একাধিক core হলে?

    একাধিক CPU core থাকলে আরেকটা সমস্যা আসে।

    একই memory data-র copy একাধিক core-এর private cache-এ থাকতে পারে। কোনো একটা core সেই data বদলালে অন্য core যেন অনির্দিষ্টকাল পুরোনো, inconsistent copy ব্যবহার করতে না থাকে — তার জন্য processor-এ cache coherence mechanism থাকে।

    একাধিক core-এর private cache থাকলে cached copy-গুলোর মধ্যে consistency রাখতে hardware-এর বাড়তি coordination দরকার হয়।

    এখানে অনেক protocol আর hardware mechanism জড়িত। MESI-র মতো protocol-এর বিস্তারিত পরে আলাদা করে দেখা যেতে পারে।

    09

    এই আর্টিকেলে কী শিখলাম

    // এই আর্টিকেলে কী শিখলাম
    • একটা memory technology সবদিক থেকে সেরা হতে পারে না — latency, capacity আর cost-এর মধ্যে trade-off আছে, তাই computer একাধিক storage layer ব্যবহার করে।
    • Memory hierarchy CPU-কে দরকারি data দ্রুত পাওয়ার সুযোগ দেয় — register আর cache ছোট কিন্তু দ্রুত, RAM অনেক বড়, storage আরও বড় কিন্তু অনেক ধীর।
    • Cache দাঁড়িয়ে আছে locality-র ওপর — recently used data আবার লাগতে পারে (temporal), আর কাছের address-ও শিগগির লাগতে পারে (spatial)।
    • Cache line কাছের data-কে কাজে লাগায় — একটা miss-এর সময় আশেপাশের data সহ একটা block আসতে পারে, ফলে পরের access-গুলো hit হতে পারে।
    • SRAM আর DRAM আলাদা trade-off দেয় — SRAM দ্রুত কিন্তু প্রতি bit-এ জায়গা বেশি; DRAM dense আর সস্তা, কিন্তু latency বেশি ও refresh লাগে।
    • Volatile আর non-volatile-এর পার্থক্য state retention-এর — volatile storage power ছাড়া state নির্ভরযোগ্যভাবে রাখতে পারে না, non-volatile পারে।
    এই পাতা খোলার পর থেকে আপনার device-এ আনুমানিক ৩৩৪.৫ কোটি বার transistor switch হয়েছে।
    cd ~  # back to terminal