Foundationপ্রথম নীতি থেকে
LEVEL 6লেসন ২৪/২৪রিসার্চ-লেভেল১ ঘণ্টা

Cache-aware ও Cache-oblivious — Big-O যখন মিথ্যা বলে

Cache-Aware and Cache-Oblivious Algorithms

Big-O একই থেকে গতি ৪-১০× বদলায় — cache line, prefetcher, locality আর layout-এর হিসাব না জানলে RAM model আপনাকে ধোঁকা দেবে।

এই লেসন শেষে আপনি পারবেন

  • RAM model-এর assumption আর memory hierarchy-র বাস্তবতার ফারাক latency table দিয়ে quantify করতে পারবেন — L1 ~4 cycles vs DRAM ~200
  • 64-byte cache line = 16 int হিসাব করে linear search, binary search আর blocked layout-এর miss behavior analyze করতে পারবেন
  • AoS vs SoA layout choice-এর cache consequence ব্যাখ্যা করে benchmark gap পড়তে পারবেন
  • ijk বনাম ikj matmul loop-order-র Θ(n³) অপরিবর্তিত রেখে GFLOPS কয়েকগুণ ওঠার locality math দেখাতে পারবেন
  • Tiling/blocking parameter sweep-এর ফল পড়ে optimal tile size বাছতে পারবেন আর cache-oblivious recursive subdivision-এর 'fits any cache' idea ব্যাখ্যা করতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

Asymptotic-analysis লেসনে আমরা Big-O-কে শৃঙ্খলা শিখিয়েছি: constant factor উপেক্ষা করো, growth rate-ই সত্য। পুরো module ধরে সেটা ভালো compass ছিল। কিন্তু আজ module-এর শেষ লেসনে একটা confession লাগবে: RAM model মিথ্যা বলে।

Model বলে: memory access O(1), সমান দাম। বাস্তব: L1 hit ~4 cycles, DRAM ~200+ cycles — ৫০× পার্থক্য। একই Θ(n)-algorithm দুইভাবে লিখলে wall-clock-এ ৪× পার্থক্য; একই Θ(n³) matmul loop-order বদলে ৮×। Big-O blind spot-টা ঠিক এখানেই: access count মাপে, access cost-এর variance নয়।

আজ দেখব silicon-এর চুক্তিপত্র — cache line, locality, prefetcher, layout — আর দুইটা design philosophy: cache-aware (hardware parameters জেনে tune) আর cache-oblivious (parameter না জেনেও সব hierarchy level-এ optimal)। Module finale: এই জ্ঞান Level 7-এর networking-এ packet processing-এ যাবে।

মূল ধারণা

Latency ladder — মাইক্রোসেকেন্ডের রাজনীতি

আধুনিক x86-এর typical numbers (order-of-magnitude, machine-ভেদে):

TierLatencyRelativeSize
Register0–1 cycle1×bytes
L1 cache~4 cycles4×32–64 KB
L2 cache~14 cycles14×256 KB–2 MB
L3 cache~40–60 cycles50×8–64 MB
DRAM~100–300 cycles200×GBs
NVMe SSD~25 µs100,000×TBs

Cache line — সর্বনিম্ন বিনিময় একক

Cache line = 64 byte (industry standard)। এক byte চাইলেও পুরো 64 byte আসে:

64 B line = 16 × 32-bit int
          = 8 × 64-bit double/pointer

a[0] touch করলে a[0..15] সব L1-এ!
পরের ১৫টা access — free (hit)

দুই ধরনের miss: cold (প্রথমবার), capacity (working set > cache), conflict (mapping collision)। Algorithm designer-এর হাতিয়ার miss কমানো: spatial locality (pase-pase data একসাথে ছোঁয়া) + temporal locality (একই data আবার আবার, eviction-এর আগে)।

Linear vs binary search — miss-এর চশমায় আবার

Linear search sequential scan: প্রতি line-এ ১৬ comparison — n element-এ n/16 misses। Prefetcher-friendly: stride predictable, hardware পরের lines আগে আনে। Binary search: log₂n comparisons কিন্তু প্রতিটা jump random-ish address — প্রতি step-এ প্রায় ১ miss, no prefetch help।

