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

Control Flow Graph ও Dataflow Analysis — প্রবাহের উপর গণিত

Control Flow Graph and Dataflow Analysis

কম্পাইলার প্রোগ্রামকে basic block-এর একটা গ্রাফে (CFG) ভেঙে সেই গ্রাফের উপর সেট-ভিত্তিক গণিত চালিয়ে জানে ঠিক কোথায় কোন মান বাঁচা আছে — এটাই প্রতিটা global optimization-এর ভিত্তি।

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

  • leader rule ব্যবহার করে যেকোনো TAC listing-কে basic block-এ ভাগ করে control flow graph আঁকতে পারবেন — successor/predecessor edge সহ
  • reaching definitions analysis-এর জন্য gen/kill সেট বের করে IN/OUT equation লিখতে পারবেন এবং fixed point-এ পৌঁছানো পর্যন্ত iteration হাতে trace করতে পারবেন
  • backward analysis হিসেবে live variable analysis চালিয়ে প্রতিটা প্রোগ্রাম পয়েন্টে কোন ভেরিয়েবল বাঁচা আছে তা হিসাব করতে পারবেন
  • সসীম lattice আর monotone transfer function-এর কারণে fixed-point iteration অবশ্যই থামবে কেন, তা ব্যাখ্যা করতে পারবেন
  • dataflow ফলাফল থেকে unreachable code, dead definition আর register pressure-এর আভাস বের করতে পারবেন

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

আগে এটা বুঝি

আগের লেসনগুলোতে toy language lexer, parser, AST, semantic analysis পার হয়ে intermediate representation লেসনে three-address code (TAC)-এ নেমে এসেছে।

কিন্তু একটা গভীর সমস্যা ঝুলে আছে। TAC-এর লিখিত ক্রম আর runtime পথ এক নয় — x ← 5-এর পরে y ← x + 1 দেখে মানুষ বলে “y তো ৬ হবেই”, কম্পাইলার পারে না: মাঝে branch থাকতে পারে, loop ফিরে এসে x বদলে দিতে পারে।

আজকের মূল দাবি: সম্ভাব্য চলার পথগুলো একটা গ্রাফে ফুটিয়ে তুললে — control flow graph — আর তার উপর সেট-গণিত চালালে কম্পাইলার প্রোগ্রাম একবারও চালিয়ে না দেখেই জানে: কোন definition কোথায় পৌঁছায়, কোন ভেরিয়েবল কখন মৃত, কোন কোড কোনোভাবেই চলবে না। এই গণিতই পরের তিন লেসনের ইঞ্জিন — optimization (১০), register allocation (১১), scheduling (১২)।

মূল ধারণা

Basic block — বিশ্লেষণের একক

পুরো TAC listing instruction ধরে বিশ্লেষণ অকারণে সূক্ষ্ম — কম্পাইলার আগে গুচ্ছ বাঁধে, basic block:

Basic block: instruction-দের এমন টানা sequence যার ঠিক একটা entry আর ঠিক একটা exit — মাঝে কোনো jump না, jump-target না। প্রথম instruction চললে বাকিগুলো অবশ্যই পরপর চলবে; বাইরের পথ মাঝে ঢুকতে পারে না, বেরোনোর রাস্তাও শেষ instruction ছাড়া নেই।

কোন instruction-গুলো নতুন ব্লক শুরু করে? তিনটা নিয়ম — leaders rule:

  1. listing-এর প্রথম instruction একটা leader।
  2. যেকোনো jump/branch-এর target label-এর instruction একটা leader।
  3. যেকোনো jump/branch/return-এর ঠিক পরের instruction একটা leader।

Leader থেকে পরের leader-এর আগ পর্যন্ত সব instruction = একটা ব্লক।

Control Flow Graph

ব্লকগুলো node, তাদের মাঝে edge টানলেই control flow graph (CFG):

  • Fall-through edge: শেষ instruction jump না হলে লিস্টের পরের ব্লকে edge।
  • Jump edge: goto L / if … goto L হলে label L-এর ব্লকে edge।
  • Conditional branch = দুইটা outgoing edge; return = শূন্য।

Entry হলো প্রথম instruction যে ব্লকে। এক ব্লকের successor-রা তার outgoing edge-এর গন্তব্য; predecessor-রা incoming edge-এর উৎস।

Dataflow framework — গ্রাফের উপর সেট-গণিত

CFG হয়ে গেলে বিশ্লেষণের সব প্রশ্ন একই ছাঁচে ফেটে আসে। প্রতিটা dataflow problem-এর চারটা উপাদান:

উপাদানপ্রশ্নreaching definitions-এ
Fact domainকী জানতে চাই?প্রতিটা assignment-এর একটা নাম (d₁, d₂, …)
Transfer functionএক ব্লক জ্ঞানকে কী করে?gen/kill (নিচে)
Meet operatorকয়েকটা আসা পথ জোড়ায় কী সিদ্ধান্ত?union — যেকোনো এক পথে পৌঁছালেই যথেষ্ট
Directionপ্রবাহের সাথে সাথে, না উল্টো?forward

Reaching definitions

Definition মানে assignment (x ← …)। প্রশ্ন: কোনো প্রোগ্রাম পয়েন্টে কোন definition ওভাররাইট না হয়ে পৌঁছাতে পারে? ওই পয়েন্টে x পড়লে মানটা কোন লাইন থেকে আসবে — উত্তর একাধিকও হতে পারে (ভিন্ন execution, ভিন্ন পথ)। প্রতিটা assignment-এর জন্য:

  • gen — নিজে যে definition তৈরি করে
  • kill — একই ভেরিয়েবলের আগের definitions, যেগুলো ওভাররাইট হয়

পুরো ব্লকের gen/kill = ভেতরের instruction-গুলোর gen/kill ক্রমান্বয়ে জোড়া (build section-এ কোডে দেখব)। প্রতি ব্লকে দুইটা equation:

IN[B]=⋃P∈pred(B)OUT[P]OUT[B]=gen(B)∪(IN[B]−kill(B))\text{IN}[B] = \bigcup_{P \in \text{pred}(B)} \text{OUT}[P] \qquad \text{OUT}[B] = \text{gen}(B) \cup \left(\text{IN}[B] - \text{kill}(B)\right)

প্রথমটা: ব্লকে ঢোকার সময় সব predecessor থেকে আসা জ্ঞানের union। দ্বিতীয়টা: ভেতর দিয়ে গেলে নিজের gen যোগ, নিজের kill বাদ।

Lattice intuition — জ্ঞানের সিঁড়ি

সেটগুলোকে lattice-এ ভাবুন: সব fact-এর power set, নিচে ∅ (“কিছুই জানি না”), উপরে full set। Union meet-এ তথ্য শুধু উপরে যায় — এই একমুখী চড়াই-ই convergence-এর চাবি। reaching definitions হলো may analysis (“পৌঁছাতে পারে”); must analysis — available expressions (“অবশ্যই পৌঁছেছে”) — ঐ একই framework-এ meet intersection; direction উল্টালে backward analysis — liveness, আজকের দ্বিতীয় উদাহরণ।

ভেতরে কী ঘটছে

Iteration কেন থামে — monotone + সসীম lattice

Equations আত্ম-নির্দেশক: OUT নির্ভর করে IN-এর উপর, IN অন্য ব্লকের OUT- এর। সমাধান: সবাইকে ∅ থেকে শুরু করে equations পালা করে চালিয়ে যাওয়া — fixed-point iteration। কেন থামবে? দুইটা নিশ্চয়তা:

  1. সসীম lattice: facts N-টা হলে সেটের সম্ভাব্য মান সর্বোচ্চ 2^N — বিশাল, কিন্তু সসীম।
  2. Monotone transfer function: input সেট বাড়লে output কখনো ছোট হয় না। Union-based gen/kill একেবারে সরাসরি monotone।

