Foundationপ্রথম নীতি থেকে
LEVEL 6লেসন ৩/২৪মাঝারি৪০ মিনিট

Array ও Dynamic Array — memory layout থেকে performance পর্যন্ত

Arrays and Dynamic Arrays

Array-র সব শক্তি তার memory layout-এ — contiguous slots-এর address arithmetic O(1) indexing দেয়, spatial locality cache থেকে ফ্রি speedup দেয়, আর একই কারণ ঘুরিয়ে middle insertion-কে O(n) বাধ্য করে।

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

  • Contiguous memory layout-কে base + i × size address arithmetic-এ নামিয়ে ব্যাখ্যা করতে পারবেন — কেন fixed-size element-ই O(1) indexing-এর পূর্বশর্ত
  • Cache line আর Locality of Reference-এর ভাষায় বলতে পারবেন কেন sequential scan random hop-কে বাস্তবে হারায়, আর সেই ফারাক nanosecond-এ কত বড়
  • Row-major আর column-major layout-এর পার্থক্য দেখিয়ে বলতে পারবেন কেন loop nesting order traversal-এর খরচ বদলে দেয়
  • Python list আসলে pointer-দের contiguous array — value না — এই সত্যটার performance-implication ব্যাখ্যা করতে পারবেন
  • Middle insertion/deletion-এর O(n) খরচ ঠিক কোথায় ঘটে (shift) তা trace করে, কখন সেই খরচ মেনে নেওয়া যুক্তিসঙ্গত তা সংখ্যা দিয়ে বিচার করতে পারবেন

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

আগে এটা বুঝি

পৃথিবীর সবচেয়ে বেশি ব্যবহৃত data structure-টা সম্ভবত আপনার পছন্দের তালিকায় সবার নিচে: plain old array। Interview-prep culture-এ এটা “basic” — মজিয়ার জিনিস linked list, tree, graph। অথচ production code-এ performance নিয়ে সবচেয়ে বড় লড়াই প্রায়ই array-র layout আর memory-র মধ্যেই জেতা-হারানো। Database engine, game engine, ML framework — সবাই টাকার বিনিময়ে linked node এড়িয়ে contiguous buffer বাছে। কেন?

উত্তরটা asymptotic notation-এ নেই — দুই-ই Θ(n) traversal। উত্তরটা আছে hardware-এ: কোথায় বসে আছে ডেটা। Array-র প্রতিশ্রুতি একটাই, কিন্তু সেটা ব্যাপক: সব slot মেমরিতে পাশাপাশি। এই একটা promise থেকে O(1) indexing-এর arithmetic, cache-এর ফ্রি speedup, prefetcher-এর সহযোগিতা — সব ওঠে। আর একই promise-এর দামও ওঠে: middle insertion মানে সব পিছিয়ে দেওয়া।

আজকের প্রশ্ন তিনটা: (১) “array[i]” লাইনটা silicon-এ ঠিক কোন arithmetic হয়? (২) পাশাপাশি থাকাটা runtime-এ কত টাকার — কোন সংখ্যায়? (৩) একই layout-এর দাম কোথায় দিতে হয়?

মূল ধারণা

Address arithmetic — O(1)-এর আসল রূপ

Computer-representation module থেকে মনে করুন: মেমরি হলো বিশাল একটা array of bytes, প্রতিটার ঠিকানা আছে। Array-র contract: তার n টা slot এই ঠিকানা-space-এ টানা বসে। Element size sz byte হলে (যেমন 64-bit integer → sz = 8), i-তম element-এর ঠিকানা:

address(i)=base+i×sz\text{address}(i) = \text{base} + i \times sz

একটা গুণ, একটা যোগ — CPU এটা এক instruction-এই করে। এখানেই O(1) indexing-এর পুরো রহস্য: খোঁজা হয় না, হিসাব করা হয়। Linked structure-এ i-তম element পেতে i টা pointer follow করতে হয়; array-তে হিসাব একবার।

শর্তটা খেয়াল করুন: sz fixed হতে হবে। Element-গুলো ভিন্ন ভিন্ন size হলে i-তমটার ঠিকানা আর formula করে পাওয়া যায় না — ঘুরে ঘুরে গুনতে হয়। Fixed-size element-ই array-কে array করে। (Variable-length data রাখতে হলে কৌশল: array-তে fixed-size pointer/index রাখো, আসল ডেটা অন্যত্র — Python list ঠিক এটাই করে, hood section-এ দেখব।)

