Stream: cranelift

Topic: efficient access to members of structure with const address


view this post on Zulip Dominik Behr (Aug 30 2026 at 02:44):

hello everyone. I have been using cranelift extensively in my (mostly vibe coded) Rust SGI Indy emulator, https://github.com/techomancer/iris . It is used both for acceleating CPU emulation and to build specialized "shaders" for emulating the drawing engine in Newport graphics REX3 chip.
I have been accessing the members of mips core (registers and cpu state) struct via base pointer + offset, which works well, but I have realized (since im emulating just single cpu) all offsets and callbacks to rust are effectively constants at compile time. The use of constants for base addresses of memory and arrays and for callbacks to Rust seems to be a good optimization at this point, but for register access it will generate loads and stores using full 64 bit address which will be nightmare for code size/density. So it seems a better approach is a base register + offset. But thatalso means burning a register for the base address which may be easily reloaded from constant. Is there any way to express something like this in IR?
Also, 2nd, possibly idiotic question. My JIT has 2 modes, one with function per entry address in a code page, and one where whole page gets compiled with dispatch table at the beginning. The tradeoff is replicated statically reachable code from multiple entrypoint, vs one single function with dispatch table that needs to do a branch to begin execution. I think it would be insteresting if it was possible to compile a single function with multiple entry points?
Finally, I ended up creating a proxy object(s) for memory arena and doing deferred sealing of functions compiled from multiple compile threads sharing single arena, mostly so I dont waste space sealing functions that do not cover full pages. This may be silly, but I wonder if my approach is the "correct" way to do it. Or are there better ways?

view this post on Zulip Chris Fallin (Aug 31 2026 at 15:07):

Cool project!

for register access it will generate loads and stores using full 64 bit address which will be nightmare for code size/density. So it seems a better approach is a base register + offset. But thatalso means burning a register for the base address which may be easily reloaded from constant. Is there any way to express something like this in IR?

It's not totally clear to me what you're asking -- both of these approaches can be encoded in CLIF. You can encode a constant address (as an iconst.i64) and load/store; or you can pass in a context pointer to the struct and encode loads/stores to offsets. Which one you do is up to you; it has the tradeoffs you mention (bad density with lots of fixed 64-bit addresses, or register pressure of one vm-context pointer). I think the tradeoff that most folks take (see e.g. Wasmtime, and most other ISA emulators I'm familiar with) is to pass a context parameter around. Yes it's a register, but let regalloc worry about that; also your liveset is likely not so large if all emulated-machine registers are actually loaded/stored.

That also has the advantage of letting you share the compiled code between multiple CPU instances, if that matters (emulating SMP or a network of multiple machines or ...)

Aside: it might be worth investigating whether you need to do a full load+op+store for each emulated instruction. You should be able to reason about register values that have already been loaded and are still fresh (not invalidated by calls into your runtime). You could use a separate alias-analysis region for each register field and let Cranelift worry about this, or you could do it yourself. In any case, from my experience compiling JS bytecode to machine code, "SSA-ifying" the operand dataflow is a pretty important early jump for perf.

I think it would be insteresting if it was possible to compile a single function with multiple entry points?

We've discussed this in the past and (i) it's either a big architectural change to the compiler, which means it's unlikely we would do it (high risk, niche use-cases), or (ii) we would "polyfill" it by essentially doing what you describe internally, by putting a dispatch branch at the beginning to reach the right entry. If I were you I'd take the latter approach and see how far it gets you.

Best of luck!

view this post on Zulip Dominik Behr (Aug 31 2026 at 16:43):

what I am asking for is this.
i can pass base address of struct as function argument and use it with offset everywhere for loads, it works fine, but cranelift needs to preserve it everywhere and keep it allocated in the register.
but I know this address at compile time. so i could make it a constant. which the i could emit every load as const u64+ offset, which would work great, except for the fact that every load now would be 64 bit address, increasing code size considerably.
what i would like is 3rd option. where i could have a variable where i load this constant and use it as base for addressing in a structure. but because this is a compile constant cranelift does not have to preserve it because at any time it can reload it from a constant. but instead of using full 64 bit address for loads everywhere it uses base+offset addressing, thus generating smaller and most likely faster code.
and yes, this will not work if i ever want to emulate multiple processors using the same compiled code. which may be an option in the future. anyway, for now i will live with context passed around in a register.

lifting a register out of memory for multiple instructions is indeed both potential performance win and a nightmare to do it right. previous jit v1 contributed to the project by someone else struggled that. simply putting the guest registers in ssa and letting cranelift deal with it doesnt work well since mips has many more registers than the host so the end result is constant register spilling and some instabilities. it would be nice if cranelift itself could figure out what values it could lift from memory and preserve in register across calls if there was a way to annotate the callouts to rust maybe? now for simplicity and stability we assume everything is always coherent in memory and the only inferred and materialized values are current pc/bdslot and instruction count which get materialized on exit/exception.

im wondering if i can shortcut the internal dispatch somehow, like have an option to query internal dispatch address and return it to the caller and then call it directly. but that assumes the signature would be the same for all dispatch destinations. that sounds hacky and unstable though.

and last night i found out that windows fastcall cannot return u32+u64 in registers, so going back to returning one return by memory, instant -20% perf. yay windows.

view this post on Zulip Chris Fallin (Aug 31 2026 at 16:47):

what i would like is 3rd option. where i could have a variable where i load this constant and use it as base for addressing in a structure. but because this is a compile constant cranelift does not have to preserve it because at any time it can reload it from a constant. but instead of using full 64 bit address for loads everywhere it uses base+offset addressing, thus generating smaller and most likely faster code.

Ah, OK. I think you already have this option today:

Cranelift will actually rematerialize constants once per block, so there is no concern about register pressure for a single constant used throughout the function body.

view this post on Zulip Chris Fallin (Aug 31 2026 at 16:48):

Unless amode folding is breaking that? Are you seeing such cases?

view this post on Zulip Dominik Behr (Aug 31 2026 at 17:49):

i will experiment with that. but in previous experiments it was always coming out as as load from 64 bit absolute address instead of base in register + offset. maybe it is also because every instruction is emitted as separate block since in many cases they have to be addressable and reachable for branches


Last updated: Sep 20 2026 at 19:05 UTC