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

JIT Compilation — bytecode থেকে native, চলতে চলতে

JIT Compilation

JIT compiler runtime-এর ভেতরেই থাকা একটা compiler — interpreter চলতে চলতে profile জমায়, hot কোডে speculation-ভিত্তিক native code বানায়, ভুল হলে deoptimize করে — আর এই লেসনে আপনি একটা কাজ করা micro-JIT নিজেই লিখবেন।

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

  • AOT-থেকে-JIT spectrum-এ interpreter, baseline আর optimizing tier-এর ভূমিকা ব্যাখ্যা করে বলতে পারবেন V8/JVM tiering-এ কে কখন কোড বানায় ব্যাখ্যা করতে পারবেন
  • Profiling-guided speculation আর deoptimization-এর mechanism ব্যাখ্যা করতে পারবেন — guard কী, inline cache কীভাবে monomorphic call সস্তা করে, hidden class কোথায় ঢোকে বোঝাতে পারবেন
  • mmap/mprotect W^X dance, ARM icache flush, macOS MAP_JIT — permission-nach বর্ণনা করতে পারবেন
  • Node-এ warmup curve মেপে দেখাতে পারবেন একই কাজ কয়েক batch পরে কেন দ্রুত হয়, আর --no-opt দিয়ে সেটার cause যাচাই করতে পারবেন
  • Python-এ একটা ছোট stack VM লিখে হট লুপ শনাক্ত করে সেটাকে x86-64 machine code-এ JIT করে interpret-versus-native speedup মাপতে পারবেন

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

আগে এটা বুঝি

আজ 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 খরচউদাহরণ
Interpreternative কোডই নেইশূন্যসর্বোচ্চV8 Ignition, CPython
Baseline JITnaive, unoptimized nativeখুব কমমাঝারিSparkplug, JVM C1
Optimizing JITspeculated, 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 উল্টে → লেখো → আবার X

macOS 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:

Bytecodex86-64 bytesঅর্থ
PUSH imm48 B8 q q q q q q q q + 50mov rax, imm; push rax
LOAD v48 8B 05 d32 + 50mov rax, [rip+d]; push rax
STORE v58 + 48 89 05 d32pop rax; mov [rip+d], rax
ADD59 + 58 + 48 01 C8 + 50pop rcx; pop rax; add rax,rcx; push
SUB59 + 58 + 48 29 C8 + 50pop rcx; pop rax; sub; push
JZ label58 + 48 85 C0 + 0F 84 rel32pop rax; test; je far
JMP labelE9 rel32jmp far
RET58 + C3pop rax; ret

দুই-pass assembly: প্রথম pass-এ প্রতিটা bytecode-index-এর native offset হিসাব (size সব fixed!), দ্বিতীয় pass-এ emit + jump-এর rel32 পূরণ — ঠিক linker-এর relocation-এর মিনি-সংস্করণ, শুধু এবার S আর P দুটোই আমরা নিজেরাই ঠিক করছি।

bytecode থেকে চলমান native পর্যন্ত — JIT-এর pipeline
  1. Stack bytecode (IR)portable, ছোট — interpreter-এর ভাষা
  2. Hot detection: backward-jump counterprofiling — কোথায় সময় যাচ্ছে
  3. Template emission → machine byteslesson 13-এর codegen, এবার runtime-এ
  4. Jump-offset patchingmini-relocation — linking লেসনের প্রতিধ্বনি
  5. 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-এ, নামা বিরল।

Tiering state machine — ওঠা counter-driven, নামা শুধু deopt-এ।

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

EXPERIMENT

Warmup curve নিজের হাতে মাপুন — শুধু ঘড়ি, কোনো profiler নেই

Linux বা macOS (node ≥ 18)· ১০ মিনিট
// 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 প্রমাণ।

নিজে বানান

BUILD IT

micro-JIT — stack VM থেকে x86-64 machine code, mmap-এ

Python · ●●●●○
  1. Stack VM define করুন — PUSH/LOAD/STORE/ADD/SUB/JZ/JMP/RET opcode
  2. Interpreter loop লিখুন, backward jump-এ hot counter বাড়ান
  3. Threshold ছুঁলে two-pass compiler চালান — pass 1 offsets, pass 2 bytes + jump patch
  4. mmap দিয়ে RWX page নিন, bytes লিখুন, CFUNCTYPE দিয়ে function pointer বানান
  5. 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।

নিজে বাড়ান:

  1. MUL opcode যোগ করুন (48 0F AF C8 = imul rax,rcx — অবশিষ্ট bytes একই) — square-sum loop compile করে যাচাই করুন
  2. Hot detection কে পুরো প্রোগ্রাম না বানিয়ে trace-ভিত্তিক করুন: backward jump থেকে linear trace ধরে শুধু সেটাই compile, শেষে trace-এর শুরুতে patch-in (TraceMonkey-র মূল ভাবনা)
  3. 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 অসম্ভব করে তোলে।

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

1

V8-এর চারটা 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 সূত্রের সাথে এটার সম্পর্ক কী?

যুক্তি

একই সূত্রের বংশধর। 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 গরম হওয়া”?

যুক্তি

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-এর গবেষণা-পটভূমি