Skip to content

perf: borrow the executing function from its module instance - #74

Open
matthargett wants to merge 2 commits into
explodingcamera:nextfrom
rebeckerspecialties:perf/cheaper-calls
Open

matthargett wants to merge 2 commits into
explodingcamera:nextfrom
rebeckerspecialties:perf/cheaper-calls

Conversation

@matthargett

Copy link
Copy Markdown
Contributor

Every call or return between two functions cloned the callee's Shared<WasmFunction> and dropped the previous one (two atomic refcount updates each way) and looked the function up in the store.

A module instance now keeps its own functions, which the store allocates contiguously (helping with locality for cache and prefetch). InterpreterRuntime holds the instance for the whole run and the executor borrows the executing function from it, so a call or return inside the instance switches a reference (I think this is what makes cache evict more often than I would expect). A direct call to one of the module's own functions also skips the address table, the host check and the owner check. Execution that moves into another instance's function (through an import, a table, a function reference, a return or an unwinding exception) ends the run and resumes with an executor for that instance. Fuel and time budgets carry over, so budgeted runs should suspend at the same points as before.

Entering a function also skips the value-stack lanes it never uses (most functions only touch the 32-bit lane), and a single-result return moves its result down once instead of popping and pushing it.

tests/cross_instance_calls.rs covers calls, tail calls, table calls, callbacks and exceptions across an instance boundary in both directions, and fuel- and time-budgeted runs that cross it.

Microbenchmark explains the uplift in the larger integrated benchmarks: a loop calling a one-line function drops from 393 to 311 instructions per iteration. Change in cycles per call against next (d1165c2), on the efficiency cores of an iPhone 12 (A14), iPhone XS Max (A12) and iPhone SE (A13), median of five interleaved launches per build:

benchmark A14 A12 A13
xmrsplayer (1024-frame buffer) −6.2% −2.6% −3.9%
audio DSP (1000 frames × 512) +0.5% +3.7% −0.1%
graphql-validation (AS) −2.4% −2.7% −3.5%
multi-memory twin: one memory +0.5% −0.3% +0.0%
crc32 (64 KB) +0.1% −0.2% −0.1%
convolution 256×256 +0.1% +0.2% +0.1%
sieve (10000) −0.2% −0.7% +0.1%
bulk_memory (memory.copy/fill) −0.1% +0.0% +0.0%
matmul relaxed-simd FMA +1.5% −5.5% +4.6%
GC binary trees (~130K struct.new) −0.9% −1.0% −1.6%
fib(30) −5.5% −5.0% −6.0%
tail-call FSM (65536 return_call) −13.3% −11.6% −8.5%
call_indirect (200K) −6.5% −10.3% −7.7%
call_ref (200K) −12.4% −14.5% −9.4%
vtable_poly4 (200K) −10.8% −10.7% −9.3%
EH parser, exnref (4096 stmts, 25% throw) −18.7% −18.8% −16.4%
geomean, cycles −4.9% −5.2% −4.0%
geomean, instructions −4.2% −4.8% −3.9%
geomean, wall time −4.8% −4.7% −4.2%
rows faster (cycles) 11/16 13/16 11/16

Entering a function reserved and zero-filled all three value-stack lanes
even when the function never touches the 64- or 128-bit lane, which is
the common case. Skip a lane whose locals and operand-stack depth are
both zero.

A single-result return now moves the result down and truncates once,
instead of popping, truncating and pushing it back.
Every call and return between two functions cloned the callee's
`Shared<WasmFunction>` and dropped the previous one, two refcount updates
each way, and looked the function up in the store.

A module instance now keeps its own functions, which the store allocates
contiguously. The interpreter holds the instance for the whole run, so
the executor borrows the executing function and its module from it: a
call or return within the instance switches a reference, and a direct
call to one of the module's own functions skips the address table, the
host check and the owner check.

Execution that continues in another instance's function (a call through
an import, table or reference, a return, or an exception unwinding into
it) ends the run, and `InterpreterRuntime` resumes that frame with an
executor for its instance. Fuel and time budgets carry over, so a run
suspends at the same points as before.

This branch has not been deployed

No deployments
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