Foundationপ্রথম নীতি থেকে
LEVEL 5লেসন ১২/১৬অ্যাডভান্সড৫৫ মিনিট

Instruction Selection ও Scheduling — IR থেকে আসল instruction

Instruction Selection and Scheduling

এক IR op-এর জন্য ISA-য় হাজারো encoding — selection ঠিক করে কোনটা, scheduling ঠিক করে কোন ক্রমে; এই শেষ মাইলেই machine code গড়ে ওঠে।

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

  • IR tree-কে pattern/tiling দিয়ে cover করে instruction selection-কে search problem হিসেবে formulate করতে পারবেন
  • addressing-mode folding আর lea ব্যবহার করে arr[i]-জাতীয় pattern-এর খরচ কমানোর tile চেনতে পারবেন
  • x86 flags (cmp + jcc) আর variable-length encoding-এর প্রভাব selection সিদ্ধান্তে দেখাতে পারবেন
  • dependency DAG বানিয়ে greedy list scheduling চালিয়ে basic block-এর cycle count কমাতে পারবেন
  • out-of-order CPU কেন static scheduling-কে আংশিক অপ্রাসঙ্গিক করেছে, আবার in-order/embedded-এ কেন critical — সেই boundary টানতে পারবেন

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

আগে এটা বুঝি

Register allocation শেষে আমাদের TAC-এ variable-রা register-এ বসা — কিন্তু opcodes-গুলো এখনো আমাদের toy language-এর: +, *, load_arr। আসল ISA এমন ভাষা না। x86-64-এ a + b লেখার অন্তত পাঁচ রাস্তা: add rax,rbx, lea rax,[rax+rbx], add rax,[mem], folded addressing mode-এর ভেতরে, এমনকি push/pop trick। কোনটা ছোট? কোনটা দ্রুত? কোনটা পরের instruction-এর জন্য রাস্তা ফাঁকা রাখে?

আজকের দুই phase এই সিদ্ধান্তগুলো নেয়:

  1. Instruction selection: প্রতিটা IR fragment-এর জন্য সবচেয়ে সস্তা machine instruction/tile বাছাই — “কী emit করব”।
  2. Instruction scheduling: নির্বাচিত instruction-দের dependency মেনে পুনর্বিন্যাস — latency লুকানো — “কোন ক্রমে”।

এই লেসনের পর compiler-এর গল্প শেষ না — code-generation (object file), linking, loader, JIT বাকি। কিন্তু “source text থেকে machine semantics পর্যন্ত অনুবাদ”-এর মূল সিঁড়িটা আজ সম্পূর্ণ হয়।

মূল ধারণা

Selection = covering সমস্যা

মূল ধারণা: IR-কে tree/DAG হিসেবে দেখো, machine instruction-দের হলো pattern (tile) — প্রতিটার একটা cost। Selection = সম্পূর্ণ overlap, কোনো gap ছাড়া, minimum total cost-এর cover খোঁজা:

IR tree (+)                দুইটা সম্ভাব্য tiling:
   / \        A: add(leaf,leaf)          → 2 tiles, cost 2
 (×)(+)       B: lea(dst, [src + src×4])  → 1 tile, cost 1  ← জিতল!
 / |  \
a  b  c

Tile-গুলো এক op-এর চেয়ে বড় হতে পারে — একাধিক IR node ঢেকে ফেলে, এটাই শক্তি। সারি-ধরা tree-তে dynamic programming সব leaves থেকে root-এ optimal cost guarantee করে (Aho-Johnson অ্যালগরিদম); বাস্তব compilers DAG-এ কাজ করে (shared subtree), সেখানে NP-hard — heuristic tiling: SelectionDAG (LLVM), BURS/tree-pattern grammar (GCC-র history-তে twig, iburg family)।

Addressing-mode folding — x86-এর বিনামূল্যের গণিত

x86 memory operand নিজেই একটা ছোট ALU:

[ base + index×scale + displacement ]     scale ∈ {1,2,4,8}
mov eax, [arr + rsi*4]      ← arr[i] পড়া: গুণ + যোগ ZERO instruction-এ!
lea rax, [rdi + rsi*4 + 8]  ← তিন operand গণনা, flags ছোঁয় না, ১ cycle

তাই t ← i * 4 দেখে imul emit করা প্রায়ই ভুল — পরের load/store-এর tile-এ fold করলে instruction গায়েব। Addressing modes লেসনের টেবিল এখানে selection-এর cost-model।