Induction: প্রতিটা মান শুধু union-এ বাড়ে (monotonicity), lattice-এর উচ্চতা ছাড়াতে পারে না (finiteness)। কোনো round-এ কিছুই না বদলালেই fixed point — equations-ই একমাত্র পরিবর্তনের উৎস, তাই আর বদলাবেও না। Iteration সর্বোচ্চ প্রায় facts × ব্লক ধাপে থামবে — polynomial।

Direction আর ব্লক-স্তরের use/def

Backward analysis-তে (liveness) প্রবাহ উল্টো — exit থেকে entry-র দিকে:

OUT[B]=⋃S∈succ(B)IN[S]IN[B]=use(B)∪(OUT[B]−def(B))\text{OUT}[B] = \bigcup_{S \in \text{succ}(B)} \text{IN}[S] \qquad \text{IN}[B] = \text{use}(B) \cup \left(\text{OUT}[B] - \text{def}(B)\right)

এখানে fact = “ভেরিয়েবলের মান ভবিষ্যতে পড়া হতে পারে”। ব্লক-স্তরে: use(B) — ভেতরে পড়া, কিন্তু পড়ার আগে define-হয়নি এমন ভেরিয়েবল (upward-exposed); def(B) — ব্লকে define হওয়া সব।

Liveness কেন দরকার? “এখন কে বাঁচা” জানলেই জানা যায়: কোন store মোছা যায় (মৃত ভেরিয়েবলে লেখা), কোন দুই ভেরিয়েবল একই register ভাগ করতে পারে (কখনো একসাথে বাঁচা না — লেসন ১১-এর ভিত্তি), আর pressure কতটা।

Source থেকে optimization সিদ্ধান্ত পর্যন্ত — আজ আমরা মাঝের দুই স্তর
  1. ASTsyntax tree — গঠন আছে, প্রবাহ নেই
  2. TAClabels + jumps সহ three-address code
  3. CFGbasic block + edge — প্রবাহের গ্রাফ, here: true
  4. Dataflow factsIN/OUT সেট — গ্রাফে চলা সেট-গণিত
  5. Optimization decisionsDCE, CSE, allocation — লেসন ১০-১২

উদাহরণ

এক listing, দুই analysis — হাতে হাতে

আজকের canonical example — ৬ instruction-এর sum-loop. Convention: #n = constant; labels L_done-জাতীয়।

        # B0 (entry)
   1:   s ← 0
   2:   i ← 0
        # B1 (loop header)
   3:   if i >= n goto L_done
        # B2 (body)
   4:   s ← s + i
   5:   i ← i + 1
        # fall-through: B2 → B1 (back edge!)
L_done: # B3 (exit)
   6:   return s

Leader: ১ (নিয়ম ১), ৪ (নিয়ম ৩), L_done-এর ৬ (নিয়ম ২)। Edges: B0→B1, B1→B2+B1→B3, B2→B1 (back edge), B3→∅। Definitions: d₁=s@1, d₂=i@2, d₃=s@4, d₄=i@5; kill(d₃)={d₁}, kill(d₄)={d₂}।

Reaching definitions — round by round

RoundIN[B0]OUT[B0]IN[B1]OUT[B1]IN[B2]OUT[B2]IN[B3]
init∅∅∅∅∅∅∅
1∅{d₁,d₂}{d₁,d₂}{d₁,d₂}{d₁,d₂}{d₃,d₄}{d₁,d₂,d₃,d₄}
2∅{d₁,d₂}{d₁,d₂,d₃,d₄}{d₁,d₂,d₃,d₄}{d₁,d₂}{d₃,d₄}{d₁,d₂,d₃,d₄}
3— কোনো পরিবর্তন নেই → fixed point

Round 1-এ B2-এর OUT ফাঁকা ছিল (processing order), তাই B1-এর IN অসম্পূর্ণ; Round 2-তে back edge-এর জ্ঞান ঘুরে এসে B1 সমৃদ্ধ হলো — loop-carried information: body-র d₃/d₄ header-তে পৌঁছাতে পারে। Round 3 নিশ্চিত করল আর কিছু বদলাচ্ছে না। আর return s-এ (B3) পৌঁছায় d₁ এবং d₃ দুইটাই — s init থেকেও আসতে পারে, body থেকেও।

Live variable analysis — একই listing, উল্টো দিক

use/def: B0 use=∅ def={s,i}; B1 use={i,n}; B2 use={s,i} def={s,i}; B3 use={s}। Backward iteration (B3→B2→B1→B0):

RoundOUT[B1]=IN[B2]∪IN[B3]IN[B2]IN[B1]IN[B0]
1{s,i}{s,i}{i,n,s}{n}
2{i,n,s}{i,n,s}{i,n,s}{n}
3— fixed point —

ব্যাখ্যা: B1-এ ঢোকার সময় i, n, s তিনটাই বাঁচা — i/n test-এ, s return-এ। max live = ৩ — কমপক্ষে ৩টা register লাগবে; {i,n,s} সেটটাই লেসন ১১-এর interference graph-এর কাঁচামাল।

ছবিতে

CFG চোখে দেখা

            ┌────────────────┐
            │  B0 (entry)    │   s ← 0 ; i ← 0
            └───────┬────────┘
                    ▼
            ┌────────────────┐◄────────────┐
            │  B1 (header)   │             │
            │  if i >= n ────│──── taken ──┼──► B3: return s
            └───────┬────────┘             │
                    │ not taken            │ back edge
                    ▼                      │ (B2 → B1)
            ┌────────────────┐             │
            │  B2 (body)     │─────────────┘
            └────────────────┘

Edge-এর দিক = runtime প্রবাহ; B2→B1 arrow-টাই loop — “জ্ঞান ঘুরে আসা”-র গ্রাফিক কারণ।

 TAC list ──leaders──► blocks[] ──edges──► CFG (succ/pred maps)
                                              │
                     ┌────────────────────────┴─────────────┐
                     ▼                                      ▼
        reaching defs (forward, union)          live vars (backward, union)
                     ▼                                      ▼
             DCE / CSE  (lesson 10)              reg alloc  (lesson 11)
Dataflow pipeline — প্রতিটা pass গ্রাফ পড়ে সেট লেখে, পরের pass সেই সেট পড়ে সিদ্ধান্ত নেয়।

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

EXPERIMENT

নিজের হাতে CFG আঁকুন, LLVM দিয়ে verify করুন

macOS / Linux (clang + LLVM opt)· ১৫ মিনিট

এই function-টার CFG আগে কাগজে আঁকুন (leader → block → edge):

// classify.c
int classify(int x) {
    int r = 0;
    if (x > 0)
        r = 1;
    else if (x < 0)
        r = -1;
    while (r != 0 && x % 2 == 0)
        x = x / 2;
    return r;
}

আশা: entry; x > 0 test (দুই successor); x < 0 test; assign blocks; while-header (back edge সহ); body; exit। LLVM-এর সত্যি CFG:

clang -S -emit-llvm -O0 classify.c -o classify.ll
opt -passes=dot-cfg -disable-output classify.ll   # hidden .classify.dot
dot -Tpng .classify.dot -o cfg.png && open cfg.png        # macOS
# Linux-এ open-এর বদলে xdg-open
opt -passes=view-cfg -disable-output classify.ll  # টেক্সট আকারে

Block-নামগুলো দেখুন (if.then, while.body জাতীয়) — LLVM-ও label থেকে ব্লক বানায়, ঠিক আমাদের মতো। -O2 দিয়ে emit করলে ব্লক সংখ্যা কমবে (পরের লেসন)। শেষে হাতে-আঁকা গ্রাফটাই networkx-এ encode করে যাচাই:

import networkx as nx

g = nx.DiGraph([
    ('entry', 'test_pos'), ('test_pos', 'r1'), ('test_pos', 'test_neg'),
    ('test_neg', 'r_m1'),  ('test_neg', 'while_hdr'),
    ('r1', 'while_hdr'),   ('r_m1', 'while_hdr'),
    ('while_hdr', 'body'), ('while_hdr', 'exit'),
    ('body', 'while_hdr'),
])
print('BFS order:', list(nx.bfs_tree(g, 'entry').nodes))