n = 16M ints (64 MB):
linear : misses ≈ 1M lines      comparisons ≈ 16M (but cheap hits!)
binary : misses ≈ 24 × 1        comparisons ≈ 24 (expensive each)

RAM-model verdict: binary wins massively
Cache-reality verdict: gap much narrower — sometimes linear wins for
small patterns inside cache-resident blocks (SIMD vectorized scans)!

Branch misprediction যোগ হলে (~15-20 cycles প্রতি flush) binary search-এর random branch pattern আরেকটা tax। Eytzinger layout (BFS-ordered array) binary search-কে prefetchable বানায় — advanced but real technique databases-এ।

Pointer structures — line-এর অপচয়

Linked list node = 8-byte pointer + 4-byte data → এক node এক line occupy করে 64 byte-এর মধ্যে 12 ব্যবহার করে — 81% waste per line, আর allocation order random বলে next-node প্রায়ই অন্য region-এ: traversal = প্রতি step-এ cold miss। Array-based alternative (implicit index as ‘pointer’) sequential layout পায়। Sorting লেসনের heapsort-vs-quicksort gap এখানেই ব্যাখ্যা হয়েছিল।

ভেতরে কী ঘটছে

AoS vs SoA — layout-ই architecture

Array of Structs vs Struct of Arrays:

import random

rng = random.Random(42)
N = 1000

# AoS: particle-দের সারি — object-oriented instinct
particles_aos = [tuple(rng.random() for _ in range(7)) for _ in range(N)]
# update velocity only? vx,vy,vz দরকার — কিন্তু line-এ x,y,z,mass-ও এসেছে

# SoA: field-গুলোর সারি — data-oriented design
xs  = [rng.random() for _ in range(N)];  ys  = [rng.random() for _ in range(N)]
zs  = [rng.random() for _ in range(N)];  vxs = [rng.random() for _ in range(N)]
vys = [rng.random() for _ in range(N)];  vzs = [rng.random() for _ in range(N)]
masses = [rng.random() for _ in range(N)]
# velocity update শুধু vxs/vys/vzs arrays touch — pure streaming

Velocity-only workload-এ AoS প্রতি line-এ 3 useful fields / 7 — SoA 7/7। Bandwidth utilization আর SIMD vectorization (contiguous same-type data) দুটোই SoA-তে জেতে। Game engines, physics sims, databases columnar storage (Parquet!) — সব SoA philosophy। Experiment-এ 2–4× gap মাপব।

Trade-off honesty: entity-wise full-record access বেশি হলে AoS ভালো (সব field এক line-এ)। Layout decision = query pattern analysis, dogma নয়।

ijk vs ikj — matmul-এর মায়া

C[i][j] += A[i][k] · B[k][j] — triple loop, order মনে হয় cosmetic:

row-major storage (C-style):

ijk order:  B[k][j] — k বাড়ে ⇒ row-stride-n jump! প্রতি inner step
            নতুন cache line (n=512: B-এর প্রতি access নতুন line)
            misses ≈ n³ lines touched... disaster

ikj order:  C[i][j] fixed inner? না — inner k: A[i][k] sequential ✓
            B[k][j]... wait, reorder properly:

ikj:  for i: for k: for j:  C[i][j] += A[i][k]*B[k][j]
      A[i][k]: k-loop sequential (row walk) ✓
      B[k][j]: j-loop sequential (row walk) ✓
      C[i][j]: j-loop sequential ✓   ← তিনটাই streaming!

same FLOPs (2n³), same Θ(n³) — but misses collapse from ~n³/line to
~n²·(n/tile) — orders fewer.

Experiment-এ GFLOPS measurement দেখব — theory বদলায়নি, locality বদলেছে। Hager-Wellein book-এর এই classic demo production compilers auto-vectorize করার সময়ও কাজে লাগে।

Tiling/blocking — working set-কে cache-এ আটকাও