imul বনাম shift/lea: x * 3 → lea rax,[rsi+rsi*2] (১ cycle); x * 5 → lea rax,[rsi+rsi*4]; x * 15 → lea rax,[rsi,rsi*4]; lea rax,[rsi+rax*2] জাতীয় chain — constant multiplier-কে shift-add decomposition করা strength reduction-এর selection-side রূপ।

Flags ও compare-and-branch

x86-এ comparison আলাদা জীবন করে: cmp a,b শুধু flags (ZF/SF/CF/OF) set করে; branch (jcc) flags পড়ে। ফলে:

if c < d goto L   →   cmp rcx, rdx ; jl L        (দুই instruction, এক সিদ্ধান্ত)

Flags-এর দুইটা selection-ফলাফল: (১) sub-ও flags set করে — dead result- ওala subtract সরাসরি cmp-র জায়গায় বসতে পারে; (২) flags partial-register hazard তৈরি করে — মাঝে flag-clobbering instruction ঢুকলে cmp আবার চালাতে হয়। Scheduling-এর সময় flags-এর dependency-ও DAG-এ edge!

ভেতরে কী ঘটছে

Encoding আর density

x86 variable-length: ১–১৫ byte। add eax,1 = ৩ byte; add r8, imm32 REX+ModRM+SIB+disp নিয়ে ৭-৮। Selection-এ size-cost রাখলে hot code compact হয় — i-cache, uop-cache, decode bandwidth সব লাভবান। ছোট encoding-এর প্রিয় target: %eax-ঘেঁষা registers (ModRM shortcut), immediate-এর sign-extension forms, xor eax,eax (৩-byte zero, mov eax,0-এর ৫-byte-এর চেয়ে ছোট + shorter-latency-zeroing)।

Dependency DAG আর latency

Block-ভেতরের instruction-দের edge: consumer ← producer (RAW); post- allocation fixed register-এ WAR/WAW edges-ও বাধ্যতামূলক। Node-weight = latency:

InstructionLatency (typical, modern x86)
mov reg,reg / add / sub / lea1
imul (64-bit)3
load (L1 hit)4–5
div20–40+

(সংখ্যা Agner Fog-এর measured tables থেকে; micro-architecture ভেদে বদলায়।)

Greedy list scheduling

1. DAG বানাও, priority = height (node থেকে block-শেষ পর্যন্ত
   longest weighted path — critical path)
2. প্রতি cycle: ready set = dependencies complete + resource free
3. ready-দের মধ্যে highest-height আগে issue; tie → program order
4. সব issue না হওয়া পর্যন্ত repeat

Priority-র যুক্তি: দীর্ঘ chain যত আগে শুরু হবে, tail তত আগে শুরু হবে — shortest-job-first-এর উল্টো, critical-path-first।

OoO যুগে scheduler-ের অবস্থান

আধুনিক big core out-of-order: rename (register renaming), reorder buffer (ROB), reservation stations — hardware নিজেই window-ভেতরে reorder করে (OoO execution)। তাহলে compiler- এর scheduler বৃথা?

আংশিক সত্য। Hardware window সসীম (~২০০-৬০০ uops): dependency-chain দীর্ঘ হলে window overflow, hardware দেখতেই পায় না দূরের independent কাজ — compiler দূর থেকে দেখে আগেই ছড়িয়ে দিতে পারে। আর:

  • In-order cores এখনো পৃথিবীর বেশিরভাগ processor: Cortex-M/A53, RISC-V embedded, DSP — সেখানে scheduler-ই ILP-র একমাত্র উৎস। computer-architecture module-এর pipelining/hazard লেসনের forwarding/stall model সরাসরি এখানে ফলে।
  • VLIW (Hexagon, DSPs, Itanium-এর legacy): hardware scheduler নেই — packet ভরাট করাটা পুরোপুরি compiler-এর দায়।
  • Front-end effects: dense schedule = better fetch/decode/macro-fusion — even OoO cores reward it.

উদাহরণ

এক statement-trio, -O0 বনাম -O2

int f(int i, int q, int r) {
    int j = i * 8;
    int k = j + q;
    return arr[i] + k;
}

-O0 — প্রতিটা TAC op আলাদা instruction, সব কিছু stack দিয়ে:

mov  dword [rbp-4], edi    ; i store
mov  eax, [rbp-4]
shl  eax, 3                ; j = i*8
mov  [rbp-8], eax
...                        ; মোট ~14 instruction, 6 stack refs

-O2 — selection + folding + scheduling:

