Cara data tersimpan di memori mempengaruhi performa secara dramatis. Di episode ini kalian memahami contiguous vs pointer-based untuk cache locality, cache line dan prefetching, struct-of-arrays vs array-of-structs, serta mempraktikkan perbandingan waktu iterasi pada data 100 ribu elemen.

Setelah di episode 24 kita memahami decision matrix untuk memilih struktur data, pada episode ini kita mendalami aspek yang sering diabaikan: memory layout — bagaimana cara data tersimpan di memori secara fisik mempengaruhi performa secara dramatis. Dua algoritma dengan kompleksitas Big-O yang sama bisa memiliki perbedaan kecepatan 10x atau lebih hanya karena salah satunya lebih "cache-friendly".
Ini adalah episode yang akan mengubah cara kalian berpikir tentang "efisien". Big-O memberitahu kalian tentang pertumbuhan, tetapi cache behavior memberitahu kalian tentang konstanta — dan di production, konstanta bisa jauh lebih penting dari yang kalian bayangkan.
Array menyimpan elemen secara berurutan di memori. Ketika CPU mengakses array[i], ia memuat cache line (biasanya 64 byte) yang berisi beberapa elemen sekaligus. Akses berikutnya ke array[i+1] sudah ada di cache → sangat cepat.
Linked list menyimpan node tersebar di memori, dihubungkan oleh pointer. Setiap node bisa berada di lokasi yang sangat berjauhan. Ketika CPU mengakses node, ia harus memuat cache line yang mungkin hanya berisi satu node → cache miss sering terjadi.
import time
data_list = list(range(1000000))
linked_values = list(range(1000000))
# Array traversal (cache-friendly)
start = time.perf_counter()
total = 0
for x in data_list:
total += x
array_time = time.perf_counter() - start
print(f"Array traversal: {array_time:.4f}s")
# Linked list traversal akan jauh lebih lambat karena cache missesCache line adalah unit data最小 yang dimuat dari RAM ke cache. Biasanya 64 byte. Ketika kalian mengakses satu byte, CPU memuat seluruh 64 byte ke cache → akses ke byte tetangga sangat cepat.
Prefetching adalah teknik CPU untuk memuat data ke cache sebelum benar-benar diminta. Prefetcher mendeteksi pola akses sekuensial dan memuat data berikutnya. Ini sangat efektif untuk array, tetapi tidak efektif untuk linked list karena node tersebar acak.
Cache miss terjadi ketika data yang diminta tidak ada di cache dan harus diambil dari RAM. Setiap cache miss menambah latensi ~100 ns — cukup untuk membuat perbedaan dramatis pada loop besar.
class Particle:
def __init__(self, x, y, z, mass):
self.x = x
self.y = y
self.z = z
self.mass = mass
particles = [Particle(1.0, 2.0, 3.0, 1.0) for _ in range(100000)]AoS: setiap partikel lengkap (x, y, z, mass) disimpan berurutan. Jika kalian hanya butuh x, kalian tetap memuat y, z, dan mass ke cache → pemborosan bandwidth.
xs = [1.0] * 100000
ys = [2.0] * 100000
zs = [3.0] * 100000
masses = [1.0] * 100000SoA: semua x tersimpan berurutan, semua y tersimpan berurutan, dst. Jika kalian hanya butuh x, kalian memuat hanya x → cache-friendly untuk akses sebagian field.
import sys
class AoS:
def __init__(self):
self.x = 0.0
self.y = 0.0
self.z = 0.0
self.mass = 0.0
class SoA:
def __init__(self):
self.xs = []
self.ys = []
self.zs = []
self.masses = []
a = AoS()
s = SoA()
print(f"AoS size per particle: {sys.getsizeof(a)} bytes")
print(f"SoA overhead: {sys.getsizeof(s)} bytes (empty)")import time
n = 100000
# AoS
class Particle:
__slots__ = ['x', 'y', 'z', 'mass']
def __init__(self, x, y, z, mass):
self.x = x
self.y = y
self.z = z
self.mass = mass
particles = [Particle(float(i), float(i), float(i), 1.0) for i in range(n)]
start = time.perf_counter()
total = 0
for p in particles:
total += p.x
aos_time = time.perf_counter() - start
# SoA
xs = [float(i) for i in range(n)]
start = time.perf_counter()
total = 0
for x in xs:
total += x
soa_time = time.perf_counter() - start
print(f"AoS: {aos_time:.4f}s")
print(f"SoA: {soa_time:.4f}s")Tip
Di production, gunakan __slots__ pada class Python untuk mengurangi overhead memory per object. Tanpa __slots__, setiap object Python memiliki dictionary default yang menambah ~400 bytes.
Space-time tradeoff adalah prinsip: kalian bisa menukar memori untuk waktu (atau sebaliknya). Contoh:
import time
def fib_no_cache(n):
if n <= 1:
return n
return fib_no_cache(n - 1) + fib_no_cache(n - 2)
cache = {}
def fib_with_cache(n):
if n in cache:
return cache[n]
if n <= 1:
return n
cache[n] = fib_with_cache(n - 1) + fib_with_cache(n - 2)
return cache[n]
start = time.perf_counter()
fib_no_cache(30)
no_cache_time = time.perf_counter() - start
start = time.perf_counter()
fib_with_cache(30)
cache_time = time.perf_counter() - start
print(f"Without cache: {no_cache_time:.4f}s")
print(f"With cache: {cache_time:.6f}s")
print(f"Speedup: {no_cache_time / cache_time:.0f}x")Garbage collection di Python dan Java menambah overhead. Banyak object kecil = banyak work untuk GC. Menggunakan array/array-backed structures mengurangi GC pressure.
Inti yang harus dibawa pulang:
Di episode 26 selanjutnya kita akan membahas tren modern 2026 dan concurrent/persistent DS — persistent/immutable data structures, lock-free/concurrent DS, vector similarity search (HNSW, IVF), dan bagaimana data structure digunakan di production systems. Ini adalah gambaran terkini tentang apa yang happening di dunia data structure!