We are trying to port a virtual machine runtime written in x86 assembly language to other platforms.
We considered using LLVM for our ‘portable assembly’ so that the VM runtime could be built for our new target platforms.
It is stack VM, and one designed to utilize all the advantages of the assembly language implementation.
Our attempt to port it to C has resulted in performance issues and our goal is to achieve the same
(or better) performance as it is for our source VM. And this is where LLVM looks very promising for us.
But there are several problems we noticed which we think require us to make some extensions to LLVM.
The official doc referred us here for questions…and so we have some We want to thank everyone ahead of
time who reviews this and provides us feedback, it is appreciated.
The VM was designed to execute a high-level methods by running a special low-level function, which is either a special optimized
low-level implementation of the high-level method or bytecode-interpreter run.
After a high-level method executed by such a low-level function, there is a continuation that follows. The continuation is passed by VM stack and
doing this using using C (by C function calls, CPS) led to significant performance loss.
The bad news for us is that we have to strictly follow the existing VM design for many reasons (ex., backward compatibility).
The current VM x86 assembly implementation uses a ‘jump prototype’ way of invoking a low-level function.
This could be interpreted as a function which never returns and expects all arguments to be passed in registers.
Arguments are limited to pointer and integer types.
The function has no prologue/epilogue (thus EBP reg is free to use) and it ‘returns’ by jumping to a continuation function by pointer popped from VM stack.
We are considering extending LLVM by creating a special calling convention which forces a function (using this convention) to pass args in registers and to
be force tail-call optimized.
We can see ‘hipe’ calling conv in LLVM, which does almost what we need… the difference is that we need all args passed in regs.
Will extending the calling convention in the way we describe work? Does this sound reasonable? Or is there another simpler way to do so?
Because the existing VM runtime is written in x86 assembly, and doesn’t do function calls, it uses ESP register for VM stack purposes
(again, it is not in use for low-level calls). We want to do the same.
In our C version we use the VM stack (and pointer) in a heap memory, which is very bad for performance. Ex., *–vmCtx->sp = obj for stack push.
LLVM allows the initialization of the machine stack pointer to an arbitrary value by using ‘savestack/restorestack’ intrinsics,
but there is no way to, for example, allocate/deallocate the VM stack frame. Note that it is VM stack frame and it could be allocated
by some of that low-level executing functions (as needed), but never in a prologue/epilogue, so implementing it as another calling conv won’t help here.
We think that it could be implemented as intrinsics as well? Or perhaps we should create intrinsics for arbitrary machine stack access?
We tried, for example, stacksave-sub-store-stackrestore sequence, but it never folds into a single push operation.
Since the machine stack is a VM stack, we are not allowed to use alloca. It’s not a problem, but the machine register allocator/spiller
can still use the machine stack for register spilling purposes.
How could this be solved? Should we provide our own register allocator? Or could it be solved by providing a machine function pass,
which will run on a function marked with our calling conv and substitute machine instructions responsible for spilling to stack
with load/store instructions to our heap memory?
Hopefully this makes some sense? We know that we have to extend LLVM for every target platform, but it is still better than to rewrite VM in every target
Thank you for your time.