lea  eax, [rdi + rsi]      ; k = i*8 + q  ← shl+add এক tile-এ!
add  eax, dword ptr [arr + rdi*4]   ; arr[i] folded, scale=8→? (নিচে)
ret

(বাস্তব asm-এ [arr + rdi*8]-এর scale ৪-byte int-এ *4; example-এ int-array ধরে lea eax,[rdi+rsi]-এর জায়গায় lea ecx,[rdi*8] জাতীয় variant-ও দেখতে পারেন — মূল পাঠ একই: তিন statement → তিন instruction, stack refs শূন্য।)

Spot-check: i*8 কোথাও আলাদা না — addressing-mode fold; j+q কোথাও না — lea tile; arr[i] — scale-index tile; scheduling করার কিছুই রইল না, dependency chain নিজেই সংকুচিত।

ছবিতে

 SELECTION (tree covering)                SCHEDULING (DAG)

      (+)                                      ┌───┐
     /   \            tile B:                  │ L │ load arr[i]  lat 4
  (×)     (+)      lea covers ×,+            └┬─┬─┘
 /| \    / \                                    │ │
i j  q  ...                                     ▼ ▼
                                          ┌─────────────┐
 naive: shl; add; add; load   (4 instr)   │ LEA k=i*8+q │ lat 1
 tiled : lea; load+add        (2 instr)   └──────┬──────┘
                                             ▼
                                       ┌──────────┐
                                       │ ADD, RET │
 program order == dep order হলে        └──────────┘
 scheduler শুধু confirm করে;
 না মিললে swap করে latency লুকায়
Tiling আর scheduling — selection ছোট করে, scheduler সাজায়।

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

EXPERIMENT

Folded addressing + lea চোখে; dependent vs independent adds ঘড়ি ধরে

macOS / Linux (clang)· ২০ মিনিট

Part A — asm diff:

clang -S -O0 sel.c -o o0.s && clang -S -O2 sel.c -o o2.s
grep -E 'lea|shl|\[a' o2.s     # folded modes + lea খুঁজুন

sel.c:

extern int arr[];
int sel(int i, int q, int r) {
    int j = i * 8;
    int k = j + q;
    int m = k ^ r;
    return arr[i] + m;
}

-O2-তে খুঁজে পাবেন: কোনো আলাদা shl/imul নেই (folded), অন্তত একটা lea, আর [rip+arr] + rdi*4-জাতীয় full addressing mode। -O0-তে সবগুলো আলাদা instruction + stack traffic।

Part B — micro-latency demo (computer-architecture module-এর forwarding/stall experiment-এর sibling):

// dep.c — চার chain, কিন্তু প্রতিটা ভেতরে সম্পূর্ণ serial
long a=1,b=2,c=3,d=4;
for (long n=0; n \< 200e6; n++) {
    a+=1; b+=1; c+=1; d+=1;         // ৪টা independent chain
}
clang -O1 -o dep dep.c && perf stat -e cycles,instructions ./dep   # Linux
# macOS: hyperfine ./dep

চার chain → IPC ৩-৪ (throughput-bound); chain-গুলো একে a+=a; b+=a; c+=a; d+=a; জাতীয় single serial chain বানালে IPC ≈ 1 — latency-bound। একই instruction count, cycle count ৩-৪ গুণ পার্থক্য — এটাই scheduling- এর কাজ করা ফাঁকা জায়গা (আর OoO CPU-র নিজের হাতের সীমা)।

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

Selection-এর tile সত্যিই instruction count গুনে কমায়, আর dependency chain বনাম independent work-এর cycle difference সরাসরি মাপা যায় — scheduling latency লুকানোর দাবি অনুমান না, measurement।

নিজে বানান

BUILD IT

isel_sched.py — mini selector + greedy list scheduler

Python · ●●●●○
  1. X86 dataclass: mnem, dest, uses, text, latency
  2. selector(): TAC op → pseudo-x86; mul-by-{2,4,8} → lea tile
  3. load_arr-এর আগের mul detect করে addressing-mode-এ fold
  4. dep_dag(): RAW/WAR/WAW edges post-allocation convention-এ
  5. list_schedule(): height priority, single-issue greedy simulation

আগের লেসনের TACInstr convention-ই চলছে; selector output structured tuple + pretty string — scheduler tuple নিয়ে কাজ করে:

from dataclasses import dataclass

@dataclass
class X86:
    mnem: str            # 'mov', 'lea', 'add', 'imul', 'load'
    dest: str | None
    uses: list
    text: str

