-
Notifications
You must be signed in to change notification settings - Fork 5.3k
[Wasm RyuJIT] Initial scaffolding for calls and helper calls #123044
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
base: main
Are you sure you want to change the base?
Conversation
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
741ddb8 to
e805a10
Compare
AndyAyersMS
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We still need to figure out in detail how to specify what method we want to call.
We know roughly what kind of Wasm we want to produce (per the calling convention doc we'll be making lots of indirect calls) but we need help from the JIT host to make this all work out.
I think it might be ok for now to just defer this part of the work and try and get everything else lined up?
| // Generate a direct call to a non-virtual user defined or helper method | ||
| assert(call->IsHelperCall() || (call->gtCallType == CT_USER_FUNC)); | ||
|
|
||
| if (call->gtEntryPoint.addr != NULL) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per the calling convention, for user and helper calls we'll also be invoking them indirectly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. I'm not clear on whether that means there won't be an addr filled in
b95ce18 to
499127d
Compare
|
This branch now hits this in crossgen2: |
|
For this: [MethodImpl(MethodImplOptions.NoInlining)]
static void voidFunc () {
}
[MethodImpl(MethodImplOptions.NoInlining)]
static void callVoidFunc () {
voidFunc();
}R2R now compiles without crashing or asserting, and disasmo generates this: ; Method Program:callVoidFunc() (FullOpts)
G_M45341_IG01: ;; offset=0x0000
local.cnt 4
local[0] type=i32
local[1] type=i64
local[2] type=f32
local[3] type=f64
;; size=9 bbWeight=1 PerfScore 5.00
G_M45341_IG02: ;; offset=0x0009
i32.const 140722950757336
i32.load 0 0
call_indirect 0 0
;; size=14 bbWeight=1 PerfScore 3.00
G_M45341_IG03: ;; offset=0x0017
end
;; size=1 bbWeight=1 PerfScore 1.00
; Total bytes of code: 24It's not correct but it's a start. |
|
cc @dotnet/jit-contrib need to figure out how this should actually be factored and shaped and implemented, now that it "works". I don't understand 75% of the code I touched here, for example whether we use EA_8BYTE or a different one still makes no sense to me, and the way we historically separate out |
|
@kg what do we need to do to unblock this? I don't think we'll have the crossgen2 relocation stuff around for a bit, so emitting long-form placeholder values for the various indices/offsets is the best we can do. If you want to try getting the reloc machinery started with this PR then we can look into that too. |
As far as I'm concerned it's done, but I was waiting for someone to review it on the presumption that there's a bunch of things wrong in this PR since I didn't really know what I was doing. |
We should be able to do some testing, especially now that #123262 is in. What happens if we try and compile a method that has a call (eg |
Is this what you mean? #123044 (comment) |
| break; | ||
|
|
||
| case GT_PUTARG_REG: | ||
| genPutArgReg(treeNode->AsOp()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I needed this for builds to work, did we remove putarg_reg in another PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, we removed it in #123262.
| // Insert a null check on "this" pointer if asked. | ||
| if (call->NeedsNullCheck()) | ||
| { | ||
| // TODO-WASM: Ensure that when the this-arg is pushed onto the stack earlier, we perform the null check there. | ||
| // Once that's done this NYI can go away. | ||
| NYI_WASM("this ptr null check in genCall"); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // Insert a null check on "this" pointer if asked. | |
| if (call->NeedsNullCheck()) | |
| { | |
| // TODO-WASM: Ensure that when the this-arg is pushed onto the stack earlier, we perform the null check there. | |
| // Once that's done this NYI can go away. | |
| NYI_WASM("this ptr null check in genCall"); | |
| } | |
| if (call->NeedsNullCheck()) | |
| { | |
| NYI_WASM("Insert nullchecks for calls that need it in lowering"); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think NeedsNullCheck will still be true even if we inserted it, right? Or will we clear that flag when inserting it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will clear it. This is similar to what happens when we discover there is a natural dominating null check.
| // TODO-WASM: Handle "fast tail calls" here if we decide to implement them. | ||
|
|
||
| genCallInstruction(call); | ||
|
|
||
| // TODO-WASM: Disabled in WASM currently. Not sure if we need it | ||
| // genDefinePendingCallLabel(call); | ||
|
|
||
| var_types returnType = call->TypeGet(); | ||
| if (returnType != TYP_VOID) | ||
| { | ||
| if (call->HasMultiRegRetVal()) | ||
| { | ||
| NYI_WASM("multi-reg return values"); | ||
| } | ||
|
|
||
| genProduceReg(call); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // TODO-WASM: Handle "fast tail calls" here if we decide to implement them. | |
| genCallInstruction(call); | |
| // TODO-WASM: Disabled in WASM currently. Not sure if we need it | |
| // genDefinePendingCallLabel(call); | |
| var_types returnType = call->TypeGet(); | |
| if (returnType != TYP_VOID) | |
| { | |
| if (call->HasMultiRegRetVal()) | |
| { | |
| NYI_WASM("multi-reg return values"); | |
| } | |
| genProduceReg(call); | |
| } | |
| assert(!call->IsTailCall()); | |
| genCallInstruction(call); | |
| genProduceReg(call); |
I suspect the logic that skips genProduceReg for VOID calls is not entirely sound either, but in a way that's 'mostly fine' (the reason it should nominally be needed is because calls can induce liveness changes with return buffer locals).
genDefinePendingCallLabel is used to implement GT_LABEL and things that rely on the return address being available, so, not going to be needed for WASM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're not going to do tail calls? Or should I make the assert a NYI_WASM?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently, we do not expect to see tailcalls here because we don't have fast tailcalls and without that morph will fall back to the "portable" tailcalling mechanism, which is not implemented for CG2, which in turn means that explicit (.tail) tailcalls will silently be converted into regular tailcalls.
For this kind of case, an assert I think is better since seeing a tailcall here is an "unexpected" scenario, not an "expected but not handled" one.
In the long run, we will have two kinds of tailcalls:
- 'Fast' / 'implicit': discards the caller's linear memory frame, emits
call. - 'Explicit' (
.tail): emitsreturn_call, a "true" tailcall.
Currently though we only have one concept for a tailcall, so, this long term thing will need some work.
| EC_MANAGED_PTR, // call_indirect to an unknown managed method or helper (two levels of indirection - ptr to | ||
| // arbitrary cell) | ||
| // should generate: <push args>; call <func-index> | ||
| EC_FUNCTION_INDEX, // call to a known wasm import or function from same module, no indirection |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For WASM we only have direct call and indirect call_indirect, which already map to EC_FUNC_TOKEN and EC_INDIR_R, so I don't think we need these kinds to be under ifdefs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't have the concept of function pointers in wasm? Or is the idea that the extra level of indirection will be handled somewhere else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The proposal is to use EC_FUNC_TOKEN for what is currently called EC_FUNCTION_INDEX, remove EC_MANAGED_CELL and EC_MANAGED_PTR by handling the indirections they represent in the caller(s), and using EC_INDIR_R to represent "emit call_indirect".
The overall principle is to have emitIns_Call only emit one WASM instruction, the call itself. It is (more) consistent with how other targets represent these things.
| // cell to support interpretation) | ||
| // should generate: <push args>; <push address-of-cell-address>; i32.load 0 0; i32.load 0 0; call_indirect | ||
| // <signature-type> | ||
| EC_MANAGED_PTR, // call_indirect to an unknown managed method or helper (two levels of indirection - ptr to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this should be an emitter concept. The design philosophy for the WASM emitter so far has been "one WASM instruction <=> one emitIns_XYZ method". It means handling this at the caller level.
| IF_DEF(F64, IS_NONE, NONE) // <opcode> <f64 immediate (stored as 64-bit integer constant)> | ||
| IF_DEF(MEMARG, IS_NONE, NONE) // <opcode> <memarg> (<align> <offset>) | ||
| IF_DEF(LOCAL_DECL, IS_NONE, NONE) // <ULEB128 immediate> <byte> | ||
| IF_DEF(ULEB128_X2, IS_NONE, NONE) // <opcode> <ULEB128 immediate> <ULEB128 immediate> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need this for indirect calls. The table index/symbol will always be __indirect_function_table_table, we can just hardcode it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you think of any other cases where we need it? If not, I'll remove it. Maybe catch since it takes a tag idx and a label idx? table.copy/table.init? memory.copy/memory.init? Out of these only memory.init seems like we'll use it with two nonzero values, and it's possible one of them will always be zero (the second one).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see anything that would require it immediately. Of the ones you list, I think catch is the only candidate, but it's a part of try_table, not a standalone instruction.
I view this case as being very similar to IF_BLOCK: you still need the new format (to append the table index), but you don't need a separate instrDesc kind (since the index is not variable).
| #ifdef TARGET_WASM | ||
| // We have no special registers, so it's not correct for this function to ever return true, | ||
| // and callers will just break if it does. | ||
| return false; | ||
| #endif | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| #ifdef TARGET_WASM | |
| // We have no special registers, so it's not correct for this function to ever return true, | |
| // and callers will just break if it does. | |
| return false; | |
| #endif |
I don't think this is needed anymore (#if HAS_FIXED_REGISTER_SET below should be taking care of it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was needed before, but I'll check, thanks.
Yes --make sure that basic cases look plausible |
Depends on #123021
Starting to put together all the code we need for helper calls and calls in general.