g.add_edge('dead_block', 'exit')     # unreachable একটা ব্লক জোড়া দিন
print('unreachable:', set(g.nodes) - nx.descendants(g, 'entry') - {'entry'})

শেষ line প্রিন্ট করবে \{'dead_block'\} — entry থেকে অগম্য node খুঁজে বের করল, ঠিক যেভাবে DCE pass করে।

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

আপনার হাতে আঁকা গ্রাফ আর LLVM-এর internal CFG একই হয় — basic block, edge, reachability এই তিনটাই compiler infrastructure-এর প্রথম-শ্রেণির object, কোনো abstract তত্ত্ব না।

নিজে বানান

BUILD IT

CFGBuilder + LiveVariableSolver — toy compiler-এর dataflow স্তর

Python · ●●●○○
  1. TACInstr dataclass-এ TAC instruction model করুন (op, dest, a, b, label)
  2. find_leaders() + build_cfg() — leaders rule, label→block map, succ/pred সেট
  3. block_use_def() — প্রতি ব্লকে upward-exposed use আর def সেট
  4. solve_liveness() — backward fixed-point loop, প্রতি round-এ টেবিল প্রিন্ট
  5. max live হিসাব করে register pressure রিপোর্ট করুন

IR লেসনের TAC emitter-এর naming-ই চলছে এখানে (TACInstr, BasicBlock, CFG) — এই ফাইলটাই পরের তিন লেসনের build-এর base।

from dataclasses import dataclass, field

TERMINATORS = {'if_>=', 'if_<', 'goto', 'return'}   # ব্লক শেষ করে এমন op

@dataclass(frozen=True)
class TACInstr:                    # toy compiler-এর three-address instruction
    op: str                        # '+', '-', '*', copy, print, if_>=, ...
    dest: str | None = None
    a: str | None = None           # operand ('#' prefix = constant)
    b: str | None = None
    label: str | None = None

    def uses(self):                # পঠিত ভেরিয়েবল (constant না)
        return [x for x in (self.a, self.b) if x and not x.startswith('#')]

@dataclass
class BasicBlock:
    bid: int
    instrs: list                   # [TACInstr]
    succs: set = field(default_factory=set)
    preds: set = field(default_factory=set)

class CFG:
    def __init__(self, blocks):
        self.blocks = blocks
        self.succ = {b.bid: sorted(b.succs) for b in blocks}
        self.pred = {b.bid: sorted(b.preds) for b in blocks}
        self.adjacency = '\n'.join(
            f"  B{b.bid} -> " + (', '.join(f'B{s}' for s in sorted(b.succs)) or '(none)')
            for b in blocks)

def find_leaders(instrs):
    leaders = {0}                                  # নিয়ম ১
    for i, ins in enumerate(instrs):
        if ins.op == 'label':
            leaders.add(i)                         # নিয়ম ২
        if ins.op in TERMINATORS and i + 1 < len(instrs):
            leaders.add(i + 1)                     # নিয়ম ৩
    return leaders

def build_cfg(instrs) -> CFG:
    starts = sorted(find_leaders(instrs))
    bounds = starts + [len(instrs)]
    blocks, label_to_bid, bid_of_index = [], {}, {}
    for bid, lo in enumerate(starts):
        blk = BasicBlock(bid=bid, instrs=instrs[bounds[bid]:bounds[bid+1]])
        head = blk.instrs[0]
        if head.op == 'label':
            label_to_bid[head.label] = bid
        bid_of_index.update({i: bid for i in range(bounds[bid], bounds[bid+1])})
        blocks.append(blk)

    for k, blk in enumerate(blocks):
        last, nxt = blk.instrs[-1], k + 1 if k + 1 < len(blocks) else None
        if last.op == 'goto':
            blk.succs.add(label_to_bid[last.label])
        elif last.op.startswith('if_'):
            blk.succs |= {label_to_bid[last.label]} | ({nxt} if nxt is not None else set())
        elif last.op != 'return' and nxt is not None:
            blk.succs.add(nxt)                     # fall-through
    for blk in blocks:
        for s in blk.succs:
            blocks[s].preds.add(blk.bid)
    return CFG(blocks)

def block_use_def(block):
    use, dfn = set(), set()
    for ins in block.instrs:
        for v in ins.uses():
            if v not in dfn:
                use.add(v)                         # upward-exposed only
        if ins.dest:
            dfn.add(ins.dest)
    return use, dfn

def solve_liveness(cfg: CFG):
    use, dfn = zip(*(block_use_def(b) for b in cfg.blocks))
    live_in = {b.bid: frozenset() for b in cfg.blocks}
    live_out = {b.bid: frozenset() for b in cfg.blocks}
    rnd, changed = 0, True
    while changed:                                 # fixed-point iteration
        rnd += 1
        changed = False
        for b in reversed(cfg.blocks):             # backward: উল্টো ক্রমে
            new_out = set().union(*(live_in[s] for s in b.succs)) if b.succs else set()
            new_in = use[b.bid] | (new_out - dfn[b.bid])
            if new_in != live_in[b.bid] or new_out != live_out[b.bid]:
                live_in[b.bid], live_out[b.bid] = new_in, new_out
                changed = True
        fmt = lambda s: '{' + ','.join(sorted(s)) + '}'
        print(f'round {rnd}: ' + '   '.join(
            f'B{b.bid} IN={fmt(live_in[b.bid])} OUT={fmt(live_out[b.bid])}'
            for b in cfg.blocks))
    return live_in, live_out

program = [
    TACInstr('copy',  's', '#0'),
    TACInstr('copy',  'i', '#0'),
    TACInstr('if_>=', None, 'i', 'n', 'L_done'),
    TACInstr('+',     's', 's', 'i'),
    TACInstr('+',     'i', 'i', '#1'),
    TACInstr('label', None, None, None, 'L_done'),
    TACInstr('return', None, 's'),
]

cfg = build_cfg(program)
print(cfg.adjacency)
live_in, live_out = solve_liveness(cfg)
print(f'max live (register pressure) = {max(len(s) for s in live_in.values())}')

প্রত্যাশিত output — example section-এর টেবিলের সাথে মিলিয়ে দেখুন:

  B0 -> B1
  B1 -> B2, B3
  B2 -> B1
  B3 -> (none)
round 1: B0 IN={n} OUT={i,n,s}   B1 IN={i,n,s} OUT={i,s}   B2 IN={i,s} OUT={}   B3 IN={s} OUT={}
round 2: B0 IN={n} OUT={i,n,s}   B1 IN={i,n,s} OUT={i,n,s}   B2 IN={i,n,s} OUT={i,n,s}   B3 IN={s} OUT={}
round 3: ...same... (no change — fixed point reached after 3 rounds)
max live (register pressure) = 3

Round 1 বনাম Round 2-এর পার্থক্যটাই back edge-এর উপহার — body-র তথ্য header-এ ফিরে এসেছে। নিজে বাড়ান: (১) reaching definitions solver — forward দিক, gen/kill আগে হিসাব করে; (২) worklist version, round-robin- এর recomputation count-এর সাথে তুলনা।

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

Dataflow যেখানে বাস্তবে চলে

LLVM/GCC-র প্রায় প্রতিটা middle-end pass (DominatorTree, LivenessAnalysis, MemorySSA) dataflow framework-এর industrial রূপ। DCE-র precondition: reachable তো? মান কোথাও পড়া হয় তো? Reg allocation-এর ভিত্তি: interference graph liveness sets থেকেই (লেসন ১১)। Static analyzers: SpotBugs-এর null-dereference, clang-tidy-র uninitialized-use — reaching-definition-family; IDA/Ghidra- র control flow view malware analysis-এ এই CFG-ই। JIT tiering: V8 bytecode-ও CFG-তে ভাঙে — hot-block counter থেকে tiering decision। আপনার toy compiler-ও আজকের ফাইলের উপরই দাঁড়াবে।

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