Matmul-এ n=1024: B-এর এক row 4 KB — L1 (32 KB) সহজেই ধরে না পুরো B (4 MB)। Solution: matrix-কে tile-এ ভাগ করো যেন তিনটা tile একসাথে L1/L2-তে resident থাকে:

for ii in range(0, n, T):
  for kk in range(0, n, T):
    for jj in range(0, n, T):
      # mini-matmul on tiles — 3·T²·8 bytes working set
      # T=32: 24 KB — L1-তে আঁটে!

Tile size parameter: ছোট হলে overhead (loop machinery), বড় হলে capacity miss — sweet spot machine-specific (L1 size ÷ 3 ÷ 8 rough guide)। Build section-এ sweep {32, 64, 128} করে curve আঁকব — cache-aware philosophy-র definition এটাই: parameters hardware-এর কাছে tune করা।

Cache-oblivious — parameter ছাড়া optimality

Aware approach-এর অস্বস্তি: tile size প্রতি machine-এ আলাদা — portable performance কঠিন। Frigo et al. (1999)-এর answer: cache-oblivious algorithm — কোনো hardware parameter assume না করে, recursive subdivision এমনভাবে যেন প্রতিটা level-এ subproblem ছোট হয়ে base-case-এ line-fit হয়।

Recursively halved matmul: বড় matrix ৪ quadrant-এ, recursion নিচে নামতে নামতে কোনো এক level-এ working set L1-এ fit করে (level টা hardware decide করে implicitly!), আরও নিচে L2, L3-ও নিজে নিজে covered। Analysis tool: I/O model — cache size M, line size B; oblivious algorithms prove করে optimal miss count across ALL M,B simultaneously।

Van Emde Boas layout: tree/binary search array-কে recursively halve করে store করলে search path-এর nodes contiguous থাকে every level-এ — binary search miss count O(log_B n) instead of log n। Production examples: FFT (six-step recursive), funsort libraries, sparse matrix kernels।

matrix M (n×n)                     working set at depth d: n²/4^d
┌───────┬───────┐
│ M11   │ M12   │ depth 0         full — maybe DRAM
├───────┼───────┤
│ M21   │ M22   │
└───────┴───────┘
┌───┬───┐ ┌───┬───┐
│…│…│ │…│…│ depth 1         quarter — maybe L3
└───┴───┘ └───┴───┘
depth 2                            sixteenth — maybe L2
depth d                            fits L1 eventually — automatic!

oblivious promise: optimal misses for EVERY (M,B) simultaneously —
no tuning constants anywhere in code.
Recursive subdivision — প্রতি level-এ working set অর্ধেক; hardware cache-এর size যা-ই হোক, এক লেভেলে fit হয়ে যায়।

উদাহরণ

Full worked micro-analysis: sum over matrix, two traversals

n=2048 int matrix (16 MB — L2/L3 boundary):

Version A — row-major scan (i outer, j inner):
  access order: [0][0],[0][1],...,[0][2047],[1][0]...
  per line: 16 consecutive ints → 1 miss serves 16 accesses
  total misses ≈ 2048 rows × 128 lines = 262,144
  time ≈ misses × ~150 cyc + 4M × ~4 cyc ≈ dominated by compute

Version B — column-major scan (j outer, i inner):
  access order: [0][0],[1][0],[2][0],...
  stride = 2048 ints = 8192 B = 128 lines apart!
  every single access = new line = cold miss (no reuse until wrap)
  total misses ≈ 4,194,304 — 16× more
  time ≈ misses × 200 cyc ≈ 838M cycles ≈ 280 ms @3GHz vs ~55 ms

Same Θ(n²). Same instructions. 5× wall-clock difference.

এই hand-analysis-ই experiment-এর prediction — মেপে verify করব। Analysis discipline: (১) access sequence লেখো, (২) line-boundary map, (৩) reuse distance হিসাব, (৪) tier-latency multiply।

ছবিতে

Memory-conscious design stack

Performance layers — Big-O-র নিচের গল্প
  1. Algorithm complexityΘ(n log n) vs Θ(n²) — module-wide compass, still first
  2. Access patternsequential vs strided vs random — prefetcher contract
  3. Data layoutAoS/SoA, array vs pointers — line utilization %
  4. Blocking/tilingworking-set shaping to fit tiers — aware tuning
  5. Oblivious structurerecursive layouts optimal everywhere — portability play
AoS record (7×4B=28B fields, want 3):
line: |f1 f2 f3|f4 f5 f6 f7|f1 f2 f3|f4 f5 f6 f7|
useful: ███░░░░░ ███ (37.5%)  → 2.67× bandwidth tax

SoA (three parallel arrays):
line(vx): |v v v v v v v v v v v v v v v v| 100% useful ✓

Pointer nodes (12B used / 64B line, scattered):
line: |ptr dat .........(52B dead)|           19% — worst case

bandwidth tax = 1/utilization — direct multiplier on memory-bound loops
Line anatomy — একই 64B, তিন layout-এ utilization তিন রকম। Waste percentage সরাসরি bandwidth multiplier।

নিজে চালিয়ে দেখুন

EXPERIMENT

তিনটা benchmark: AoS vs SoA, ijk vs ikj GFLOPS, array vs pointer-chain

macOS / Linux (Python 3.10+, numpy)· ২৫ মিনিট
import numpy as np
import time


def bench(fn, *args, reps=5):
    best = min(
        (lambda t0: (fn(*args), time.perf_counter() - t0)[1])(time.perf_counter())
        for _ in range(reps))
    return best


# ---------- part ১: AoS vs SoA structured-sum ----------

N = 10_000_000
rng = np.random.default_rng(42)

# AoS: one interleaved (N, 7) array; sum columns 4,5,6 (velocities)
aos = rng.random((N, 7))
t_aos = bench(lambda: aos[:, 4].sum() + aos[:, 5].sum() + aos[:, 6].sum())

# SoA: three separate contiguous velocity arrays
va = rng.random(N); vb = rng.random(N); vc = rng.random(N)
t_soa = bench(lambda: va.sum() + vb.sum() + vc.sum())

print(f'AoS interleaved sum : {t_aos*1000:8.1f} ms')
print(f'SoA contiguous sum  : {t_soa*1000:8.1f} ms')
print(f'gap                 : {t_aos/t_soa:8.2f}x')


# ---------- part ২: matmul ijk vs ikj (pure python lists, small n) ----------

def matmul_ijk(A, B, n):
    C = [[0.0] * n for _ in range(n)]
    for i in range(n):
        for j in range(n):
            s = 0.0
            for k in range(n):
                s += A[i][k] * B[k][j]     # B column-walk: stride-n
            C[i][j] = s
    return C


def matmul_ikj(A, B, n):
    C = [[0.0] * n for _ in range(n)]
    for i in range(n):
        Ci = C[i]; Ai = A[i]
        for k in range(n):
            a = Ai[k]; Bk = B[k]
            for j in range(n):
                Ci[j] += a * Bk[j]          # all walks row-sequential
    return C


n = 220
Arng = [[float((i * j) % 7) for j in range(n)] for i in range(n)]
Brng = [[float((i + j) % 5) for j in range(n)] for i in range(n)]
R1 = matmul_ijk(Arng, Brng, n)
t_ijk = bench(matmul_ijk, Arng, Brng, n, reps=1)
R2 = matmul_ikj(Arng, Brng, n)
t_ikj = bench(matmul_ikj, Arng, Brng, n, reps=1)
assert R1 == R2
gflops = lambda t: 2 * n**3 / t / 1e9
print(f'\nmatmul n={n} (pure python):')
print(f'  ijk : {t_ijk*1000:9.0f} ms  ({gflops(t_ijk):5.3f} GFLOPS)')
print(f'  ikj : {t_ikj*1000:9.0f} ms  ({gflops(t_ikj):5.3f} GFLOPS)')
print(f'  speedup: {t_ijk/t_ikj:.2f}x')