Spatial locality — পাশাপাশি থাকার লভ্যাংশ

আপনি computer-architecture module-এ দেখেছেন: CPU মেমরি থেকে সরাসরি এক byte পড়ে না — cache line ধরে পড়ে (সাধারণত 64 byte)। Byte x চাইলে x-কে ঘিরে পুরো 64-byte block cache-এ চলে আসে। Array-র জন্য এটা উৎসব: element 0 ছোঁলে element 1..7 (8-byte int হলে) ফ্রিতে cache-এ ঢুকে আছে। Sequential scan-এ প্রতি ৮টা access-এর ৭টা cache hit — মেমরি bandwidth ৮ গুণ effective।

এই ঘটনাটার নামই spatial locality — “একটা জায়গা access করলে কাছাকাছি জায়গাও শিগগির access হবে” এই prediction, আর array সেই prediction-কে প্রতিবার সত্য করে তোলে। Prefetcher আরও এগিয়ে যায়: চলমান sequential pattern দেখে CPU নিজেই পরের cache line-গুলো আগে এনে রাখে। Random hop-এ prefetcher অন্ধ — প্রতিটা access potential cache miss।

sequential scan (stride = element size):      random hop:
line:  [########]                             line: [#.......]
       ▶▶▶▶▶▶▶▶ ১ টা miss, ৭ টা hit                 ▶ ১ টা miss, বাকি ফেলে
line:  [########]                             line: [......#.]
       ▶▶▶▶▶▶▶▶ ১ টা miss                              ▶ আবার ১ miss
miss প্রতি ৮ element                          miss প্রতি ১ element

Insertion/deletion — একই layout-এর বিল

Middle-এ একটা slot ঢোকাতে হলে তার ডানের সব element এক ঘর করে ডানে সরতে হয় — গড়ে n/2 টা move, মানে Θ(n)। Deletion-ও একই, উল্টো দিকে। End insertion একমাত্র free — সেখানে কাউকে সরাতে হয় না, আর capacity ফুরালে dynamic array-র amortized doubling (গত লেসন) খরচ টেনে নেয়।

Trade-off-টা এখন সম্পূর্ণ দৃশ্যমান: contiguity indexing আর locality দেয়, কিন্তু structural change দামি করে। Structure বদলানোর স্বাধীনতা চাইলে pointer-ভিত্তিক structure-এর দিকে যেতে হবে — পরের লেসনের প্রস্তাবনা।

ভেতরে কী ঘটছে

Python list — সত্যি কথাটা

CPython-এর list আসলে কী? Values-এর array না — pointer-দের contiguous array। প্রতিটা slot 8 byte-এর একটা PyObject* — আসল integer/string object গুলো heap-এ ছড়ানো-ছিটানো বসে আছে। Layout:

Python list:  [ptr|ptr|ptr|ptr|ptr|...]     ← এই অংশ contiguous ✓
                     │        │
                     ▼        └──→ PyLong(7)   ← আসল object heap-এ
                     └───────→ PyLong(3)          যেখানে সে, সেখানেই

Implication তিনটা:

  • Indexing তবু O(1) — pointer-table-এর উপর base + 8·i arithmetic
  • Locality আংশিক — pointer-table scan করলে table নিজে sequential; কিন্তু প্রতিটা dereference আসল object-এ যায়, যেটা হয়তো cache-এর অন্যপ্রান্তে। Small int cache (-5..256) আর interned string কিছুটা বাঁচায়।
  • Element size uniform — pointer সব 8 byte, তাই arithmetic চলে

C-র int arr[1000]-এর সঙ্গে তুলনা করুন: সেখানে values নিজেরাই পাশাপাশি, কোনো indirection নেই — locality full। NumPy array Python list-কে এই কারণেই হারায়: typed, unboxed, contiguous — 10–100× gap interpreter tax + layout দুই-ই মিলিয়ে।

xs[i] একটা expression-এর সিলিকন-যাত্রা
  1. BytecodeBINARY_SUBSCR — interpreter প্রথমে bounds check, type check
  2. Pointer tablelist-এর internal ob_item array-তে base + 8*i — এই পড়াটা cache-friendly
  3. Dereferenceslot-এর pointer অনুসরণ — আসল PyLong object, যেকোনো heap ঠিকানায়; এখানেই possible cache miss
  4. Unboxobject-এর ভেতরের digit field পড়ে value বের করা — ছোট int-এ cached, বড়-তে arbitrary precision path

Row-major — ২D-র সত্যিটা এক-dimension-এ

Memory আসলে ১D — 2D array বলে কিছু নেই, আছে layout convention। Row-major (C, Python, NumPy default): row-গুলো পরপর —

offset(r,c)=r×cols+c\text{offset}(r, c) = r \times \text{cols} + c

Column-major (Fortran, MATLAB): কলামগুলো পরপর। Convention দুটোই valid — কিন্তু traversal order-এর সঙ্গে মিলটাই সব। Row-major layout-এ row-wise scan sequential; column-wise scan প্রতি ধাপে cols × element_size লাফ — প্রতিটা লাফ নতুন cache line, প্রতিটা potential miss। একই গাণিতিক কাজ, ভিন্ন ভৌত layout-touch pattern। Experiment-এ এই পার্থক্য নিজের মেশিনে মাপবেন।

উদাহরণ

ছোট্ট একটা 3×4 matrix — হাতে হিসাব

Row-major flat storage, 4 কলাম, element 8 byte, base = 1000:

logical view:          flat slots (index = r*4+c):      address = 1000 + 8*index:

[ 10  20  30  40 ]     idx 0  1  2  3  4  5 ...         idx 0 -> 1000
[ 50  60  70  80 ]          10 20 30 40 50 60           idx 1 -> 1008
[ 90 100 110 120 ]                                      idx 5 -> 1040

m[2][1] = 100?
  offset = 2*4 + 1 = 9  ->  address = 1000 + 72 = 1072  ✓

কোনো search নেই — দুই গুণ, এক যোগ। এবার insertion trace: index 1-এ নতুন value 15 ঢোকাতে হলে:

before:  10  20  30  40  50  60  _
step 1:  10  20  30  40  50  _  60     (60 ডানে)
step 2:  10  20  30  40  _  50  60     (50 ডানে)
step 3:  10  20  30  _  40  50  60     (40 ডানে)
step 4:  10  20  30  _  40  50  60     ...
final:   10  15  20  30  40  50  60    (20 পর্যন্ত সরল)

n = ৬ element, head-insertion — ৬টা move। Tail-insertion হলে ০ move। গড় করলে n/2। এটাই O(n)-এর দৈহিক রূপ — কোনো abstract complexity না, হাতে গোনা assignment।

ছবিতে

দুই layout, দুই traversal — পাশাপাশি

Row-major flat storage (cols=4):

addr:   1000 1008 1016 1024 | 1032 1040 1048 1056 | 1064 ...
        [ r0c0 r0c1 r0c2 r0c3 ][ r1c0 r1c1 r1c2 r1c3 ][ r2c0 ...

row-wise scan:  ────────────────────────────────────▶  প্রতি ধাপ পরের slot
col-wise scan:  ─┐   ┌───────────────────────────────▶  প্রতি ধাপে 32-byte লাফ
                 └─▶ │   (c স্থির, r++ → addr + 32)
                     └── প্রতি access নতুন cache line
cache line (64 B) consumption while visiting 16 elements:

row-wise:   line1 ████████  (৮টা 8-byte element — পুরোটা কাজে লাগে)
            line2 ████████
            মোট miss ≈ ২

col-wise:   line1 █▁▁▁▁▁▁▁  (১টা element, ৭টা ফেলে দেওয়া)
            line2 █▁▁▁▁▁▁▁
            ...
            line8 █▁▁▁▁▁▁▁
            মোট miss ≈ ৮  — ৪× বেশি মেমরি traffic, একই গণনা
Same work, different memory rhythm — row-wise touch pattern matches physical order so every cache line is fully consumed; column-wise jumps discard most of each fetched line, multiplying misses by roughly the row length.

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

EXPERIMENT

Traversal rhythm মাপুন — row বনাম column, sequential বনাম strided

Linux/macOS, Python 3.8+· ১৫ মিনিট
# array_lab.py — traversal pattern-এর আসল দাম
# চালান: python3 array_lab.py
import time

N = 1000
M = [[r * N + c for c in range(N)] for r in range(N)]

def sum_rowmajor():
    total = 0
    for row in M:               # row list ধরে direct iteration — sequential
        for v in row:
            total += v
    return total

def sum_colmajor():
    total = 0
    for c in range(N):          # c স্থির, r বাড়ে — M[r][c] প্রতি ধাপে লাফ
        for r in range(N):
            total += M[r][c]
    return total

def bench(fn, reps=3):
    best = float('inf')
    for _ in range(reps):
        t0 = time.perf_counter()
        fn()
        best = min(best, time.perf_counter() - t0)
    return best

tr = bench(sum_rowmajor)
tc = bench(sum_colmajor)
print(f'matrix 1000x1000:')
print(f'  row-major    : {tr*1000:7.1f} ms')
print(f'  column-major : {tc*1000:7.1f} ms   ratio {tc/tr:.2f}x')

xs = list(range(10_000_000))

def scan_stride(step):
    total = 0
    for i in range(0, len(xs), step):
        total += xs[i]
    return total

print('strided 1D scan (same element counts differ):')
for step in (1, 8, 64):
    t = bench(lambda s=step: scan_stride(s))
    visited = len(range(0, len(xs), step))
    print(f'  stride {step:>2}: {t*1000:8.1f} ms '
          f'({visited:,} elements, {t/visited*1e9:6.1f} ns/element)')
python3 array_lab.py

Typical ফলাফল (laptop, CPython 3.11):

matrix 1000x1000:
  row-major    :    28.4 ms
  column-major :    51.9 ms   ratio 1.83x
strided 1D scan (same element counts differ):
  stride  1:    241.3 ms (10,000,000 elements,   24.1 ns/element)
  stride  8:     47.9 ms ( 1,250,000 elements,   38.3 ns/element)
  stride 64:      9.8 ms (   156,250 elements,   62.7 ns/element)

পড়ার সততা দুই স্তরে:

  • Matrix test — ratio ~1.8×। তবে সতর্ক থাকুন: CPython-এ M[r][c] double-indexing-এর interpreter overhead-ও col-major path-এ বেশি; cache effect আর interpreter effect এখানে মিশে আছে। Compiled language-এ (C/Rust) একই experiment সাধারণত 5–10× দেখায় — কারণ সেখানে interpreter noise নেই, খরচ প্রায় pure memory rhythm।
  • Stride test — এটা cleaner isolation: stride 8 মানে প্রতি access-এ এক cache line ফেলে দেওয়া (64 B line / 8 B pointer), আর per-element খরচ সত্যিই 24 → 63 ns-এ উঠছে। Visited কমলে মোট সময় কমে, কিন্তু per-touched-element দাম বাড়ে — এটাই locality- র মূল্যের receipt।

যা প্রমাণিত: “পাশাপাশি থাকা” একটা মাপযোগ্য সম্পদ। Access pattern layout-এর সঙ্গে মিললে hardware আপনাকে subsidise করে; না মিললে একই algorithmic কাজের দাম বাড়ে — Big-O একই থেকে।

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

একই গাণিতিক কাজ layout-touch order বদলালে runtime বদলে যায় — row-major matrix-এ row-wise sum column-wise sum-কে বাস্তবে হারায়, আর 1D scan-এ stride বাড়ালে per-element খরচ বাড়ে; locality একটা measurable hardware বাস্তবতা, textbook শব্দ না।

নিজে বানান

BUILD IT

FlatMatrix — এক-dimension-এ 2D, iterator দিয়ে rhythm benchmark

Python · ●●○○○
  1. Flat list-এ rows × cols slot রাখুন — set/get করুন r*cols+c arithmetic দিয়ে
  2. iter_rows() generator লিখুন — flat index ধাপে ধাপে +1, sequential touch
  3. iter_cols() generator লিখুন — flat index প্রতি ধাপে +cols, strided touch
  4. দুই iterator-এ sum করে সমান কি না assert করুন — correctness প্রথম
  5. দুই traversal benchmark করে side-by-side timing ছাপুন

লক্ষ্য: 2D-র মিথ্যাটা নিজের হাতে ভাঙা — memory-তে শুধু এক dimension, বাকিটা আপনার arithmetic আর iteration order।

# flatmatrix.py — flat storage + rhythm-aware iterators
# চালান: python3 flatmatrix.py

class FlatMatrix:
    __slots__ = ('rows', 'cols', 'data')

    def __init__(self, rows, cols, fill=0):
        self.rows = rows
        self.cols = cols
        self.data = [fill] * (rows * cols)

    def index(self, r, c):
        if r >= self.rows or c >= self.cols or r * -1 > self.rows or c * -1 > self.cols:
            raise IndexError('out of range')
        return r * self.cols + c

    def get(self, r, c):
        return self.data[self.index(r, c)]

    def set(self, r, c, value):
        self.data[self.index(r, c)] = value

    def iter_rows(self):
        """Sequential touch — প্রতি ধাপে flat-index +1"""
        pos = 0
        for _ in range(self.rows):
            for _ in range(self.cols):
                yield self.data[pos]
                pos += 1

    def iter_cols(self):
        """Strided touch — প্রতি ধাপে flat-index +cols"""
        for c in range(self.cols):
            pos = c
            for _ in range(self.rows):
                yield self.data[pos]
                pos += self.cols


def bench(fn, reps=5):
    best = float('inf')
    for _ in range(reps):
        t0 = time.perf_counter()
        fn()
        best = min(best, time.perf_counter() - t0)
    return best


if __name__ == '__main__':
    import time
    R = C = 1000
    m = FlatMatrix(R, C)
    for i in range(R * C):
        m.data[i] = i

    # spot-check the arithmetic
    m.set(2, 3, 999)
    assert m.get(2, 3) == 999
    assert m.index(2, 3) == 2 * C + 3

    sr = sum(m.iter_rows())
    sc = sum(m.iter_cols())
    print(f'sum check  : {sr} == {sc} -> {sr == sc}')

    tr = bench(lambda: sum(m.iter_rows()))
    tc = bench(lambda: sum(m.iter_cols()))
    print(f'row iterator : {tr*1000:7.1f} ms')
    print(f'col iterator : {tc*1000:7.1f} ms   ratio {tc/tr:5.2f}x')

প্রত্যাশিত output-এর আকৃতি:

sum check  : 499499500000 == 499499500000 -> True
row iterator :    31.2 ms
col iterator :    44.8 ms   ratio  1.44x

(Ratio Python-এ modest — generator overhead দুই path-এই common। Generator দুটোর ভেতরের flat-index increment-এর পার্থক্য ছাড়া কাজ identical, তাই যা পার্থক্য থাকে সেটা memory rhythm + branch pattern- এর। C-তে একই হারনেস 3–8× দেখাত।)

নিজে বাড়ান:

  1. Transpose method যোগ করুন — flat swap করে করুন, কোন copy ছাড়া; transpose-এর পরে iter_rows কোন rhythm-এ চলে?
  2. from_nested(nested_list) classmethod — nested Python list থেকে flat conversion, row-major assumption লিখিত করুন docstring-এ
  3. Column-major variant class বানিয়ে (index(c,r) মানে c*rows+r) দুই class-এর দুই iterator cross-benchmark করুন — layout আর traversal-এর চার combination-এর পূর্ণ ছক

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

Layout thinking বাস্তবে যেখানে দেখা যায়

NumPy আর Pandas। NumPy ndarray-র C-order মানেই row-major flat buffer — arr.sum(axis=1) axis=0-এর চেয়ে দ্রুত কারণ এটা sequential touch। Pandas DataFrame ভেতরে column-major (প্রতিটা কলাম একটা block) — কলাম-wise operation দ্রুত, row-wise ধীর। একই library family, উল্টো layout — documentation এসব explicitly বলে।

Image processing। RGB bitmap row-major: নিচের pixel = পরের few byte। Horizontal blur sequential-friendly; vertical blur strided — production image pipelines vertical pass-কে tile করে বা transposed buffer-এ চালায় ঠিক এই কারণে। Game engines-এ texture swizzling আরও অগ্রসর রূপ।

Database row-store বনাম column-store। Analytics query “সব row-এর শুধু age কলাম যোগ করো” — row-store-এ প্রতি record-এর পুরো width scan (অপ্রাসঙ্গিক কলামগুলোসহ); column-store-এ age values পাশাপাশি — pure sequential। ClickHouse, Parquet, DuckDB-র পুরো thesis এই lesson-এর macro-scale version: query-র touch-pattern layout-কে নির্ধারণ করা উচিত।

Data-oriented design। Game studio culture-এর জন্ম দেওয়া movement: class hierarchy থেকে struct-of-arrays-এ — position.x সব entity-র এক array-তে, position.y আরেকটায় — যাতে update loop sequential হয়। Entity Component System (ECS) architecture-র কেন্দ্রীয় motivation ঠিক এই লেসনের content-ই।

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

“Array-র O(1) indexing মানে array access সবসময় দ্রুত।”

O(1) মানে constant count of arithmetic, constant nanoseconds না। Gigabyte-sized array-তে random index মানে প্রায় প্রতিবার cache miss, অনেক সময় TLB miss পর্যন্ত — একটা access ১০০ ns+ খেতে পারে, যখন L1-hit access ~১ ns। Complexity model আর machine model আলাদা layer — এই module-এর প্রথম লেসন থেকেই আমাদের মূল থিম। Array-র আসল সুবিধা random access-এও আংশিক: address জানা থাকে, শুধু ডেটা আসতে দেরি।

“Python list-ও C array-র মতোই contiguous — values পাশাপাশি বসে আছে।”

Contiguous শুধু pointer table — প্রতি slot 8-byte reference। আসল objects heap-এ ছড়ানো। ফলে Python list-এর locality হলো two-level: table scan sequential (ভালো), dereference random (দামি)। একই কারণে sys.getsizeof([0]*10) প্রায় ১৩৬ byte — values-এর জন্য না, pointers+ overhead-এর জন্য। Performance-critical Python-এর প্রথম advice এজন্যই NumPy/array.array-তে যাওয়া — boxed object layer সরিয়ে true contiguous values।

“Row-major বনাম column-major শুধু naming convention — performance সমান।”

Convention হ্যাঁ, harmless না। Loop nesting order layout-এর সঙ্গে মিলছে কি না — এই একটা সিদ্ধান্তে miss rate কয়েকগুণ বদলায়। Fortran-এ লেখা numeric code C-তে port করতে গিয়ে loop গুলো না উল্টালে performance ধ্বংস — ইতিহাসে বহুবার ঘটেছে। নিয়মটা মুখস্থ রাখুন: ভেতরের loop-এর index টা layout-এর fastest-varying dimension হতে হবে (row-major-তে column index)।

“Insertion O(n) তাই array থেকে insertion-heavy কাজে array এড়ানোই শ্রেষ্ঠ।”

Constants আবার ফিরে এসেছে। memmove-ভিত্তিক shift প্রতি nanosecond-এ কয়েক byte সরায় — ১০,০০০-element array-র middle insert আসলে কয়েক microsecond। Linked list-এর “O(1)” insertion-এর আগে তো O(n) search লাগবেই, প্রতিটা node allocate + GC pressure + pointer chase। বাস্তব benchmark-এ ছোট-মাঝারি n-এ array-with-insertion প্রায়ই জেতে। Rule: asymptotic class দেখে shortlist করুন, measurement দিয়ে final pick — এই module-এর পদ্ধতিই এটা।

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

1

Base address 2000, element size 16 byte। Index 25-এর address কত? কোন শর্তে এই এক-লাইনের হিসাব সম্ভব?

স্মরণ

2000 + 25 × 16 = 2000 + 400 = 2400। শর্ত: সব element same fixed size আর slots contiguous। Element size vary করলে বা slots ছড়ানো থাকলে i-তম element-এর location আর formula-য় আসে না — ঘুরে গুনতে হয়, আর তখনই O(1)-এর জাদু ভাঙে।

2

Row-major, 512 কলামের matrix-তে m[100][200]-এর flat index কত? Column-major হলে কত? (rows = 512)

প্রয়োগ

Row-major: r × cols + c = 100 × 512 + 200 = 51,400। Column-major: c × rows + r = 200 × 512 + 100 = 102,500। একই logical cell, ভিন্ন physical home — layout বদলালে একই element- এর neighborhood পুরোপুরি বদলে যায়, তাই traversal-এর locality-ও।

3

আপনার nested loop-এ বাইরের loop c (column), ভেতরের loop r (row) — row-major layout-এ। এটা fast না slow pattern? ঠিক করতে কী করবেন?

যুক্তি

Slow pattern: ভেতরের loop r বাড়ায় address + cols × size লাফ দেয় — প্রতি access-এ নতুন cache line, পুরো line-এর বাকি byte বরবাদ। Fix দুইভাবে: (১) loop swap করুন — ভেতরে c, বাইরে r — এখন ভেতরের loop sequential; (২) loop swap করা সম্ভব না হলে (dependency) block tiling করুন — matrix-কে cache-fit sub-blocks-এ ভাগ করে প্রতি block- এ row-wise কাজ। মূলনীতি: ভেতরের loop-এর stride যত ছোট তত ভালো, ideal 1।

4

আপনার app-এ ৫০ লক্ষ user record, প্রতিদিন দুই ধরনের query: (a) এক user-এর সব field আনো, (b) সব user-এর শুধু last_login তুলনা করো। Single table design-এ কোন layout বেছে নেবেন — আর দুই query-ই critical হলে কী?

ডিজাইন

(a) dominant হলে row-store: এক record-এর সব field পাশাপাশি — এক region read। (b) dominant হলে column-store: last_login values sequential — pure scan। দুই-ই critical হলে hybrid আছে: HTAP systems (row store OLTP + column replica for analytics), বা PAX-style within-page mixing। Design principle একই: dominant access pattern-এর stride-ই storage layout নির্ধারণ করবে — এটা array lesson-এর সরাসরি scaling।

5

Python list-এ ১০ লক্ষ element scan করার সময় pointer-table locality পেলেও dereference-এ miss হয় — কোন কোন অবস্থায় এই দাম কমে যায়?

স্মরণ

যখন objects নিজেরাও cluster করা: (১) small int cache — -5..256 সব scan একই কয়েক object share করে, প্রথম miss-এর পরে সব hot; (২) interned strings/identifiers; (৩) একসাথে বানানো object-রা প্রায়ই heap-এ পাশাপাশি allocation-এ পড়ে (allocator bump behavior)। (৪) সবচেয়ে নিশ্চিত রাস্তা: array.array বা NumPy — boxed layer-ই বাদ, table-ই হয়ে যায় values। মানে: Python-এর locality tax configurable, শূন্য না।

এরপর কী

এরপর কী

Array-র সব লাভ একটা promise থেকে এসেছে: সবাই পাশাপাশি। পরের লেসনে promise-টা ভেঙে দেব — প্রতিটা node নিজে নিজের ঠিকানায়, পরের node-এর নাম হাতে ধরে। Linked list asymptotically কিছু জায়গায় জেতে (O(1) splice, stable iterator), কিন্তু pointer chasing-এর প্রতিটা হপ একটা potential cache miss — আর আমরা দেখব এই hardware বাস্তবতা Big-O টেবিল- কে কীভাবে উল্টে দেয়: যেখানে theory বলে list জেতার কথা, measurement বলে array জেতে। শেষে dict + doubly-linked list মিলিয়ে সত্যিকারের LRU cache বানাব — যে structure-টা Redis, OS page cache আর প্রতিটা interview panel-এর প্রিয়।

পরের লেসন: Linked List — pointer chasing-এর আসল দাম।

আরও পড়ুন

  • Computer Systems: A Programmer's Perspective (3rd edition), Chapter 6 — The Memory Hierarchy — Randal E. Bryant, David R. O'Hallaron · Cache locality আর strided access-এর quantitative treatment — এই লেসনের experiment-এর theoretical ভিত্তি
  • Computer Organization and Design (RISC-V Edition), Chapter 2 & 5 — David A. Patterson, John L. Hennessy · Address arithmetic আর byte-addressable memory — computer-representation module-এর ধারাবাহিকতা
  • Algorithms (4th edition), Section 1.3 — Bags, Queues, and Stacks — Robert Sedgewick, Kevin Wayne · Fixed-capacity array থেকে resizing array-র ডিজাইন reasoning