Instruction Selection ও Scheduling — IR থেকে আসল instruction
Instruction Selection and Scheduling
এক IR op-এর জন্য ISA-য় হাজারো encoding — selection ঠিক করে কোনটা, scheduling ঠিক করে কোন ক্রমে; এই শেষ মাইলেই machine code গড়ে ওঠে।
আগে এটা বুঝি
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 এই সিদ্ধান্তগুলো নেয়:
- Instruction selection: প্রতিটা IR fragment-এর জন্য সবচেয়ে সস্তা machine instruction/tile বাছাই — “কী emit করব”।
- 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 cTile-গুলো এক 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:
| Instruction | Latency (typical, modern x86) |
|---|---|
| mov reg,reg / add / sub / lea | 1 |
| imul (64-bit) | 3 |
| load (L1 hit) | 4–5 |
| div | 20–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 না হওয়া পর্যন্ত repeatPriority-র যুক্তি: দীর্ঘ 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 লুকায়নিজে চালিয়ে দেখুন
Folded addressing + lea চোখে; dependent vs independent adds ঘড়ি ধরে
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।
নিজে বানান
isel_sched.py — mini selector + greedy list scheduler
- X86 dataclass: mnem, dest, uses, text, latency
- selector(): TAC op → pseudo-x86; mul-by-{2,4,8} → lea tile
- load_arr-এর আগের mul detect করে addressing-mode-এ fold
- dep_dag(): RAW/WAR/WAW edges post-allocation convention-এ
- 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 আগেই থামে।
বুঝেছেন কি না দেখুন
1t ← x * 24 — shift-add decomposition করে সবচেয়ে ছোট lea/shl sequence
লিখুন। Cost কত cycle (lea/shl = 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।
2sub eax,1-এর পরে je done — sub-এর result (eax) কেউ না পড়লে selection
এটাকে কী করতে পারে, কেন?
যুক্তি
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 করে)।
3Latency 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 কী বদলাবে?
যুক্তি
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 আগে।
4Embedded 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।
5Post-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 উৎস