playX18 opened issue #9539:
Feature
What I am proposing is to introduce "array-call" calling convention, the name is just whatt I thought would fit it perfectly. The main goal of this feature is to allow implementing dynamically typed languages easier and making them more performant. The proposed feature should allow users of Cranelift to check argument count passed to a function on callee-side and fetch arguments dynamically akin accessing array. What it could look like is:
function u0:0(...) array_call { v0 = get_argument_count.i32 v1 = icmp.lt v0, 2 brif v1, block0, block1 block0: v2 = get_argument.i64 0 // "constant" fetch of argument, index is know so we can just map it to `rdi` on x64 for example. v3 = iconst.i32 1 v4 = get_argument.i64 v3 // also allow to fetch arguments dynamically block1: ... }
Benefit
This allows for implementation of fast and efficient calls when compiling dynamically-typed languages where function arguments and function signature are not known in advance. One might argue that you can implement the same behavior by passing
argc: usize, argv: *mut Value
but it won't work in case of tail-calls and also is not as performant as directly passing arguments in register when opportunity presents itself.One example where such calling convention can be used is Scheme compilers. There's many Scheme compilers in the wild which produce binaries and have to rely on their own backends simply because no other backend has "arraycall"-like calling-convention. In my own implementation I have to pass arguments on runstack which is stored in TLS state (uses
pinned_reg
feature to access the runstack though) and it's a huge performance hit compared to doing calls using arraycall-like calling convention in my baseline backend which compiles Scheme to assembly directly from bytecode.Implementation
The implementation could be based on existing SystemV and TailCall calling conventions.
The following implementation is just an example and uses X64 as a base:
RAX
can be used to pass argument count (or return value count) to callee/callerRDI
-R8
registers can be used for arguments.- As for arguments that do not fit into registers stack space should be allocated and cleaned up by callee on exit/return.
- If argument is larger than platform word size we might disallow the usage of this convention as I am unsure how to mix different argument sizes well with this.
Alternatives
- One alternative is passing
argc: usize, argv: *mut Value
. It works, works really well and is easy to implement from user-side. But this approach has downsides of not allowing tail-calls and degraded performance since now we're always forced to pass everything on the stack.- Second alternative is heap-allocated buffer stored somewhere in VM context and accessed through
pinned_reg
feature. It's similar to the first alternative but allows to perform tail-calls as now stack is not used across calls thus arguments are not overwritten during tail-calls. But the downside of degraded performance still remains.
Last updated: Dec 23 2024 at 12:05 UTC