emscripten-core / emscripten-core/emscripten
Smarter way for preventing LLVM from placing a function local variable on the hidden WebAssembly stack?
- Dominant language
- C++
- Stars
- 27.6k
- Forks
- 3.6k
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 105
Description
I am experimenting with how to enable stack scanning based garbage collection of a managed language in WebAssembly.
With the `emscripten/stack.h` API that we added some time ago, we are now able to scan the Emscripten "spillover" data stack from C code. (this API was originally added for lightning fast thread-local variable-length `alloca()`, but curiously works well for this purpose now also)
However, as you all know, most function locals are not placed on this data stack but they instead live as Wasm locals in the hidden/"secure" Wasm VM stack, so in order to implement correctly functioning stack scanning for a GC, we need to guide LLVM to put all the managed objects in the Emscripten spillover stack so that they'll be visible.
I wrote a small experiment that does achieve that, based on the "simple" effect that taking an address of a variable and passing it out to a JS function will prevent LLVM from being able to utilize the Wasm stack, and prevent it from doing much any optimizations at all on it.
Here is the example that illustrates the effect:
```c
#include
#include
#include
#define MAGIC 0x11223344
// Represents some kind of managed language object.
class ManagedObject
{
public:
explicit ManagedObject(const char *name):magic(MAGIC),name(name)
{
printf("ctor: \"%s\"\n", name);
}
~ManagedObject()
{
printf("dtor: \"%s\"\n", name);
}
uint32_t magic;
const char *name;
};
// Call on a stack instantiated pointer to pull the given object on the Emscripten "spillover" stack,
// instead placing the data on the hidden WebAssembly stack.
EM_JS(void, PIN_ON_STACK, (ManagedObject *obj), {});
void scan_stack()
{
printf("The following managed objects are found on the stack:\n");
// Scan the current thread's spillover stack.
uint32_t *lo = (uint32_t *)emscripten_stack_get_current();
for(uint32_t *ptr = (uint32_t *)emscripten_stack_get_base(); ptr > lo; --ptr)
{
if (*ptr == MAGIC)
printf("%p: \"%s\"\n", ptr, *(char**)(ptr+1));
}
}
void garage()
{
printf("\nIn garage:\n");
ManagedObject ferrari("ferrari");
ManagedObject tesla("tesla");
PIN_ON_STACK(&ferrari);
// Intentionally skip pinning tesla on the stack - as result, it won't be visible when stack scanning when built with -O1 or higher.
scan_stack();
}
void farm()
{
printf("\nIn farm:\n");
ManagedObject sheep("sheep");
ManagedObject duck("duck");
PIN_ON_STACK(&sheep);
PIN_ON_STACK(&duck);
scan_stack();
garage();
}
int main()
{
printf("In main:\n");
ManagedObject main("main");
PIN_ON_STACK(&main);
scan_stack();
farm();
printf("\nAt end of main: ");
scan_stack();
}
/* when run, prints
In main:
ctor: "main"
The following managed objects are found on the stack:
0x500c08: "main"
In farm:
ctor: "sheep"
ctor: "duck"
The following managed objects are found on the stack:
0x500c08: "main"
0x500bc8: "sheep"
0x500bc0: "duck"
In garage:
ctor: "ferrari"
ctor: "tesla"
The following managed objects are found on the stack:
0x500c08: "main"
0x500bc8: "sheep"
0x500bc0: "duck"
0x500b68: "ferrari"
dtor: "tesla"
dtor: "ferrari"
dtor: "duck"
dtor: "sheep"
At end of main: The following managed objects are found on the stack:
0x500c08: "main"
dtor: "main"
*/
```
The magic happens in the `PIN_ON_STACK()` JS function.
This works under all `-O*` settings fine, and is good enough for us to run some proof of concept tests.
However, there are some unfortunate drawbacks about this. Mainly that it is **a bit too pessimistic**, since it also prevents practically all other LLVM optimizations from operating on the pinned variable.
For example LLVM won't be able to optimize out any of the pinned `ManagedObject`s, since it won't know if some of them would actually be redundant copies of each other (in the same local function stack frame) - this is because extern JS functions are practically black boxes to LLVM. And such temp copies unfortunately commonly occur in AOT style IL codegen.
That leads me to question: can you recommend if there might be a better way to achieve this same effect, without causing pessimizations/deoptimizations in LLVM?
I.e. I would have something like
```c
void foo() {
ManagedObject __attribute__((do_not_place_on_wasm_stack)) ferrari;
ManagedObject __attribute__((do_not_place_on_wasm_stack)) ferrari2 = ferrari;
ManagedObject __attribute__((do_not_place_on_wasm_stack)) ferrari3 = ferrari2;
do_something_on(&ferrari3);
}
```
where the duplicate assignments of locals `ferrari`, `ferrari2` would still be optimized away, and only `ferrari3` would remain, but it would not be generated on the Wasm stack as a local, but instead would reside on the Emscripten data stack? (or if the function `do_something_on` actually optimized away to a no-op, then `ferrari3` would naturally also DCE away)
CC @dschuff @tlively @sbc100 @kripken thanks for any smart ideas! :)
Contributor guide
Assessment
This issue has not been assessed yet.