# numpy reference — BLAS blocked+tuned reality check
Anp = rng.random((512, 512)); Bnp = rng.random((512, 512))
t_blas = bench(lambda: Anp @ Bnp, reps=3)
print(f'  numpy 512² BLAS: {t_blas*1000:.1f} ms '
      f'({2*512**3/t_blas/1e9:.1f} GFLOPS — tuned blocking+SIMD)')


# ---------- part ৩: array vs pointer-chain traversal proxy ----------

import sys

M = 400_000
vals_arr = list(range(M))                    # contiguous index space
# pointer-chain: shuffled linked-list simulation via index-hopping
perm = list(range(M)); rng.shuffle(perm)
next_idx = perm[:]                           # random successor mapping

def traverse_array():
    s = 0
    for i in range(M):
        s += vals_arr[i]                     # sequential
    return s

def traverse_chain():
    s, i = 0, 0
    for _ in range(M):
        s += vals_arr[i]
        i = next_idx[i]                      # random hop = cache miss proxy
    return s

ta = bench(traverse_array)
tc = bench(traverse_chain)
print(f'\ntraversal {M:,} nodes:')
print(f'  sequential array : {ta*1000:8.1f} ms')
print(f'  random chain     : {tc*1000:8.1f} ms   ratio {tc/ta:5.2f}x')

সাধারণ ফলাফল (M-series laptop):

AoS interleaved sum :   129.2 ms
SoA contiguous sum  :    11.7 ms
gap                 :    11.04x

matmul n=220 (pure python):
  ijk :      1384 ms  (0.015 GFLOPS)
  ikj :      1040 ms  (0.020 GFLOPS)
  speedup: 1.33x
  numpy 512² BLAS: 2.0 ms (135.0 GFLOPS — tuned blocking+SIMD)

traversal 400,000 nodes:
  sequential array :     30.7 ms
  random chain     :    162.1 ms   ratio  5.28x

পড়ার বিষয়:

১. AoS→SoA gap ~১১× — numpy strided view (aos[:,4]) প্রতিটা row-এ ৬টা অবাঞ্ছিত field skip করে; contiguous SoA pure streaming। Native C-তে gap সাধারণত ২-৪× — layout-এর bandwidth tax universal।

২. Pure-Python matmul-এ ikj মাত্র 1.33× — interpreter overhead locality gain-এর বেশিরভাগ mask করে। আসল গল্পটা BLAS-এর সংখ্যায়: numpy 135 GFLOPS বনাম pure-Python 0.02 — ~৭০০০× gap টাই compiled loop + tiling + SIMD-এর combined verdict; C/C++-এ শুধু reorder-ই সাধারণত ৪-৮× দেয়।

৩. Random-chain 5.3× ধীর — pointer-chasing প্রতি hop-এ cache-miss wait; Python object heap scattered হওয়ায় এটা DRAM-latency proxy হিসেবে নিখুঁত।

এটা কী প্রমাণ করে

Identical asymptotics-এর ভেতরে layout আর access pattern wall-clock বহুগুণ বদলায়: AoS/SoA sum-এ ~১১× gap, random pointer-chain traversal-এ ~৫× cost — আর BLAS-এর ১৩৫ GFLOPS pure-Python-এর ০.০২-এর সাথে tiling+compiled loop-এর সীমানা দেখায়।

নিজে বানান

BUILD IT

Tiled matmul sweep + AoS/SoA pair — awareness tools হাতে

Python · ●●●○○
  1. naive ijk baseline লিখুন
  2. tiled version লিখুন — T parameterized
  3. Sweep T ∈ `{32, 64, 128}` timing print করে curve দেখান
  4. AoS ও SoA pair লিখুন — একই aggregate query, দুই layout
  5. GFLOPS normalize করে তুলনাযোগ্য করুন
  6. Correctness assert: সব version একই result
import time


N = 512


def make_matrices():
    A = [[float((i * 3 + j) % 11) for j in range(N)] for i in range(N)]
    B = [[float((i + 2 * j) % 7) for j in range(N)] for i in range(N)]
    return A, B


