Skip to content

Replace the IR tree-walking evaluator with a bytecode VM.#128

Merged
philipaconrad merged 4 commits intoopen-policy-agent:mainfrom
koponen:bytecode-vm
Apr 21, 2026
Merged

Replace the IR tree-walking evaluator with a bytecode VM.#128
philipaconrad merged 4 commits intoopen-policy-agent:mainfrom
koponen:bytecode-vm

Conversation

@koponen
Copy link
Copy Markdown
Contributor

@koponen koponen commented Apr 1, 2026

Introduce a Bytecode package that compiles IR into a compact instruction stream and executes it in a tight PC loop, replacing the recursive tree-walking IR evaluator.

Bytecode is more compact than the IR, more cache-friendly, and eliminates per-evaluation pointer chasing. The tree-walking evaluator copies Statement enum associated values on every dispatch; the VM decodes operands directly from the byte stream instead.

The IR-to-bytecode converter runs once at load time. Compact opcode variants pack their operand into the 24-bit header length field, saving a payload word for the most frequent single-operand statements. Validation bounds-checks all string/number/function table references before execution so the VM hot loop can skip those checks entirely.

The bytecode VM is up to 25% faster in benchmarks, with the biggest gains on iteration and call-heavy workloads.

@philipaconrad philipaconrad self-assigned this Apr 3, 2026
@philipaconrad
Copy link
Copy Markdown
Member

ℹ️ Self-assigning for review.

Copy link
Copy Markdown
Member

@philipaconrad philipaconrad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a massive PR, so I focused most of my review efforts on the join points and interfaces between parts. I read closely through the major conversion logic, and skimmed only lightly over most of the tests. I assume that the 30+ instructions and the stack management are all ported over correctly, or basic tests we already had (like the compliance tests) simply would not pass.

return buffer
}

/// Get the current bytecode buffer as Data (allocates a copy; use for serialisation only)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[comment]: I appreciate the usage hints here!

Comment thread Sources/Bytecode/Opcode.swift Outdated
Comment on lines +458 to +461
/// Decode operand without bounds checking (for validated bytecode, little-endian)
/// SAFETY: Caller must ensure offset is valid and bytes contain enough data
@inline(__always)
public static func decodeUnchecked(from bytes: ContiguousArray<UInt8>, at offset: Int) -> (operand: EncodedOperand, size: Int) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[comment]: This looks like a good use of the @inline(__always) marker.

Comment thread Sources/Rego/VM.swift
Comment on lines +281 to +286
/// CallKey is a key for memoizing a bytecode user-function (rule) call.
/// Arguments are captured as raw-encoded operand values rather than resolved RegoValues,
/// as hashing resolved values is expensive. This relies on the invariant that the plan
/// will not modify a local after it has been initially set.
/// Only 2-arg calls (OPA rules: input + data) are memoized.
struct CallKey: Hashable {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question]: How is that invariant preserved when dealing with "in-place modifying" instruction types, like ArrayAppendStmt? Or is this only for Call*Stmt instructions?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, it's only for the call statements. This same assumption was actually made by the replaced code (see comments of InvocationKey for similar content).

Copy link
Copy Markdown
Member

@philipaconrad philipaconrad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marking Approve here preemptively, as I didn't see anything wrong or worth delaying the PR further over. I'd love an answer to my earlier review question (just to make sure I'm understanding that part correctly), but it's not a blocker.

Thanks again @koponen for your hard work on bringing this big optimization to life! 😄

Teemu Koponen and others added 4 commits April 21, 2026 18:54
Introduce a Bytecode package that compiles IR into a compact
instruction stream and executes it in a tight PC loop, replacing the
recursive tree-walking IR evaluator.

Bytecode is more compact than the IR, more cache-friendly, and
eliminates per-evaluation pointer chasing. The tree-walking evaluator
copies Statement enum associated values on every dispatch; the VM
decodes operands directly from the byte stream instead.

The IR-to-bytecode converter runs once at load time. Compact opcode
variants pack their operand into the 24-bit header length field,
saving a payload word for the most frequent single-operand statements.
Validation bounds-checks all string/number/function table references
before execution so the VM hot loop can skip those checks entirely.

The bytecode VM is up to 25% faster in benchmarks, with the biggest
gains on iteration and call-heavy workloads.

Signed-off-by: Teemu Koponen <tkoponen@apple.com>
…ime.

Signed-off-by: Teemu Koponen <tkoponen@apple.com>
Signed-off-by: Philip Conrad <philip_conrad@apple.com>
Signed-off-by: Philip Conrad <philip_conrad@apple.com>
@philipaconrad
Copy link
Copy Markdown
Member

ℹ️ Rebasing to catch up with main, using the Github Web UI...

@philipaconrad philipaconrad merged commit cc05349 into open-policy-agent:main Apr 21, 2026
11 checks passed
@philipaconrad philipaconrad mentioned this pull request Apr 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants