-
Notifications
You must be signed in to change notification settings - Fork 1
shared-suspendable and shared-fixed as separate function types #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
The big design question here is whether the difference between If the two are not differentiated at the type level, then there would be no way to statically disallow indirect calls from Alternatively, if we do differentiate at the type level, we can statically disallow Personally, I think the former option, where we do not distinguish at the type level, is more attractive. Having to have wrapper functions and explicit shared-barriers to achieve the same runtime semantics and function interoperability is a bunch of complexity and code size for no benefit AFAICT. |
For this reason, my intuition is that we will need differentiation at the type level. Based on @rossberg's sketch here I believe that such a barrier would carry an eager runtime cost (at least setting a bit in the stack). This would mean that even code just compiling to
I think the benefit would be that code just using |
Actually, now I'm wondering if the same argument about overheads applies to EDIT: IMO the variant I discuss in the OP with a |
Actually, I realise there's an alternative design that may make more sense. Instead of allowing only I think this might fit the existing model of stack switching better, where functions that may suspend can still be called even without a handler, but attempting to actually suspend just traps. It would also allow |
I don't think there's any reason to disallow shared continuations from being resumed from |
Sorry, I meant that |
The fact that disallowing
If we use a "zero-cost" |
It seems one or the other is needed, because the bad case is a call stack of the form
We need to make sure one way or the other that the middle
If we expect |
I had a chance to think about this some more. If we start out by assuming that every
On the other hand, if we make all the required
Sorry for the wall of text. We should probably move on to a live discussion soon. |
Just a few points to add: explicit case Note that instead of having a block-level shared barrier instruction, it's possible to instead have a call-level barrier instruction (essentially the Also, as I sketched here one can instead restrict the One other issue with restricting the
It's hard for me to see how a producer could actually support work-stealing and non-shared function parameters simultaneously even in the most optimistic case. I'd bet that "shared-suspendableness" would infect almost every non-trivial function, unless there's a strict static partition at the source/language runtime level, in which case static annotations in Wasm are still ok. I'd even bet that this problem would happen in the implicit case (i.e. most calls would just start trapping if any clever partition were attempted). EDIT: and I should emphasise again that this is why I still think we push for thread-local functions. If we believe work-stealing is going to be real in the future, we're just kicking the can down the road until then, and complicating the language in the meantime. implicit case
I'd like to understand more explicitly how you'd plan to distinguish I also don't have a clear view of how the dynamic check semantics avoids regressing every existing "fixed" function call. Morally it seems like inserting an extra
Can you expand on how this works currently for exception handling? This may be the piece I'm missing. I'd expect at least a penalty in compilation time and/or cache effects/branch prediction. |
Yes, this is what I had originally envisioned. I had imagined that producers who wanted to use shared-continuations would choose the 'shared-suspendable' type for all of the functions they generate for source language functions, as all of their source language types are likely shared and so the strictest semantics are not an issue. For calling out to JS for local host functions, they would need to perform the barrier at those points.
That would require having a sequence of
Agreed, for producers using shared-continuations, non-shared function parameters can't be used. As I sketched in #42, I believe that we could support a scheme where non-shared context locals and the shared-barrier can be used to access non-shared state inside shared continuations. I also wonder if we could mix these functions at indirect call sites by having
At least for SM, we implement |
The situation I have in mind is just |
Hmm, I'm not sure I follow without seeing where the handler/suspend are in that situation. It also seems like this would be a problem even if we don't split up the function types (as it doesn't involve shared-fixed at all)? |
The shared suspension is initiated in the I also wouldn't rule out |
That's interesting, I guess with thread-local functions in the proposal as-is we already could have a call stack |
During our discussion on #42, we discussed that a "safety valve" decision for JS function access, if we can't reach consensus on (strong/weak) thread-local functions, would be to (re)introduce a version of
shared
function that cannot have its execution suspended as part of a (hypothetical) shared continuation.Currently our design doesn't permit
nonshared
parameters toshared
functions in order to be forward-compatible with shared continuations, which would allow such anonshared
object to be smuggled into another thread by suspending execution and resuming in another thread. By forbidding such suspensions, we could allownonshared
parameters, and thus pass in a JS context (e.g. a struct containing unshared references to JS functions) as a regular parameter that would be threaded through execution (or a context local as sketched here), giving a mechanism to call JS functions fromshared
Wasm functions.This issue is to discuss the design implications of this approach. A few initial points:
shared-suspendable
semantics with some mechanism for thread-local functions (weak if necessary) over the below. That being said, I don't think the below is awful, and it seems less controversial from a GC engineering perspective.Note that, if we still believe that shared continuations will eventually exist, the below approach doesn't permanently solve our current problems, but instead pushes them into the future. Any compilation scheme wanting to use shared continuations will need to mark most functions as
shared-suspendable
, and so for such a scheme we'd still need to solve the same problem of JS access that we have in the current design (e.g. by introducing thread-local functions).Design Sketch
Terminology
For the purposes of this discussion, I'm going to refer to functions as being either
nonshared
,shared-suspendable
, orshared-fixed
(a different name forshared-nonsuspendable
, which is a mouthful). The distinction between these functions would be enforced by a static annotation on the function type.nonshared
functions are what we have today. Remember that in general,shared
things can't capturenonshared
things.shared-suspendable
functions are the "fully shared" functions we've been discussing before this point:nonshared
type (globals/tables/functions)nonshared
parameters/localsnonshared
references in the body (e.g.struct.new
)shared
continuationsshared-fixed
functions are somewhat more relaxed:nonshared
type (globals/tables/functions)nonshared
parameters/locals are allowednonshared
references in the body (e.g.struct.new
) are allowedshared
continuation, but can be part of anonshared
continuationIntuitively, the call stack of a
shared-suspendable
function could be captured as part of a shared continuation, and resumed in another thread. This means it's not safe for the frame to ever capture anonshared
object, even transiently. In contrast,shared-fixed
function calls are guaranteed to stay in the same thread for the entire duration of their execution. Therefore it's safe to pass innonshared
objects as parameters, and materialise them during the function call's execution. Prior to the standardisation of shared continuations, onlyshared-fixed
functions would be definable.Restrictions on calling
(EDIT: see this comment for an alternative approach with different restrictions)
shared-suspendable
functions can always callshared-fixed
functions with no restrictions. The extent to whichshared-fixed
functions can callshared-suspendable
functions depends on some design decisions of stack switching. By default, all forms ofshared-fixed
->shared-suspendable
call would be disallowed by validation (implying annotations/type tracking of[non]suspendable
on relevant call instructions).If the stack switching proposal includes a lexical barrier instruction (e.g. see here), it seems feasible to also include a concept of a "shared-only" barrier which traps upon an attempt to capture a shared continuation, but not a non-shared one. All forms of call to
shared-suspendable
functions could be allowed inside the body of this barrier. On reflection, I don't think that ashared-fixed
call should implicitly introduce such a barrier, since this would mean that everyshared-fixed
call would have the implicit overhead of "Check if I'm in a continuation and if so, set the barrier bit". Instead I think the shared-only barrier should always be explicit (and would switch validation fromshared-fixed
mode toshared-suspendable
mode within its body). @rossberg please correct me if I'm wrong about the above.A
shared-fixed
function could also call ashared-suspendable
function by wrapping the latter as a shared continuation and using a hypotheticalresume_barrier
instruction (as sketched here WebAssembly/stack-switching#44 (comment)). If a "shared-only" barrier proves infeasible, this would be the only way to make ashared-fixed
->shared-suspendable
call.Note that in either case, it's still ok for a
shared-fixed
function to hold a reference to ashared-suspendable
function; only calling is complicated. This means that we don't need to distinguish between different kinds ofshared
for tables and globals - the[non]suspendable
distinction is only needed for callable things.The text was updated successfully, but these errors were encountered: