My Profile Photo

Chang Min Park


Senior Software Engineer at Yahoo!



Stack-Based vs Register-Based VMs: Why Dalvik Chose Registers

Two ways to fake a CPU

A process virtual machine is not VMware — it is a runtime that executes bytecode for one app process. Java’s JVM, Python’s CPython, and Android’s DEX/ART pipeline all compile source to an intermediate form, then interpret or compile that IR on the host CPU.

Implementers pick one of two operand models:

  1. Stack-based — operands live on an evaluation stack; instructions push and pop (classic JVM).
  2. Register-based — operands live in named virtual registers; instructions name sources and destinations (Dalvik/DEX, Lua, some WASM lowering paths).

The choice affects instruction count, code size, dispatch-loop cost, and how easily a compiler can reuse subexpressions. On a phone SoC, those details showed up in install size and cold-start time — which is why Android’s lineage matters beyond trivia.

For how DEX fits into process startup and ART today, see Dalvik and ART.

Stack-based VMs — implicit addresses via the stack pointer

In a stack VM, arithmetic is expressed as a sequence of pushes, an operation, and a push of the result. Adding 20 and 7 might look like:

PUSH 20
PUSH 7
ADD          ; pop two operands, push sum

Four conceptual steps for one addition. The upside: operand addresses are implicit. The stack pointer tells the interpreter what to pop next; compact opcodes, simple compiler back ends.

Stack-based virtual machine with stack pointer and evaluation stack for operands

Figure 1. Stack-based VM — the stack pointer selects operands implicitly.

Stack operations to add two numbers: push, push, add, push result

Figure 2. Adding two values on a stack machine takes multiple push/pop steps.

Every arithmetic and logic operation flows through that push/pop cycle. Dispatch loops spend work moving data on the stack even when the “registers” of the algorithm are really just deep stack slots.

Pro: small instructions, no explicit register fields, straightforward bytecode verification.

Con: more instructions per operation; harder for the optimizer to keep a computed value live without reloading from the stack.

Register-based VMs — explicit operands, fewer moves

Register VMs map operands to virtual registers (fixed-size slots in a frame, not physical CPU registers). The same addition is often one instruction:

ADD R1, R2, R3    ; R3 = R1 + R2

Register-based virtual machine with named registers R1 R2 R3 for operands

Figure 3. Register-based VM — operands and results are named explicitly.

No push/pop traffic for that add. The interpreter’s inner loop can dispatch fewer times for the same source-level expression.

Pros:

  • Less stack traffic in the dispatch loop — often faster per opcode on register-starved mobile cores of the Dalvik era.
  • Common subexpression reuse: if (a + b) appears twice, a register VM can compute once, keep the result in v4, and reuse it. Stack code may recompute and push again.

Con: instructions are wider — you must encode register numbers. Average DEX instructions are larger than stack-JVM opcodes, which affects .dex file size (the multidex era made that tradeoff visible).

How this connects to Android

Java/Kotlin still compile through .class files, but Android’s d8/R8 output is DEX — a register-based IR. ART (successor to Dalvik) compiles DEX to native code; the register shape of the bytecode is still the contract your APK ships.

Conceptually:

// JVM-style (stack)
iload_1
iload_2
iadd

// DEX-style (registers)
add-int v0, v1, v2

You do not hand-write DEX, but you feel the model when R8 changes live ranges, when method count pushes you toward multidex, or when a verifier error mentions register width. The Dalvik/ART post walks the runtime side — Zygote fork, OAT compilation — this post is the instruction-set half of the same picture.

Tradeoffs at a glance

  Stack (JVM) Register (DEX/Dalvik)
Operand location Evaluation stack Named virtual registers
Typical add Multiple push/pop ops Often one add-int-style op
Instruction size Smaller opcodes Larger (register fields)
CSE / keeping temps Reload from stack Hold in a register slot
Android relevance Desktop/server Java APK bytecode ART runs

Neither design is “better” in the abstract — JVM stack bytecode dominates server Java; Android picked registers for mobile bytecode density and interpreter performance tradeoffs that made sense when Dalvik shipped. If you are comparing stack traces from JNI or reading smali/dexdump output, you are looking at register names (v0, v1, …) — the same model DEX and ART still execute today.

References