“CFG আর call graph একই জিনিস।”

না। CFG node = এক function-এর basic block, edge = ভেতরের jump; Call graph node = পুরো function, edge = call relation। ৫০০-লাইনের function call graph-এ এক node; CFG-তে শত শত। Inlining (পরের লেসন) call graph পড়ে, DCE/CSE CFG — ভুল গ্রাফ, ভুল উত্তর।

“Dataflow analysis চালাতে প্রোগ্রামটা চালাতে হয়।”

ঠিক উল্টো — fixed-point iteration পুরোপুরি compile time-এ, input ছাড়াই চলে; উত্তরও input-independent (“s এখানে d₁ অথবা d₃ থেকে আসতে পারে”), তাই run না দেখেই optimize করা যায়। Runtime profiling (PGO) ভিন্ন: কোন পথ ঘন ঘন, correctness না।

“Fixed point মানে প্রোগ্রাম থেমে গেছে।”

Fixed point analysis-এর state-এর বৈশিষ্ট্য, program-এর না — equation- গুলো আর কোনো মান বদলাতে পারে না এমন self-consistent অবস্থা, যেখানে থেমে যেতে iteration-কে বলা হয়। বিশ্লেষিত প্রোগ্রাম তখনো loop ঘোরে। সুবিধা: থামার সিদ্ধান্তটাই একটা theorem (monotone + finite ⇒ terminates), solver-এ timeout-এর ভয় নেই।

“Unreachable code detect করাই DCE।”

Reachability মাত্র অর্ধেক কাজ। Definition reachable হয়েও dead হতে পারে — মান গণনা হয়, কিন্তু পরে কেউ পড়ে না: t ← expensive(); return 1 — t-র definition reachable, তবু মোছা safe। পরের লেসনের mark-sweep DCE দুই প্রশ্ন একসাথে করে: reachability + usefulness।

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

1

নিচের ৭-line TAC-তে কয়টা basic block, আর edge কোন কোনগুলো?

1: x ← a + b
2: goto L2
3: y ← x * 2
L2:
4: if x > y goto L1
5: z ← 0
L1:
6: return z
প্রয়োগ

Leaders: 1 (নিয়ম ১), 3 (line 2-এর পরে, নিয়ম ৩), L2-এর 4 (নিয়ম ২), 5 (line 4-এর পরে, নিয়ম ৩), L1-এর 6 (নিয়ম ২)। মোট ৫টা ব্লক: B1={1,2}, B2={3}, B3={4}, B4={5}, B5={6}।

Edges: B1→B4 (goto L2), B2→B3 (fall-through), B3→B5 (taken) + B3→B4 (fall-through), B4→B5। আর line 3-এর B2 unreachable — কোনো edge তার দিকে যায় না, কারণ goto-টা তাকে স্কিপ করে গেছে।

2

Reaching definitions-এ যদি meet operator union-এর বদলে intersection ভাবেন, ফলাফল কী অর্থ বহন করবে? এটা কি কোনো দরকারি analysis হতে পারে?

যুক্তি

হবে must analysis: “যে definitions সব পথেই পৌঁছায়”। অর্থ আছে — available expressions-এর আত্মীয়: expression সব পথে পৌঁছালে ও operands অক্ষত থাকলে CSE করা যায় (পরের লেসনের pass!)। Framework-এর সৌন্দর্য: meet (∅-এর বদলে full set) বদলালেই নতুন analysis — Kildall 1973।

3

Example section-এর sum-loop-এ s ← 0 (d₁) কোন কোন ব্লকের OUT-এ পৌঁছায়, কোথায় kill হয়? উত্তরটা কেন “s-এর মান ০ থেকে এসেছে” বলার অনুমতি দেয় না?

যুক্তি

