Optimization — কোনটা safe, কোনটা profitable, কোনটা fast
Optimization: Safe, Profitable, Fast
প্রতিটা optimization একই চুক্তির উপর দাঁড়ায় — semantics অটুট রেখে সত্যিই দ্রুত করা — আর সেই চুক্তির প্রতিটা clause-এর একটা লুকানো ব্যতিক্রম আছে।
আগে এটা বুঝি
আগের লেসনে আমরা কম্পাইলারকে চোখ এনে দিয়েছি — CFG আর dataflow analysis দিয়ে সে এখন জানে কোন definition কোথায় পৌঁছায়, কোন ভেরিয়েবল কখন বাঁচা। আজ সেই জ্ঞান প্রথমবার অর্থ তৈরি করবে: কোড মোছা, গণনা বাদ, loop ছেঁটে ফেলা।
কিন্তু শুরুতেই একটা শৃঙ্খলা। কম্পাইলার “ভালো লাগার” জন্য কিছু optimize করে না; প্রতিটা রূপান্তর দুইটা শর্ত পূরণ করতে হয়:
- Safe: প্রোগ্রামের observable behavior হুবহু এক থাকে — প্রতিটা সম্ভাব্য input-এ, প্রতিটা সম্ভাব্য execution-এ।
- Profitable: average case-এ সত্যিই faster/smaller — নাহলে compile time নষ্ট, icache নষ্ট, সব নষ্ট।
এই দুই শর্তের মাঝেই আজকের পুরো নাটক। কারণ x * 0 মোছা দেখতে একেবারে
নিরাপদ — যতক্ষণ না x NaN, বা x একটা function call যার side effect
আছে, বা overflow undefined behavior ছুঁয়ে ফেলে।
মূল ধারণা
Local optimizations — এক basic block-এর ভেতরে
Basic block সোজা-সুজা: branch নেই, তাই instruction ক্রমান্বয়ে বিশ্লেষণ করলেই চলে। সবচেয়ে common তিনটা local pass:
Constant folding ও propagation
আগে: পরে:
t1 ← 3 * 5 t1 ← 15 ← folding (compile time-এ গুণ!)
t2 ← t1 + 2 t2 ← 17 ← propagation + folding
print t2 print #17 ← সব ঢুকে গেলFolding = operands দুটোই constant হলে গণনাটা compiler-এর ঘড়িতে চালানো। Propagation = যে ভেরিয়েবল constant জানা, তার প্রতিটা ব্যবহারে সেই constant বসানো — তখন নতুন নতুন folding-এর সুযোগ জন্মায়, তাই দুটো pass সাধারণত জোড়ায় চলে।
Algebraic simplification
| লেখা | হয়ে যায় | শর্ত |
|---|---|---|
x + 0, x − 0 | x | always |
x * 1, x / 1 | x | always |
x * 0 | 0 | শুধু যদি x নিশ্চিত finite |
0 + x*0-জাতীয় chain | … | একই caveat |
ওই তৃতীয় row-ই বোমা। IEEE floating point-এ 0 * NaN == NaN,
0 * ∞ == NaN — তাই double-এর জন্য x * 0 কখনোই fold করা যায় না,
কম্পাইলার সত্যিই গুণ করে। Integer-এ NaN নেই, কিন্তু C-তে signed
overflow undefined behavior — আর UB
contract অনুযায়ী “overflow হবে ধরে না” কম্পাইলারের অধিকার: সেই অনুমান
থেকেই x + 1 > x সবসময় true, INT_MIN * -1 safe, জাতীয় দূরদর্শি
transformation চলে।
NaN আর UB — দুটোই একই শিক্ষা: safe শর্তটা language semantics-এর contract-এ লেখা, গণিতে না।
Common subexpression elimination (CSE)
আগে: পরে:
t1 ← a + b t1 ← a + b
t2 ← a + b t2 ← t1 ← available!
t3 ← t1 * t2 t3 ← t1 * t1“a+b আগে হিসাব হয়েছে, আর মাঝে a/b বদলায়নি — তাহলে result available, আবার গুণতে হবে না।” “Available expressions” dataflow analysis ঠিক আগের লেসনের framework: forward direction, meet = intersection (must analysis — সব পথে পৌঁছাতে হবে), transfer function = gen(expression)/kill( operand-defining assignments)। Block-এর ভেতরে value numbering নামের hash-trick একই কাজ করে।
ভেতরে কী ঘটছে
Global passes — CFG-র উপর চলা যন্ত্র
Dead code elimination — mark-sweep
GC-র mark-sweep-এর সাথে অবিশ্বাস্য মিল — শুধু memory না, instruction সাফ হয়:
1. ROOT চিহ্নিত করো: print/store/return/branch — side effect আছে
এমন সব instruction
2. MARK: root-দের operand যে definitions তৈরি করে,
সেগুলো চিহ্নিত → তাদের operands-এর definitions → …
(def-use chain ধরে transitive closure)
3. SWEEP: অচিহ্নিত (non-label) instruction মুছে দাওReachability (আগের লেসনের graph search) + usefulness (def-use chain) — DCE দুইটা dataflow উত্তরের AND।
Inlining — call overhead vs code bloat
Function body সরাসরি call-site-এ বসিয়ে দিলে: call/ret overhead যায়, parameter passing যায়, আর সবচেয়ে বড় — ভেতরের কোড call-context-এ দেখা যায়, তাই constant propagation আর CSE-র সুযোগ গুণে যায়। খরচ: binary ফুলে ওঠে (code bloat), instruction cache চাপ পড়ে, hot loop-এর পাশে বসানো cold code locality নষ্ট করে। Heuristic ছোট/হট/এক-call-site function inline করে — এটাই -O2-র সবচেয়ে প্রভাবশালী pass।
Loop optimizations — সবচেয়ে ফলদায়ী জায়গা
Loop চলে iteration-এর গুণে; ভেতরের এক instruction বাদ = N বার বাদ।
- LICM (loop-invariant code motion): loop-এর ভেতরে এমন গণনা যার
operands প্রতি iteration-এ এক — তাকে header-এর আগে তুলে দাও।
for(i=0;i<n;i++) s += a * limit * i;→t = a*limitloop-এর বাইরে। Safety শর্ত: invariant হওয়া + যে block-এ উঠবে সেখানে dominance (সব path-এ পৌঁছায়) + ওভারফ্লো trap না থাকা। Loop-invariant detection নিজেই একটা dataflow iteration। - Unrolling:
for(i=0;i<n;i+=4){ s+=a[i]; s+=a[i+1]; s+=a[i+2]; s+=a[i+3]; }— branch count এক-চতুর্থাঙ্ক, আর চারটা independent add CPU-র ILP খায়। Branch misprediction penalty-র সাথে সম্পর্ক সরাসরি computer- architecture module-এর branch prediction লেসনের। খরচ: code size, আবার। - Peephole: generated assembly-র ছোট window-তে pattern-match:
add rax,1; add rax,1→add rax,2; redundantmovpair মুছে ফেলা। সস্তা, সবসময় শেষে চলে।
-O flags — কোন level-এ কী চালু
| Flag | বাস্তবে কী |
|---|---|
-O0 | প্রায় কিছুই না — debugging-এর জন্য source-এর সাথে faithful |
-O1 | folding/simplify/DCE/CSE-র হালকা সেট |
-O2 | aggressive inlining, LICM, unrolling-heuristics — release default |
-O3 | vectorization, heavy unrolling — বড় হয়, সবসময় দ্রুত না |
-Os/-Oz | size-tuned — embedded-এর পছন্দ |
এই ladder-টাই optimization level-এর গল্প: level বাড়ে = আরও pass + আরও aggressive heuristics।
volatile — optimizer-এর kill-switch
volatile int *p মানে: প্রতিটা read/write hardware-এ ঘটবেই, ক্রম
অক্ষত — MMIO register polling, signal handler, setjmp-এর জগৎ। ফলে
folding, caching-in-register, LICM, DCE — সব pass volatile access-
এর কাছে নতজানু। while(*status == 0) {} মুছে ফেলা infinite-loop-
এর সমান হতে পারে — তাই compiler চুপ।
PGO/LTO: profile-guided optimization প্রোগ্রাম চালিয়ে branch/block frequency জুড়ে নেয় — inlining/unrolling এখন measurement-ভিত্তিক। Link-time optimization পুরো প্রোগ্রাম এক unit ধরে cross-module inlining করে। দুটোই “কোনটা hot” প্রশ্নের আরও ভালো উত্তর — মূলত এই লেসনের passes- গুলোকেই আরও সঠিক input দেয়।
- CanonicalizationSSA/normal form — সব pass এক ভাষায়
- Local passesfold · propagate · simplify · CSE
- Global passesDCE · inlining · LICM · unrolling
- Peepholeassembly window-এ শেষ ঝাড়ু
- Code generationselection · scheduling · allocation — পরের লেসন
উদাহরণ
এক প্রোগ্রাম, পাঁচ pass — step by step
1: x ← 6
2: y ← x * 7
3: z ← y * 0
4: w ← z + y
5: v ← w + y # w আর ভবিষ্যৎ কেউ পড়ে না (নিচে দেখুন)
6: print vPass 1 — folding + propagation: x=6 জানা → y ← 42; z,w,v সব
constant-এ নেমে আসে:
1: (মুছে গেছে — x আর কেউ পড়ে না) ← DCE-র কাজ হবে
2: y ← 42
3: z ← 42 * 0 → 0 ← algebraic (int toy: safe)
4: w ← 0 + 42 → 42 ← fold
5: v ← 42 + 42 → 84 ← fold
6: print 84 ← propagated!Pass 2 — DCE: seeds = {print}। print-এর use 84 constant — কোনো
definition টানে না। y/z/w/v/x-এর সব assignment অচিহ্নিত → sweep।
চূড়ান্ত (১ instruction):
print 84৬ instruction → ১। Runtime-এ যা ঘটবে সেটা compile time-এই শেষ — এটাই optimizer-এর সামগ্রিক লক্ষ্য: গণনা runtime থেকে compile-time-এ সরিয়ে নেওয়া, বা সম্ভব হলে পুরোপুরি মুছে ফেলা।
লক্ষ্য করুন line 3: যদি y float হতো, y * 0 fold করতে পারত না
— z অজানা থাকত, w/v/print সব জীবিত থাকত। একটা semantics-সিদ্ধান্ত
পুরো cascade বদলে দেয়।
ছবিতে
Pass pipeline চোখে দেখা
┌─────────────────────────────────────────────┐
TAC in ──► │ fold+propagate ──► simplify ──► CSE │
│ │ │
│ ▼ │──► smaller TAC
│ DCE (mark-sweep over CFG) │ out
│ ▼ │
│ inlining ──► LICM ──► unrolling ──► peephole│
└─────────────────────────────────────────────┘
প্রতিটা pass: safe? ✓ profitable? ✓Ordering-ও বিষয়: folding আগে চললে DCE-র কাছে বেশি dead instruction পৌঁছায়; inlining আগে চললে ভেতরের constant-গুলো outer context-এ fold হয়। Pass ordering নিজেই একটা research topic — Click-এর GCM paper একই framework-এ কয়েকটা pass একসাথে চালানোর প্রস্তাব।
নিজে চালিয়ে দেখুন
Godbolt locally — clang -O0 বনাম -O2, ছয় snippet
clang -S -O0 t.c -o o0.s && clang -S -O2 t.c -o o2.s
diff o0.s o2.s | less # বা দুটো পাশাপাশি খুলুনপ্রতিটার জন্য -O2 output-এ কোন named optimization ফায়ার করল, খুঁজুন:
// 1 — কোনো ALU op-ই থাকবে না
int f1(void) { int a = 3 * 5; return a + 2; }
// 2 — mulsd/divsd রয়ে যাবে! (NaN-safe folding অসম্ভব)
double f2(double x) { return x * 0.0; }
// 3 — পুরো ফাংশন mov eax,1 (UB-based: overflow ধরে নেয়)
int f3(int x) { return x + 1 > x; }
// 4 — a+b একবারই গণনা হবে (lea/add একবার)
int f4(int a, int b) { return (a+b) * (a+b); }
// 5 — limit*4 loop-এর বাইরে উঠে যাবে
int f5(int n, int limit) { int s = 0;
for (int i = 0; i \< n; i++) s += i + limit * 4; return s; }
// 6 — গণনা গায়েব, শুধু store থাকবে
int f6(void) { int unused = 99 * 99; return 1; }উত্তর টেবিল (আগে নিজে চেষ্টা করুন!):
| Snippet | -O2-তে যা দেখবেন | Fired pass |
|---|---|---|
| f1 | mov eax, 17 | constant folding + propagation + DCE |
| f2 | xorps/mulsd রয়ে গেছে | (attempted fold blocked by NaN) |
| f3 | mov eax, 1 | UB-based algebraic reasoning |
| f4 | একটা lea eax,[rdi+rsi], দ্বিতীয়বার না | CSE + addressing fold |
| f5 | imul/lea loop-এর বাইরে | LICM + strength reduction |
| f6 | mov eax, 1 — 9809 কোথাও নেই | DCE |
এবার f2/f5-এ volatile যোগ করে দেখুন — -O2-ও হাত গুটিয়ে ফেলে।
Benchmark করতে চাইলে (Linux, perf সহ):
perf stat -e cycles,instructions ./sum_O0 # একই sum-to-n loop
perf stat -e cycles,instructions ./sum_O2 # cycles কমবে, IPC বাড়বেপ্রতিটা named optimization বাস্তব asm-এ নিজের signature রেখে যায় — folding mov-immediate, UB-based reasoning constant-return, LICM loop-এর বাইরে, volatile-এ কিছুই না — optimizer কোনো black box না।
নিজে বানান
tac_optimizer.py — folding + algebraic + DCE pipeline
- আগের লেসনের TACInstr/BasicBlock/build_cfg জুড়ে নিন
- fold_pass(): প্রতি ব্লকে env dict রেখে constant fold + propagate
- simplify_algebraic(): identity টেবিল (x+0, x*1, x*0 — int-only!)
- dce(): mark-sweep — side-effect seeds থেকে def-use closure
- demo program-এ instruction count আগে/পরে প্রিন্ট করুন
আগের লেসনের cfg_liveness.py-এর TACInstr, BasicBlock, build_cfg
একই ফাইলে রাখুন — নিচের কোড সরাসরি তার উপর চলে।
ARITH = {'+', '-', '*'} # toy ভাষা: শুধু int!
def lit(x): # '#12'-জাতীয় literal?
return x is not None and x.startswith('#')
def val(x):
return int(x[1:])
def simplify_algebraic(op, a, b):
"""শুধু integer toy ভাষার জন্য SAFE identities"""
if op == '+' or op == '-':
if lit(b) and val(b) == 0:
return a # x ± 0 → x
if op == '+' and lit(a) and val(a) == 0:
return b # 0 + x → x
if op == '*':
if (lit(a) and val(a) == 0) or (lit(b) and val(b) == 0):
return '#0' # x*0 → 0 (float হলে ভুল হত!)
if lit(a) and val(a) == 1:
return b # 1*x → x
if lit(b) and val(b) == 1:
return a # x*1 → x
return None
def fold_pass(blocks):
"""per-block constant folding + propagation + algebraic simplify"""
killed = 0
for blk in blocks:
env = {} # var → known '#const'
kept = []
for ins in blk.instrs:
if ins.op == 'label':
env.clear() # join-point: conservative reset
kept.append(ins)
continue
if ins.op in ARITH and ins.dest:
a, b = env.get(ins.a, ins.a), env.get(ins.b, ins.b)
if lit(a) and lit(b): # pure folding
r = {'+': val(a)+val(b), '-': val(a)-val(b),
'*': val(a)*val(b)}[ins.op]
env[ins.dest] = f'#{r}'
killed += 1
continue # instruction মুছে গেল
rep = simplify_algebraic(ins.op, a, b)
if rep is not None: # identity hit
env[ins.dest] = rep
kept.append(TACInstr('copy', ins.dest, rep))
killed += 1
continue
ins = TACInstr(ins.op, ins.dest, a, b)
kept.append(ins)
continue
if ins.op == 'copy' and lit(env.get(ins.a, ins.a)):
env[ins.dest] = env.get(ins.a, ins.a)
killed += 1 # copy-of-const → env entry
continue
kept.append(TACInstr(ins.op, ins.dest, # বাকিগুলোতে
env.get(ins.a, ins.a), # const ঢোকাও
env.get(ins.b, ins.b), ins.label))
blk.instrs[:] = kept
return killed
SIDE_EFFECT = {'print', 'return', 'goto', 'if_>=', 'if_<', 'store'}
def dce(cfg):
"""mark-sweep: side-effect seeds থেকে def-use transitive closure"""
def_sites = {}
for b in cfg.blocks:
for idx, ins in enumerate(b.instrs):
if ins.dest:
def_sites.setdefault(ins.dest, []).append((b.bid, idx))
marked, stack = set(), []
for b in cfg.blocks:
for idx, ins in enumerate(b.instrs):
if ins.op in SIDE_EFFECT:
marked.add((b.bid, idx)); stack.append((b.bid, idx))
while stack: # MARK phase
bid, idx = stack.pop()
for v in cfg.blocks[bid].instrs[idx].uses():
for site in def_sites.get(v, []):
if site not in marked:
marked.add(site); stack.append(site)
removed = 0 # SWEEP phase
for b in cfg.blocks:
kept = [i for j, i in enumerate(b.instrs)
if (b.bid, j) in marked or i.op == 'label']
removed += len(b.instrs) - len(kept)
b.instrs[:] = kept
return removed
demo = [
TACInstr('copy', 'x', '#6'),
TACInstr('*', 'y', 'x', '#7'), # 6*7 → 42
TACInstr('*', 'z', 'y', '#0'), # 42*0 → 0 (algebraic)
TACInstr('+', 'w', 'z', 'y'),
TACInstr('+', 'v', 'w', 'y'),
TACInstr('return', None, 'v'),
]
cfg = build_cfg(demo)
before = sum(len(b.instrs) for b in cfg.blocks)
k1 = fold_pass(cfg.blocks); k2 = dce(cfg)
after = sum(len(b.instrs) for b in cfg.blocks)
print(f'আগে: {before} | fold/simplify: -{k1} | DCE: -{k2} | পরে: {after}')
for b in cfg.blocks:
for ins in b.instrs:
print(f' B{b.bid}: {ins.op:6} {ins.dest or ""} {ins.a or ""} {ins.b or ""}')প্রত্যাশিত output:
আগে: 6 | fold/simplify: -5 | DCE: -0 | পরে: 1
B0: return #84example section-এর trace-এর সাথে মিলুন: v-র মান compile time-ই জানা হয়ে গেছে, তাই return-এর operand propagation-এ constant হয়ে গেছে।
নিজে বাড়ান: (১) z ← y * 0.0-জাতীয় float variant যোগ করে দেখান
fold আটকে যায়; (২) CSE pass লিখুন — ব্লকে expression→temp map; (৩)
LICM: loop header-এ back-edge detect করে invariant তুলুন।
বাস্তব সিস্টেমে
LLVM/GCC pass pipelines। -O2 LLVM-এ শতাধিক pass চালায় — InstCombine
(folding+simplify-র supercharged রূপ), GVN (CSE), ADCE, LoopUnroll, SROA।
GCC-র FORWARDED/RTL passes একই family।
SQLite। -O2 + LTO + careful PGO দিয়ে বিল্ড — একই সোর্স, measure- ably দ্রুত; প্রমাণ করে এই লেসনের passes গুরুত্বপূর্ণ software-এ production-grade লাভ দেয়।
JavaScript V8। Hot function inline করে, type-feedback দিয়ে fold করে, deopt guard-এর বিনিময়ে — interpreter tier থেকে optimizing tier- এ যাওয়াটাই আজকের passes-এর dynamic version।
Game engines ও HPC। Unrolling/vectorization (-O3, pragma) আর PGO frame budget-এর জীবন-মরণ; embedded-তে উল্টো -Os — প্রতিটা জগৎ নিজের profitable সংজ্ঞা বেছে নেয়।
আপনার toy compiler। এই লেসনের pipeline সরাসরি তার optimizer phase — পরের লেসনের allocator এই ছোট হয়ে যাওয়া TAC-ই পাবে।
যে ভুলগুলো সবাই করে
“-O3 সবসময় -O2-এর চেয়ে দ্রুত।”
মাপলে প্রায়ই উল্টো। -O3 বেশি inlining/unrolling/vectorization করে — binary বড় হয়, instruction cache চাপ পড়ে, branch footprint ছড়ায়। Working set icache-তে আটকে থাকাটা অনেক ক্ষেত্রে unrolling-এর লাভের চেয়ে বড়। তাই distro-গুলো -O2 default রাখে, আর performance-critical code-ও profile ছাড়া -O3 বেছে নেয় না। Profitable ≠ aggressive।
“-O2-তে প্রোগ্রাম ভাঙলে optimizer-এ bug।”
Signed overflow, out-of-bounds access, uninitialized read, data race —
C/C++-এ এগুলো undefined behavior: standard কোনো behavior define করে
না। Compiler UB-কে “ঘটবেই না” assume করে optimize করার অধিকার পায় —
x + 1 > x → true, array-bounds check মুছা, infinite loop removal।
-O0 এই assumption-গুলোর ফায়দা নেয় কম, তাই “ভুলটা লুকিয়ে থাকে”। Bug
source-এ, sanitizer (-fsanitize=undefined,address) দিয়ে ধরুন।
“volatile কোড দ্রুত বা ধীর করে।”
volatile speed-knob না — visibility contract: প্রতিটা access hardware পর্যন্ত যাবে, reorder/copy/elide হবে না। ফলাফল সাধারণত ধীর (optimization বন্ধ), কিন্তু উদ্দেশ্য correctness: MMIO register-এ volatile ছাড়া poll মুছে গেলে ডিভাইস-ই respond করবে না। Speed নিয়ন্ত্রণ করতে হলে atomics/memory-ordering আলাদা জগৎ।
“Optimizer-এর output পড়া অর্থহীন — যা হবে হবে।”
ঠিক উল্টো: এই লেসনের experiment-ই দেখাল প্রতিটা pass-এর asm-signature চেনা যায়। Assembly পড়তে পারলে বোঝা যায় কোন abstraction leak করছে (বড় struct pass, virtual call hot-path-এ), aliasing কোথায় CSE আটকাচ্ছে, register pressure কোথায় spill ডাকছে। reading-compiler-output দক্ষতা optimizer-কে debug করার প্রধান হাতিয়ার।
বুঝেছেন কি না দেখুন
1নিচের প্রতিটার জন্য বলুন: কোন pass প্রযোজ্য, আর safe কি না?
A: t ← a + a B: u ← b * 0 (u,b : double)
C: v ← c + 0 D: w ← expensive(); (w কেউ পড়ে না)
প্রয়োগ
A: t ← a + a B: u ← b * 0 (u,b : double)
C: v ← c + 0 D: w ← expensive(); (w কেউ পড়ে না)A — CSE/value-numbering: a+a → shift/double, একই expression reuse;
safe। B — algebraic x*0→0 unsafe (NaN/∞); শুধু strength-reduction
সম্ভব না — mulsd থেকে যাবে। C — c+0 → c; int/float দুটোতেই safe
(float-এ +0 identity, −0 edge-case ছাড়া)। D — DCE: unreachable না
হয়েও dead — expensive() pure হলে পুরোটা মুছে যাবে; side-effect থাকলে
call থাকবে, result discard হবে।
2x * 0 → 0 transformation-টা ঠিক কোন তিনটা শর্তে ভেঙে পড়ে? প্রতিটার
একটা করে concrete উদাহরণ দিন।
যুক্তি
x * 0 → 0 transformation-টা ঠিক কোন তিনটা শর্তে ভেঙে পড়ে? প্রতিটার
একটা করে concrete উদাহরণ দিন।(১) IEEE special values: x=NaN ⇒ 0*NaN=NaN≠0; x=−∞ তেও NaN।
(২) Side effects: x = f() যেখানে f print/io করে — গুণটা মুছলে
side effect-ও মুছে যায় (তাই compilers pure-ness prove না করলে ছুঁয়ে না)।
(৩) Traps/UB: কোনো language-এ overflow trap থাকলে (Rust debug,
Java-তে না কিন্তু C-তে UB) evaluation-টা বাদ দেওয়া behavior বদলায়।
মূল শিক্ষা: safety proof language semantics পড়ে লিখতে হয়।
3Mark-sweep DCE-তে branch/goto-কে side-effect (root) ধরা হয়। কেন?
Branch তো মান তৈরি করে না।
যুক্তি
Control dependence: একটা branch ঠিক করে কোন কোড execute হবে — সেটাই তার side effect। Branch মুছলে প্রোগ্রামের control flow বদলে যায়, unreachable হয়ে যাওয়া কোডের side effect আর ঘটবে না। তাই control- flow instructions সব root; বিনিময়ে যে computation শুধু dead path-এ ছিল, সেটা root-chain-এ না এলে sweep তাকে ধরে ফেলে। Jump-threading/ branch-folding আলাদা pass — সে সাবধানে branch-দের মুছে।
4আপনি একটা image-processing library-র maintainer। Profiler বলছে ৮০%
সময় একটা ৫-লাইনের inner loop-এ। কোন তিনটা pass-এর উপর বাজি ধরবেন,
কীভাবে verify করবেন?
ডিজাইন
(১) LICM/strength reduction: loop-constant ও index-math বাইরে —
asm-এ imul/lea loop-এর বাইরে আছে কি না দেখুন। (২) Unrolling +
vectorization: independent iterations SIMD-এ — -O3/pragmas, asm-এ
vector registers (ymm/xmm) খুঁজুন। (৩) Inlining: helper call loop-
এর ভেতরে থাকলে ভাঙবে। Verify: perf stat cycles আগে/পরে, asm diff,
আর golden-image correctness test (safe শর্ত!) — speed লাভের সাথে
behavior এক আছে কি না।
5-O0 থেকে -O2-তে গেলে সাধারণত instruction count কমে, কিন্তু কোন কোন
ক্ষেত্রে count বাড়ে তবুও দ্রুত হয়? উদাহরণসহ।
স্মরণ
Unrolling: loop-body ৪ কপি — instruction count বাড়ে, কিন্তু branch/ counter-update প্রতি ৪ iteration-এ একবার → dispatch কম, ILP বাড়ে, mispredict rate কমে। Vectorization: এক instruction-এ ৪-৮ data — count কমেও যায় বাড়েও, throughput বাড়ে-ই। Inlining: callee কপি হয়ে size বাড়ে, কিন্তু call overhead আর lost-optimization-opportunity বাঁচে। মোরাল: metric হিসেবে cycle-count, instruction-count না।
এরপর কী
এরপর কী
আজকের passes কোড কমাল — কিন্তু এখনো সব গণনা abstract variable-এ,
অসীম supply-র temporary-তে। t1, t2, v, w — IR লেসন থেকে চলা
অভ্যাস: নতুন ফলাফলের জন্য নতুন নাম। Hardware-এ সেই luxury নেই:
x86-64-এ ১৬টা GPR, তারও কয়েকটা reserved।
পরের লেসন (Register Allocation) liveness-কে interference graph বানায়, graph coloring-এর NP-complete জগতে নামে, spilling-এর খরচ মাপে, আর JIT-প্রিয় linear scan-এর সাথে Chaitin-Briggs-এর লড়াই দেখায়। আজকের shrink করা TAC তারই ইনপুট।
আরও পড়ুন
- Compilers: Principles, Techniques, and Tools (2nd ed.) — Chapter 9: Machine-Independent Optimizations — Alfred V. Aho, Monica S. Lam, Ravi Sethi, Jeffrey D. Ullman · ৯.৩-৯.৫: local/global optimization, dataflow-ভিত্তিক সব classic pass
- Engineering a Compiler (2nd ed.) — Scalar Optimizations — Keith Cooper, Linda Torczon · প্রতিটা pass-এর profitability analysis — এই লেসনের কেন্দ্রীয় প্রশ্নের গভীর treatment
- "Global Code Motion/Global Value Numbering" — Cliff Click · PLDI 1995 — global optimization-কে এক framework-এ বাঁধার landmark
- Advanced Compiler Design and Implementation — Steven S. Muchnick · Optimization pass-গুলোর পূর্ণ catalog — এই লেসনের প্রতিটা pass-এর গভীর version