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

Optimization — কোনটা safe, কোনটা profitable, কোনটা fast

Optimization: Safe, Profitable, Fast

প্রতিটা optimization একই চুক্তির উপর দাঁড়ায় — semantics অটুট রেখে সত্যিই দ্রুত করা — আর সেই চুক্তির প্রতিটা clause-এর একটা লুকানো ব্যতিক্রম আছে।

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

  • প্রতিটা optimization-কে দুই-শর্তের কসাইতে ফেলার মাধ্যমে যাচাই করতে পারবেন — semantics-preserving (safe) এবং measurable speedup (profitable)
  • constant folding/propagation, algebraic simplification আর CSE হাতে apply করে basic block scope-এ কোড shrink করতে পারবেন
  • `x * 0` ধরনের 'স্পষ্ট' simplification কেন float NaN আর signed overflow UB-এর কারণে ভুল হতে পারে তা ব্যাখ্যা করতে পারবেন
  • mark-sweep dead code elimination আর loop-invariant code motion-এর algorithm ট্রেস করে নিজে হাতে চালাতে পারবেন
  • -O0 থেকে -O3 flag-গুলো কী চালু করে, আর `volatile` কীভাবে পুরো optimizer-কে হাত-গুটিয়ে দেয় তা বলতে পারবেন

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

আগে এটা বুঝি

আগের লেসনে আমরা কম্পাইলারকে চোখ এনে দিয়েছি — CFG আর dataflow analysis দিয়ে সে এখন জানে কোন definition কোথায় পৌঁছায়, কোন ভেরিয়েবল কখন বাঁচা। আজ সেই জ্ঞান প্রথমবার অর্থ তৈরি করবে: কোড মোছা, গণনা বাদ, loop ছেঁটে ফেলা।

কিন্তু শুরুতেই একটা শৃঙ্খলা। কম্পাইলার “ভালো লাগার” জন্য কিছু optimize করে না; প্রতিটা রূপান্তর দুইটা শর্ত পূরণ করতে হয়:

  1. Safe: প্রোগ্রামের observable behavior হুবহু এক থাকে — প্রতিটা সম্ভাব্য input-এ, প্রতিটা সম্ভাব্য execution-এ।
  2. 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 − 0xalways
x * 1, x / 1xalways
x * 00শুধু যদি 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*limit loop-এর বাইরে। 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; redundant mov pair মুছে ফেলা। সস্তা, সবসময় শেষে চলে।

-O flags — কোন level-এ কী চালু

Flagবাস্তবে কী
-O0প্রায় কিছুই না — debugging-এর জন্য source-এর সাথে faithful
-O1folding/simplify/DCE/CSE-র হালকা সেট
-O2aggressive inlining, LICM, unrolling-heuristics — release default
-O3vectorization, heavy unrolling — বড় হয়, সবসময় দ্রুত না
-Os/-Ozsize-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 দেয়।

Optimization pass pipeline — একটা typical -O2 সিকোয়েন্সের সরলীকৃত চিত্র
  1. CanonicalizationSSA/normal form — সব pass এক ভাষায়
  2. Local passesfold · propagate · simplify · CSE
  3. Global passesDCE · inlining · LICM · unrolling
  4. Peepholeassembly window-এ শেষ ঝাড়ু
  5. 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 v

Pass 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? ✓
TAC ঢুকে যায়, pass-chain পার হয়ে ছোট TAC বেরোয় — প্রতিটা pass নিজের IN/OUT জানে।

Ordering-ও বিষয়: folding আগে চললে DCE-র কাছে বেশি dead instruction পৌঁছায়; inlining আগে চললে ভেতরের constant-গুলো outer context-এ fold হয়। Pass ordering নিজেই একটা research topic — Click-এর GCM paper একই framework-এ কয়েকটা pass একসাথে চালানোর প্রস্তাব।

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

EXPERIMENT

Godbolt locally — clang -O0 বনাম -O2, ছয় snippet

macOS / Linux (clang)· ২০ মিনিট
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
f1mov eax, 17constant folding + propagation + DCE
f2xorps/mulsd রয়ে গেছে(attempted fold blocked by NaN)
f3mov eax, 1UB-based algebraic reasoning
f4একটা lea eax,[rdi+rsi], দ্বিতীয়বার নাCSE + addressing fold
f5imul/lea loop-এর বাইরেLICM + strength reduction
f6mov 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 না।

নিজে বানান

BUILD IT

tac_optimizer.py — folding + algebraic + DCE pipeline

Python · ●●●○○
  1. আগের লেসনের TACInstr/BasicBlock/build_cfg জুড়ে নিন
  2. fold_pass(): প্রতি ব্লকে env dict রেখে constant fold + propagate
  3. simplify_algebraic(): identity টেবিল (x+0, x*1, x*0 — int-only!)
  4. dce(): mark-sweep — side-effect seeds থেকে def-use closure
  5. 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 #84

example 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 — 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 হবে।

2

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 পড়ে লিখতে হয়।

3

Mark-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