Register Allocation — অসীম temporary, সসীম register
Register Allocation
IR-এর অসীম temporary-দের সসীম x86-64 register-এ ফেটে পড়াটাই graph coloring — যেখানে রঙ না মিললে মান স্ট্যাকে ঠেলে দিতে হয়।
আগে এটা বুঝি
আজ পর্যন্ত আমাদের toy compiler একটা অলৌকিক সুবিধা ভোগ করেছে। IR লেসন
থেকে প্রতিটা intermediate ফলাফল পেতেই নতুন নাম: t1, t2, … — supply
অসীম, কেউ ফুরায় না। Optimization লেসনের passes আরও ঢেলেছে। এই virtual
world-এ সব সম্ভব।
বাস্তব machine-এ গিয়ে হিসাব একদম আলাদা: x86-64-এ ১৬টা general-purpose
register (assembly module-এর calling convention লেসনে দেখা) — তার
মধ্যে %rsp স্ট্যাক pointer হিসেবে বাঁধা, %rbp প্রায়ই frame pointer,
আর কয়েকটা caller-saved যেগুলো call পার করলেই উড়ে যায়। কার্যত usable
হয় ~১০-১৪টা।
Register allocation = অসীম virtual register-কে সসীম physical register-এ বসানো, যেন কোনো মুহূর্তে একই register-এ দুইটা ভিন্ন মান দরকার না হয়। যেখানে বসানো যায় না — spill: মান স্ট্যাক স্লটে, প্রতি ব্যবহারে load/store। এই সিদ্ধান্ত প্রতি instruction-এ ছুঁয়ে যায়; allocation-এর quality সরাসরি final code-এর speed ঠিক করে।
মূল ধারণা
Live range থেকে interference graph
আগের লেসনের liveness analysis ইনপুট। এক ভেরিয়েবলের live range = যে সব program point-এ তার মান বাঁচা (ভবিষ্যতে পড়া হতে পারে)। সংজ্ঞা:
দুইটা ভেরিয়েবল-এর live range ওভারল্যাপ করলে — অর্থাৎ কোনো এক point-এ দুজনেই একসাথে বাঁচা — তারা একই register পেতে পারে না। এই প্রতিটা নিষেধাজ্ঞা interference graph-এর একটা edge।
Nodes = ভেরিয়েবল/temporary; edges = “একসাথে বাঁচা”। এবার প্রশ্নটা graph theory হয়ে গেল: k-টা physical register থাকলে graph-টাকে k-রঙে color করা যায় কি — adjacent node-দের ভিন্ন রঙ, রঙ = register?
Graph coloring-এর সাধারণ রূপ NP-complete — polynomial time-এ optimal করার কোনো known algorithm নেই (mathematics module-এর complexity আলোচনার স্মরণে: P ≠ NP হলে কখনোই না)। তাই compilers heuristic চালায়: বেশিরভাগ real graph সহজে রঙ হয়ে যায়, আর যেখানে হয় না — spill।
Spilling-এর খরচ
Spilled variable-এর প্রতিটা use → mov from stack slot; প্রতিটা def →
store। খরচ model:
loop depth খরচ exponential-ভাবে weight করে — inner-loop spill কয়েকশো গুণ ব্যয়বহুল outer-loop spill-এর চেয়ে। তাই allocator spill বাছাই করে: সবচেয়ে কম cost, সবচেয়ে বেশি degree — এক পাথরে দুই পাখি।
ভেতরে কী ঘটছে
Chaitin-Briggs — simplify/select loop
Classic allocator-এর হৃদয়স্পর্শী সরলতা:
repeat until graph empty:
SIMPLIFY: degree < k এমন একটা node খুঁজো → স্ট্যাকে push → গ্রাফ থেকে
node + তার edge বাদ
(edge বাদ দিলে প্রতিবেশীরও degree কমে — chain reaction!)
যদি এমন node না থাকে:
SELECT-POTENTIAL: highest-degree node → 'potential spill' হিসেবে
push → বাদ দাও (optimistic!)
until done, then:
SELECT: স্ট্যাক pop করে উল্টো ক্রমে রঙ দাও —
প্রতিবেশীদের ব্যবহৃত রঙ বাদে যেকোনো free রঙ
potential spill node-এ রঙ না মিললে → REAL SPILL → সেই variable-
এর সব use/def-এ load/store বসিয়ে নতুন ছোট live-range
তৈরি → গ্রাফ rebuild → loop আবারদুইটা subtle পয়েন্ট:
- কেন simplify ঠিক ছিল: degree
< knode-টা বাদ দিলে তার প্রতিবেশীরা রঙ পেতে পারে — কারণ সে ফিরে এসে বরাবর< kরঙ পাবেই (প্রতিবেশী সর্বোচ্চk−1ভিন্ন রঙ দখল করতে পারে)। Induction — এটাই Kempe-র 1879-এর theorem-এর compiler-avatar। - Optimistic coloring (Briggs): potential-spill node-টাও select- এ সুযোগ পায় — তার উচ্চ-degree প্রতিবেশীরা অনেক সময় একই রঙে শেষ হয়, ফলে আসলে রঙ মিলে যায়। Spill শুধু তখনই, যখন সত্যিই বাধ্য। Naive Chaitin এখানে pessimistic ছিল — Briggs-এর এই এক change-ই spill সংখ্যা লক্ষণীয় কমায়।
Coalescing — move মুছে ফেলা
x ← y (move) যদি x,y-র live range merge করে একই register দেওয়া
যায় — move instruction-টাই মুছে যায়। কিন্তু merge করলে union-node-এর
degree বেড়ে spill risk বাড়ে — তাই conservative/iterated coalescing
(George-Appel): শুধু “নিরাপদ” merge। SSA-form-এ parallel copies +
swap problem handle করা আজকের production allocators-এর daily bread।
Linear scan — JIT-এর গতির আস্থা
Graph coloring-এর compile-time খরচ heavy। Poletto-Sarkar-এর linear scan:
live interval (start,end) sort করে একবার sweep — active set-এ জায়গা
না থাকলে farthest-end interval expel। Quality কিছুটা খারাপ, গতি
O(n log n) — তাই LLVM -O0 (regalloc=fast), HotSpot client compiler,
পুরনো V8 — সবার পছন্দ। Greedy split-allocation (LLVM -O2-র
greedy) মাঝামাঝি: coloring-এর কাছে quality, scan-এর কাছে speed।
Callee-saved বনাম caller-saved — convention নিজেই strategy
Calling convention লেসনের টেবিল মনে আছে? Call পার করা ভেরিয়েবলের জন্য:
- Caller-saved (
%rax,%rcx,%rdx,%rsi,%rdi,%r8-%r11): call মানে value হারাবে — caller-কে save করতে হয় (যদি বাঁচা থাকে) - Callee-saved (
%rbx,%r12-%r15): call পার করেও অক্ষত — callee ছুঁলে restore করবেই
Allocator-এর কৌশল: call-এর পারে বাঁচা live range-গুলো callee-saved- এ বসালে per-call save/store লাগে না; না বাঁচাগুলো caller-saved-এ বিনামূল্যে। Live-range crossing-call detection আবার liveness — dataflow আবার হাজির।
- TAC (virtual regs)অসীম t1, t2, ... — আগের লেসনের output
- Livenessপ্রতি point-এ কে বাঁচা — lesson 9
- Interference graphedge = একসাথে বাঁচা নিষেধ
- Color / spillsimplify-select, k = usable GPR
- Physical registers%rax...%r15 + stack slots — assembly module
উদাহরণ
হাতে-হাতে: ৫ variable, k=3 আর k=2
1: c ← a + 1 # a,b : ফাংশন parameter
2: d ← b * 2
3: if c < d goto L_then
4: e ← 0
5: goto L_end
L_then:
6: e ← c + d
L_end:
7: return a + c + eLiveness (lesson 9-এর solver): in[B0]={a,b}, out[B0]={a,c}, in[B2]={a,c,d}, in[B3]={a,c,e}। Instruction-level walk-backward (def-time rule) + parameter-clique (a,b entry-তে parallel def):
edges: a—b, a—c, a—d, a—e, b—c, c—d, c—e
degrees: a:4 b:2 c:4 d:2 e:2 (triangle a-c-e আছে ⇒ χ ≥ 3)k = 3 (usable regs r0,r1,r2):
| ধাপ | কী ঘটে | স্ট্যাক |
|---|---|---|
| simplify | b(deg 2) push, বাদ | [b] |
| simplify | d(2) push | [b,d] |
| simplify | a(এখন 2) push | [b,d,a] |
| simplify | c(এখন 1) push, e(0) push | [b,d,a,c,e] |
| select | e→r0; c→r1; a: প্রতিবেশী {r0,r1}→r2; d→r0; b→r0 | ✅ ৫/৫ |
k = 2: শুরুতেই কারো degree ২-এর কম না (a:4, c:4, বাকিরা ২) —
| ধাপ | কী ঘটে | ফলাফল |
|---|---|---|
| potential | a (highest degree) push — optimistic | [a*] |
| simplify | a বাদ পড়তেই b(1), d(1) — push; তারপর c(1), e(0) | [a*,b,d,c,e] |
| select | e→r0; c→r1; d→r0; b→r0 | ৪/৫ |
| select a | প্রতিবেশী b,c,d,e = {r0,r1,r0,r0} — দুটোই blocked | 💥 spill a |
Spill rewrite: a স্ট্যাক স্লট [rsp-8]-এ; প্রতিটা use-এর আগে reload:
1: ta1 ← load [rsp-8]; c ← ta1 + 1 # use-site reload
...
7: tb ← load [rsp-8]; tc ← c + e; return tb + tcRewrite-এ নতুন ছোট live-range-রা (ta1, tb) প্রায় সবসময় সহজে color হয়ে যায় — তাই rebuild-and-retry এক-দুই round-ই থামে।
ছবিতে
Interference graph চোখে
┌─────┐ ┌─────┐
│ a │────────│ b │
└─┬┬┬─┘ └──┬──┘
││└─────►───────┤ (a—b)
│└──────►───────┼──┐ (a—c)
└───────────────┼──┘ (a—d)
▼
┌─────┐ ┌─────┐ ┌─────┐
│ e │────────│ c │────────│ d │
└─────┘ ↑ └─────┘ └─────┘
(c—e), (c—d)
k=3: a=r2 b=r0 c=r1 d=r0 e=r0 spill: নেই
k=2: a → 💥 stack slot spill: aTriangle a-c-e আর a-c-d খেয়াল করুন — odd cycle-ই chromatic number ৩
দাবি করে। Bipartite (cycle-free/bijugate) graph হলে ২ রঙেই চলত।
নিজে চালিয়ে দেখুন
সত্যিকারের compiler-এর spill গুনুন — -O0 বনাম -O2 asm
// alloc.c — অনেকগুলো একসাথে-বাঁচা local + একটা call (cross-call liveness)
extern int sink(int);
int alloc(int p) {
int a=p+1, b=p*2, c=a^b, d=c-3, e=d*5, f=e+p;
int g=f^e, h=g+b, i=h-c, j=i+d, k=j+e, l=k+f;
return sink(a+h) + i + j + k + l;
}clang -S -O0 alloc.c -o o0.s && clang -S -O2 alloc.c -o o2.s
# স্ট্যাক-slot access গুনুন (spill pattern):
grep -cE '\(%rbp\)|\(%rsp\)' o0.s o2.s
# মোট mov count:
grep -c 'mov' o0.s o2.sসাধারণ প্যাটার্ন (exact numbers machine/version-ভেদে বদলাবে):
o0.s : stack refs ≈ 40+ mov ≈ 45+ # প্রতিটা op আগে load, পরে store
o2.s : stack refs ≈ 2-6 mov ≈ 12-18 # প্রায় সব %eax/%ecx/... এ-O2-তে যে দু-একটা stack ref থেকে যায়, সেগুলো ঠিক sink() call-
এর চারপাশে — caller-saved register-এ থাকা cross-call values-এর
save/restore। Hood section-এর callee/caller-saved আলোচনার সরাসরি
প্রমাণ। -O2 এর সাথে -mllvm -regalloc=greedy জাতীয় flag ঘেঁটে
allocator-এর ভেতরটা আরও দেখতে চাইলে llvm/docs-এর regalloc হিল্টন
docs পড়ুন — তবে সেটা ঐচ্ছিক গভীরতা।
-O0 প্রতিটা variable স্ট্যাকে রাখে (debug-faithful), -O2 allocator প্রায় সব register-এ চাপায় — allocation quality-র পার্থক্য সরাসরি mov-count আর stack-slot count-এ দেখা যায়।
নিজে বানান
regalloc.py — interference builder + simplify/select
- আগের দুই লেসনের TACInstr/build_cfg/solve_liveness জুড়ে নিন
- build_interference(): প্রতি ব্লক backward walk — def × live-out edges
- parameter clique: entry-তে parallel def — params pairwise edge
- simplify_select(k): degree<k push, নাহলে potential spill (highest degree)
- select phase: pop করে first-fit রঙ; fail হলে spill report
আগের লেসনের কোডের সাথে জোড়া দিন — নিচে শুধু allocation layer:
class InterferenceGraph:
def __init__(self):
self.adj = {}
def add(self, u, v):
if u != v:
self.adj.setdefault(u, set()).add(v)
self.adj.setdefault(v, set()).add(u)
def nodes(self):
return set(self.adj)
def neighbors(self, v):
return self.adj.get(v, set())
def edges(self):
return sorted({tuple(sorted((u, w)))
for u in self.adj for w in self.adj[u]})
def without(self, v): # v + edges বাদ দেওয়া shallow copy
g2 = InterferenceGraph()
g2.adj = {u: ns - {v} for u, ns in self.adj.items() if u != v}
return g2
def build_interference(cfg, params):
ig = InterferenceGraph()
for p in params: # parameter clique: entry-তে
for q in params: # parallel definition
ig.add(p, q)
for b in cfg.blocks: # lesson 9-এর live_out ব্যবহার
live = set(live_out[b.bid])
for ins in reversed(b.instrs):
if ins.dest:
for v in live:
ig.add(ins.dest, v) # def interferes with live-out
live.discard(ins.dest)
live.update(ins.uses())
return ig
def simplify_select(ig, k):
work = InterferenceGraph()
work.adj = {v: set(ns) for v, ns in ig.adj.items()}
stack = []
while work.adj:
simple = sorted(v for v in work.adj if len(work.adj[v]) < k)
if simple: # SIMPLIFY
v = ('+', simple[0]) # deterministic: alphabetical
else: # POTENTIAL SPILL (optimistic)
v = ('*', min(work.adj, # highest degree, alpha tie-break
key=lambda n: (-len(work.adj[n]), n)))
stack.append(v)
work = work.without(v[1])
color, spilled = {}, []
for mark, v in reversed(stack): # SELECT: উল্টো ক্রমে
taken = {color[n] for n in ig.neighbors(v) if n in color}
free = [c for c in range(k) if c not in taken]
if free:
color[v] = free[0]
else:
spilled.append(v) # optimistic attempt failed
return color, spilled
params = ['a', 'b']
program = [
TACInstr('+', 'c', 'a', '#1'),
TACInstr('*', 'd', 'b', '#2'),
TACInstr('if_<', None, 'c', 'd', 'L_then'),
TACInstr('copy', 'e', '#0'),
TACInstr('goto', None, None, None, 'L_end'),
TACInstr('label', None, None, None, 'L_then'),
TACInstr('+', 'e', 'c', 'd'),
TACInstr('label', None, None, None, 'L_end'),
TACInstr('+', 't', 'a', 'c'),
TACInstr('+', 'u', 't', 'e'),
TACInstr('return', None, 'u'),
]
cfg = build_cfg(program)
live_in, live_out = solve_liveness(cfg)
ig = build_interference(cfg, params)
print('edges:', ', '.join(f'{u}-{w}' for u, w in ig.edges()))
for k in (3, 2):
color, spilled = simplify_select(ig, k)
tag = ', '.join(f'{v}=r{color[v]}' for v in sorted(color))
print(f'k={k}: {tag}' + (f' SPILL: {sorted(spilled)}' if spilled else ' (no spill)'))প্রত্যাশিত output (t, u = return a+c+e chain-এর দুই temporary;
তাদের ছোট range সহজেই color হয়):
edges: a-b, a-c, a-d, a-e, b-c, c-d, c-e, e-t
k=3: a=r2, b=r1, c=r0, d=r1, e=r1, t=r0, u=r0 (no spill)
k=2: b=r0, c=r1, d=r0, e=r0, t=r0, u=r0 SPILL: ['a']example section-এর টেবিলের হাতে-trace-এর সাথে মিলিয়ে নিন — একই graph, একই ফয়সালা।
নিজে বাড়ান: (১) spill rewrite pass — spilled var-এর use-এ reload TAC বসান, rebuild করে দেখান retry-তে সব color হয়; (২) spill-cost weight (loop depth × 10^depth) দিয়ে potential-pick বদলান; (৩) conservative coalescing: move-এর দুই node merge করে দেখান degree-check কেন দরকার।
বাস্তব সিস্টেমে
LLVM। -O0-এ linear-scan-family fast allocator (compile-speed-first),
-O2-এ Greedy allocator — live-range splitting, spill-weight, hint
system সহ; PBQP allocator special cases-এর জন্য।
JavaScript engines। V8-এর পুরনো full-codegen আর HotSpot client compiler linear scan ব্যবহার করত — JIT warm-up latency-র কারণে; V8-এর TurboFan এখন constraint-based optimized allocation করে।
Embedded/MCU। Cortex-M0-র মতো low-register-count target-এ spill প্রতিনিয়ত — allocator quality সরাসরি flash-size আর cycle budget।
Crypto kernels। ChaCha20/Salsa20 state ইচ্ছা করেই ৮-১২ lane register-resident রাখে — source-level structure টাকে allocator-এর সহজ করে দেয়; hand-tuned asm-এর কারণই partial এটা।
আপনার toy compiler। আজকের k=16 run করে দেখুন — প্রায় কখনো spill হবে না; k=3 দিলেই আসল চরিত্র বেরোয়।
যে ভুলগুলো সবাই করে
“Graph coloring optimal allocation দেয়।”
Coloring NP-complete — production allocators polynomial heuristics চালায়: simplify-order, spill-choice, coalescing সবই greedy। Optimal (minimum spill, minimum move) খোঁজা সাধারণত infeasible, আর “optimal coloring”-এর সংজ্ঞাতেই cost-model লুকানো। Real-world question: heuristic কতটা ভালো — উত্তর: বাস্তব প্রোগ্রামের graph বিস্ময়করভাবে সহজে color হয়, তাই gap সাধারণত ছোট।
“Spill মানে programmer খারাপ কোড লিখেছে।”
Allocator live-range দেখে, source variable নয় — একটা innocent-looking expression-এর temporaries-ও spill খেতে পারে, আবার ৩০টা local variable থেকেও একসাথে ৫টা বাঁচলে zero spill। Programmer-এর control indirect: shorter live ranges (declaration ব্যবহারের কাছে), ছোট expression, hot loop-এ কম distinct value। Compiler explorer দিয়ে verify করা যায়, guess করে না।
“বেশি register = সবসময় দ্রুত CPU।”
Register বাড়লে spill কমে — কিন্তু প্রতিটা register-এর দাম আছে: instruction encoding-এ bits (x86-এ ৮→১৬ register-এ REX prefix), context-switch/interrupt- এ save-set বড়, rename hardware বড় (register-renaming লেসন দেখুন)। ISA design একটা balance — RISC-V এর ৩২টা আর x86-64-এর ১৬টা দুটোই সচেতন trade-off।
“Allocation সবসময় selection-এর আগে চলে।”
Phase ordering ঠিক করা হয় না, বেছে নেওয়া হয়: LLVM-এ selection (ISel) আগে, allocation পরে — physical-reg-নামের virtual-reg দিয়ে। অন্য design-এ (unified allocators, stack-machine targets) উল্টো। Integrated approach-ও আছে। প্রতিটা ordering-এর নিজের blind spot: allocation-পরে- selection selection-কে register-starved রাখে, উল্টোটা selection-কে অসীম-temp fantasy-তে রাখে।
বুঝেছেন কি না দেখুন
1x ← y + z একটাই instruction — x,y,z-এর মধ্যে কোন কোন edge তৈরি হবে?
(def-time rule মনে রাখুন: def interferes with values live after it.)
প্রয়োগ
x ← y + z একটাই instruction — x,y,z-এর মধ্যে কোন কোন edge তৈরি হবে?
(def-time rule মনে রাখুন: def interferes with values live after it.)Def-time rule-এ x-এর edge হবে শুধু তাদের সাথে যারা instruction-টার পরেও বাঁচা। y,z এখানে last-use হলে (পরে কেউ পড়ে না) তারা die-at-use — edge x-y/x-z নেই, তাই x, y, z তিনটাই একই register ভাগ করতে পারে! কিন্তু y/z পরে আবার ব্যবহৃত হলে edge হবে। এখানেই naive “operands-সবার- সাথে-edge” মডেল extra spill ডাকে — def-time precision-এর মূল্য।
2Simplify কেন সবসময় safe? degree < k node-টা ফিরে এসে রঙ না পাওয়ার
কীভাবে অসম্ভব?
যুক্তি
< k node-টা ফিরে এসে রঙ না পাওয়ার
কীভাবে অসম্ভব?Select-phase-এ node-টা pop হয় যখন তার সব প্রতিবেশী already-colored (উল্টো ক্রম)। প্রতিবেশী সংখ্যা ≤ original-degree < k, তাই তারা সর্বোচ্চ k−1 ভিন্ন রঙ দখল করতে পারে — pigeonhole: k রঙের মধ্যে অন্তত একটা free। এটাই Kempe-induction; আর potential-spill node-এর ক্ষেত্রে degree ≥ k ছিল, তাই guarantee নেই — সেখানেই optimism আর spill-এর দরজা।
3একটা variable inner loop-এ ৩ বার ব্যবহৃত, outer scope-এ ১ বার। Spill
বাছাইয়ে কেন প্রায় সবসময় outer টাকেই বেছে নেওয়া হয়? Cost formula ব্যবহার
করে দেখান।
যুক্তি
cost ≈ Σ 10^depth per use: inner var (depth 1): 3×10¹ = 30; outer var (depth 0): 1×10⁰ = 1। Loop execute N বার হলে বাস্তব dynamic cost হয় inner: 3N loads/stores, outer: N। 30 ≫ 1 — inner spill ৩০ গুণ ব্যয়বহুল। তাই spill-heuristic min-cost বাছে, আর loop-depth weighting করে যেন static count দেখে inner-var-কে ভুলে বেছে না নেয়।
4আপনি একটা JS engine-এর baseline tier লিখছেন। Linear scan নাকি graph
coloring? Compile latency বনাম steady-state throughput-এর হিসাব করে
যুক্তি দিন।
ডিজাইন
Baseline tier-এর কাজ user-কে দ্রুত চালু করা — প্রতিটা ms compile-latency user-experience; কোড নিজে হয়তো কয়েকবারই চলবে (short-lived pages/scripts)। Linear scan O(n log n), cache-friendly, implementation ছোট; quality-loss (spill বেশি) তখনও মোট execution-এ সামান্য। Optimizing tier-এ উল্টো: function hot প্রমাণিত, thousands of iterations ahead — coloring/greedy- splitting-এর compile cost amortize, quality লাভ direct throughput। তাই baseline = linear scan, hot tier = advanced allocator — দুটোই একসাথে রাখা production-এর স্ট্যান্ডার্ড answer।
5Callee-saved register-এ রাখা একটা cross-call variable আর caller-saved-এ
রাখা একটা — call-site-এ কার খরচ কোথায় বসে?
স্মরণ
Caller-saved: খরচ caller-এর — value বাঁচা থাকলে call-এর আগে store, পরে reload (call-এর চারপাশে প্রতি site-এ)। Callee-saved: খরচ callee-র — callee সেই register ব্যবহার করতে চাইলে prologue-এ save, epilogue-এ restore, একবারই; caller কিছু করে না। Allocator-এর view: cross-call range callee-saved-এ গেলে per-call overhead zero (callee না ছুঁলে), তাই hot-call-heavy code-এ callee-saved preference; তবে callee ছুঁলে সেটা callee-র সব-caller-এর পক্ষে ঋণ।
এরপর কী
এরপর কী
Variable-রা এখন register-এ, spilled-রা স্ট্যাকে। কিন্তু আমাদের TAC-এখনো
abstract: +, *, load_arr — আর x86-64-এর ভান্ডার বিশাল: addressing
mode-এ scale-index-base, lea-র তিন-operand জাদু, flags-ভিত্তিক branch,
variable-length encoding।
পরের লেসন (Instruction Selection ও Scheduling) দুইটা শেষ রূপান্তর করে: selection — IR tree-কে pattern/tiling ঢেকে সবচেয়ে সস্তা x86 instruction বাছে; scheduling — dependency DAG-এ latency লুকাতে instruction reorder। আর দেখব কেন out-of-order CPU-র যুগেও scheduler মরেনি।
আরও পড়ুন
- "Register Allocation & Spilling via Graph Coloring" — Gregory J. Chaitin · SIGPLAN 1982 — interference-graph approach-এর জনক-প্রবন্ধ
- "Improvements to Graph Coloring Register Allocation" — Preston Briggs, Keith Cooper, Linda Torczon · TOPLAS 1994 — optimistic coloring আর modern Chaitin-Briggs loop
- "Linear Scan Register Allocation" — Massimiliano Poletto, Vivek Sarkar · TOPLAS 1999 — JIT-প্রিয় fast approximation-এর মূল কাগজ
- Engineering a Compiler (2nd ed.) — Register Allocation অধ্যায় — Keith Cooper, Linda Torczon · সমগ্র অধ্যায়টাই এই লেসনের গভীর-reading