d₁ OUT[B0]-তে জন্ম; B1 ধরে রাখে; B2-এ d₃ kill করে, তাই OUT[B2]={d₃,d₄}। কিন্তু IN[B3] = {d₁,d₂} ∪ {d₃,d₄} — d₁ exit-এও পৌঁছায়, B0→B1→B3 পথে (n ≤ 0) body না চললে সেটাই একমাত্র s-definition। তাই s = “০ অথবা Σi” — may-analysis কোনো একটা বেছে নেয় না।

4

আপনার solver-এ ব্লকগুলো আপনি reversed order-এ ঘুরিয়েছেন (backward analysis)। কেউ সাজেস্ট করল forward analysis-এর জন্যও একই reversed order রাখো। এটা কি correctness ভাঙবে, নাকি শুধু speed? যুক্তি দিন।

ডিজাইন

Correctness ভাঙবে না — monotone finite framework-এ যেকোনো order শেষমেশ একই fixed point-এ পৌঁছায়। Speed ভাঙবে: backward analysis-তে তথ্য exit থেকে entry-র দিকে বহমান; reversed order সেই ঢালের সমান্তরালে হাঁটে, প্রতি round- এ তথ্য downstream-এ ছড়ায়। Forward-order-এ একই তথ্য প্রতি round-এ এক ধাপ উল্টো যাবে — extra rounds। Fixed point অনন্য, পথ নয় — কিন্তু পথ convergence- এর গতি ঠিক করে (worklist আরও ভালো)।

5

max live = ৩। এই একটা সংখ্যা থেকে কোন দুইটা downstream সিদ্ধান্ত নেওয়া যায়, আর pressure ৩ → ৪ হলে সেই সিদ্ধান্ত কীভাবে বদলায়?

স্মরণ

(১) Feasibility: x86-64-এ ~১৫ usable GPR — ৩ সহজে বসে; সীমা ছাড়ালেই spill অনিবার্য। (২) Interference prediction: একসাথে বাঁচা জোড়া (s,i), (s,n), (i,n) একই register পাবে না — লেসন ১১-র graph-এর edge। ৩→৪ উঠলে নতুন edge, coloring কঠিন, সীমান্ত machine-এ spill cost-এর দরজা খোলে।

এরপর কী

এরপর কী

আজ প্রবাহের গ্রাফ আর তার উপর গণিত দিয়ে জানলাম কোন definition কোথায় পৌঁছায়, কোন ভেরিয়েবল কখন বাঁচা — কিন্তু এখনো কোনো কোড মোছেনি। পরের লেসন (Optimization) সেই উত্তরকে action বানায়: constant folding, algebraic simplification, CSE, mark-sweep DCE, LICM, unrolling — প্রতিটার সামনে দুই-শর্তের পরীক্ষা: safe আর profitable। দেখব কেন x * 0 মোছা সবসময় নিরাপদ না — NaN আর undefined behavior ফাঁদ পাতে।

আজকের build ফাইল রেখে দিন — পরের লেসনের pass pipeline তার উপরই চলবে।

আরও পড়ুন

  • Compilers: Principles, Techniques, and Tools (2nd ed.) — Chapter 9: Machine-Independent Optimizations — Alfred V. Aho, Monica S. Lam, Ravi Sethi, Jeffrey D. Ullman · ৯.১ Basic Blocks and Flow Graphs এবং ৯.২ Foundations of Data-Flow Analysis — এই লেসনের কাঠামোর প্রধান উৎস
  • Engineering a Compiler (2nd ed.) — Data-Flow Analysis অধ্যায় — Keith Cooper, Linda Torczon · Iterative solver আর worklist algorithm-এর সবচেয়ে practical আলোচনা
  • A Unified Approach to Global Program Optimization — Gary A. Kildall · POPL 1973 — dataflow framework abstraction-এর জন্মসূত্র
  • Advanced Compiler Design and Implementation — Steven S. Muchnick · প্রতিটা dataflow problem আর তার optimization-ব্যবহারের এনসাইক্লোপিডিক রেফারেন্স