Control Flow Graph ও Dataflow Analysis — প্রবাহের উপর গণিত
Control Flow Graph and Dataflow Analysis
কম্পাইলার প্রোগ্রামকে basic block-এর একটা গ্রাফে (CFG) ভেঙে সেই গ্রাফের উপর সেট-ভিত্তিক গণিত চালিয়ে জানে ঠিক কোথায় কোন মান বাঁচা আছে — এটাই প্রতিটা global optimization-এর ভিত্তি।
আগে এটা বুঝি
আগের লেসনগুলোতে 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:
- listing-এর প্রথম instruction একটা leader।
- যেকোনো jump/branch-এর target label-এর instruction একটা leader।
- যেকোনো 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হলে labelL-এর ব্লকে 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:
প্রথমটা: ব্লকে ঢোকার সময় সব 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। কেন থামবে? দুইটা নিশ্চয়তা:
- সসীম lattice: facts N-টা হলে সেটের সম্ভাব্য মান সর্বোচ্চ 2^N — বিশাল, কিন্তু সসীম।
- 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-র দিকে:
এখানে fact = “ভেরিয়েবলের মান ভবিষ্যতে পড়া হতে পারে”। ব্লক-স্তরে: use(B) — ভেতরে পড়া, কিন্তু পড়ার আগে define-হয়নি এমন ভেরিয়েবল (upward-exposed); def(B) — ব্লকে define হওয়া সব।
Liveness কেন দরকার? “এখন কে বাঁচা” জানলেই জানা যায়: কোন store মোছা যায় (মৃত ভেরিয়েবলে লেখা), কোন দুই ভেরিয়েবল একই register ভাগ করতে পারে (কখনো একসাথে বাঁচা না — লেসন ১১-এর ভিত্তি), আর pressure কতটা।
- ASTsyntax tree — গঠন আছে, প্রবাহ নেই
- TAClabels + jumps সহ three-address code
- CFGbasic block + edge — প্রবাহের গ্রাফ, here: true
- Dataflow factsIN/OUT সেট — গ্রাফে চলা সেট-গণিত
- 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 sLeader: ১ (নিয়ম ১), ৪ (নিয়ম ৩), 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
| Round | IN[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):
| Round | OUT[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)নিজে চালিয়ে দেখুন
নিজের হাতে CFG আঁকুন, LLVM দিয়ে verify করুন
এই 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 তত্ত্ব না।
নিজে বানান
CFGBuilder + LiveVariableSolver — toy compiler-এর dataflow স্তর
- TACInstr dataclass-এ TAC instruction model করুন (op, dest, a, b, label)
- find_leaders() + build_cfg() — leaders rule, label→block map, succ/pred সেট
- block_use_def() — প্রতি ব্লকে upward-exposed use আর def সেট
- solve_liveness() — backward fixed-point loop, প্রতি round-এ টেবিল প্রিন্ট
- 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) = 3Round 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
প্রয়োগ
1: x ← a + b
2: goto L2
3: y ← x * 2
L2:
4: if x > y goto L1
5: z ← 0
L1:
6: return zLeaders: 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-টা তাকে স্কিপ করে গেছে।
2Reaching definitions-এ যদি meet operator union-এর বদলে intersection
ভাবেন, ফলাফল কী অর্থ বহন করবে? এটা কি কোনো দরকারি analysis হতে পারে?
যুক্তি
হবে must analysis: “যে definitions সব পথেই পৌঁছায়”। অর্থ আছে — available expressions-এর আত্মীয়: expression সব পথে পৌঁছালে ও operands অক্ষত থাকলে CSE করা যায় (পরের লেসনের pass!)। Framework-এর সৌন্দর্য: meet (∅-এর বদলে full set) বদলালেই নতুন analysis — Kildall 1973।
3Example section-এর sum-loop-এ s ← 0 (d₁) কোন কোন ব্লকের OUT-এ পৌঁছায়,
কোথায় kill হয়? উত্তরটা কেন “s-এর মান ০ থেকে এসেছে” বলার অনুমতি দেয় না?
যুক্তি
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 আরও ভালো)।
5max 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-ব্যবহারের এনসাইক্লোপিডিক রেফারেন্স