Workspace IndexAlgorithms › Day 25

Stack Machines vs. Register Machines TODO

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

Concept

A stack machine doesn't name its operands explicitly — it implicitly pops them off the top of the stack — which gives short instruction encodings and a simple compiler backend, but it needs more instructions to do the same computation and adds stack-shuffling operations like DUP and SWAP. A register machine names operands explicitly, so it needs fewer instructions and value reuse is explicit, which favors optimizations like register allocation and JIT compilation, at the cost of longer encodings and a more complex instruction set. The EVM is a stack machine that operates on 256-bit words with a stack depth capped at 1024, a design that prioritizes specification simplicity and deterministic reproducibility across every node over raw performance. WebAssembly's specification is also a stack-based validation model, but it provides function-local variables and structured control flow (blocks, loops, and branch labels), which makes it easy to compile AOT/JIT into native register code. So the real difference between the two VMs isn't stack versus register per se, but whether they prioritize verifiable, deterministic metering or native execution speed.

When you read EVM bytecode or fight over gas, stack-manipulation overhead shows up directly in the cost, and following discussions about alternative VMs requires knowing this trade-off as background. It's also what explains why compiler output looks the way it does.

Code & Formula

# 스택 머신 vs 레지스터 머신 — 같은 식 (a+b)*c 를 두 모델로 실행하고 명령 수를 비교한다.

def run_stack_machine(program):
    stack = []
    for op in program:
        if isinstance(op, (int, float)):
            stack.append(op)
        elif op == "ADD":
            b, a = stack.pop(), stack.pop()
            stack.append(a + b)
        elif op == "MUL":
            b, a = stack.pop(), stack.pop()
            stack.append(a * b)
    return stack[-1]

def run_register_machine(program, regs):
    regs = dict(regs)
    for dest, op, *args in program:
        if op == "ADD":
            regs[dest] = regs[args[0]] + regs[args[1]]
        elif op == "MUL":
            regs[dest] = regs[args[0]] * regs[args[1]]
    return regs

env = {"A": 3, "B": 4, "C": 5}

# (a+b)*c 를 스택 머신 명령으로: PUSH a, PUSH b, ADD, PUSH c, MUL (피연산자는 암묵적으로 스택 상단)
stack_program = [env["A"], env["B"], "ADD", env["C"], "MUL"]

# (a+b)*c 를 3-주소 레지스터 명령으로: r1 = A + B; r2 = r1 * C (피연산자를 이름으로 명시)
register_program = [("r1", "ADD", "A", "B"), ("r2", "MUL", "r1", "C")]

stack_result = run_stack_machine(stack_program)
register_result = run_register_machine(register_program, env)["r2"]

assert stack_result == register_result == (3 + 4) * 5

print(f"스택 머신 결과={stack_result}, 명령 수={len(stack_program)} "
      f"(피연산자 암묵적 — EVM처럼 DUP/SWAP 같은 스택 정리 연산이 늘 수 있음)")
print(f"레지스터 머신 결과={register_result}, 명령 수={len(register_program)} "
      f"(피연산자 명시 — 값 재사용이 이름으로 드러나 레지스터 할당·JIT에 유리)")

Exercise

Pick a simple arithmetic expression, hand-translate it into both a stack-machine instruction sequence and a three-address register instruction sequence, then check what opcode sequence the same expression actually produces in solc's output bytecode.

Practical Connection

When optimizing a Solidity contract for gas, judging where stack-too-deep errors or unnecessary DUP/SWAP and memory round-trips come from requires exactly this understanding of the model difference.

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


한국어

스택 머신 vs 레지스터 머신 TODO

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

개념