LAT = {'mov': 1, 'lea': 1, 'add': 1, 'imul': 3, 'load': 4}

def select(tac, regs):
    """TACInstr list → X86 list; regs = var→physical reg map"""
    out, pending_mul = [], {}            # var → (idx of imul-to-fold)
    for ins in tac:
        if ins.op == '*' and ins.b in ('#2', '#4', '#8'):
            rd, rs = regs[ins.dest], regs[ins.a]
            s = {'#2': 1, '#4': 2, '#8': 3}[ins.b]
            out.append(X86('lea', ins.dest, [ins.a],
                           f'lea {rd}, [{rs}*{2**s}]'))   # strength-reduce
            pending_mul[ins.dest] = len(out) - 1
            continue
        if ins.op == 'load_arr':         # arr[i]: scale fold!
            rb, ri = regs.get(ins.a, ins.a), regs[ins.b]
            out.append(X86('load', ins.dest, [ins.a, ins.b],
                           f'mov {regs[ins.dest]}, DWORD PTR '
                           f'[{rb} + {ri}*4]'))
            continue
        if ins.op == '+':
            rd, ra, rb_ = regs[ins.dest], regs[ins.a], regs[ins.b]
            out.append(X86('add', ins.dest, [ins.a, ins.b],
                           f'add {rd}, {ra}, {rb_}'))
            continue
        if ins.op == '*':
            rd = regs[ins.dest]
            rs1, rs2 = regs.get(ins.a, ins.a), regs.get(ins.b, '#1')
            out.append(X86('imul', ins.dest, [ins.a, ins.b],
                           f'imul {rd}, {rs1}, {rs2}'))
            continue
        if ins.op == '-':
            rd = regs[ins.dest]
            rs1, rs2 = regs[ins.a], regs[ins.b]
            out.append(X86('add', ins.dest, [ins.a, ins.b],
                           f'sub {rd}, {rs1}, {rs2}'))
            continue
        if ins.op == 'return':
            out.append(X86('mov', None, [ins.a],
                           f'ret   ; uses {regs[ins.a]}'))
            continue
        raise ValueError(ins.op)
    return out

def build_dag(code):
    deps = [[] for _ in code]            # deps[j] = producers j must wait
    last_write = {}
    last_reads = {}
    for j, ins in enumerate(code):
        for v in ins.uses:               # RAW
            if v in last_write:
                deps[j].append(last_write[v])
            last_reads.setdefault(v, []).append(j)
        if ins.dest:                     # WAR + WAW (fixed phys regs)
            for r in last_reads.get(ins.dest, []):
                deps[j].append(r)
            if ins.dest in last_write:
                deps[j].append(last_write[ins.dest])
            last_write[ins.dest] = j
            last_reads[ins.dest] = []
    return deps

def heights(code, deps):                 # priority = critical-path height
    memo = {}
    def h(i):
        if i not in memo:
            cons = [j for j in range(len(code)) if i in deps[j]]
            memo[i] = LAT[code[i].mnem] + max((h(j) for j in cons), default=0)
        return memo[i]
    return [h(i) for i in range(len(code))]

def list_schedule(code):
    deps, hgt = build_dag(code), heights(code, build_dag(code))
    done_at, order, cycle = {}, [], 1
    remaining = set(range(len(code)))
    while remaining:
        ready = [i for i in remaining
                 if all(d in done_at and done_at[d] <= cycle for d in deps[i])]
        if not ready:
            cycle += 1
            continue
        pick = max(ready, key=lambda i: (hgt[i], -i))
        done_at[pick] = cycle + LAT[code[pick].mnem] - 1
        order.append(pick)
        remaining.discard(pick)
        cycle += 1
    return order

REGS = {'i': 'rsi', 'q': 'rdi', 'r': 'r8', 't': 'rcx',
        'k': 'eax', 'm': 'edx', 'w': 'ebx'}

tac = [
    TACInstr('*',        't', 'i', '#8'),
    TACInstr('+',        'k', 't', 'q'),
    TACInstr('load_arr', 'x', 'arr', 'i'),
    TACInstr('-',        'm', 'k', 'r'),
    TACInstr('+',        'y', 'x', 'm'),
]

code = select(tac, REGS)
print('=== selected (program order) ===')
for i, ins in enumerate(code):
    print(f'  {i}: [{LAT[ins.mnem]:>2}cy] {ins.text}')

order = list_schedule(code)
print('=== scheduled ===')
for step, i in enumerate(order, 1):
    print(f'  issue@{step}: {code[i].text}')

