Workspace IndexAlgorithms › Day 24

JIT Tiering, Warmup, and Deoptimization (Deopt) TODO

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

Concept

Modern VMs don't optimize all code from the start — they use a tiered strategy. Code first runs on an interpreter or a fast-emitting baseline compiler while the VM collects call counts, loop-iteration counts, and type profiles; only hot code that crosses a threshold gets recompiled by the optimizing compiler. The optimizing tier uses the profile to make assumptions such as "this argument is always an integer" or "this call target is always the same function," inlining and specializing on those assumptions, and it embeds guards in the code to check them. When a guard fails, deoptimization (deopt) kicks in, rolling the optimized frame's state back into an interpreter frame and continuing execution on the slow path. This is why programs have a low-performance warmup period at the start, and why, if assumptions keep breaking, repeated recompilation and deopt can make performance fall off a cliff.

A microbenchmark that ignores warmup produces numbers far slower or faster than reality and leads to the wrong optimization decisions. It's also common in practice for a single polymorphic object shape used at one call site to push a hot loop into deopt, dropping throughput by several times.

Code & Formula

# JIT 계층화·워밍업·역최적화(deopt) — 호출 횟수로 티어를 올리고, 타입 가정이 깨지면 deopt한다.

class JitFunction:
    def __init__(self, threshold=5):
        self.call_count = 0
        self.threshold = threshold
        self.tier = "interpreter"
        self.assumed_type = None
        self.deopt_count = 0

    def call(self, x):
        self.call_count += 1
        if self.tier == "interpreter" and self.call_count >= self.threshold:
            self.tier = "optimized"
            self.assumed_type = type(x)          # 관측한 타입으로 특수화(가정 수립)

        if self.tier == "optimized":
            if type(x) is not self.assumed_type:  # guard 실패
                self.tier = "interpreter"          # deopt: 인터프리터 프레임으로 복귀
                self.deopt_count += 1
                self.assumed_type = None
                self.call_count = 0
            else:
                return x * 2                       # 특수화된 빠른 경로
        return x * 2                                # 일반(느린) 경로

fn = JitFunction(threshold=3)
trace = []
for x in [1, 2, 3, 4, 5, "oops", 6, 7, 8, 9]:
    tier_before = fn.tier
    result = fn.call(x)
    trace.append((x, tier_before, fn.tier, result))

for x, before, after, result in trace:
    marker = " <- DEOPT" if before == "optimized" and after == "interpreter" else ""
    print(f"call({x!r:>7}): tier {before:>11} -> {after:<11} result={result}{marker}")
print(f"\n총 deopt 횟수: {fn.deopt_count}")

Exercise

In Node.js, benchmark a hot function called with a single type only, versus calling it with a mix of differently-shaped objects partway through, and check the recompilation/deopt logs with --trace-deopt and --trace-opt.

Practical Connection

A hot loop written in TypeScript, like the Verex matching engine's, needs to keep order-object shapes consistent to stay in the optimized tier, and when benchmarking, the warmup period needs to be discarded for the p99 numbers to mean anything.

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/.


한국어

JIT 계층화·워밍업·역최적화(deopt) TODO

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

개념

현대 VM은 모든 코드를 처음부터 최적화하지 않고 계층화(tiered) 전략을 쓴다. 처음에는 인터프리터나 빠르게 뱉는 베이스라인 컴파일러로 실행하면서 호출 횟수·루프 반복 수·타입 프로파일을 수집하고, 임계치를 넘은 hot 코드만 최적화 컴파일러로 다시 컴파일한다. 최적화 단계는 프로파일을 근거로 "이 인자는 항상 정수다", "이 호출 대상은 항상 같은 함수다" 같은 가정을 세워 인라인·특수화하고, 그 가정을 검사하는 guard를 코드에 심는다. guard가 깨지면 역최적화(deopt)가 일어나 최적화 프레임의 상태를 인터프리터 프레임으로 되돌리고 느린 경로에서 실행을 이어간다. 그래서 프로그램 초반에는 성능이 낮은 워밍업 구간이 존재하고, 가정이 계속 깨지면 재컴파일과 deopt가 반복되며 성능이 절벽처럼 떨어질 수 있다.

워밍업을 무시한 마이크로벤치마크는 실제보다 훨씬 느리거나 빠른 값을 내놓아 잘못된 최적화 결정을 유도한다. 또 한 곳에서 다형적으로 쓰이는 객체 모양 하나 때문에 hot loop가 deopt에 빠져 처리량이 몇 배 떨어지는 일이 실제로 자주 생긴다.

코드 · 수식

# JIT 계층화·워밍업·역최적화(deopt) — 호출 횟수로 티어를 올리고, 타입 가정이 깨지면 deopt한다.

class JitFunction:
    def __init__(self, threshold=5):
        self.call_count = 0
        self.threshold = threshold
        self.tier = "interpreter"
        self.assumed_type = None
        self.deopt_count = 0

    def call(self, x):
        self.call_count += 1
        if self.tier == "interpreter" and self.call_count >= self.threshold:
            self.tier = "optimized"
            self.assumed_type = type(x)          # 관측한 타입으로 특수화(가정 수립)

        if self.tier == "optimized":
            if type(x) is not self.assumed_type:  # guard 실패
                self.tier = "interpreter"          # deopt: 인터프리터 프레임으로 복귀
                self.deopt_count += 1
                self.assumed_type = None
                self.call_count = 0
            else:
                return x * 2                       # 특수화된 빠른 경로
        return x * 2                                # 일반(느린) 경로

fn = JitFunction(threshold=3)
trace = []
for x in [1, 2, 3, 4, 5, "oops", 6, 7, 8, 9]:
    tier_before = fn.tier
    result = fn.call(x)
    trace.append((x, tier_before, fn.tier, result))

for x, before, after, result in trace:
    marker = " <- DEOPT" if before == "optimized" and after == "interpreter" else ""
    print(f"call({x!r:>7}): tier {before:>11} -> {after:<11} result={result}{marker}")
print(f"\n총 deopt 횟수: {fn.deopt_count}")

연습

Node.js에서 hot 함수 하나를 단일 타입으로만 호출한 경우와 중간에 다른 모양의 객체를 섞어 호출한 경우를 각각 벤치마크하고, --trace-deopt--trace-opt로 재컴파일·deopt 로그를 확인하라.

실무 · Verex 연결

Verex 매칭 엔진처럼 TypeScript로 짠 hot loop는 주문 객체의 shape를 일관되게 유지해야 최적화 티어에 머무르며, 벤치마크 시 워밍업 구간을 버려야 p99 수치가 의미를 갖는다.

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

← 23. 인라이닝·루프 변환·자동 벡터화25. 스택 머신 vs 레지스터 머신 →