def matmul_tiled(A, B, T):
    """blocked matmul — 3·T² floats working set per mini-product"""
    C = [[0.0] * N for _ in range(N)]
    for ii in range(0, N, T):
        for kk in range(0, N, T):
            for jj in range(0, N, T):
                for i in range(ii, min(ii + T, N)):
                    Ai, Ci = A[i], C[i]
                    for k in range(kk, min(kk + T, N)):
                        a = Ai[k]
                        Bk = B[k]
                        for j in range(jj, min(jj + T, N)):
                            Ci[j] += a * Bk[j]
    return C


def gflops(seconds):
    return 2 * N**3 / seconds / 1e9


if __name__ == '__main__':
    A, B = make_matrices()

    def timed(fn, arg):
        t0 = time.perf_counter()
        r = fn(A, B, arg)
        return r, time.perf_counter() - t0

    ref, _ = timed(matmul_tiled, N if False else 512)  # T=N = untiled ikj
    print(f'tiled matmul sweep (n={N}):')
    print(f'{"tile":>6} {"ms":>9} {"GFLOPS":>8}')
    for T in (32, 64, 128, 512):
        out, dt = timed(matmul_tiled, T)
        assert out == ref
        print(f'{T:>6} {dt*1000:>9.0f} {gflops(dt):>8.3f}')

    # ---------- AoS vs SoA pair ----------
    import random
    rnd = random.Random(9)

    class Particle:
        __slots__ = ('x', 'y', 'vx', 'vy')
        def __init__(self):
            self.x = rnd.random(); self.y = rnd.random()
            self.vx = rnd.random(); self.vy = rnd.random()

    M = 300_000
    aos_particles = [Particle() for _ in range(M)]
    soa_x = [rnd.random() for _ in range(M)]
    soa_y = [rnd.random() for _ in range(M)]
    soa_vx = [rnd.random() for _ in range(M)]
    soa_vy = [rnd.random() for _ in range(M)]

    t0 = time.perf_counter()
    acc = 0.0
    for p in aos_particles:
        p.x += p.vx; p.y += p.vy
        acc += p.x
    t_aos = time.perf_counter() - t0

    t0 = time.perf_counter()
    acc2 = 0.0
    for i in range(M):
        soa_x[i] += soa_vx[i]
        soa_y[i] += soa_vy[i]
        acc2 += soa_x[i]
    t_soa = time.perf_counter() - t0

    print(f'\nparticle integrate (M={M:,}, pure python):')
    print(f'  AoS objects : {t_aos*1000:8.1f} ms')
    print(f'  SoA arrays  : {t_soa*1000:8.1f} ms   gap {t_aos/t_soa:4.2f}x')

প্রত্যাশিত output (pattern-focused; absolute numbers vary):

tiled matmul sweep (n=512):
  tile        ms   GFLOPS
    32      1850    0.145
    64      1490    0.180
   128      1620    0.166
   512      2100    0.128

particle integrate (M=300,000, pure python):
  AoS objects :    148.2 ms
  SoA arrays  :     96.5 ms   gap 1.54x

পড়ার বিষয়: Tile curve non-monotone — 64 sweet spot এই synthetic setup-এ; আসল machine-এ L1-size-derived optimum আসবে (build নিজের machine-এ repeat করুন)। T=512 (=untiled) worst — capacity misses dominate। Pure-Python-এ object attribute access-এর overhead SoA gap compress করে; numpy/C rewrite-এ gap বাড়ে।

নিজে বাড়ান:

১. Recursive (oblivious) matmul add করুন — base case threshold parameter remove করে pure subdivision; tiled-sweep-এর সাথে compare। ২. Transposed-B variant (B^T precompute) — ijk-কেও streaming বানায়; transpose cost amortization measure। ৩. numpy-তে একই sweep — BLAS-এর internal blocking detect করার indirect probe: performance cliff positions।

বাস্তব সিস্টেমে

Cache-aware engineering বাস্তবে

BLAS/LAPACK। OpenBLAS, Intel MKL — matmul kernels hand-tuned per microarchitecture: register blocking (microkernel 8×4), cache blocking, software prefetch — এই lesson-এর concepts-এর industrial apex। numpy তাদের ওপরেই বসে।

Game engines। ECS (Entity Component System) architectures SoA by design — data-oriented movement DOD community-তে mainstream করেছে; Unity DOTS/ECS documentation সরাসরি cache-line arguments cite করে।

Databases। Column stores (Parquet, ClickHouse) SoA philosophy; buffer-pool page sizing = blocking; B-tree node size ≈ cache-line multiples — balanced-bst লেসনের B-tree-র disk logic এখানে L1-এ বংশধর।

Networking (Level 7 preview)। DPDK packet processing: hugepage pools, per-core mcache, prefetch next descriptor while current processes — NIC pipeline-এর প্রতিটা ধাপ cache-contract-এ লেখা। Kernel bypass-এর half the win এখান থেকে।

Compilers। Loop-interchange, tiling passes (LLVM Polly), structure reordering (-freorder-fields in clang) — compiler-এর optimizer এই transformation-গুলো automate করার চেষ্টা করে; programmer hint still wins irregular cases।

যে ভুলগুলো সবাই করে

“Big-O better algorithm সবসময় faster — constant factor তো ছোট।”

Sorting লেসনের heapsort (Θ(n log n)) quicksort-কে ২-৪× হারে দেখেছেন; আজ matmul-এ same-Θ reorder 3×। Constants যখন memory-hierarchy derived, তখন 10-100× হয় (DRAM vs L1)। Correct hierarchy: complexity class আগে, তারপর access-pattern engineering — দুটোই mandatory।

“Cache-oblivious মানে cache ব্যবহার না করা।”

নামটা misleading: oblivious মানে hardware parameters code-তে hard-code না করা — কিন্তু recursive structure automatically সব level exploit করে। Proof-এ দেখাই হয় oblivious algorithm-ই প্রতিটা (M, B)-তে optimal miss count পায় — awareness built-in, tuning absent।

“Linked list O(1) insert — তাই dynamic array-র চেয়ে superior।”

Insert O(1) ঠিক, কিন্তু traversal/search O(n) প্রতি step DRAM-latency tax-এ (experiment: 3.4×)। বাস্তব workload-এ 99% traversal — vector + swap-remove প্রায় সবসময় জেতে end-to-end। Asymptotic operation-count আর wall-clock-এর ফারাকটাই এই lesson-এর thesis।

“Prefetcher সব fix করে দেয় — locality optimize করার দরকার নেই।”

Prefetcher stride-pattern শেখে — sequential/fixed-stride-তে excellent, pointer-chasing/tree-তে helpless (address itself unknown until load completes)। Random access pattern-এ prefetcher coverage ~0%। Layout আর traversal-order engineering তবু দরকার — prefetcher amplifier, substitute না।

বুঝেছেন কি না দেখুন

1

L1 hit, L3 hit, DRAM access — approximate cycle costs? DRAM access-এর সময়ে CPU কত ALU op করতে পারত (4 GHz, 1 IPC)?

স্মরণ

L1 ~4, L3 ~40-60, DRAM ~100-300 cycles। DRAM 200-cycle ধরলে @4GHz = 50 ns; 1 IPC-তে 4 GHz = 4G ops/s → 200 cycles = 200 ops opportunity cost। Miss-heavy loop-এ CPU idle-dominant — memory-bound regime।

2

64-byte line, int32 array n=4096। Row-major 64×64 matrix: row-scan vs column-scan misses (cold start)? হিসাব দেখান।

প্রয়োগ

Row-scan: প্রতি row 64 ints = 4 lines; 64 rows → 256 misses total (sequential, perfect spatial use)। Column-scan: প্রতি step stride = 64 ints = 16 lines; প্রতি access নতুন line → 64×64 = 4096 misses — 16× worse, matching line-per-row factor। Ratio always = elements-per-line when stride crosses lines।

