Workspace IndexAlgorithms › Day 22

Register Allocation (Graph Coloring) and Spill Cost TODO

Algorithms · Day 22 / 100 · B. Compilers, Runtimes & VMs (Day 20-35)

Concept

Register allocation is the stage that maps an intermediate representation with an unbounded number of virtual registers onto the k actual physical registers available. Treating each simultaneously live value as a vertex and connecting values whose live ranges overlap with an edge produces an interference graph, turning the problem into graph k-coloring; since k-coloring a general graph is NP-complete, compilers use Chaitin-style heuristics (push vertices with degree < k onto a stack, then pop and color them back). A value that fails to get a color is spilled to memory, and spill cost is typically estimated with a heuristic like access count weighted by loop-nesting depth, divided by degree. In SSA form the interference graph is chordal, so optimal coloring is possible in polynomial time, which is why some modern compilers use SSA-based allocation or linear scan for JITs. So the core trade-off is allocation quality versus compile time.

Poor performance in a hot loop is often caused not by the algorithm but by register pressure causing spills and reloads, and recognizing this lets you respond at the source level — for example by inlining or shrinking variable live ranges.

Code & Formula

# 레지스터 할당(그래프 컬러링) — 간섭 그래프를 k개 물리 레지스터로 그리디 색칠하고, 실패하면 스필한다.

interference = {
    "t1": {"t2", "t3"},
    "t2": {"t1", "t3", "t4"},
    "t3": {"t1", "t2", "t4"},
    "t4": {"t2", "t3", "t5"},
    "t5": {"t4"},
}
spill_cost = {"t1": 3, "t2": 1, "t3": 5, "t4": 2, "t5": 4}  # 접근횟수*중첩깊이 근사치
K = 3  # 사용 가능한 물리 레지스터 수

def simplify_order(graph, k):
    g = {n: set(neigh) for n, neigh in graph.items()}
    stack, spilled = [], []
    while g:
        low_degree = [n for n, neigh in g.items() if len(neigh) < k]
        if low_degree:
            n = min(low_degree, key=lambda n: spill_cost[n])   # 차수<k 정점을 스택으로
        else:
            n = min(g, key=lambda n: spill_cost[n] / max(1, len(g[n])))  # 잠재적 스필 후보
            spilled.append(n)
        stack.append(n)
        for neigh in g.values():
            neigh.discard(n)
        del g[n]
    return stack, spilled

def color(order, graph, k):
    colors = {}
    for n in reversed(order):
        used = {colors[m] for m in graph[n] if m in colors}
        available = [c for c in range(k) if c not in used]
        colors[n] = available[0] if available else None    # None = 실제 스필
    return colors

order, potential_spills = simplify_order(interference, K)
colors = color(order, interference, K)

print(f"제거 순서(스택): {order}")
print(f"단계에서 걸러진 잠재 스필 후보: {potential_spills}")
for t, c in colors.items():
    where = f"R{c}" if c is not None else "MEMORY(spill)"
    print(f"  {t} -> {where}")

Exercise

Pick a small function, compile it at -O0 and -O2, compare the assembly, and count how the number of stack-slot accesses (spills/reloads) changes.

Practical Connection

The EVM is a stack machine rather than a register machine, so its 16-deep stack limit effectively creates the same kind of pressure — Solidity's "stack too deep" error is isomorphic to a spill that pushes local variables out to memory.

If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.


한국어

레지스터 할당(그래프 컬러링)과 스필 비용 TODO

Algorithms · Day 22 / 100 · B. 컴파일러·런타임·VM (Day 20–35)

개념

레지스터 할당은 무한한 가상 레지스터를 갖는 중간 표현을 실제 물리 레지스터 개수 k개에 사상하는 단계다. 동시에 살아 있는(live) 값들을 정점으로, 생존 구간이 겹치는 쌍을 간선으로 하는 간섭 그래프(interference graph)를 만들면 문제는 그래프 k-컬러링이 되고, 일반 그래프의 k-컬러링은 NP-완전이므로 Chaitin류의 휴리스틱(차수 < k인 정점을 스택에 밀어내고 되돌리며 색칠)을 쓴다. 색칠에 실패한 값은 메모리로 내보내는 스필(spill)을 하며, 스필 비용은 보통 접근 횟수를 반복문 중첩 깊이로 가중한 값을 차수로 나눈 형태의 휴리스틱으로 추정한다. SSA 형태에서는 간섭 그래프가 chordal이라 최적 색칠이 다항 시간에 가능해, 현대 컴파일러는 SSA 기반 할당이나 JIT용 linear scan을 쓰기도 한다. 즉 핵심 트레이드오프는 할당 품질과 컴파일 시간이다.

핫 루프에서 성능이 안 나오는 원인이 알고리즘이 아니라 레지스터 압박에 의한 스필/리로드인 경우가 흔하고, 이를 알아야 인라이닝이나 변수 생존 구간을 줄이는 식의 소스 수준 대응을 할 수 있다.

코드 · 수식

# 레지스터 할당(그래프 컬러링) — 간섭 그래프를 k개 물리 레지스터로 그리디 색칠하고, 실패하면 스필한다.

interference = {
    "t1": {"t2", "t3"},
    "t2": {"t1", "t3", "t4"},
    "t3": {"t1", "t2", "t4"},
    "t4": {"t2", "t3", "t5"},
    "t5": {"t4"},
}
spill_cost = {"t1": 3, "t2": 1, "t3": 5, "t4": 2, "t5": 4}  # 접근횟수*중첩깊이 근사치
K = 3  # 사용 가능한 물리 레지스터 수

def simplify_order(graph, k):
    g = {n: set(neigh) for n, neigh in graph.items()}
    stack, spilled = [], []
    while g:
        low_degree = [n for n, neigh in g.items() if len(neigh) < k]
        if low_degree:
            n = min(low_degree, key=lambda n: spill_cost[n])   # 차수<k 정점을 스택으로
        else:
            n = min(g, key=lambda n: spill_cost[n] / max(1, len(g[n])))  # 잠재적 스필 후보
            spilled.append(n)
        stack.append(n)
        for neigh in g.values():
            neigh.discard(n)
        del g[n]
    return stack, spilled

def color(order, graph, k):
    colors = {}
    for n in reversed(order):
        used = {colors[m] for m in graph[n] if m in colors}
        available = [c for c in range(k) if c not in used]
        colors[n] = available[0] if available else None    # None = 실제 스필
    return colors

order, potential_spills = simplify_order(interference, K)
colors = color(order, interference, K)

print(f"제거 순서(스택): {order}")
print(f"단계에서 걸러진 잠재 스필 후보: {potential_spills}")
for t, c in colors.items():
    where = f"R{c}" if c is not None else "MEMORY(spill)"
    print(f"  {t} -> {where}")

연습

작은 함수 하나를 골라 -O0-O2로 컴파일한 어셈블리를 비교하고, 스택 슬롯 접근(스필/리로드) 개수가 어떻게 달라지는지 세어 볼 것.

실무 · Verex 연결

EVM은 레지스터가 아니라 스택 머신이라 스택 깊이 16 제한이 사실상 같은 압박으로 나타나며, Solidity의 "stack too deep"은 로컬 변수를 메모리로 내보내는 스필과 동형의 문제다.

공부한 날 원본 커리큘럼(docs/knowledge/dev-100-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 21. 데이터플로 분석23. 인라이닝·루프 변환·자동 벡터화 →