প্রত্যাশিত আউটপুট-এর মূল পাঠ: t ← i*8 lea-tile হয়ে ১cy; load_arr scale-4 folded; আর scheduler dependency-free, দীর্ঘ-latency instruction- কে সামনে টেনে critical path ছোট করে — before/after issue cycle তুলনা করলেই লাভ দেখা যায়।

নিজে বাড়ান: (১) multi-issue (2-wide) resource model — scheduler কীভাবে বদলায়; (২) flags-dependency edge (cmp→jcc) যোগ করে দেখান flag-clobber মাঝে ঢুকলে cmp re-issue হয়; (৩) superoptimizer spirit-এ brute-force 4-instruction windows-এর best sequence খোঁজা (Massalin-এর 1987 paper-এর toy version)।

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

LLVM SelectionDAG / GlobalISel। Selection এখানে literal DAG-এ: legalize (target-supported op-তে নামানো) → combine (peephole-on-DAG) → select (TableGen-জেনারেটেড matcher) → schedule। GCC RTL-এ উল্টো পথ।

VLIW family। TI C6000, Qualcomm Hexagon, ঐতিহাসিক Itanium — packet- based VLIW-এ compiler-ই একমাত্র scheduler; static scheduling failure মানে stall। DSP জগতে আজও selection+scheduling সবচেয়ে বেশি মূল্যবান pass।

JIT। WebAssembly/BPF JIT ছোট linear-scan + সরল selector চালায় — compile-speed first; hot tier-এ গেলে scheduling যোগ হয়।

Macro-fusion আর uop cache। Modern x86 decode-এ cmp+jcc এক uop-এ fuse হয় — selection-এর cmp/jcc pairing আসলে front-end throughput কিনে দেয়; dense hot loop uop cache-এ বসলে fetch power কমে।

আপনার toy compiler। আজকের selector-এর output-ই final assembly — assembly module-এর reading-compiler-output দক্ষতা দিয়ে clang-এর সাথে diff করুন।

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

“Compiler ঠিক আমার CPU model-এর জন্য tune করে।”

Default -march generic/x86-64 baseline — সব chip-এ চলবে এমন সবচেয়ে পুরনো feature-set। Haswell-নির্দিষ্ট latency দিয়ে schedule করা Skylake- এ ভুল হতে পারে। তাই -march=native/-mtune আলাদা flag — আর vendor portability ভেঙে হলেও সেটা explicit choice। Scheduler আসলে abstract cost model চালায়, আপনার ঘরের chip না।

“OoO CPU-তে scheduling একেবারে অপ্রয়োজনীয়।”

Hardware window সসীম; long dependency chain window ভরিয়ে দিলে OoO-ও অন্ধ হয়ে পড়ে — compiler দূর-দৃষ্টি দিয়ে independent work আগে ছড়িয়ে দিতে পারে। Plus: in-order core পৃথিবীর processor-count-এর majority, VLIW-এ hardware scheduler নেই-ই, আর front-end (fetch/decode/fusion) সব CPU-তে static layout পছন্দ করে। “কম গুরুত্বপূর্ণ” হ্যাঁ, “অপ্রয়োজনীয়” না।

“কম assembly line মানেই দ্রুত।”

Instruction count ভুল metric: এক div ৩০+ cycle আর ১৫টা add-shift chain একই কাজ করতে পারে; fused-domain uop count, port pressure, icache footprint — সব গণনায় আসে। rep movsb vs SIMD copy loop-এর পুরনো debate-ই প্রমাণ: fastest code মাঝে মাঝে বেশি instruction-এর।

“Scheduling শুধু speed-এর বিষয়, ভুল হলে শুধু ধীর হবে।”

Dependency ভাঙা schedule correctness ভাঙে: store→load ordering, volatile/ atomic access, syscall barrier — সবই reorder-proof। Compiler-এর scheduler শুধু true-dependency-safe reorder করে, আর memory-model barriers সেখানেই সীমা টানে। Hardware speculation ভুল পথে গেলে roll back করতে পারে; published side-effect ফেরানো যায় না — তাই compiler আগেই থামে।

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

1

t ← x * 24 — shift-add decomposition করে সবচেয়ে ছোট lea/shl sequence লিখুন। Cost কত cycle (lea/shl = 1)?

প্রয়োগ

24 = 16+8 = 3×8: lea t,[x+x*2] (t=x·3, 1cy) → shl t,3 (t=24x, 1cy) — মোট ২ instruction, ২ cycle। Alternative shl x,3; lea t,[x+x*2]-ও ২/২। Single lea অসম্ভব (scale ≤ 8, disp যোগ করে কিন্তু দুই-x term-ই লাগে)। Point: multiplier decomposition নিজেই একটা mini-search।