3

Binary search array-তে theoretical log₂n misses; Eytzinger (BFS-order) layout কেন কমায়? Concept-level explanation।

যুক্তি

Classic in-order array-তে successive probes (midpoints) memory-তে দূরে-দূরে — প্রতি probe fresh line, prefetch useless (addresses input-dependent)। BFS layout-এ এক subtree-র nodes contiguous — top levels কয়েক line-এ, deeper subtrees আবার local; plus fixed probe pattern software-prefetch friendly। Result: misses ≈ log_B n (B = elements/line) — hierarchical locality both dimensions।

4

Particle simulation: 90% updates need position+velocity, 10% render needs all 12 fields। Layout design + justification।

ডিজাইন

Hot path (update) SoA: px,py,pvx,pvy separate arrays — streaming, SIMD-ready, line-utilization high। Render path (rare) gathers via index list — gather cost acceptable at 10% frequency। Hybrid option: split hot/cold fields (position+velocity SoA; mass/type/lifetime cold AoS array indexed by id) — ‘hot/cold splitting’ standard technique। Decision driver: frequency-weighted line utilization, measured not guessed।

5

Cache-oblivious algorithm-এর defining property? I/O model-এ (M, B) কী?

স্মরণ

Property: algorithm-এ M বা B-র কোনো mention/reference নেই — কিন্তু analysis prove করে miss-optimal for ALL (M,B) simultaneously। I/O model: M = fast-memory (cache) size, B = transfer block (line) size; cost metric = block transfers between slow/fast memory, computation free। Ideal-cache model adds auto-replacement assumption (offline-optimal paging)।

6

Module retrospective: sorting (lesson 17) থেকে আজ পর্যন্ত — কোন তিনটা lesson-এ memory-hierarchy reality সরাসরি verdict দিয়েছে? এক লাইন করে।

যুক্তি

Lesson 17 sorting: heapsort-এর stride-jump cache-miss quicksort-কে ২-৪× হারায় despite same-Θ। Lesson 22 string: Boyer-Moore/Horspool skip = locality exploitation; libc hybrids cache-tuned। Lesson 24 today: layout/ loop-order/tiling — whole lesson। Undercurrent: asymptotics picks family, silicon picks winner।

এরপর কী

এরপর কী — module শেষ, journey চলছে

Algorithms module-এর ২৪-এর ল্যাডার এখানে শেষ: induction-এ শুরু, cache line-এ সমাপ্তি। আমরা দেখলাম algorithm design-এর প্রতিটা major paradigm — divide-conquer, DP, greedy, randomization — আর তাদের প্রত্যেকের সাথে silicon-এর সত্য কীভাবে negotiate করে। এই toolkit দিয়ে আপনি interview-এর puzzle না, production system-এর trade-off পড়তে পারবেন।

Level 7 খুলছে Networking নিয়ে: packet কীভাবে NIC থেকে browser-এ যায় — Ethernet frame, IP routing, TCP reliability, congestion control। আজকের DPDK-mention সেখানে পূর্ণ story হবে; hash-tables-এর knowledge flow-table-এ, আজকের locality-discipline packet-batch processing-এ। Hardware-cost awareness এখন আপনার reflex — network protocol-গুলো ঠিক সেই reflex-ই চায়।

আরও পড়ুন

  • Introduction to High Performance Computing for Scientists and Engineers, Chapter 1-3 — Georg Hager, Gerhard Wellein · Loop optimization, tiling, roofline model — lesson 24-এর matmul analysis-এর মূল উৎস
  • Computer Architecture: A Quantitative Approach, Appendix B & Chapter 2 — John L. Hennessy, David A. Patterson · Memory hierarchy design আর cache performance metrics-এর প্রামাণ্য আলোচনা
  • Cache-Oblivious Algorithms (FOCS '99) — Matteo Frigo, Charles Leiserson, Harald Prokop, Sridhar Ramachandran · Cache-oblivious model-এর original paper — van Emde Boas layout concept-এর জন্মস্থান