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

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

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

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

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

    কিন্তু আসলে laptop-এ এই মুহূর্তে পাঁচ, ছয় বা কখনো সাত-আট রকমের memory একসাথে কাজ করছে। কিছু এত ছোট যে সবমিলিয়ে কয়েক kilobyte, কিন্তু এত দ্রুত যে CPU-র clock speed-এর সাথে তাল মিলিয়ে ডেটা আদান-প্রদান করতে পারে। কিছু এত বড় যে টেরাবাইট পর্যন্ত ধরে, কিন্তু তাদের কাছে পৌঁছাতে CPU-কে অপেক্ষা করতে হয় হাজার হাজার clock cycle।

    কেন এই ব্যবস্থা? আগের আর্টিকেলে দেখেছি register কীভাবে instruction আর ডেটা ধরে রাখে। কিন্তু কেন একটাই memory দিয়ে কাজ চালানো যায় না? এটাই আজকের গল্প।

    01

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

    সোজা প্রশ্ন। একটাই বড়, দ্রুত memory বানিয়ে সব কাজ চালালে হতো না?

    হতো, যদি সেটা সস্তায় পাওয়া যেত।

    Memory design-এর একটা fundamental trilemma আছে। আমরা তিনটা জিনিস চাই একসাথে — দ্রুততা (speed), ধারণক্ষমতা (capacity), আর কম দাম (low cost)। কিন্তু একই memory-তে এই তিনটার সবগুলো কখনো পাওয়া যায় না। যেকোনো দুইটা পাবেন, তৃতীয়টা ছাড়তে হবে।

    • খুব দ্রুত + বড় = ভয়ানক দামি (কেউ afford করতে পারবে না)
    • খুব দ্রুত + সস্তা = ছোট (কম জায়গা, কম ডেটা)
    • বড় + সস্তা = ধীর (RAM, disk)

    এই trilemma-র কারণেই এক memory-তে সব সম্ভব না। তাই engineering-এর একটা চতুর সমাধান — hierarchy। একটা মাত্র memory না। বহু layer, একটার পর একটা।

    সবচেয়ে দ্রুত layer সবার ওপরে — কিন্তু এতই ছোট যে শুধু কিছু ডেটা রাখা যায়। তার নিচে একটু বড়, একটু ধীর। তার নিচে আরও বড়, আরও ধীর। এভাবে নামতে নামতে সবার নিচে সবচেয়ে বড়, সবচেয়ে ধীর memory — যেখানে কমবেশি সবকিছু জমা রাখা সম্ভব।

    02

    Layer-গুলো একটু কাছ থেকে

    আপনার laptop-এ এই মুহূর্তে যেসব memory কাজ করছে:

    • Register — CPU-র একদম ভেতরে। আকার সবমিলিয়ে কয়েক হাজার bit। Speed এক clock cycle। CPU এই মুহূর্তে যা নিয়ে কাজ করছে — সব এখানে।
    • L1 Cache — CPU-র ভেতরেই, প্রতিটা core-এর জন্য আলাদা। আকার 32-64 KB। Speed 4-5 clock cycles। দুই ভাগে বিভক্ত — L1i (instruction) আর L1d (data)।
    • L2 Cache — এটাও CPU-র ভেতরে, প্রতি core-এর জন্য আলাদা। আকার 256 KB থেকে 1 MB। Speed 3-10 clock cycle।
    • L3 Cache — সব core-এর মধ্যে shared। আকার 4 থেকে 64 MB। Speed 10-30 clock cycle।
    • RAM (Main Memory) — CPU-র বাইরে, motherboard-এ। আকার 8-32 GB, কখনো আরও বেশি। Speed 100-300 clock cycle।
    • SSD/HDD — Non-volatile storage। আকার 256 GB থেকে অনেক TB পর্যন্ত। Speed 100,000-এর বেশি clock cycle।

    সবমিলিয়ে ছয় বা সাত layer। উপর থেকে নিচে — ছোট থেকে বড়, দ্রুত থেকে ধীর, দামি থেকে সস্তা। নিচের যন্ত্রে একেকটা layer-এ চাপ দিয়ে তার আকার আর গতি দেখুন:

    THE PYRAMID
    আকার: 32-64 KBগতি: 1-2 cycles
    ওপর থেকে নিচে — ছোট থেকে বড়, দ্রুত থেকে ধীর, দামি থেকে সস্তা।
    03

    Register access যদি ১ সেকেন্ড হতো

    সংখ্যাগুলো একটু abstract লাগতে পারে। "1 clock cycle" আর "100 clock cycle"-এর পার্থক্য মাথায় ধরানো সহজ না।

    একটা comparison দিয়ে ভাবা যাক। ধরুন register access করতে যদি ১ সেকেন্ড লাগে, তাহলে বাকিদের অবস্থাটা এমন:

    IF REGISTER = 1 SECOND
    বাস্তব hardware latency: ~60-100 ns
    Register access ১ সেকেন্ড ধরলে, RAM ৫-৭ মিনিট আর SSD/HDD দিন-মাস দূরে। bar-টা log scale-এ আঁকা — নাহলে বাকিগুলো screen-এই ধরত না।

    এই স্কেলে দাঁড়িয়ে ভাবুন: CPU যদি প্রতিটা data-র জন্য সরাসরি storage বা RAM-এর ওপর নির্ভর করত, তবে একেকটা গাণিতিক অপারেশনের মাঝখানে তাকে মিনিটের পর মিনিট নিষ্ক্রিয় বসে থাকতে হতো (একে বলে CPU Stall)। তাই দ্রুততম memory-কে প্রসেসরের যতটা সম্ভব কাছাকাছি রাখা এত গুরুত্বপূর্ণ।

    কিন্তু একটা প্রশ্ন থেকে যায়। যদি সব ডেটা L1 বা L2 cache-এ ধরত, তাহলে সমস্যা মিটে যেত। কিন্তু L1 তো মাত্র 64 KB। এত অল্প জায়গায় পুরো program-এর ডেটা রাখা অসম্ভব। তাহলে কোন ডেটা fast cache-এ থাকবে, কোনটা RAM-এ পড়ে থাকবে?

    এখানেই আসে locality-র concept। এবং এই একটা idea-ই পুরো hierarchy-কে কাজ করায়।

    04

    Locality: কেন এই ব্যবস্থা কাজ করে

    প্রোগ্রাম কীভাবে memory access করে, সেটা random না। প্রোগ্রাম যখন একটা variable access করে, একটু পরে সেটাকে আবার access করার সম্ভাবনা অনেক বেশি। যখন array-এর index 5 access করে, পরের access-এ সাধারণত সে index 6 চাইবে — index 500 না।

    এই দুই pattern-এর নাম locality

    Temporal locality — সময়ের locality। এই মুহূর্তে যে ডেটা access হচ্ছে, কয়েক মুহূর্ত পর সেটাকে আবার access করার সম্ভাবনা অনেক বেশি। for loop-এর counter variable, কোনো recursive function-এর argument, বা বার বার কল হওয়া কোনো method — এগুলো ঘন ঘন access হয়।

    Spatial locality — জায়গার locality। এই মুহূর্তে যে address access হচ্ছে, তার আশেপাশের address-এও access-এর সম্ভাবনা বেশি। Array traverse করলে, struct-এর field access করলে, string-এর character পড়লে — সব একটার পাশের অন্যটা।

    CPU যখন RAM থেকে ১টি byte দাবি করে, memory controller শুধু সেই ১টি byte পাঠায় না। সে তার সাথে পুরো 64-byte-এর একটা ব্লক একবারে তুলে নিয়ে আসে cache-এ। এই 64-byte ব্লককে বলা হয় cache line

    Spatial locality-র কারণে, আপনি যখন array-এর index[0] রিড করেন, পুরো cache line-এ index[0] থেকে index[15] (4-byte integer হলে) পর্যন্ত cache-এ চলে আসে। ফলে পরের ১৫টি iteration-এ CPU-কে আর ধীরগতির RAM-এ যেতেই হয় না। নিচের যন্ত্রে address-এ চাপ দিয়ে দেখুন কীভাবে পুরো line একসাথে চলে আসে:

    ARRAY ACCESS & THE CACHE LINE
    ৪টা করে cell একসাথে ৮টা cache line গঠন করে।
    একটা address চাইলে পুরো line-টাই cache-এ চলে আসে (spatial locality)। সেই line-এর ভেতরের বাকি address পরে চাইলে সেটা HIT — RAM-এ যেতে হয় না।

    কোড লেভেলে প্রভাব: Row-Major vs Column-Major

    Memory hierarchy কেবল হার্ডওয়্যার ইঞ্জিনিয়ারদের মাথাব্যথার কারণ নয় — হাই-লেভেল সফটওয়্যার পারফরম্যান্সেও এর সরাসরি প্রভাব আছে।

    C বা C++-এর মতো ভাষায় 2D array মেমরিতে মূলত row-major order-এ পর পর সাজানো থাকে — প্রথম সারির সব উপাদান পাশাপাশি বসে, তার ঠিক পরপরই দ্বিতীয় সারির উপাদান বসে।

    নিচের দুটি loop লক্ষ করুন। দুটোই একই matrix-এর সব উপাদানের যোগফল বের করে, কিন্তু পারফরম্যান্স সম্পূর্ণ ভিন্ন:

    #define SIZE 2048
    int matrix[SIZE][SIZE];
    
    // Approach A: Cache-Friendly (Row-Major Traversal)
    long long sumA = 0;
    for (int i = 0; i < SIZE; i++) {
        for (int j = 0; j < SIZE; j++) {
            sumA += matrix[i][j]; // contiguous memory access
        }
    }
    
    // Approach B: Cache-Hostile (Column-Major Traversal)
    long long sumB = 0;
    for (int j = 0; j < SIZE; j++) {
        for (int i = 0; i < SIZE; i++) {
            sumB += matrix[i][j]; // large-stride jumps
        }
    }

    Approach A (matrix[i][j]): Inner loop-এ index j বাড়ে, তাই পাশাপাশি memory address পড়া হয়। matrix[0][0] access করলে পুরো 64-byte cache line লোড হয়ে যায়, যাতে matrix[0][1], matrix[0][2] আগে থেকেই থাকে। পরের প্রতিটা read একটা cache HIT।

    Approach B (matrix[i][j]): Inner loop-এ index i বাড়ে, মানে প্রতি ধাপে SIZE * sizeof(int) byte (~৮ KB) দূরে লাফ দিতে হয়। এই লাফের নাম stride। প্রতিবার নতুন cache line লাগে — প্রায় প্রতিটা access-ই একটা cache MISS।

    এ কারণেই array traversal সবসময় linked list-এর চেয়ে দ্রুত, Redis কেন in-memory হয়ে এত কম latency দেয়, আর কেন matrix multiplication অপটিমাইজ করতে cache blocking ব্যবহার করা হয়। নিচের যন্ত্রে দুই mode toggle করে hit/miss গুনে দেখুন:

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

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

    L1, L2, L3 cache আর RAM — সবই semiconductor memory। কিন্তু ভেতরের transistor বিন্যাসে বড় ফারাক আছে।

    SRAM (Static RAM): প্রতি ১-bit ডেটা ধরে রাখতে ৬টি transistor দিয়ে তৈরি একটা flip-flop latch circuit ব্যবহার হয়। কোনো চার্জ leak-এর ঝামেলা নেই, অত্যন্ত দ্রুত। কিন্তু ৬টি transistor অনেক বেশি জায়গা নেয়, দাম বেশি — তাই শুধু CPU cache-এ অল্প পরিমাণে ব্যবহার হয়।

    DRAM (Dynamic RAM): প্রতি ১-bit ডেটার জন্য মাত্র ১টি transistor আর ১টি ক্ষুদ্র capacitor ব্যবহার হয়। Density মারাত্মক বেশি — কোটি কোটি bit বসানো যায়, তাই সস্তা। কিন্তু capacitor একটা চার্জ ধরে রাখা বালতির মতো, যার electron সময়ের সাথে leak হয়ে যায়। তাই প্রতি ৬৪ millisecond-এর মধ্যে প্রতিটা cell-কে অন্তত একবার refresh করতে হয় (উচ্চ তাপমাত্রায় ৩২ ms) — এই refresh cycle-ই DRAM-কে SRAM-এর চেয়ে ধীর করে দেয়।

    SRAM vs DRAM
    1-transistor + capacitor — চার্জ ফুটো হয়, তাই বারবার refresh লাগে
    charge: 32% · refresh হয়েছে 0 বার
    transistor/bit: 1 + capacitorব্যবহার: Main RAM
    SRAM-এর flip-flop নিজে থেকেই state ধরে রাখে — দ্রুত কিন্তু ব্যয়বহুল। DRAM-এর capacitor ফুটো করে, তাই বারবার refresh লাগে — সস্তা কিন্তু ধীর।
    06

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

    মেমরি layer-গুলোর মধ্যে স্থায়িত্বের ভিত্তিতে একটা মৌলিক বিভাজন আছে।

    Volatile memory: Register, L1/L2/L3 cache, আর RAM। বিদ্যুৎ সরবরাহ বন্ধ হওয়ার সাথে সাথেই এদের ভেতরের সব charge আর flip-flop-এর voltage state শূন্য হয়ে যায় — সব ডেটা মুছে যায়।

    Non-volatile storage: SSD আর HDD। বিদ্যুৎ ছাড়াও ডেটা ধরে রাখতে পারে, কিন্তু গঠন সম্পূর্ণ ভিন্ন:

    • HDD: একটা metallic platter-এর ওপর magnetic field (North/South orientation) হিসেবে ০ আর ১ সংরক্ষিত হয়। Physical read/write head স্পিন করে ডেটা লেখে বা পড়ে।
    • SSD: কোনো নড়াচড়া করার যন্ত্রাংশ নেই। NAND Flash Memory দিয়ে তৈরি — Floating Gate Transistor-এর ভেতরের insulator স্তরের মাঝে electron আটকে রাখা হয় (electron tunneling)। একবার electron ট্র্যাপড হলে বিদ্যুৎ ছাড়াই বছরের পর বছর সেই state ধরে থাকে।
    07

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

    কল্পনা করা যাক CPU কোনো একটা instruction পালনের জন্য একটা নির্দিষ্ট memory address-এর ডেটা চাইল:

    • Register check: CPU প্রথমে নিজের register চেক করে। পেলে সাথে সাথে ব্যবহার করে।
    • L1, L2, L3 lookup: না পেলে L1 cache-এ যায়। সেখানে না থাকলে (miss) L2, তারপর L3 স্ক্যান করে।
    • RAM access: L3-তেও না থাকলে system bus পেরিয়ে RAM-এ যায়। ডেটা পেলে সেই 64-byte cache line L3, L2 পার হয়ে L1 আর register-এ রিফিল হয়।
    • Page fault (storage access): RAM-এও না থাকলে (virtual memory page fault) operating system সিগন্যাল পায়, drive থেকে (SSD/HDD) ব্লক এনে RAM-এ লোড করে। এই সময় CPU লক্ষ লক্ষ cycle অলস বসে থাকে।

    এই কারণেই ভালো developer memory hierarchy-র দিকে খেয়াল রাখে — array-এ locality maintain করে, random access কম করে, ছোট কাজের ডেটা cache-fit রাখার চেষ্টা করে। নিচের যন্ত্রে data কোথায় পাওয়া গেল সেটা বেছে নিয়ে পুরো cascade-টা দেখুন:

    THE FULL LOOKUP
    Register
    L1 Cache
    L2 Cache
    L3 Cache
    RAM
    Register থেকে শুরু করে যেখানে data পাওয়া যায়, ততক্ষণ পর্যন্ত প্রতিটা layer-এ miss। যত নিচে যেতে হয়, cycle-এর হিসাব তত ভয়ংকরভাবে বাড়ে।
    08

    আধুনিক প্রসেসরের বাস্তব জটিলতা: Cache Coherence

    বাস্তব প্রসেসরে memory hierarchy চালানো আরও চ্যালেঞ্জিং, বিশেষ করে modern multi-core CPU-তে।

    একটা প্রসেসরে যদি ৮টা core থাকে, তবে ৮টা core-এর আলাদা আলাদা L1 আর L2 cache থাকে। Core 1 যদি তার L1 cache-এ থাকা কোনো variable-এর মান বদলে X = 5 থেকে X = 10 করে দেয়, আর একই সময়ে Core 2 যদি তার নিজস্ব L1 cache থেকে X-এর মান পড়তে চায় — সে তো পুরনো মান X = 5 পাবে!

    09

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

    // এই আর্টিকেলে কী শিখলাম
    • এক memory দিয়ে কাজ হয় না — speed, capacity, cost-এর মধ্যে trade-off আছে, তাই hierarchy দরকার।
    • Cache কাজ করে locality-র কারণে — প্রোগ্রাম random access করে না, যা এখন লাগছে তার আশপাশও শীঘ্রই লাগবে।
    • Cache line হলো hierarchy-র মূল ingredient — ডেটা byte-by-byte না, chunk হিসেবে move করে।
    • SRAM দ্রুত কিন্তু ৬-transistor-এর কারণে বড় ও ব্যয়বহুল; DRAM ঘন ও সস্তা, কিন্তু capacitor refresh-এর কারণে ধীর।
    • Volatile আর non-volatile-এর পার্থক্য physical — flip-flop বা capacitor বিদ্যুৎ ছাড়া state রাখতে পারে না, কিন্তু trapped electron বা magnetic pattern পারে।
    এই পাতা খোলার পর থেকে আপনার device-এ আনুমানিক ৩৩৪.৮ কোটি বার transistor switch হয়েছে।
    cd ~  # back to terminal