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) বাধ্য করে।
আগে এটা বুঝি
পৃথিবীর সবচেয়ে বেশি ব্যবহৃত 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-এর ঠিকানা:
একটা গুণ, একটা যোগ — 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 প্রতি ১ elementInsertion/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 দুই-ই মিলিয়ে।
- BytecodeBINARY_SUBSCR — interpreter প্রথমে bounds check, type check
- Pointer tablelist-এর internal ob_item array-তে base + 8*i — এই পড়াটা cache-friendly
- Dereferenceslot-এর pointer অনুসরণ — আসল PyLong object, যেকোনো heap ঠিকানায়; এখানেই possible cache miss
- 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-গুলো পরপর —
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 linecache line (64 B) consumption while visiting 16 elements:
row-wise: line1 ████████ (৮টা 8-byte element — পুরোটা কাজে লাগে)
line2 ████████
মোট miss ≈ ২
col-wise: line1 █▁▁▁▁▁▁▁ (১টা element, ৭টা ফেলে দেওয়া)
line2 █▁▁▁▁▁▁▁
...
line8 █▁▁▁▁▁▁▁
মোট miss ≈ ৮ — ৪× বেশি মেমরি traffic, একই গণনানিজে চালিয়ে দেখুন
Traversal rhythm মাপুন — row বনাম column, sequential বনাম strided
# 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.pyTypical ফলাফল (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 শব্দ না।
নিজে বানান
FlatMatrix — এক-dimension-এ 2D, iterator দিয়ে rhythm benchmark
- Flat list-এ rows × cols slot রাখুন — set/get করুন r*cols+c arithmetic দিয়ে
- iter_rows() generator লিখুন — flat index ধাপে ধাপে +1, sequential touch
- iter_cols() generator লিখুন — flat index প্রতি ধাপে +cols, strided touch
- দুই iterator-এ sum করে সমান কি না assert করুন — correctness প্রথম
- দুই 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× দেখাত।)
নিজে বাড়ান:
- Transpose method যোগ করুন — flat swap করে করুন, কোন copy ছাড়া; transpose-এর পরে iter_rows কোন rhythm-এ চলে?
from_nested(nested_list)classmethod — nested Python list থেকে flat conversion, row-major assumption লিখিত করুন docstring-এ- 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-এর পদ্ধতিই এটা।
বুঝেছেন কি না দেখুন
1Base 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)-এর জাদু ভাঙে।
2Row-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।
5Python 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