We have nearly reached the end of the current roadmap. infix is fast, secure, and portable. But optimization is an addiction, and I am already looking at the next bottleneck. The new direct marshalling API wins the benchmark races because it removes the intermediate step of packing C values into a buffer. Double indirection is slower than pulling them straight from the source SV*s.
But what if we start with raw C values? What if we pretend we aren't wrapping a scripting language, but building a high-performance event system or a hot-reloadable game engine?
Currently, you have to do this:
int a = 10;
double b = 20.0;
void* args[] = { &a, &b }; // <--- Indirection #1: The Array
// Inside the JIT:
// 1. Read args[0] -> get pointer to 'a'
// 2. Read *pointer -> get value 10
This is the libffi model. It's flexible, but it is cache-unfriendly. The args array is in one place, a is on the stack, b might be on the heap. The CPU has to gather scattered memory from a wide range of locations.
I am plotting the implementation of a something that has been on my TODO list almost from the start: packed trampolines. Now that we have a functioning type graph and introspection system, this should be possible.
The Concept: Arguments as a Struct
Instead of passing an array of pointers, what if we passed a single pointer to a block of memory where the arguments are laid out contiguously?
// Concept: The Packed Buffer
struct PackedArgs {
int a;
// 4 bytes padding for alignment
double b;
};
struct PackedArgs args = { 10, 20.0 };
infix_packed_cif(&args);
This changes the JIT's job from "gather" to "offset".
The JIT Transformation
Let's look at the assembly difference on x86-64 (ARM always take a back seat...) for loading the second argument (double b).
Current Approach:
; R14 points to void** args
MOV R15, [R14 + 8] ; Load the pointer to 'b' into scratch reg
MOVSD XMM0, [R15] ; Dereference to get the value
We have two memory loads. If [R14 + 8] and [R15] are far apart, we might stall on two cache misses.
Proposed Approach:
; R14 points to the packed buffer
MOVSD XMM0, [R14 + 8] ; Load value directly from offset 8
One instruction. One memory load. Perfect spatial locality and that translates into speed.
Implementation Challenges
Implementing infix_forward_create_packed requires a new kind of logic in the ABI layer: layout computation.
The JIT doesn't just need to know that Arg 1 is a double; it needs to know exactly where that double sits in the packed buffer relative to Arg 0.
This implies a pre-calculation phase that mimics our struct layout logic:
// Hypothetical layout logic
size_t current_offset = 0;
for (int i=0; i < num_args; ++i) {
infix_type* type = arg_types[i];
// Align the current offset
size_t align = infix_type_get_alignment(type);
current_offset = (current_offset + align - 1) & ~(align - 1);
// Store this offset for the JIT emitter
layout->arg_offsets[i] = current_offset;
current_offset += infix_type_get_size(type);
}
The JIT emitter then uses these pre-calculated offsets to generate the MOV instructions.
Why bother?
For general application logic, saving one MOV instruction is negligible. But for data-oriented design, this is a game changer.
Imagine a system where you have a buffer of network packets or a file loaded from disk. The data is already packed.
- Current FFI: You must parse the buffer, copy values into local variables, take their addresses, and build a
void*array. - Packed FFI: You just pass the pointer to the buffer directly to the JIT.
This enables Zero-Copy FFI!
If infix supports this, it becomes more than just a function caller; it becomes a bridge that can map binary data directly to function invocations without deserialization overhead.
The Roadmap
This feature is currently in the design phase. It requires:
- Extending the
infix_call_frame_layoutto support explicit byte offsets for sources. - Adding a
infix_packed_layout_createAPI to helper users build the input buffers correctly. - Updating the architecture emitters to support "Base + Offset" loading (which they mostly do already).
It’s the logical conclusion of the project: moving from "calling functions dynamically" to "executing data."
This is not a top priority but stay tuned.