2

sub eax,1-এর পরে je done — sub-এর result (eax) কেউ না পড়লে selection এটাকে কী করতে পারে, কেন?

যুক্তি

cmp eax,1; je done-এ রূপান্তর — বা আরও ভালো: result-dead sub-ই flags set করে, তাই sub থেকেই je চলে, result-def শুধু বাদ। Flags একটা free side-channel: arithmetic op-এর “বোনাস output”। শর্ত: পরের flag-user-এর আগে আর কোনো flag-write না ঢুকে (scheduler-এর flag-dependency edge সেটাই guard করে)।

3

Latency table-এ load = 4। Block-এ L: load x; A: add y,x; B: add z,y — critical path কত cycle? load w, indep যোগ করলে (w-র কোনো user নেই block-এ) total কী বদলাবে?

যুক্তি

Path L→A→B = 4+1+1 = ৬ cycle — critical path; single-issue in-order-তে মোট ৬+। Independent load w যোগ করলে critical path অপরিবর্তিত (৬) — কিন্তু scheduler w-কে L-এর পাশে issue করে shadow-তে লুকায়: OoO/superscalar- এ wall-clock unchanged, in-order-তে ১ cycle extra। Priority = height এই জন্যই কাজ করে: লম্বা chain আগে।

4

Embedded Cortex-M0 (in-order, no branch predictor worth the name, tight flash) targeting compiler-এ selection আর scheduling budget কোথায় দেবেন? Two reasons each.

ডিজাইন

Selection: (১) Thumb-16/32 mixed encoding — dense 16-bit form বাছাই flash-size সরাসরি বাঁচায়; (২) M0-র limited addressing modes — fold opportunity কম, তাই wrong tile-এর শাস্তিও relative বেশি। Scheduling: (১) in-order pipeline-এ stall = direct lost cycle (forwarding limited) — dependency-gap fill সস্তা win; (২) flash wait-states — instruction density আর prefetch behavior scheduling-sensitive। তাই embedded-এ দুটোই heavy, কিন্তু metric size+energy, not peak throughput।

5

Post-allocation scheduling-এ WAR/WAW edge কেন হঠাৎ দরকার, যখন pre- allocation (virtual register) scheduling-এ ছিল না?

স্মরণ

Virtual register-দের unlimited supply — allocator নতুন name দিয়ে false dependency এড়াতো (SSA-র মতো)। Allocation-এর পর নাম সসীম physical reg: দুই unrelated computation একই %ecx ভাগ করলে write-after-read/write-after- write hazard সত্যিকারের — reorder করলে value corrupt। এই জন্যই production pipelines scheduling-কে প্রায়ই allocation-এর আগে রাখে (post-RA scheduling শুধু cleanup pass) — false-edge explosion এড়াতে।

এরপর কী

এরপর কী

Machine instruction তৈরি — selection, scheduling, allocation সব শেষ। কিন্তু .mdx থেকে live website যেমন build artifact চায়, তেমনি আপনার program চায় file: bytes, sections, symbols, relocation entries।

পরের লেসন (Code Generation) সেই শেষ মাইল: instruction encode করে object file ঢালা, symbol table আর relocation record কেন দরকার — আর assembly module-এর ELF লেসনের format-গুলো এবার producer-এর চোখে দেখা যাবে। তারপর linking, loading, আর JIT — runtime-এ কোড যেখানে জন্মায়।

আরও পড়ুন

  • Compilers: Principles, Techniques, and Tools (2nd ed.) — Chapter 8: Code Generation — Alfred V. Aho, Monica S. Lam, Ravi Sethi, Jeffrey D. Ullman · ৮.৩-এ tree tiling ও simple code generation; selection-এর textbook treatment
  • Engineering a Compiler (2nd ed.) — Instruction Selection ও Instruction Scheduling অধ্যায় — Keith Cooper, Linda Torczon · Tree-pattern matching থেকে list scheduling — দুই অধ্যায়ই এই লেসনের ভিত
  • "Instruction Tables" (Agner Fog) — Agner Fog · প্রতি micro-architecture-এর measured latency/throughput table — experiment section-এর latency source
  • Intel 64 and IA-32 Software Developer's Manual, Vol. 2 — Instruction Set Reference — Intel Corporation · Encoding, flag semantics আর latency-র authoritative উৎস