Lazy flags: how an x86 emulator avoids computing EFLAGS after every instruction

Almost every x86 arithmetic instruction updates the carry, zero, sign, overflow and parity flags, and almost no instruction reads them. An emulator that computes all five after every ADD spends most of its ALU time on results nobody looks at. Wine-Assembly, a Windows 98 emulator written in WebAssembly Text, uses lazy flags: it records what the last flag-setting operation was and computes a flag only when a Jcc, SETcc, ADC or PUSHF actually asks. This article explains the scheme, the bugs it produced, and what a second implementation in the project's toy VM taught about it.

The four globals

Instead of a flags register, the interpreter keeps four mutable globals:

Global Meaning
flag_op which operation last set the flags (add, sub, logic, shift, inc, ...)
flag_a, flag_b the two operands of that operation
flag_res its result
flag_sign_shift 31, 15 or 7: where the sign bit is for a 32-, 16- or 8-bit operation

An ADD handler stores its operands and result and moves on. A JZ handler calls $get_zf, which is flag_res == 0 masked to the operation width. $get_cf for a subtraction is an unsigned compare of the operands; for a shift it is the last bit shifted out, which the shift handler has to stash because it cannot be recovered from the result. $get_of needs the sign of both operands and the result, which is why flag_sign_shift exists.

sequenceDiagram
    participant P as Guest program
    participant ALU as ADD / SUB / TEST handlers
    participant F as flag_op, flag_a, flag_b, flag_res
    participant J as JZ / SETcc / ADC handlers
    P->>ALU: add eax, ebx
    ALU->>F: store op=add, a, b, res (no flags computed)
    P->>ALU: sub ecx, 1
    ALU->>F: overwrite op=sub, a, b, res
    P->>ALU: mov / lea / push ... (flags untouched)
    P->>J: jz target
    J->>F: $get_zf: is flag_res == 0 at this width?
    F-->>J: 0 or 1, computed now
    J-->>P: branch taken or not

The saving is real because flag reads cluster: a loop body of ten instructions typically reads flags once, at its branch.

Where it bit

Lazy flags are a classic source of subtle emulator bugs, and the project's history is a catalogue of them. All of these are in the story, most from the first week:

The lesson recorded from these is that every lazy-flag bug looks like something else: a heap corruption, a string that resolves wrong, a game whose logic goes odd. The x86 test-vector suite (test/test-x86-ops.js) grew alongside them, and every one of the bugs above is now a pinned case.

The toy VM's second opinion

In August 2026 the project built a second, much smaller x86 machine, the toy VM, specifically to test interpreter designs on something small enough to rewrite. Lazy flags were one of the designs re-examined there:

Both docs quote the measured numbers rather than expectations. Read them before concluding that lazy flags are "always faster"; the answer depends on how the interpreter dispatches and how much of the flag state the JIT can keep in registers.

Further reading