스택 머신은 피연산자를 명시하지 않고 암묵적으로 스택 상단에서 꺼내 쓰는 구조라 명령어 인코딩이 짧고 컴파일러 백엔드가 단순하지만, 같은 계산을 하는 데 필요한 명령어 수가 많고 DUP·SWAP 같은 스택 정리 연산이 추가된다. 레지스터 머신은 피연산자를 이름으로 지정하므로 명령어 수가 적고 값의 재사용이 명시적이라 레지스터 할당·JIT 같은 최적화에 유리한 대신, 인코딩이 길고 명령어 집합이 복잡해진다. EVM은 256비트 워드를 다루는 스택 머신이고 스택 깊이가 1024로 제한되며, 성능보다 명세의 단순함과 모든 노드에서의 결정론적 재현을 우선한 설계다. WebAssembly도 명세상으로는 스택 기반 검증 모델을 쓰지만, 함수 지역 변수와 구조화된 제어 흐름(블록·루프·분기 라벨)을 제공해 네이티브 레지스터 코드로 AOT/JIT 컴파일하기 쉽게 되어 있다. 즉 두 VM의 차이는 스택이냐 레지스터냐 자체보다, 검증 가능한 결정론적 계량을 우선했는지 네이티브 실행 속도를 우선했는지의 차이에 가깝다.

EVM 바이트코드를 읽거나 가스를 다투다 보면 스택 조작 오버헤드가 비용에 그대로 잡히고, 대안 VM 논의를 따라가려면 두 모델의 트레이드오프가 전제 지식이 된다. 컴파일러 출력이 왜 그렇게 생겼는지도 여기서 설명된다.

코드 · 수식

# 스택 머신 vs 레지스터 머신 — 같은 식 (a+b)*c 를 두 모델로 실행하고 명령 수를 비교한다.

def run_stack_machine(program):
    stack = []
    for op in program:
        if isinstance(op, (int, float)):
            stack.append(op)
        elif op == "ADD":
            b, a = stack.pop(), stack.pop()
            stack.append(a + b)
        elif op == "MUL":
            b, a = stack.pop(), stack.pop()
            stack.append(a * b)
    return stack[-1]

def run_register_machine(program, regs):
    regs = dict(regs)
    for dest, op, *args in program:
        if op == "ADD":
            regs[dest] = regs[args[0]] + regs[args[1]]
        elif op == "MUL":
            regs[dest] = regs[args[0]] * regs[args[1]]
    return regs

env = {"A": 3, "B": 4, "C": 5}

# (a+b)*c 를 스택 머신 명령으로: PUSH a, PUSH b, ADD, PUSH c, MUL (피연산자는 암묵적으로 스택 상단)
stack_program = [env["A"], env["B"], "ADD", env["C"], "MUL"]

# (a+b)*c 를 3-주소 레지스터 명령으로: r1 = A + B; r2 = r1 * C (피연산자를 이름으로 명시)
register_program = [("r1", "ADD", "A", "B"), ("r2", "MUL", "r1", "C")]

stack_result = run_stack_machine(stack_program)
register_result = run_register_machine(register_program, env)["r2"]

assert stack_result == register_result == (3 + 4) * 5

print(f"스택 머신 결과={stack_result}, 명령 수={len(stack_program)} "
      f"(피연산자 암묵적 — EVM처럼 DUP/SWAP 같은 스택 정리 연산이 늘 수 있음)")
print(f"레지스터 머신 결과={register_result}, 명령 수={len(register_program)} "
      f"(피연산자 명시 — 값 재사용이 이름으로 드러나 레지스터 할당·JIT에 유리)")

연습

간단한 산술식 하나를 골라 스택 머신 명령열과 3-주소 레지스터 명령열로 각각 손으로 번역한 뒤, 실제 solc 출력 바이트코드에서 같은 식이 어떤 opcode 열로 나오는지 대조하라.

실무 · Verex 연결

Solidity 컨트랙트를 가스 기준으로 최적화할 때 스택 깊이 초과 오류나 불필요한 DUP/SWAP·메모리 왕복이 어디서 생기는지 판단하려면 이 모델 차이가 바로 필요하다.

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

← 24. JIT 계층화·워밍업·역최적화(deopt)26. 가스 회계 설계 →