Skip to content

vm: compile short-circuit and/or for the dispatcher - #430

Open
shinyobjectz wants to merge 1 commit into
tv-labs:mainfrom
OpenRelationship:perf/dispatcher-and-or
Open

shinyobjectz wants to merge 1 commit into
tv-labs:mainfrom
OpenRelationship:perf/dispatcher-and-or

Conversation

@shinyobjectz

Copy link
Copy Markdown

A function containing and or or was never compiled for the dispatcher. The encoder had no lowering for :test_and / :test_or, so it returned :fallback and the whole prototype ran on the instruction interpreter. That is common in real code: x = x or default, return c and a or b, guards in if conditions.

Change

  • Two new opcodes, @op_test_and (77) and @op_test_or (78), encode {tag, dest, source, body}. When the left operand decides the result it is copied into dest. Otherwise body (the right operand, ending in a write to dest) runs as a nested block and resumes through the same cont marker :test uses.
  • No changes to codegen or the interpreter.

Tests

  • test/lua/vm/and_or_engines_test.exs: 14 cases run under both engines. They cover value-returning and/or; nil/false/0/"" operands; nesting; right-operand side effects that must not run; and/or in if, while and repeat conditions; default-argument assignments to locals, fields and upvalues; multi-return truncation; break after an and/or in a loop; a closure built in the right operand; and an error raised in the right operand. Each case also asserts Bytecode.fully_compiled?.
  • Some existing tests used and/or to keep a function on the interpreter. max_instructions_test (cross-engine mutual recursion) and peephole_test (:call_self on the interpreter) now strip that function's bytecode instead. The bytecode_test fallback and cascade cases use a synthetic prototype with {:set_list, _, _, 0, _}, the one shape the encoder still rejects. max_registers_invariant_test walks the new opcodes.

The full suite passes: mix test gives 2914 passed, and mix test --include lua53 --include differential --include slow gives 2935 passed.

Benchmark

Each number is the median of 15 :timer.tc runs after 3 warmup runs, in ms. The machine is an Apple M4 with OTP 29 and Elixir 1.20.3, MIX_ENV=prod. "budget" means Lua.new(max_instructions: 1_000_000_000).

program before after before (budget) after (budget)
fib(25) written with and/or 59.8 34.2 60.0 43.3
date functions (and/or) 11.1 9.3 12.0 10.2
fib(25) with if (control) 29.2 28.9 38.9 38.3
-- fib with and/or
local function fib(n) return n < 2 and n or fib(n - 1) + fib(n - 2) end
return fib(25)

-- date functions: day_of_year for the 15th of every month, 1900-2100
local function is_leap(y) return (y % 4 == 0 and y % 100 ~= 0) or y % 400 == 0 end
local function days_in_month(y, m)
  return m == 2 and (is_leap(y) and 29 or 28) or ((m == 4 or m == 6 or m == 9 or m == 11) and 30 or 31)
end
local function day_of_year(y, m, d)
  local n = d
  for i = 1, m - 1 do n = n + days_in_month(y, i) end
  return n
end
local s = 0
for y = 1900, 2100 do for m = 1, 12 do s = s + day_of_year(y, m, 15) end end
return s

A function containing `and` or `or` fell back to the instruction
interpreter, because the encoder had no lowering for :test_and / :test_or.
They now encode to two opcodes, @op_test_and and @op_test_or, that run the
right operand as a nested body and resume through the same cont marker
:test uses.

Tests that used and/or to force a function onto the interpreter now strip
its bytecode instead, and the fallback tests use a synthetic prototype.
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.

1 participant