I put the original mail in a document on google docs. I didn't bother answering individually to each mail so here are my answers.
Handlers and runtime context (RC):
_Size_
At first I wanted to avoid duplication between the different RCs but let's see how much memory would really be used if things got duplicated.
Let's assume for now that the RC might contain the global object reference, the root object reference, the heap pointer and all the handlers. Maxime talked of ~100 handlers so I'll take her word for it. So the size of the RC object would be ~400 or ~800 bytes depending on the platform (the 3 references becomes negligible). Given something like 100 instances of tachyon (1 per browser tab for a super power user) the memory used for that would be ~40 or ~80 kB.
I don't know how many "objects from the low-level Javascript library" Maxime was referring to but as long as it is 10-100 references, it still seems reasonable. However, we should not plan on supporting something like 100 000 user-level threads, each with their own RC, using this scheme.
_Switching_
If I understand the problem correctly, different user-level instances of tachyon should never be able to directly call functions or access objects from one another. So the two cases that might require a context change are between the VM and a user-level instance (such as saving/resuming the execution of an instance) or between a user-level instance and the VM (such as calling the compiler with eval).
In the first case, the VM should have access to all instance contexts to manage them so I guess the context changes will be manually coded where we need them.
In the second case, I see two ways of handling the problem. My first idea is that we might identify which calls might result in a context switch (like eval) and add the necessary code on the caller side. My second idea is that we might use an "expected context" on the callee side.
Each compiled function would have an expected context that would be associated with it at linking time. An expected context is the reference to a runtime context object. The expected context would be checked at the function entry. Three cases might happen: 1. The current context is the same as the expected context. Nothing would be done. 2. The current context is different from the expected context. The current context is saved on the stack, and replaced by the expected context. The current context would be restored when returning from the function. 3. The expected context is a special "identity value" which would make the function behave as in (1), whatever context is currently active. This would allow standard library functions to work with both the compiler and the user-level code without having to duplicate them (as in a statically linked context).
The runtime overhead would be 1 check at each function entry and 1 check at each return point. The space overhead would be 1 reference in each stack frame where there is a context change. Once the context has been changed, subsequent calls to functions expecting the same context would not incur an additional space overhead.
_Interesting Configurations_
I initially decided to separate the different conceptual elements in case some other interesting combinations could be found along the way. However, it doesn't mean that all combinations are interesting. As Marc pointed out, having only handlers accessible from registers might not be worth it. So here is a listing of which combinations I think might be worth trying. I call a unique combination, a scenario.
As a reminder the different elements are: -Stack Pointer (SP) -Heap Pointer (HP) -Runtime Context (RC) -Handlers (Hdlr) -Argument Nb (ArgNb) -Return Address (RA) -Implicit Args (ImpArg) -Explicit Args (ExpArg)
REG is any of X86_REG or X64_REG. Non-specified items are passed on the stack or accessible from memory as globals. ExpArg(k) means first k args of ExpArg.
Worst-Case 1. Only the stack pointer (SP) in a register. Everything else passed on the stack or accessible globally. Used as a base reference to calculate the speedup achieved by passing things in registers.
Maximize of function arguments passing 2. SP in xSP, ImpArg and ExpArg(5) in registers;
Predominance to runtime context accesses 3. SP in xSP, RC in REG, HP, Hdlr, ArgNb accessible from RC, ImpArg and ExpArg(4) in registers;
Predominance to object allocation 4. SP in xBP, HP in xSP, ImpArg and ExpArg(4) in registers;
RC + Object allocation 5. SP in xBP, HP in xSP, RC in Reg, HP, Hdlr, ArgNb accessible from RC, ImpArg and ExpArg(3) in registers;
RC + Return Address 6. SP in xSP, RA in Reg, RC in Reg, HP, Hdlr, ArgNb accessible from RC, ImpArg and ExpArg(3) in registers;
Most of runtime elements 7. SP in xBP, HP in xSP, RA in Reg, RC in Reg, HP, Hdlr, ArgNb accessible from RC, ImpArg and ExpArg(2) in registers;
In each scenario, the total number of registers reserved for register allocation is always ImpArg + ExpArg(k).
Further things to try would be: - Passing the ImpArg on the stack; - Making RA and maybe some other registers callee-save to liberate them for register allocation, possibly selectively for functions with certain properties (like more than n temporaries).
I would suggest doing this once we figured which scenario works best.
I am not settled yet on how the code generation will be parametrized since non-trivial changes to handlers might be required for the different configurations to work. I'll see when we agree on a number of interesting scenarios.
Erick
Afficher les réponses par date
I don't know how many "objects from the low-level Javascript
library" Maxime was referring to but as long as it is 10-100 references, it still seems reasonable.
Probably less than 30. The global object prototype, string prototype, prototypes for the error classes, the error constructors, etc.
However, we should not plan on supporting something like 100 000
user-level threads, each with their own RC, using this scheme.
I think that a < 1KB overhead per thread is extremely reasonable. The biggest overhead will be maintaining a stack and a GC pool for each thread. However, if the context size ever turns out to be a problem, then we could decompose the context into multiple objects. The things that need to be different, per-thread, are things like the heap pointer, and possibly the performance counters, but the JavaScript objects we keep in there will be shared among threads, at least if we stick to a Java-like memory model.
In the second case, I see two ways of handling the problem. My first
idea is that we might identify which calls might result in a context switch (like eval) and add the necessary code on the caller side. My second idea is that we might use an "expected context" on the callee side.
I think the problem is "where do you store your expected context?". If you're storing it somewhere, and loading it in a register to compare it to the current one, you're already wasting CPU cycles by performing a memory access.
Marc suggested annotating functions requiring a context change, or performing the change manually. I think that this could work, but since I also think we need to avoid shooting ourselves in the foot, I think that perhaps we could code an assertion/debug mechanism into this... We could store a flag in the context header to say whether it is a Tachyon or user-level context... And all the tachyon code we generate, when compiled in debug mode, could have a test to make sure that the context has been appropriately switched. This test would disappear in release mode.
- Maxime