JIT: Add GT_SWIFT_ERROR_RET to represent loading error register upon return - #100692

Merged
amanasifkhalid merged 29 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg-ret
Apr 12, 2024
Merged

JIT: Add GT_SWIFT_ERROR_RET to represent loading error register upon return#100692
amanasifkhalid merged 29 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg-ret

Conversation

@amanasifkhalid

Copy link
Copy Markdown
Contributor

Follow-up to #100429. When creating return nodes during importation, if the method has a SwiftError* out parameter, the JIT will create a GT_SWIFT_ERROR_RET node before the regular GT_RETURN node to represent loading the SwiftError pseudolocal's value into the error register. We initially keep this node before the GT_RETURN block so we don't have to teach the JIT that it is a new valid terminator for BBJ_RETURN blocks; during lowering, we move this node to the end of the block so the error register won't get trashed after loading the error value. I found this to be an easier approach than trying to replace GT_RETURN with a binary node that holds the regular and error return values.

Old codegen:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T01] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov qword ptr [rbp-0x10], rdi
;; size=6 bbWeight=0.50 PerfScore 0.62
G_M3386_IG05: ;; offset=0x003C
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x0049
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 80, prolog size 27, PerfScore 16.33, instruction count 25, allocated bytes for code 80 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

New codegen:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 1, 1 ) struct ( 8) [rbp-0x10] must-init "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07 ] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08 ] ( 0, 0 ) long -> zero-ref "field V03.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp6 [V09 ] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov edi, 21
mov r12, rdi
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.38
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov r12, rdi
;; size=5 bbWeight=0.50 PerfScore 0.25
G_M3386_IG05: ;; offset=0x003B
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
nop ;; size=10 bbWeight=1 PerfScore 1.75
G_M3386_IG06: ;; offset=0x0045
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 76, prolog size 27, PerfScore 14.96, instruction count 26, allocated bytes for code 76 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Apr 5, 2024
@jakobbotsch

jakobbotsch commented Apr 5, 2024

Copy link
Copy Markdown
Member

We initially keep this node before the GT_RETURN block so we don't have to teach the JIT that it is a new valid terminator for BBJ_RETURN blocks; during lowering, we move this node to the end of the block so the error register won't get trashed after loading the error value. I found this to be an easier approach than trying to replace GT_RETURN with a binary node that holds the regular and error return values.

I'm not sure it's quite that simple due transforms within the JIT that assume knowledge about GT_RETURN and its semantics, and base transformations on this knowledge. This representation makes it error prone to write transforms around GT_RETURN since you aren't forced to handle this new terminator node. It seems intrinsic to me that the JIT has to be taught that this is a new kind of terminator node because of this.
For example, what happens in a function that has a switch with 50 returns in it? I would expect us to do return merging and end up with IR that doesn't handle the error correctly with resulting potential silent bad codegen.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

For example, what happens in a function that has a switch with 50 returns in it? I would expect us to do return merging and end up with IR that doesn't handle the error correctly with resulting potential silent bad codegen.

I see. In that case, let me try substituting GT_SWIFT_ERROR_RET for GT_RETURN then. In the meantime, let's see how much TP we're trading to get this codegen improvement...

@jakobbotsch

Copy link
Copy Markdown
Member

FWIW I think it's very likely we'll end up similarly having to expand the JIT's set of known and handled terminator nodes as part of async2. It has a similar situation with an async continuation that is returned in a different register than usual. I'm pretty sure the prototype has similar issues around return merging as the one I described above.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

FWIW I think it's very likely we'll end up similarly having to expand the JIT's set of known and handled terminator nodes as part of async2.

This might be too radical, but what if we expanded GT_RETURN to have an optional second argument to handle these scenarios, instead of introducing new nodes for them?

@jakobbotsch

Copy link
Copy Markdown
Member

I'm not sure whether it makes things much simpler -- you still have to audit the interesting uses of GT_RETURN on whether they need special handling of the potential second operand. It also becomes a bit easier to forget to handle the second operand. I like the explicitness of just having new types of nodes. In my experience this kind of explicitness pays off in the long run.

A more uniform representation would be to introduce the ability for the JIT to create new class layouts on the fly. In fact we can already do that for layouts without GC pointers, which would be sufficient for Swift (see typGetBlkLayout). Then you would consider a Swift function with error returns to return a tuple (x, y) where y is the error.
We wouldn't be able to handle something like this efficiently today, but it's sort of on my list in #93105 (the items around supporting FIELD_LIST for returns).

@jakobbotsch

jakobbotsch commented Apr 5, 2024

Copy link
Copy Markdown
Member

I think the uniform representation above would also generalize to GT_CALL with swift error returns where we would similarly create a new layout for the return that defines the error too, allowing us to remove GT_SWIFT_ERROR. But again, this requires a lot of work to handle efficiently -- it's essentially the "allow struct fields of physically promoted call returns to stay in registers" of #93105.
We would also need to generalize the ABI information carried in ReturnTypeDesc so that the frontend can set this up and have the backend automatically handle the details around getting the values out of the corresponding registers (for GT_CALL) or putting them into the right registers (for GT_RETURN).

It's probably a lot of work, but seems like it would be the right long term direction.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

I have a binop implementation of GT_SWIFT_ERROR_RET working locally. There are some quirks that I have to clean up and document before pushing, but the codegen looks slightly better:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 1, 1 ) struct ( 8) [rbp-0x10] must-init "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T02] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
; V08 tmp5 [V08,T01] ( 3, 2 ) long -> rbx "field V03.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp6 [V09,T03] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov ebx, 21
jmp SHORT G_M3386_IG05
;; size=9 bbWeight=0.50 PerfScore 1.25
G_M3386_IG04: ;; offset=0x0033
xor ebx, ebx
;; size=2 bbWeight=0.50 PerfScore 0.12
G_M3386_IG05: ;; offset=0x0035
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, rbx
;; size=12 bbWeight=1 PerfScore 1.75
G_M3386_IG06: ;; offset=0x0041
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 72, prolog size 27, PerfScore 14.71, instruction count 24, allocated bytes for code 72 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

@jakobbotsch PTAL. GT_SWIFT_ERROR_RET now takes the error value as its first operand, and the normal return value as its second operand. I wanted to make the normal return value the first operand to match GT_RETURN, but if the method has a void return type, then it would be possible for the first operand of GT_SWIFT_ERROR_RET to be null -- modeling this in the JIT isn't elegant, so I think it's better to flip the operands, and swap them whenever we can handle GT_RETURN and GT_SWIFT_ERROR_RET nodes simultaneously to reduce code duplication.

@jakobbotsch

Copy link
Copy Markdown
Member

I'll take a closer look tomorrow, but I'm curious why the null op1 didn't work out given that we have some prior art with GenTree::NullOp1Legal. Wasn't that sufficient?

Might be a bit cleaner to introduce something like

GenTree* GetReturnedValue(GenTreeOp* op)
{
assert(op->OperIs(GT_RETURN, GT_RETFILT, GT_SWIFT_ERROR_RET));
return op->OperIs(GT_SWIFT_ERROR_RET) ? op->gtGetOp2() : op->gtGetOp1();
}

Mutating GenTree at that point seems a bit iffy...

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

I'll take a closer look tomorrow, but I'm curious why the null op1 didn't work out given that we have some prior art with GenTree::NullOp1Legal. Wasn't that sufficient?

I initially tried that, but when handling simple operators in a bunch of places, we either assume binops have a first operand, or we assume the lack of a first operand implies the second operand is null as well (like in Compiler::gtSetEvalOrder). NullOp1Legal handles some binops already, but we only call it when handling unops. Perhaps I moved on from trying to match the operand order of GT_RETURN too quickly, though I think your proposed GetReturnedValue would work nicely; right now, I think we'd only need it in a few places.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/gtlist.h Outdated

GTNODE(SWIFT_ERROR , GenTree ,0,0,GTK_LEAF) // Error register value post-Swift call
GTNODE(SWIFT_ERROR , GenTree ,0,0,GTK_LEAF) // Error register value post-Swift call
GTNODE(SWIFT_ERROR_RET , GenTreeOp ,0,1,GTK_BINOP|GTK_NOVALUE) // Returns normal return value, and SwiftError pseudolocal's value in REG_SWIFT_ERROR

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice with a comment that op1 has the error and op2 has the normal return value (or null if method returns void)

Comment threadsrc/coreclr/jit/importer.cpp Outdated
Comment on lines +10887 to +10899
// If this method has a SwiftError* out parameter, we need to set the error register upon returning.
// By placing the GT_SWIFT_ERROR_RET node before the GT_RETURN node,
// we don't have to teach the JIT that GT_SWIFT_ERROR_RET is a valid terminator for BBJ_RETURN blocks.
// During lowering, we will move the GT_SWIFT_ERROR_RET to the end of the block
// so the error register won't get trashed when returning the normal value.
if (lvaSwiftErrorArg != BAD_VAR_NUM)
{
assert(info.compCallConv == CorInfoCallConvExtension::Swift);
assert(lvaSwiftErrorLocal != BAD_VAR_NUM);
GenTree* const swiftErrorNode = gtNewLclFldNode(lvaSwiftErrorLocal, TYP_I_IMPL, 0);
op1 = new (this, GT_SWIFT_ERROR_RET)
GenTreeOp(GT_SWIFT_ERROR_RET, op1->TypeGet(), swiftErrorNode, op1->gtGetOp1());
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is outdated now, right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use op1->SetOper(GT_SWIFT_ERROR_RET) instead here to avoid the new node allocation. Of course that requires some shuffling of the operands, so just allocating a new node is also ok with me, if you like that better feel free to ignore this.

Comment threadsrc/coreclr/jit/lower.cpp Outdated
Comment on lines +4640 to +4648
GenTree* Lowering::LowerSwiftErrorRet(GenTree* swiftErrorRet)
{
assert(swiftErrorRet->OperIs(GT_SWIFT_ERROR_RET));
GenTree* const nextNode = swiftErrorRet->gtNext;
LIR::Range& blockRange = BlockRange();
blockRange.Remove(swiftErrorRet);
blockRange.InsertAtEnd(swiftErrorRet);
return nextNode;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this isn't needed anymore?

Comment on lines +2582 to +2605
for (BasicBlock* block : Blocks())
{
if (block->KindIs(BBJ_RETURN))
{
GenTree* const ret = block->lastNode();
assert(ret->OperIs(GT_RETURN));
ret->SetOperRaw(GT_SWIFT_ERROR_RET);
ret->AsOp()->gtOp2 = ret->AsOp()->gtOp1;

// If this is the merged return block, use the merged return error local as the error operand.
// Else, load the error value from the SwiftError pseudolocal (this will probably get promoted, anyway).
if (block == genReturnBB)
{
assert(genReturnErrorLocal == BAD_VAR_NUM);
genReturnErrorLocal = lvaGrabTemp(true DEBUGARG("Single return block SwiftError value"));
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);
ret->AsOp()->gtOp1 = gtNewLclvNode(genReturnErrorLocal, TYP_I_IMPL);
}
else
{
ret->AsOp()->gtOp1 = gtNewLclFldNode(lvaSwiftErrorLocal, TYP_I_IMPL, 0);
}
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think it would be nice to do the replacement of lvaSwiftErrorArg with lvaSwiftErrorLocal here too.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my plan, though I was hitting asserts around fgNodeThreading being invalid when trying to use any of the GenTree iterators -- LocalsGenTreeList seems like it would be the most efficient one for finding the SwiftError* arg uses. Is there a clean way of doing the IR traversal without breaking any of these iterators' invariants?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point we don't have threaded IR nodes (the comment on fgNodeThreading has some relevant information), but you can use GenTreeVisitor to do it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optReplaceScalarUsesWithConst looks close to what you want to do I think.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optReplaceScalarUsesWithConst looks close to what you want to do I think.

Thanks! GenTreeVisitor works.

Comment threadsrc/coreclr/jit/ifconversion.cpp Outdated
}

case GT_RETURN:
case GT_SWIFT_ERROR_RET:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if-conversion supports GT_RETURN to transform

JTRUE(EQ(x, 0)) -> BB01, BB02
BB01:
RETURN(0)
BB02:
RETURN(1)

into

RETURN(SELECT(EQ(x, 0), 0, 1))

I don't see us being able to support this transform for GT_SWIFT_ERROR_RET, so I think we can't add this support here.

Comment threadsrc/coreclr/jit/morph.cpp Outdated
Comment on lines +14119 to +14121
const DebugInfo& di = lastStmt->GetDebugInfo();
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1(), CHECK_SPILL_NONE, nullptr, di, block);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const DebugInfo& di = lastStmt->GetDebugInfo();
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1(), CHECK_SPILL_NONE, nullptr, di, block);
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1());

(For pAfterStmt == nullptr these last parameters aren't relevant)

Comment on lines +11700 to +11711
case GT_SWIFT_ERROR_RET:
if (tree->gtGetOp2() != nullptr)
{
tree->gtVNPair = vnStore->VNPWithExc(vnStore->VNPForVoid(),
vnStore->VNPExceptionSet(tree->gtGetOp2()->gtVNPair));
}
else
{
tree->gtVNPair = vnStore->VNPForVoid();
}
break;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have the exceptions from the error operand propagated too.

Comment on lines +2597 to +2604
fgWalkResult PreOrderVisit(GenTree** use, GenTree* user)
{
if ((*use)->OperIs(GT_LCL_VAR) && ((*use)->AsLclVarCommon()->GetLclNum() == m_compiler->lvaSwiftErrorArg))
{
*use = m_compiler->gtNewLclVarAddrNode(m_compiler->lvaSwiftErrorLocal, genActualType(*use));
}

return fgWalkResult::WALK_CONTINUE;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we BADCODE here if we see a use of the error parameter that isn't a GT_LCL_VAR? E.g. any store or taking the address of it.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems reasonable to me. Do you know if we have any plans upstream to regulate the usage of the error parameter? I imagine we wouldn't want the JIT to do these checks alone.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. cc @jkoritzinsky@kotlarmilos. Does it seem appropriate to you to put restrictions on what the IL can do with the SwiftError* parameter? Specifically we're interested in disallowing stores and disallowing taking its address.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
{
if (!(*use)->OperIs(GT_LCL_VAR))
{
BADCODE("Expected GT_LCL_VAR of SwiftError* parameter, got other GenTreeLclVarCommon node");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the message should be phrased in terms of IL, not IR constructs

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would something like "Found invalid use of SwiftError* parameter" be too vague?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good to me

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
{
assert(genReturnErrorLocal == BAD_VAR_NUM);
genReturnErrorLocal = lvaGrabTemp(true DEBUGARG("Single return block SwiftError value"));
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);
lvaGetDesc(genReturnErrorLocal)->lvType = TYP_I_IMPL;

Comment threadsrc/coreclr/jit/ifconversion.cpp Outdated
Comment on lines +143 to +152
#ifdef SWIFT_SUPPORT
if (m_comp->lvaSwiftErrorArg != BAD_VAR_NUM)
{
m_mainOper = GT_SWIFT_ERROR_RET;
}
else
#endif // SWIFT_SUPPORT
{
m_mainOper = GT_RETURN;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these changes necessary?
It looks like IfConvertCheckStmts will fail naturally for GT_SWIFT_ERROR_RET, so perhaps no changes in if-conversion are necessary?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, we don't check m_mainOper until after we call IfConvertCheckStmts, and we bail if the terminator node is a GT_SWIFT_ERROR_RET, so it should be safe to undo these changes.

Comment on lines +523 to 527
case GT_SWIFT_ERROR_RET:
LowerNode(node->gtGetOp1());

FALLTHROUGH;
case GT_RETURN:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
caseGT_SWIFT_ERROR_RET:
LowerNode(node->gtGetOp1());
FALLTHROUGH;
caseGT_RETURN:
caseGT_SWIFT_ERROR_RET:
caseGT_RETURN:

Otherwise we invoke lowering of op1 twice.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LowerRet should probably be changed to take GenTreeOp* instead.

tree->SetAllEffectsFlags(tree->AsOp()->gtGetOp1());
tree->SetAllEffectsFlags(retVal);

return tree;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case needs to invoke morph on op1 as well for GT_SWIFT_ERROR_RET (I am not sure why the code returns here in the first place, it probably ought to just fall through after inserting the cast)

Comment threadsrc/coreclr/jit/morph.cpp

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great to me now. Thanks for your diligence in implementing this!

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

This looks great to me now. Thanks for your diligence in implementing this!

Thank you for all the reviews! In case you're curious, here's the updated codegen for ConditionallySetErrorTo21:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rsp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> r12 single-def
; V02 tmp0 [V02 ] ( 1, 1 ) struct ( 8) [rsp+0x10] "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V03 OutArgs [V03 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V04 tmp2 [V04 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
;* V05 tmp3 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp4 [V06 ] ( 3, 3 ) struct (16) [rsp+0x00] do-not-enreg[XS] addr-exposed "Reverse Pinvoke FrameVar"
; V07 tmp5 [V07,T01] ( 2, 4 ) long -> r12 "Single return block SwiftError value"
;* V08 tmp6 [V08 ] ( 0, 0 ) long -> zero-ref "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp7 [V09 ] ( 0, 0 ) long -> zero-ref "field V04.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V10 tmp8 [V10 ] ( 0, 0 ) long -> zero-ref "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
sub rsp, 24
mov r12d, edi
;; size=7 bbWeight=1 PerfScore 0.50
G_M3386_IG02: ;; offset=0x0007
lea rdi, [rsp]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
xor edi, edi
mov eax, 21
test r12d, r12d
mov r12, rax
cmove r12, rdi
lea rdi, [rsp]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
nop
;; size=36 bbWeight=1 PerfScore 4.50
G_M3386_IG03: ;; offset=0x002B
add rsp, 24
ret
;; size=5 bbWeight=1 PerfScore 1.25
; Total bytes of code 48, prolog size 4, PerfScore 6.25, instruction count 14, allocated bytes for code 48 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

Quite the improvement.

@amanasifkhalid
amanasifkhalid merged commit ec76487 into dotnet:mainApr 12, 2024
@amanasifkhalid
amanasifkhalid deleted the swift-error-reg-ret branch April 12, 2024 19:26
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
…return (dotnet#100692)
Follow-up to dotnet#100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 13, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@amanasifkhalid@jakobbotsch
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

JIT: Add GT_SWIFT_ERROR_RET to represent loading error register upon return - #100692

Merged
amanasifkhalid merged 29 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg-ret
Apr 12, 2024
Merged

JIT: Add GT_SWIFT_ERROR_RET to represent loading error register upon return#100692
amanasifkhalid merged 29 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg-ret

Conversation

@amanasifkhalid

Copy link
Copy Markdown
Contributor

Follow-up to #100429. When creating return nodes during importation, if the method has a SwiftError* out parameter, the JIT will create a GT_SWIFT_ERROR_RET node before the regular GT_RETURN node to represent loading the SwiftError pseudolocal's value into the error register. We initially keep this node before the GT_RETURN block so we don't have to teach the JIT that it is a new valid terminator for BBJ_RETURN blocks; during lowering, we move this node to the end of the block so the error register won't get trashed after loading the error value. I found this to be an easier approach than trying to replace GT_RETURN with a binary node that holds the regular and error return values.

Old codegen:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T01] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov qword ptr [rbp-0x10], rdi
;; size=6 bbWeight=0.50 PerfScore 0.62
G_M3386_IG05: ;; offset=0x003C
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x0049
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 80, prolog size 27, PerfScore 16.33, instruction count 25, allocated bytes for code 80 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

New codegen:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 1, 1 ) struct ( 8) [rbp-0x10] must-init "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07 ] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08 ] ( 0, 0 ) long -> zero-ref "field V03.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp6 [V09 ] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov edi, 21
mov r12, rdi
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.38
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov r12, rdi
;; size=5 bbWeight=0.50 PerfScore 0.25
G_M3386_IG05: ;; offset=0x003B
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
nop ;; size=10 bbWeight=1 PerfScore 1.75
G_M3386_IG06: ;; offset=0x0045
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 76, prolog size 27, PerfScore 14.96, instruction count 26, allocated bytes for code 76 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Apr 5, 2024
@jakobbotsch

jakobbotsch commented Apr 5, 2024

Copy link
Copy Markdown
Member

We initially keep this node before the GT_RETURN block so we don't have to teach the JIT that it is a new valid terminator for BBJ_RETURN blocks; during lowering, we move this node to the end of the block so the error register won't get trashed after loading the error value. I found this to be an easier approach than trying to replace GT_RETURN with a binary node that holds the regular and error return values.

I'm not sure it's quite that simple due transforms within the JIT that assume knowledge about GT_RETURN and its semantics, and base transformations on this knowledge. This representation makes it error prone to write transforms around GT_RETURN since you aren't forced to handle this new terminator node. It seems intrinsic to me that the JIT has to be taught that this is a new kind of terminator node because of this.
For example, what happens in a function that has a switch with 50 returns in it? I would expect us to do return merging and end up with IR that doesn't handle the error correctly with resulting potential silent bad codegen.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

For example, what happens in a function that has a switch with 50 returns in it? I would expect us to do return merging and end up with IR that doesn't handle the error correctly with resulting potential silent bad codegen.

I see. In that case, let me try substituting GT_SWIFT_ERROR_RET for GT_RETURN then. In the meantime, let's see how much TP we're trading to get this codegen improvement...

@jakobbotsch

Copy link
Copy Markdown
Member

FWIW I think it's very likely we'll end up similarly having to expand the JIT's set of known and handled terminator nodes as part of async2. It has a similar situation with an async continuation that is returned in a different register than usual. I'm pretty sure the prototype has similar issues around return merging as the one I described above.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

FWIW I think it's very likely we'll end up similarly having to expand the JIT's set of known and handled terminator nodes as part of async2.

This might be too radical, but what if we expanded GT_RETURN to have an optional second argument to handle these scenarios, instead of introducing new nodes for them?

@jakobbotsch

Copy link
Copy Markdown
Member

I'm not sure whether it makes things much simpler -- you still have to audit the interesting uses of GT_RETURN on whether they need special handling of the potential second operand. It also becomes a bit easier to forget to handle the second operand. I like the explicitness of just having new types of nodes. In my experience this kind of explicitness pays off in the long run.

A more uniform representation would be to introduce the ability for the JIT to create new class layouts on the fly. In fact we can already do that for layouts without GC pointers, which would be sufficient for Swift (see typGetBlkLayout). Then you would consider a Swift function with error returns to return a tuple (x, y) where y is the error.
We wouldn't be able to handle something like this efficiently today, but it's sort of on my list in #93105 (the items around supporting FIELD_LIST for returns).

@jakobbotsch

jakobbotsch commented Apr 5, 2024

Copy link
Copy Markdown
Member

I think the uniform representation above would also generalize to GT_CALL with swift error returns where we would similarly create a new layout for the return that defines the error too, allowing us to remove GT_SWIFT_ERROR. But again, this requires a lot of work to handle efficiently -- it's essentially the "allow struct fields of physically promoted call returns to stay in registers" of #93105.
We would also need to generalize the ABI information carried in ReturnTypeDesc so that the frontend can set this up and have the backend automatically handle the details around getting the values out of the corresponding registers (for GT_CALL) or putting them into the right registers (for GT_RETURN).

It's probably a lot of work, but seems like it would be the right long term direction.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

I have a binop implementation of GT_SWIFT_ERROR_RET working locally. There are some quirks that I have to clean up and document before pushing, but the codegen looks slightly better:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 1, 1 ) struct ( 8) [rbp-0x10] must-init "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T02] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
; V08 tmp5 [V08,T01] ( 3, 2 ) long -> rbx "field V03.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp6 [V09,T03] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov ebx, 21
jmp SHORT G_M3386_IG05
;; size=9 bbWeight=0.50 PerfScore 1.25
G_M3386_IG04: ;; offset=0x0033
xor ebx, ebx
;; size=2 bbWeight=0.50 PerfScore 0.12
G_M3386_IG05: ;; offset=0x0035
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, rbx
;; size=12 bbWeight=1 PerfScore 1.75
G_M3386_IG06: ;; offset=0x0041
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 72, prolog size 27, PerfScore 14.71, instruction count 24, allocated bytes for code 72 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

@jakobbotsch PTAL. GT_SWIFT_ERROR_RET now takes the error value as its first operand, and the normal return value as its second operand. I wanted to make the normal return value the first operand to match GT_RETURN, but if the method has a void return type, then it would be possible for the first operand of GT_SWIFT_ERROR_RET to be null -- modeling this in the JIT isn't elegant, so I think it's better to flip the operands, and swap them whenever we can handle GT_RETURN and GT_SWIFT_ERROR_RET nodes simultaneously to reduce code duplication.

@jakobbotsch

Copy link
Copy Markdown
Member

I'll take a closer look tomorrow, but I'm curious why the null op1 didn't work out given that we have some prior art with GenTree::NullOp1Legal. Wasn't that sufficient?

Might be a bit cleaner to introduce something like

GenTree* GetReturnedValue(GenTreeOp* op)
{
assert(op->OperIs(GT_RETURN, GT_RETFILT, GT_SWIFT_ERROR_RET));
return op->OperIs(GT_SWIFT_ERROR_RET) ? op->gtGetOp2() : op->gtGetOp1();
}

Mutating GenTree at that point seems a bit iffy...

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

I'll take a closer look tomorrow, but I'm curious why the null op1 didn't work out given that we have some prior art with GenTree::NullOp1Legal. Wasn't that sufficient?

I initially tried that, but when handling simple operators in a bunch of places, we either assume binops have a first operand, or we assume the lack of a first operand implies the second operand is null as well (like in Compiler::gtSetEvalOrder). NullOp1Legal handles some binops already, but we only call it when handling unops. Perhaps I moved on from trying to match the operand order of GT_RETURN too quickly, though I think your proposed GetReturnedValue would work nicely; right now, I think we'd only need it in a few places.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/gtlist.h Outdated

GTNODE(SWIFT_ERROR , GenTree ,0,0,GTK_LEAF) // Error register value post-Swift call
GTNODE(SWIFT_ERROR , GenTree ,0,0,GTK_LEAF) // Error register value post-Swift call
GTNODE(SWIFT_ERROR_RET , GenTreeOp ,0,1,GTK_BINOP|GTK_NOVALUE) // Returns normal return value, and SwiftError pseudolocal's value in REG_SWIFT_ERROR

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice with a comment that op1 has the error and op2 has the normal return value (or null if method returns void)

Comment threadsrc/coreclr/jit/importer.cpp Outdated
Comment on lines +10887 to +10899
// If this method has a SwiftError* out parameter, we need to set the error register upon returning.
// By placing the GT_SWIFT_ERROR_RET node before the GT_RETURN node,
// we don't have to teach the JIT that GT_SWIFT_ERROR_RET is a valid terminator for BBJ_RETURN blocks.
// During lowering, we will move the GT_SWIFT_ERROR_RET to the end of the block
// so the error register won't get trashed when returning the normal value.
if (lvaSwiftErrorArg != BAD_VAR_NUM)
{
assert(info.compCallConv == CorInfoCallConvExtension::Swift);
assert(lvaSwiftErrorLocal != BAD_VAR_NUM);
GenTree* const swiftErrorNode = gtNewLclFldNode(lvaSwiftErrorLocal, TYP_I_IMPL, 0);
op1 = new (this, GT_SWIFT_ERROR_RET)
GenTreeOp(GT_SWIFT_ERROR_RET, op1->TypeGet(), swiftErrorNode, op1->gtGetOp1());
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is outdated now, right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use op1->SetOper(GT_SWIFT_ERROR_RET) instead here to avoid the new node allocation. Of course that requires some shuffling of the operands, so just allocating a new node is also ok with me, if you like that better feel free to ignore this.

Comment threadsrc/coreclr/jit/lower.cpp Outdated
Comment on lines +4640 to +4648
GenTree* Lowering::LowerSwiftErrorRet(GenTree* swiftErrorRet)
{
assert(swiftErrorRet->OperIs(GT_SWIFT_ERROR_RET));
GenTree* const nextNode = swiftErrorRet->gtNext;
LIR::Range& blockRange = BlockRange();
blockRange.Remove(swiftErrorRet);
blockRange.InsertAtEnd(swiftErrorRet);
return nextNode;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this isn't needed anymore?

Comment on lines +2582 to +2605
for (BasicBlock* block : Blocks())
{
if (block->KindIs(BBJ_RETURN))
{
GenTree* const ret = block->lastNode();
assert(ret->OperIs(GT_RETURN));
ret->SetOperRaw(GT_SWIFT_ERROR_RET);
ret->AsOp()->gtOp2 = ret->AsOp()->gtOp1;

// If this is the merged return block, use the merged return error local as the error operand.
// Else, load the error value from the SwiftError pseudolocal (this will probably get promoted, anyway).
if (block == genReturnBB)
{
assert(genReturnErrorLocal == BAD_VAR_NUM);
genReturnErrorLocal = lvaGrabTemp(true DEBUGARG("Single return block SwiftError value"));
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);
ret->AsOp()->gtOp1 = gtNewLclvNode(genReturnErrorLocal, TYP_I_IMPL);
}
else
{
ret->AsOp()->gtOp1 = gtNewLclFldNode(lvaSwiftErrorLocal, TYP_I_IMPL, 0);
}
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think it would be nice to do the replacement of lvaSwiftErrorArg with lvaSwiftErrorLocal here too.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my plan, though I was hitting asserts around fgNodeThreading being invalid when trying to use any of the GenTree iterators -- LocalsGenTreeList seems like it would be the most efficient one for finding the SwiftError* arg uses. Is there a clean way of doing the IR traversal without breaking any of these iterators' invariants?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point we don't have threaded IR nodes (the comment on fgNodeThreading has some relevant information), but you can use GenTreeVisitor to do it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optReplaceScalarUsesWithConst looks close to what you want to do I think.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optReplaceScalarUsesWithConst looks close to what you want to do I think.

Thanks! GenTreeVisitor works.

Comment threadsrc/coreclr/jit/ifconversion.cpp Outdated
}

case GT_RETURN:
case GT_SWIFT_ERROR_RET:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if-conversion supports GT_RETURN to transform

JTRUE(EQ(x, 0)) -> BB01, BB02
BB01:
RETURN(0)
BB02:
RETURN(1)

into

RETURN(SELECT(EQ(x, 0), 0, 1))

I don't see us being able to support this transform for GT_SWIFT_ERROR_RET, so I think we can't add this support here.

Comment threadsrc/coreclr/jit/morph.cpp Outdated
Comment on lines +14119 to +14121
const DebugInfo& di = lastStmt->GetDebugInfo();
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1(), CHECK_SPILL_NONE, nullptr, di, block);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const DebugInfo& di = lastStmt->GetDebugInfo();
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1(), CHECK_SPILL_NONE, nullptr, di, block);
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1());

(For pAfterStmt == nullptr these last parameters aren't relevant)

Comment on lines +11700 to +11711
case GT_SWIFT_ERROR_RET:
if (tree->gtGetOp2() != nullptr)
{
tree->gtVNPair = vnStore->VNPWithExc(vnStore->VNPForVoid(),
vnStore->VNPExceptionSet(tree->gtGetOp2()->gtVNPair));
}
else
{
tree->gtVNPair = vnStore->VNPForVoid();
}
break;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have the exceptions from the error operand propagated too.

Comment on lines +2597 to +2604
fgWalkResult PreOrderVisit(GenTree** use, GenTree* user)
{
if ((*use)->OperIs(GT_LCL_VAR) && ((*use)->AsLclVarCommon()->GetLclNum() == m_compiler->lvaSwiftErrorArg))
{
*use = m_compiler->gtNewLclVarAddrNode(m_compiler->lvaSwiftErrorLocal, genActualType(*use));
}

return fgWalkResult::WALK_CONTINUE;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we BADCODE here if we see a use of the error parameter that isn't a GT_LCL_VAR? E.g. any store or taking the address of it.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems reasonable to me. Do you know if we have any plans upstream to regulate the usage of the error parameter? I imagine we wouldn't want the JIT to do these checks alone.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. cc @jkoritzinsky@kotlarmilos. Does it seem appropriate to you to put restrictions on what the IL can do with the SwiftError* parameter? Specifically we're interested in disallowing stores and disallowing taking its address.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
{
if (!(*use)->OperIs(GT_LCL_VAR))
{
BADCODE("Expected GT_LCL_VAR of SwiftError* parameter, got other GenTreeLclVarCommon node");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the message should be phrased in terms of IL, not IR constructs

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would something like "Found invalid use of SwiftError* parameter" be too vague?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good to me

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
{
assert(genReturnErrorLocal == BAD_VAR_NUM);
genReturnErrorLocal = lvaGrabTemp(true DEBUGARG("Single return block SwiftError value"));
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);
lvaGetDesc(genReturnErrorLocal)->lvType = TYP_I_IMPL;

Comment threadsrc/coreclr/jit/ifconversion.cpp Outdated
Comment on lines +143 to +152
#ifdef SWIFT_SUPPORT
if (m_comp->lvaSwiftErrorArg != BAD_VAR_NUM)
{
m_mainOper = GT_SWIFT_ERROR_RET;
}
else
#endif // SWIFT_SUPPORT
{
m_mainOper = GT_RETURN;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these changes necessary?
It looks like IfConvertCheckStmts will fail naturally for GT_SWIFT_ERROR_RET, so perhaps no changes in if-conversion are necessary?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, we don't check m_mainOper until after we call IfConvertCheckStmts, and we bail if the terminator node is a GT_SWIFT_ERROR_RET, so it should be safe to undo these changes.

Comment on lines +523 to 527
case GT_SWIFT_ERROR_RET:
LowerNode(node->gtGetOp1());

FALLTHROUGH;
case GT_RETURN:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
caseGT_SWIFT_ERROR_RET:
LowerNode(node->gtGetOp1());
FALLTHROUGH;
caseGT_RETURN:
caseGT_SWIFT_ERROR_RET:
caseGT_RETURN:

Otherwise we invoke lowering of op1 twice.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LowerRet should probably be changed to take GenTreeOp* instead.

tree->SetAllEffectsFlags(tree->AsOp()->gtGetOp1());
tree->SetAllEffectsFlags(retVal);

return tree;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case needs to invoke morph on op1 as well for GT_SWIFT_ERROR_RET (I am not sure why the code returns here in the first place, it probably ought to just fall through after inserting the cast)

Comment threadsrc/coreclr/jit/morph.cpp

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great to me now. Thanks for your diligence in implementing this!

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

This looks great to me now. Thanks for your diligence in implementing this!

Thank you for all the reviews! In case you're curious, here's the updated codegen for ConditionallySetErrorTo21:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rsp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> r12 single-def
; V02 tmp0 [V02 ] ( 1, 1 ) struct ( 8) [rsp+0x10] "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V03 OutArgs [V03 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V04 tmp2 [V04 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
;* V05 tmp3 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp4 [V06 ] ( 3, 3 ) struct (16) [rsp+0x00] do-not-enreg[XS] addr-exposed "Reverse Pinvoke FrameVar"
; V07 tmp5 [V07,T01] ( 2, 4 ) long -> r12 "Single return block SwiftError value"
;* V08 tmp6 [V08 ] ( 0, 0 ) long -> zero-ref "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp7 [V09 ] ( 0, 0 ) long -> zero-ref "field V04.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V10 tmp8 [V10 ] ( 0, 0 ) long -> zero-ref "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
sub rsp, 24
mov r12d, edi
;; size=7 bbWeight=1 PerfScore 0.50
G_M3386_IG02: ;; offset=0x0007
lea rdi, [rsp]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
xor edi, edi
mov eax, 21
test r12d, r12d
mov r12, rax
cmove r12, rdi
lea rdi, [rsp]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
nop
;; size=36 bbWeight=1 PerfScore 4.50
G_M3386_IG03: ;; offset=0x002B
add rsp, 24
ret
;; size=5 bbWeight=1 PerfScore 1.25
; Total bytes of code 48, prolog size 4, PerfScore 6.25, instruction count 14, allocated bytes for code 48 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

Quite the improvement.

@amanasifkhalid
amanasifkhalid merged commit ec76487 into dotnet:mainApr 12, 2024
@amanasifkhalid
amanasifkhalid deleted the swift-error-reg-ret branch April 12, 2024 19:26
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
…return (dotnet#100692)
Follow-up to dotnet#100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 13, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@amanasifkhalid@jakobbotsch
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

JIT: Add GT_SWIFT_ERROR_RET to represent loading error register upon return - #100692

Merged
amanasifkhalid merged 29 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg-ret
Apr 12, 2024
Merged

JIT: Add GT_SWIFT_ERROR_RET to represent loading error register upon return#100692
amanasifkhalid merged 29 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg-ret

Conversation

@amanasifkhalid

Copy link
Copy Markdown
Contributor

Follow-up to #100429. When creating return nodes during importation, if the method has a SwiftError* out parameter, the JIT will create a GT_SWIFT_ERROR_RET node before the regular GT_RETURN node to represent loading the SwiftError pseudolocal's value into the error register. We initially keep this node before the GT_RETURN block so we don't have to teach the JIT that it is a new valid terminator for BBJ_RETURN blocks; during lowering, we move this node to the end of the block so the error register won't get trashed after loading the error value. I found this to be an easier approach than trying to replace GT_RETURN with a binary node that holds the regular and error return values.

Old codegen:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T01] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov qword ptr [rbp-0x10], rdi
;; size=6 bbWeight=0.50 PerfScore 0.62
G_M3386_IG05: ;; offset=0x003C
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x0049
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 80, prolog size 27, PerfScore 16.33, instruction count 25, allocated bytes for code 80 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

New codegen:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 1, 1 ) struct ( 8) [rbp-0x10] must-init "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07 ] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08 ] ( 0, 0 ) long -> zero-ref "field V03.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp6 [V09 ] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov edi, 21
mov r12, rdi
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.38
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov r12, rdi
;; size=5 bbWeight=0.50 PerfScore 0.25
G_M3386_IG05: ;; offset=0x003B
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
nop ;; size=10 bbWeight=1 PerfScore 1.75
G_M3386_IG06: ;; offset=0x0045
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 76, prolog size 27, PerfScore 14.96, instruction count 26, allocated bytes for code 76 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Apr 5, 2024
@jakobbotsch

jakobbotsch commented Apr 5, 2024

Copy link
Copy Markdown
Member

We initially keep this node before the GT_RETURN block so we don't have to teach the JIT that it is a new valid terminator for BBJ_RETURN blocks; during lowering, we move this node to the end of the block so the error register won't get trashed after loading the error value. I found this to be an easier approach than trying to replace GT_RETURN with a binary node that holds the regular and error return values.

I'm not sure it's quite that simple due transforms within the JIT that assume knowledge about GT_RETURN and its semantics, and base transformations on this knowledge. This representation makes it error prone to write transforms around GT_RETURN since you aren't forced to handle this new terminator node. It seems intrinsic to me that the JIT has to be taught that this is a new kind of terminator node because of this.
For example, what happens in a function that has a switch with 50 returns in it? I would expect us to do return merging and end up with IR that doesn't handle the error correctly with resulting potential silent bad codegen.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

For example, what happens in a function that has a switch with 50 returns in it? I would expect us to do return merging and end up with IR that doesn't handle the error correctly with resulting potential silent bad codegen.

I see. In that case, let me try substituting GT_SWIFT_ERROR_RET for GT_RETURN then. In the meantime, let's see how much TP we're trading to get this codegen improvement...

@jakobbotsch

Copy link
Copy Markdown
Member

FWIW I think it's very likely we'll end up similarly having to expand the JIT's set of known and handled terminator nodes as part of async2. It has a similar situation with an async continuation that is returned in a different register than usual. I'm pretty sure the prototype has similar issues around return merging as the one I described above.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

FWIW I think it's very likely we'll end up similarly having to expand the JIT's set of known and handled terminator nodes as part of async2.

This might be too radical, but what if we expanded GT_RETURN to have an optional second argument to handle these scenarios, instead of introducing new nodes for them?

@jakobbotsch

Copy link
Copy Markdown
Member

I'm not sure whether it makes things much simpler -- you still have to audit the interesting uses of GT_RETURN on whether they need special handling of the potential second operand. It also becomes a bit easier to forget to handle the second operand. I like the explicitness of just having new types of nodes. In my experience this kind of explicitness pays off in the long run.

A more uniform representation would be to introduce the ability for the JIT to create new class layouts on the fly. In fact we can already do that for layouts without GC pointers, which would be sufficient for Swift (see typGetBlkLayout). Then you would consider a Swift function with error returns to return a tuple (x, y) where y is the error.
We wouldn't be able to handle something like this efficiently today, but it's sort of on my list in #93105 (the items around supporting FIELD_LIST for returns).

@jakobbotsch

jakobbotsch commented Apr 5, 2024

Copy link
Copy Markdown
Member

I think the uniform representation above would also generalize to GT_CALL with swift error returns where we would similarly create a new layout for the return that defines the error too, allowing us to remove GT_SWIFT_ERROR. But again, this requires a lot of work to handle efficiently -- it's essentially the "allow struct fields of physically promoted call returns to stay in registers" of #93105.
We would also need to generalize the ABI information carried in ReturnTypeDesc so that the frontend can set this up and have the backend automatically handle the details around getting the values out of the corresponding registers (for GT_CALL) or putting them into the right registers (for GT_RETURN).

It's probably a lot of work, but seems like it would be the right long term direction.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

I have a binop implementation of GT_SWIFT_ERROR_RET working locally. There are some quirks that I have to clean up and document before pushing, but the codegen looks slightly better:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 1, 1 ) struct ( 8) [rbp-0x10] must-init "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T02] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
; V08 tmp5 [V08,T01] ( 3, 2 ) long -> rbx "field V03.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp6 [V09,T03] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov ebx, 21
jmp SHORT G_M3386_IG05
;; size=9 bbWeight=0.50 PerfScore 1.25
G_M3386_IG04: ;; offset=0x0033
xor ebx, ebx
;; size=2 bbWeight=0.50 PerfScore 0.12
G_M3386_IG05: ;; offset=0x0035
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, rbx
;; size=12 bbWeight=1 PerfScore 1.75
G_M3386_IG06: ;; offset=0x0041
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 72, prolog size 27, PerfScore 14.71, instruction count 24, allocated bytes for code 72 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

@jakobbotsch PTAL. GT_SWIFT_ERROR_RET now takes the error value as its first operand, and the normal return value as its second operand. I wanted to make the normal return value the first operand to match GT_RETURN, but if the method has a void return type, then it would be possible for the first operand of GT_SWIFT_ERROR_RET to be null -- modeling this in the JIT isn't elegant, so I think it's better to flip the operands, and swap them whenever we can handle GT_RETURN and GT_SWIFT_ERROR_RET nodes simultaneously to reduce code duplication.

@jakobbotsch

Copy link
Copy Markdown
Member

I'll take a closer look tomorrow, but I'm curious why the null op1 didn't work out given that we have some prior art with GenTree::NullOp1Legal. Wasn't that sufficient?

Might be a bit cleaner to introduce something like

GenTree* GetReturnedValue(GenTreeOp* op)
{
assert(op->OperIs(GT_RETURN, GT_RETFILT, GT_SWIFT_ERROR_RET));
return op->OperIs(GT_SWIFT_ERROR_RET) ? op->gtGetOp2() : op->gtGetOp1();
}

Mutating GenTree at that point seems a bit iffy...

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

I'll take a closer look tomorrow, but I'm curious why the null op1 didn't work out given that we have some prior art with GenTree::NullOp1Legal. Wasn't that sufficient?

I initially tried that, but when handling simple operators in a bunch of places, we either assume binops have a first operand, or we assume the lack of a first operand implies the second operand is null as well (like in Compiler::gtSetEvalOrder). NullOp1Legal handles some binops already, but we only call it when handling unops. Perhaps I moved on from trying to match the operand order of GT_RETURN too quickly, though I think your proposed GetReturnedValue would work nicely; right now, I think we'd only need it in a few places.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/gtlist.h Outdated

GTNODE(SWIFT_ERROR , GenTree ,0,0,GTK_LEAF) // Error register value post-Swift call
GTNODE(SWIFT_ERROR , GenTree ,0,0,GTK_LEAF) // Error register value post-Swift call
GTNODE(SWIFT_ERROR_RET , GenTreeOp ,0,1,GTK_BINOP|GTK_NOVALUE) // Returns normal return value, and SwiftError pseudolocal's value in REG_SWIFT_ERROR

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice with a comment that op1 has the error and op2 has the normal return value (or null if method returns void)

Comment threadsrc/coreclr/jit/importer.cpp Outdated
Comment on lines +10887 to +10899
// If this method has a SwiftError* out parameter, we need to set the error register upon returning.
// By placing the GT_SWIFT_ERROR_RET node before the GT_RETURN node,
// we don't have to teach the JIT that GT_SWIFT_ERROR_RET is a valid terminator for BBJ_RETURN blocks.
// During lowering, we will move the GT_SWIFT_ERROR_RET to the end of the block
// so the error register won't get trashed when returning the normal value.
if (lvaSwiftErrorArg != BAD_VAR_NUM)
{
assert(info.compCallConv == CorInfoCallConvExtension::Swift);
assert(lvaSwiftErrorLocal != BAD_VAR_NUM);
GenTree* const swiftErrorNode = gtNewLclFldNode(lvaSwiftErrorLocal, TYP_I_IMPL, 0);
op1 = new (this, GT_SWIFT_ERROR_RET)
GenTreeOp(GT_SWIFT_ERROR_RET, op1->TypeGet(), swiftErrorNode, op1->gtGetOp1());
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is outdated now, right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use op1->SetOper(GT_SWIFT_ERROR_RET) instead here to avoid the new node allocation. Of course that requires some shuffling of the operands, so just allocating a new node is also ok with me, if you like that better feel free to ignore this.

Comment threadsrc/coreclr/jit/lower.cpp Outdated
Comment on lines +4640 to +4648
GenTree* Lowering::LowerSwiftErrorRet(GenTree* swiftErrorRet)
{
assert(swiftErrorRet->OperIs(GT_SWIFT_ERROR_RET));
GenTree* const nextNode = swiftErrorRet->gtNext;
LIR::Range& blockRange = BlockRange();
blockRange.Remove(swiftErrorRet);
blockRange.InsertAtEnd(swiftErrorRet);
return nextNode;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this isn't needed anymore?

Comment on lines +2582 to +2605
for (BasicBlock* block : Blocks())
{
if (block->KindIs(BBJ_RETURN))
{
GenTree* const ret = block->lastNode();
assert(ret->OperIs(GT_RETURN));
ret->SetOperRaw(GT_SWIFT_ERROR_RET);
ret->AsOp()->gtOp2 = ret->AsOp()->gtOp1;

// If this is the merged return block, use the merged return error local as the error operand.
// Else, load the error value from the SwiftError pseudolocal (this will probably get promoted, anyway).
if (block == genReturnBB)
{
assert(genReturnErrorLocal == BAD_VAR_NUM);
genReturnErrorLocal = lvaGrabTemp(true DEBUGARG("Single return block SwiftError value"));
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);
ret->AsOp()->gtOp1 = gtNewLclvNode(genReturnErrorLocal, TYP_I_IMPL);
}
else
{
ret->AsOp()->gtOp1 = gtNewLclFldNode(lvaSwiftErrorLocal, TYP_I_IMPL, 0);
}
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think it would be nice to do the replacement of lvaSwiftErrorArg with lvaSwiftErrorLocal here too.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my plan, though I was hitting asserts around fgNodeThreading being invalid when trying to use any of the GenTree iterators -- LocalsGenTreeList seems like it would be the most efficient one for finding the SwiftError* arg uses. Is there a clean way of doing the IR traversal without breaking any of these iterators' invariants?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point we don't have threaded IR nodes (the comment on fgNodeThreading has some relevant information), but you can use GenTreeVisitor to do it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optReplaceScalarUsesWithConst looks close to what you want to do I think.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optReplaceScalarUsesWithConst looks close to what you want to do I think.

Thanks! GenTreeVisitor works.

Comment threadsrc/coreclr/jit/ifconversion.cpp Outdated
}

case GT_RETURN:
case GT_SWIFT_ERROR_RET:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if-conversion supports GT_RETURN to transform

JTRUE(EQ(x, 0)) -> BB01, BB02
BB01:
RETURN(0)
BB02:
RETURN(1)

into

RETURN(SELECT(EQ(x, 0), 0, 1))

I don't see us being able to support this transform for GT_SWIFT_ERROR_RET, so I think we can't add this support here.

Comment threadsrc/coreclr/jit/morph.cpp Outdated
Comment on lines +14119 to +14121
const DebugInfo& di = lastStmt->GetDebugInfo();
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1(), CHECK_SPILL_NONE, nullptr, di, block);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const DebugInfo& di = lastStmt->GetDebugInfo();
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1(), CHECK_SPILL_NONE, nullptr, di, block);
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1());

(For pAfterStmt == nullptr these last parameters aren't relevant)

Comment on lines +11700 to +11711
case GT_SWIFT_ERROR_RET:
if (tree->gtGetOp2() != nullptr)
{
tree->gtVNPair = vnStore->VNPWithExc(vnStore->VNPForVoid(),
vnStore->VNPExceptionSet(tree->gtGetOp2()->gtVNPair));
}
else
{
tree->gtVNPair = vnStore->VNPForVoid();
}
break;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have the exceptions from the error operand propagated too.

Comment on lines +2597 to +2604
fgWalkResult PreOrderVisit(GenTree** use, GenTree* user)
{
if ((*use)->OperIs(GT_LCL_VAR) && ((*use)->AsLclVarCommon()->GetLclNum() == m_compiler->lvaSwiftErrorArg))
{
*use = m_compiler->gtNewLclVarAddrNode(m_compiler->lvaSwiftErrorLocal, genActualType(*use));
}

return fgWalkResult::WALK_CONTINUE;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we BADCODE here if we see a use of the error parameter that isn't a GT_LCL_VAR? E.g. any store or taking the address of it.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems reasonable to me. Do you know if we have any plans upstream to regulate the usage of the error parameter? I imagine we wouldn't want the JIT to do these checks alone.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. cc @jkoritzinsky@kotlarmilos. Does it seem appropriate to you to put restrictions on what the IL can do with the SwiftError* parameter? Specifically we're interested in disallowing stores and disallowing taking its address.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
{
if (!(*use)->OperIs(GT_LCL_VAR))
{
BADCODE("Expected GT_LCL_VAR of SwiftError* parameter, got other GenTreeLclVarCommon node");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the message should be phrased in terms of IL, not IR constructs

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would something like "Found invalid use of SwiftError* parameter" be too vague?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good to me

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
{
assert(genReturnErrorLocal == BAD_VAR_NUM);
genReturnErrorLocal = lvaGrabTemp(true DEBUGARG("Single return block SwiftError value"));
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);
lvaGetDesc(genReturnErrorLocal)->lvType = TYP_I_IMPL;

Comment threadsrc/coreclr/jit/ifconversion.cpp Outdated
Comment on lines +143 to +152
#ifdef SWIFT_SUPPORT
if (m_comp->lvaSwiftErrorArg != BAD_VAR_NUM)
{
m_mainOper = GT_SWIFT_ERROR_RET;
}
else
#endif // SWIFT_SUPPORT
{
m_mainOper = GT_RETURN;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these changes necessary?
It looks like IfConvertCheckStmts will fail naturally for GT_SWIFT_ERROR_RET, so perhaps no changes in if-conversion are necessary?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, we don't check m_mainOper until after we call IfConvertCheckStmts, and we bail if the terminator node is a GT_SWIFT_ERROR_RET, so it should be safe to undo these changes.

Comment on lines +523 to 527
case GT_SWIFT_ERROR_RET:
LowerNode(node->gtGetOp1());

FALLTHROUGH;
case GT_RETURN:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
caseGT_SWIFT_ERROR_RET:
LowerNode(node->gtGetOp1());
FALLTHROUGH;
caseGT_RETURN:
caseGT_SWIFT_ERROR_RET:
caseGT_RETURN:

Otherwise we invoke lowering of op1 twice.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LowerRet should probably be changed to take GenTreeOp* instead.

tree->SetAllEffectsFlags(tree->AsOp()->gtGetOp1());
tree->SetAllEffectsFlags(retVal);

return tree;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case needs to invoke morph on op1 as well for GT_SWIFT_ERROR_RET (I am not sure why the code returns here in the first place, it probably ought to just fall through after inserting the cast)

Comment threadsrc/coreclr/jit/morph.cpp

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great to me now. Thanks for your diligence in implementing this!

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

This looks great to me now. Thanks for your diligence in implementing this!

Thank you for all the reviews! In case you're curious, here's the updated codegen for ConditionallySetErrorTo21:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rsp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> r12 single-def
; V02 tmp0 [V02 ] ( 1, 1 ) struct ( 8) [rsp+0x10] "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V03 OutArgs [V03 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V04 tmp2 [V04 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
;* V05 tmp3 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp4 [V06 ] ( 3, 3 ) struct (16) [rsp+0x00] do-not-enreg[XS] addr-exposed "Reverse Pinvoke FrameVar"
; V07 tmp5 [V07,T01] ( 2, 4 ) long -> r12 "Single return block SwiftError value"
;* V08 tmp6 [V08 ] ( 0, 0 ) long -> zero-ref "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp7 [V09 ] ( 0, 0 ) long -> zero-ref "field V04.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V10 tmp8 [V10 ] ( 0, 0 ) long -> zero-ref "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
sub rsp, 24
mov r12d, edi
;; size=7 bbWeight=1 PerfScore 0.50
G_M3386_IG02: ;; offset=0x0007
lea rdi, [rsp]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
xor edi, edi
mov eax, 21
test r12d, r12d
mov r12, rax
cmove r12, rdi
lea rdi, [rsp]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
nop
;; size=36 bbWeight=1 PerfScore 4.50
G_M3386_IG03: ;; offset=0x002B
add rsp, 24
ret
;; size=5 bbWeight=1 PerfScore 1.25
; Total bytes of code 48, prolog size 4, PerfScore 6.25, instruction count 14, allocated bytes for code 48 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

Quite the improvement.

@amanasifkhalid
amanasifkhalid merged commit ec76487 into dotnet:mainApr 12, 2024
@amanasifkhalid
amanasifkhalid deleted the swift-error-reg-ret branch April 12, 2024 19:26
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
…return (dotnet#100692)
Follow-up to dotnet#100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 13, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@amanasifkhalid@jakobbotsch
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

JIT: Add GT_SWIFT_ERROR_RET to represent loading error register upon return - #100692

Merged
amanasifkhalid merged 29 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg-ret
Apr 12, 2024
Merged

JIT: Add GT_SWIFT_ERROR_RET to represent loading error register upon return#100692
amanasifkhalid merged 29 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg-ret

Conversation

@amanasifkhalid

Copy link
Copy Markdown
Contributor

Follow-up to #100429. When creating return nodes during importation, if the method has a SwiftError* out parameter, the JIT will create a GT_SWIFT_ERROR_RET node before the regular GT_RETURN node to represent loading the SwiftError pseudolocal's value into the error register. We initially keep this node before the GT_RETURN block so we don't have to teach the JIT that it is a new valid terminator for BBJ_RETURN blocks; during lowering, we move this node to the end of the block so the error register won't get trashed after loading the error value. I found this to be an easier approach than trying to replace GT_RETURN with a binary node that holds the regular and error return values.

Old codegen:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T01] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov qword ptr [rbp-0x10], rdi
;; size=6 bbWeight=0.50 PerfScore 0.62
G_M3386_IG05: ;; offset=0x003C
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x0049
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 80, prolog size 27, PerfScore 16.33, instruction count 25, allocated bytes for code 80 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

New codegen:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 1, 1 ) struct ( 8) [rbp-0x10] must-init "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07 ] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08 ] ( 0, 0 ) long -> zero-ref "field V03.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp6 [V09 ] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov edi, 21
mov r12, rdi
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.38
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov r12, rdi
;; size=5 bbWeight=0.50 PerfScore 0.25
G_M3386_IG05: ;; offset=0x003B
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
nop ;; size=10 bbWeight=1 PerfScore 1.75
G_M3386_IG06: ;; offset=0x0045
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 76, prolog size 27, PerfScore 14.96, instruction count 26, allocated bytes for code 76 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Apr 5, 2024
@jakobbotsch

jakobbotsch commented Apr 5, 2024

Copy link
Copy Markdown
Member

We initially keep this node before the GT_RETURN block so we don't have to teach the JIT that it is a new valid terminator for BBJ_RETURN blocks; during lowering, we move this node to the end of the block so the error register won't get trashed after loading the error value. I found this to be an easier approach than trying to replace GT_RETURN with a binary node that holds the regular and error return values.

I'm not sure it's quite that simple due transforms within the JIT that assume knowledge about GT_RETURN and its semantics, and base transformations on this knowledge. This representation makes it error prone to write transforms around GT_RETURN since you aren't forced to handle this new terminator node. It seems intrinsic to me that the JIT has to be taught that this is a new kind of terminator node because of this.
For example, what happens in a function that has a switch with 50 returns in it? I would expect us to do return merging and end up with IR that doesn't handle the error correctly with resulting potential silent bad codegen.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

For example, what happens in a function that has a switch with 50 returns in it? I would expect us to do return merging and end up with IR that doesn't handle the error correctly with resulting potential silent bad codegen.

I see. In that case, let me try substituting GT_SWIFT_ERROR_RET for GT_RETURN then. In the meantime, let's see how much TP we're trading to get this codegen improvement...

@jakobbotsch

Copy link
Copy Markdown
Member

FWIW I think it's very likely we'll end up similarly having to expand the JIT's set of known and handled terminator nodes as part of async2. It has a similar situation with an async continuation that is returned in a different register than usual. I'm pretty sure the prototype has similar issues around return merging as the one I described above.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

FWIW I think it's very likely we'll end up similarly having to expand the JIT's set of known and handled terminator nodes as part of async2.

This might be too radical, but what if we expanded GT_RETURN to have an optional second argument to handle these scenarios, instead of introducing new nodes for them?

@jakobbotsch

Copy link
Copy Markdown
Member

I'm not sure whether it makes things much simpler -- you still have to audit the interesting uses of GT_RETURN on whether they need special handling of the potential second operand. It also becomes a bit easier to forget to handle the second operand. I like the explicitness of just having new types of nodes. In my experience this kind of explicitness pays off in the long run.

A more uniform representation would be to introduce the ability for the JIT to create new class layouts on the fly. In fact we can already do that for layouts without GC pointers, which would be sufficient for Swift (see typGetBlkLayout). Then you would consider a Swift function with error returns to return a tuple (x, y) where y is the error.
We wouldn't be able to handle something like this efficiently today, but it's sort of on my list in #93105 (the items around supporting FIELD_LIST for returns).

@jakobbotsch

jakobbotsch commented Apr 5, 2024

Copy link
Copy Markdown
Member

I think the uniform representation above would also generalize to GT_CALL with swift error returns where we would similarly create a new layout for the return that defines the error too, allowing us to remove GT_SWIFT_ERROR. But again, this requires a lot of work to handle efficiently -- it's essentially the "allow struct fields of physically promoted call returns to stay in registers" of #93105.
We would also need to generalize the ABI information carried in ReturnTypeDesc so that the frontend can set this up and have the backend automatically handle the details around getting the values out of the corresponding registers (for GT_CALL) or putting them into the right registers (for GT_RETURN).

It's probably a lot of work, but seems like it would be the right long term direction.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

I have a binop implementation of GT_SWIFT_ERROR_RET working locally. There are some quirks that I have to clean up and document before pushing, but the codegen looks slightly better:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 1, 1 ) struct ( 8) [rbp-0x10] must-init "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T02] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
; V08 tmp5 [V08,T01] ( 3, 2 ) long -> rbx "field V03.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp6 [V09,T03] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov ebx, 21
jmp SHORT G_M3386_IG05
;; size=9 bbWeight=0.50 PerfScore 1.25
G_M3386_IG04: ;; offset=0x0033
xor ebx, ebx
;; size=2 bbWeight=0.50 PerfScore 0.12
G_M3386_IG05: ;; offset=0x0035
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, rbx
;; size=12 bbWeight=1 PerfScore 1.75
G_M3386_IG06: ;; offset=0x0041
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 72, prolog size 27, PerfScore 14.71, instruction count 24, allocated bytes for code 72 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

@jakobbotsch PTAL. GT_SWIFT_ERROR_RET now takes the error value as its first operand, and the normal return value as its second operand. I wanted to make the normal return value the first operand to match GT_RETURN, but if the method has a void return type, then it would be possible for the first operand of GT_SWIFT_ERROR_RET to be null -- modeling this in the JIT isn't elegant, so I think it's better to flip the operands, and swap them whenever we can handle GT_RETURN and GT_SWIFT_ERROR_RET nodes simultaneously to reduce code duplication.

@jakobbotsch

Copy link
Copy Markdown
Member

I'll take a closer look tomorrow, but I'm curious why the null op1 didn't work out given that we have some prior art with GenTree::NullOp1Legal. Wasn't that sufficient?

Might be a bit cleaner to introduce something like

GenTree* GetReturnedValue(GenTreeOp* op)
{
assert(op->OperIs(GT_RETURN, GT_RETFILT, GT_SWIFT_ERROR_RET));
return op->OperIs(GT_SWIFT_ERROR_RET) ? op->gtGetOp2() : op->gtGetOp1();
}

Mutating GenTree at that point seems a bit iffy...

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

I'll take a closer look tomorrow, but I'm curious why the null op1 didn't work out given that we have some prior art with GenTree::NullOp1Legal. Wasn't that sufficient?

I initially tried that, but when handling simple operators in a bunch of places, we either assume binops have a first operand, or we assume the lack of a first operand implies the second operand is null as well (like in Compiler::gtSetEvalOrder). NullOp1Legal handles some binops already, but we only call it when handling unops. Perhaps I moved on from trying to match the operand order of GT_RETURN too quickly, though I think your proposed GetReturnedValue would work nicely; right now, I think we'd only need it in a few places.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/gtlist.h Outdated

GTNODE(SWIFT_ERROR , GenTree ,0,0,GTK_LEAF) // Error register value post-Swift call
GTNODE(SWIFT_ERROR , GenTree ,0,0,GTK_LEAF) // Error register value post-Swift call
GTNODE(SWIFT_ERROR_RET , GenTreeOp ,0,1,GTK_BINOP|GTK_NOVALUE) // Returns normal return value, and SwiftError pseudolocal's value in REG_SWIFT_ERROR

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice with a comment that op1 has the error and op2 has the normal return value (or null if method returns void)

Comment threadsrc/coreclr/jit/importer.cpp Outdated
Comment on lines +10887 to +10899
// If this method has a SwiftError* out parameter, we need to set the error register upon returning.
// By placing the GT_SWIFT_ERROR_RET node before the GT_RETURN node,
// we don't have to teach the JIT that GT_SWIFT_ERROR_RET is a valid terminator for BBJ_RETURN blocks.
// During lowering, we will move the GT_SWIFT_ERROR_RET to the end of the block
// so the error register won't get trashed when returning the normal value.
if (lvaSwiftErrorArg != BAD_VAR_NUM)
{
assert(info.compCallConv == CorInfoCallConvExtension::Swift);
assert(lvaSwiftErrorLocal != BAD_VAR_NUM);
GenTree* const swiftErrorNode = gtNewLclFldNode(lvaSwiftErrorLocal, TYP_I_IMPL, 0);
op1 = new (this, GT_SWIFT_ERROR_RET)
GenTreeOp(GT_SWIFT_ERROR_RET, op1->TypeGet(), swiftErrorNode, op1->gtGetOp1());
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is outdated now, right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use op1->SetOper(GT_SWIFT_ERROR_RET) instead here to avoid the new node allocation. Of course that requires some shuffling of the operands, so just allocating a new node is also ok with me, if you like that better feel free to ignore this.

Comment threadsrc/coreclr/jit/lower.cpp Outdated
Comment on lines +4640 to +4648
GenTree* Lowering::LowerSwiftErrorRet(GenTree* swiftErrorRet)
{
assert(swiftErrorRet->OperIs(GT_SWIFT_ERROR_RET));
GenTree* const nextNode = swiftErrorRet->gtNext;
LIR::Range& blockRange = BlockRange();
blockRange.Remove(swiftErrorRet);
blockRange.InsertAtEnd(swiftErrorRet);
return nextNode;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this isn't needed anymore?

Comment on lines +2582 to +2605
for (BasicBlock* block : Blocks())
{
if (block->KindIs(BBJ_RETURN))
{
GenTree* const ret = block->lastNode();
assert(ret->OperIs(GT_RETURN));
ret->SetOperRaw(GT_SWIFT_ERROR_RET);
ret->AsOp()->gtOp2 = ret->AsOp()->gtOp1;

// If this is the merged return block, use the merged return error local as the error operand.
// Else, load the error value from the SwiftError pseudolocal (this will probably get promoted, anyway).
if (block == genReturnBB)
{
assert(genReturnErrorLocal == BAD_VAR_NUM);
genReturnErrorLocal = lvaGrabTemp(true DEBUGARG("Single return block SwiftError value"));
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);
ret->AsOp()->gtOp1 = gtNewLclvNode(genReturnErrorLocal, TYP_I_IMPL);
}
else
{
ret->AsOp()->gtOp1 = gtNewLclFldNode(lvaSwiftErrorLocal, TYP_I_IMPL, 0);
}
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think it would be nice to do the replacement of lvaSwiftErrorArg with lvaSwiftErrorLocal here too.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my plan, though I was hitting asserts around fgNodeThreading being invalid when trying to use any of the GenTree iterators -- LocalsGenTreeList seems like it would be the most efficient one for finding the SwiftError* arg uses. Is there a clean way of doing the IR traversal without breaking any of these iterators' invariants?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point we don't have threaded IR nodes (the comment on fgNodeThreading has some relevant information), but you can use GenTreeVisitor to do it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optReplaceScalarUsesWithConst looks close to what you want to do I think.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optReplaceScalarUsesWithConst looks close to what you want to do I think.

Thanks! GenTreeVisitor works.

Comment threadsrc/coreclr/jit/ifconversion.cpp Outdated
}

case GT_RETURN:
case GT_SWIFT_ERROR_RET:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if-conversion supports GT_RETURN to transform

JTRUE(EQ(x, 0)) -> BB01, BB02
BB01:
RETURN(0)
BB02:
RETURN(1)

into

RETURN(SELECT(EQ(x, 0), 0, 1))

I don't see us being able to support this transform for GT_SWIFT_ERROR_RET, so I think we can't add this support here.

Comment threadsrc/coreclr/jit/morph.cpp Outdated
Comment on lines +14119 to +14121
const DebugInfo& di = lastStmt->GetDebugInfo();
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1(), CHECK_SPILL_NONE, nullptr, di, block);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const DebugInfo& di = lastStmt->GetDebugInfo();
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1(), CHECK_SPILL_NONE, nullptr, di, block);
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1());

(For pAfterStmt == nullptr these last parameters aren't relevant)

Comment on lines +11700 to +11711
case GT_SWIFT_ERROR_RET:
if (tree->gtGetOp2() != nullptr)
{
tree->gtVNPair = vnStore->VNPWithExc(vnStore->VNPForVoid(),
vnStore->VNPExceptionSet(tree->gtGetOp2()->gtVNPair));
}
else
{
tree->gtVNPair = vnStore->VNPForVoid();
}
break;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have the exceptions from the error operand propagated too.

Comment on lines +2597 to +2604
fgWalkResult PreOrderVisit(GenTree** use, GenTree* user)
{
if ((*use)->OperIs(GT_LCL_VAR) && ((*use)->AsLclVarCommon()->GetLclNum() == m_compiler->lvaSwiftErrorArg))
{
*use = m_compiler->gtNewLclVarAddrNode(m_compiler->lvaSwiftErrorLocal, genActualType(*use));
}

return fgWalkResult::WALK_CONTINUE;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we BADCODE here if we see a use of the error parameter that isn't a GT_LCL_VAR? E.g. any store or taking the address of it.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems reasonable to me. Do you know if we have any plans upstream to regulate the usage of the error parameter? I imagine we wouldn't want the JIT to do these checks alone.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. cc @jkoritzinsky@kotlarmilos. Does it seem appropriate to you to put restrictions on what the IL can do with the SwiftError* parameter? Specifically we're interested in disallowing stores and disallowing taking its address.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
{
if (!(*use)->OperIs(GT_LCL_VAR))
{
BADCODE("Expected GT_LCL_VAR of SwiftError* parameter, got other GenTreeLclVarCommon node");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the message should be phrased in terms of IL, not IR constructs

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would something like "Found invalid use of SwiftError* parameter" be too vague?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good to me

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
{
assert(genReturnErrorLocal == BAD_VAR_NUM);
genReturnErrorLocal = lvaGrabTemp(true DEBUGARG("Single return block SwiftError value"));
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);
lvaGetDesc(genReturnErrorLocal)->lvType = TYP_I_IMPL;

Comment threadsrc/coreclr/jit/ifconversion.cpp Outdated
Comment on lines +143 to +152
#ifdef SWIFT_SUPPORT
if (m_comp->lvaSwiftErrorArg != BAD_VAR_NUM)
{
m_mainOper = GT_SWIFT_ERROR_RET;
}
else
#endif // SWIFT_SUPPORT
{
m_mainOper = GT_RETURN;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these changes necessary?
It looks like IfConvertCheckStmts will fail naturally for GT_SWIFT_ERROR_RET, so perhaps no changes in if-conversion are necessary?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, we don't check m_mainOper until after we call IfConvertCheckStmts, and we bail if the terminator node is a GT_SWIFT_ERROR_RET, so it should be safe to undo these changes.

Comment on lines +523 to 527
case GT_SWIFT_ERROR_RET:
LowerNode(node->gtGetOp1());

FALLTHROUGH;
case GT_RETURN:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
caseGT_SWIFT_ERROR_RET:
LowerNode(node->gtGetOp1());
FALLTHROUGH;
caseGT_RETURN:
caseGT_SWIFT_ERROR_RET:
caseGT_RETURN:

Otherwise we invoke lowering of op1 twice.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LowerRet should probably be changed to take GenTreeOp* instead.

tree->SetAllEffectsFlags(tree->AsOp()->gtGetOp1());
tree->SetAllEffectsFlags(retVal);

return tree;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case needs to invoke morph on op1 as well for GT_SWIFT_ERROR_RET (I am not sure why the code returns here in the first place, it probably ought to just fall through after inserting the cast)

Comment threadsrc/coreclr/jit/morph.cpp

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great to me now. Thanks for your diligence in implementing this!

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

This looks great to me now. Thanks for your diligence in implementing this!

Thank you for all the reviews! In case you're curious, here's the updated codegen for ConditionallySetErrorTo21:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rsp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> r12 single-def
; V02 tmp0 [V02 ] ( 1, 1 ) struct ( 8) [rsp+0x10] "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V03 OutArgs [V03 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V04 tmp2 [V04 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
;* V05 tmp3 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp4 [V06 ] ( 3, 3 ) struct (16) [rsp+0x00] do-not-enreg[XS] addr-exposed "Reverse Pinvoke FrameVar"
; V07 tmp5 [V07,T01] ( 2, 4 ) long -> r12 "Single return block SwiftError value"
;* V08 tmp6 [V08 ] ( 0, 0 ) long -> zero-ref "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp7 [V09 ] ( 0, 0 ) long -> zero-ref "field V04.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V10 tmp8 [V10 ] ( 0, 0 ) long -> zero-ref "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
sub rsp, 24
mov r12d, edi
;; size=7 bbWeight=1 PerfScore 0.50
G_M3386_IG02: ;; offset=0x0007
lea rdi, [rsp]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
xor edi, edi
mov eax, 21
test r12d, r12d
mov r12, rax
cmove r12, rdi
lea rdi, [rsp]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
nop
;; size=36 bbWeight=1 PerfScore 4.50
G_M3386_IG03: ;; offset=0x002B
add rsp, 24
ret
;; size=5 bbWeight=1 PerfScore 1.25
; Total bytes of code 48, prolog size 4, PerfScore 6.25, instruction count 14, allocated bytes for code 48 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

Quite the improvement.

@amanasifkhalid
amanasifkhalid merged commit ec76487 into dotnet:mainApr 12, 2024
@amanasifkhalid
amanasifkhalid deleted the swift-error-reg-ret branch April 12, 2024 19:26
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
…return (dotnet#100692)
Follow-up to dotnet#100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 13, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@amanasifkhalid@jakobbotsch
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

JIT: Add GT_SWIFT_ERROR_RET to represent loading error register upon return - #100692

Merged
amanasifkhalid merged 29 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg-ret
Apr 12, 2024
Merged

JIT: Add GT_SWIFT_ERROR_RET to represent loading error register upon return#100692
amanasifkhalid merged 29 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg-ret

Conversation

@amanasifkhalid

Copy link
Copy Markdown
Contributor

Follow-up to #100429. When creating return nodes during importation, if the method has a SwiftError* out parameter, the JIT will create a GT_SWIFT_ERROR_RET node before the regular GT_RETURN node to represent loading the SwiftError pseudolocal's value into the error register. We initially keep this node before the GT_RETURN block so we don't have to teach the JIT that it is a new valid terminator for BBJ_RETURN blocks; during lowering, we move this node to the end of the block so the error register won't get trashed after loading the error value. I found this to be an easier approach than trying to replace GT_RETURN with a binary node that holds the regular and error return values.

Old codegen:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T01] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov qword ptr [rbp-0x10], rdi
;; size=6 bbWeight=0.50 PerfScore 0.62
G_M3386_IG05: ;; offset=0x003C
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x0049
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 80, prolog size 27, PerfScore 16.33, instruction count 25, allocated bytes for code 80 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

New codegen:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 1, 1 ) struct ( 8) [rbp-0x10] must-init "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07 ] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08 ] ( 0, 0 ) long -> zero-ref "field V03.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp6 [V09 ] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov edi, 21
mov r12, rdi
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.38
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov r12, rdi
;; size=5 bbWeight=0.50 PerfScore 0.25
G_M3386_IG05: ;; offset=0x003B
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
nop ;; size=10 bbWeight=1 PerfScore 1.75
G_M3386_IG06: ;; offset=0x0045
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 76, prolog size 27, PerfScore 14.96, instruction count 26, allocated bytes for code 76 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Apr 5, 2024
@jakobbotsch

jakobbotsch commented Apr 5, 2024

Copy link
Copy Markdown
Member

We initially keep this node before the GT_RETURN block so we don't have to teach the JIT that it is a new valid terminator for BBJ_RETURN blocks; during lowering, we move this node to the end of the block so the error register won't get trashed after loading the error value. I found this to be an easier approach than trying to replace GT_RETURN with a binary node that holds the regular and error return values.

I'm not sure it's quite that simple due transforms within the JIT that assume knowledge about GT_RETURN and its semantics, and base transformations on this knowledge. This representation makes it error prone to write transforms around GT_RETURN since you aren't forced to handle this new terminator node. It seems intrinsic to me that the JIT has to be taught that this is a new kind of terminator node because of this.
For example, what happens in a function that has a switch with 50 returns in it? I would expect us to do return merging and end up with IR that doesn't handle the error correctly with resulting potential silent bad codegen.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

For example, what happens in a function that has a switch with 50 returns in it? I would expect us to do return merging and end up with IR that doesn't handle the error correctly with resulting potential silent bad codegen.

I see. In that case, let me try substituting GT_SWIFT_ERROR_RET for GT_RETURN then. In the meantime, let's see how much TP we're trading to get this codegen improvement...

@jakobbotsch

Copy link
Copy Markdown
Member

FWIW I think it's very likely we'll end up similarly having to expand the JIT's set of known and handled terminator nodes as part of async2. It has a similar situation with an async continuation that is returned in a different register than usual. I'm pretty sure the prototype has similar issues around return merging as the one I described above.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

FWIW I think it's very likely we'll end up similarly having to expand the JIT's set of known and handled terminator nodes as part of async2.

This might be too radical, but what if we expanded GT_RETURN to have an optional second argument to handle these scenarios, instead of introducing new nodes for them?

@jakobbotsch

Copy link
Copy Markdown
Member

I'm not sure whether it makes things much simpler -- you still have to audit the interesting uses of GT_RETURN on whether they need special handling of the potential second operand. It also becomes a bit easier to forget to handle the second operand. I like the explicitness of just having new types of nodes. In my experience this kind of explicitness pays off in the long run.

A more uniform representation would be to introduce the ability for the JIT to create new class layouts on the fly. In fact we can already do that for layouts without GC pointers, which would be sufficient for Swift (see typGetBlkLayout). Then you would consider a Swift function with error returns to return a tuple (x, y) where y is the error.
We wouldn't be able to handle something like this efficiently today, but it's sort of on my list in #93105 (the items around supporting FIELD_LIST for returns).

@jakobbotsch

jakobbotsch commented Apr 5, 2024

Copy link
Copy Markdown
Member

I think the uniform representation above would also generalize to GT_CALL with swift error returns where we would similarly create a new layout for the return that defines the error too, allowing us to remove GT_SWIFT_ERROR. But again, this requires a lot of work to handle efficiently -- it's essentially the "allow struct fields of physically promoted call returns to stay in registers" of #93105.
We would also need to generalize the ABI information carried in ReturnTypeDesc so that the frontend can set this up and have the backend automatically handle the details around getting the values out of the corresponding registers (for GT_CALL) or putting them into the right registers (for GT_RETURN).

It's probably a lot of work, but seems like it would be the right long term direction.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

I have a binop implementation of GT_SWIFT_ERROR_RET working locally. There are some quirks that I have to clean up and document before pushing, but the codegen looks slightly better:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 1, 1 ) struct ( 8) [rbp-0x10] must-init "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T02] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
; V08 tmp5 [V08,T01] ( 3, 2 ) long -> rbx "field V03.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp6 [V09,T03] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov ebx, 21
jmp SHORT G_M3386_IG05
;; size=9 bbWeight=0.50 PerfScore 1.25
G_M3386_IG04: ;; offset=0x0033
xor ebx, ebx
;; size=2 bbWeight=0.50 PerfScore 0.12
G_M3386_IG05: ;; offset=0x0035
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, rbx
;; size=12 bbWeight=1 PerfScore 1.75
G_M3386_IG06: ;; offset=0x0041
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 72, prolog size 27, PerfScore 14.71, instruction count 24, allocated bytes for code 72 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

@jakobbotsch PTAL. GT_SWIFT_ERROR_RET now takes the error value as its first operand, and the normal return value as its second operand. I wanted to make the normal return value the first operand to match GT_RETURN, but if the method has a void return type, then it would be possible for the first operand of GT_SWIFT_ERROR_RET to be null -- modeling this in the JIT isn't elegant, so I think it's better to flip the operands, and swap them whenever we can handle GT_RETURN and GT_SWIFT_ERROR_RET nodes simultaneously to reduce code duplication.

@jakobbotsch

Copy link
Copy Markdown
Member

I'll take a closer look tomorrow, but I'm curious why the null op1 didn't work out given that we have some prior art with GenTree::NullOp1Legal. Wasn't that sufficient?

Might be a bit cleaner to introduce something like

GenTree* GetReturnedValue(GenTreeOp* op)
{
assert(op->OperIs(GT_RETURN, GT_RETFILT, GT_SWIFT_ERROR_RET));
return op->OperIs(GT_SWIFT_ERROR_RET) ? op->gtGetOp2() : op->gtGetOp1();
}

Mutating GenTree at that point seems a bit iffy...

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

I'll take a closer look tomorrow, but I'm curious why the null op1 didn't work out given that we have some prior art with GenTree::NullOp1Legal. Wasn't that sufficient?

I initially tried that, but when handling simple operators in a bunch of places, we either assume binops have a first operand, or we assume the lack of a first operand implies the second operand is null as well (like in Compiler::gtSetEvalOrder). NullOp1Legal handles some binops already, but we only call it when handling unops. Perhaps I moved on from trying to match the operand order of GT_RETURN too quickly, though I think your proposed GetReturnedValue would work nicely; right now, I think we'd only need it in a few places.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/gtlist.h Outdated

GTNODE(SWIFT_ERROR , GenTree ,0,0,GTK_LEAF) // Error register value post-Swift call
GTNODE(SWIFT_ERROR , GenTree ,0,0,GTK_LEAF) // Error register value post-Swift call
GTNODE(SWIFT_ERROR_RET , GenTreeOp ,0,1,GTK_BINOP|GTK_NOVALUE) // Returns normal return value, and SwiftError pseudolocal's value in REG_SWIFT_ERROR

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice with a comment that op1 has the error and op2 has the normal return value (or null if method returns void)

Comment threadsrc/coreclr/jit/importer.cpp Outdated
Comment on lines +10887 to +10899
// If this method has a SwiftError* out parameter, we need to set the error register upon returning.
// By placing the GT_SWIFT_ERROR_RET node before the GT_RETURN node,
// we don't have to teach the JIT that GT_SWIFT_ERROR_RET is a valid terminator for BBJ_RETURN blocks.
// During lowering, we will move the GT_SWIFT_ERROR_RET to the end of the block
// so the error register won't get trashed when returning the normal value.
if (lvaSwiftErrorArg != BAD_VAR_NUM)
{
assert(info.compCallConv == CorInfoCallConvExtension::Swift);
assert(lvaSwiftErrorLocal != BAD_VAR_NUM);
GenTree* const swiftErrorNode = gtNewLclFldNode(lvaSwiftErrorLocal, TYP_I_IMPL, 0);
op1 = new (this, GT_SWIFT_ERROR_RET)
GenTreeOp(GT_SWIFT_ERROR_RET, op1->TypeGet(), swiftErrorNode, op1->gtGetOp1());
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is outdated now, right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use op1->SetOper(GT_SWIFT_ERROR_RET) instead here to avoid the new node allocation. Of course that requires some shuffling of the operands, so just allocating a new node is also ok with me, if you like that better feel free to ignore this.

Comment threadsrc/coreclr/jit/lower.cpp Outdated
Comment on lines +4640 to +4648
GenTree* Lowering::LowerSwiftErrorRet(GenTree* swiftErrorRet)
{
assert(swiftErrorRet->OperIs(GT_SWIFT_ERROR_RET));
GenTree* const nextNode = swiftErrorRet->gtNext;
LIR::Range& blockRange = BlockRange();
blockRange.Remove(swiftErrorRet);
blockRange.InsertAtEnd(swiftErrorRet);
return nextNode;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this isn't needed anymore?

Comment on lines +2582 to +2605
for (BasicBlock* block : Blocks())
{
if (block->KindIs(BBJ_RETURN))
{
GenTree* const ret = block->lastNode();
assert(ret->OperIs(GT_RETURN));
ret->SetOperRaw(GT_SWIFT_ERROR_RET);
ret->AsOp()->gtOp2 = ret->AsOp()->gtOp1;

// If this is the merged return block, use the merged return error local as the error operand.
// Else, load the error value from the SwiftError pseudolocal (this will probably get promoted, anyway).
if (block == genReturnBB)
{
assert(genReturnErrorLocal == BAD_VAR_NUM);
genReturnErrorLocal = lvaGrabTemp(true DEBUGARG("Single return block SwiftError value"));
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);
ret->AsOp()->gtOp1 = gtNewLclvNode(genReturnErrorLocal, TYP_I_IMPL);
}
else
{
ret->AsOp()->gtOp1 = gtNewLclFldNode(lvaSwiftErrorLocal, TYP_I_IMPL, 0);
}
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think it would be nice to do the replacement of lvaSwiftErrorArg with lvaSwiftErrorLocal here too.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my plan, though I was hitting asserts around fgNodeThreading being invalid when trying to use any of the GenTree iterators -- LocalsGenTreeList seems like it would be the most efficient one for finding the SwiftError* arg uses. Is there a clean way of doing the IR traversal without breaking any of these iterators' invariants?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point we don't have threaded IR nodes (the comment on fgNodeThreading has some relevant information), but you can use GenTreeVisitor to do it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optReplaceScalarUsesWithConst looks close to what you want to do I think.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optReplaceScalarUsesWithConst looks close to what you want to do I think.

Thanks! GenTreeVisitor works.

Comment threadsrc/coreclr/jit/ifconversion.cpp Outdated
}

case GT_RETURN:
case GT_SWIFT_ERROR_RET:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if-conversion supports GT_RETURN to transform

JTRUE(EQ(x, 0)) -> BB01, BB02
BB01:
RETURN(0)
BB02:
RETURN(1)

into

RETURN(SELECT(EQ(x, 0), 0, 1))

I don't see us being able to support this transform for GT_SWIFT_ERROR_RET, so I think we can't add this support here.

Comment threadsrc/coreclr/jit/morph.cpp Outdated
Comment on lines +14119 to +14121
const DebugInfo& di = lastStmt->GetDebugInfo();
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1(), CHECK_SPILL_NONE, nullptr, di, block);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const DebugInfo& di = lastStmt->GetDebugInfo();
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1(), CHECK_SPILL_NONE, nullptr, di, block);
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1());

(For pAfterStmt == nullptr these last parameters aren't relevant)

Comment on lines +11700 to +11711
case GT_SWIFT_ERROR_RET:
if (tree->gtGetOp2() != nullptr)
{
tree->gtVNPair = vnStore->VNPWithExc(vnStore->VNPForVoid(),
vnStore->VNPExceptionSet(tree->gtGetOp2()->gtVNPair));
}
else
{
tree->gtVNPair = vnStore->VNPForVoid();
}
break;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have the exceptions from the error operand propagated too.

Comment on lines +2597 to +2604
fgWalkResult PreOrderVisit(GenTree** use, GenTree* user)
{
if ((*use)->OperIs(GT_LCL_VAR) && ((*use)->AsLclVarCommon()->GetLclNum() == m_compiler->lvaSwiftErrorArg))
{
*use = m_compiler->gtNewLclVarAddrNode(m_compiler->lvaSwiftErrorLocal, genActualType(*use));
}

return fgWalkResult::WALK_CONTINUE;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we BADCODE here if we see a use of the error parameter that isn't a GT_LCL_VAR? E.g. any store or taking the address of it.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems reasonable to me. Do you know if we have any plans upstream to regulate the usage of the error parameter? I imagine we wouldn't want the JIT to do these checks alone.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. cc @jkoritzinsky@kotlarmilos. Does it seem appropriate to you to put restrictions on what the IL can do with the SwiftError* parameter? Specifically we're interested in disallowing stores and disallowing taking its address.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
{
if (!(*use)->OperIs(GT_LCL_VAR))
{
BADCODE("Expected GT_LCL_VAR of SwiftError* parameter, got other GenTreeLclVarCommon node");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the message should be phrased in terms of IL, not IR constructs

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would something like "Found invalid use of SwiftError* parameter" be too vague?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good to me

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
{
assert(genReturnErrorLocal == BAD_VAR_NUM);
genReturnErrorLocal = lvaGrabTemp(true DEBUGARG("Single return block SwiftError value"));
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);
lvaGetDesc(genReturnErrorLocal)->lvType = TYP_I_IMPL;

Comment threadsrc/coreclr/jit/ifconversion.cpp Outdated
Comment on lines +143 to +152
#ifdef SWIFT_SUPPORT
if (m_comp->lvaSwiftErrorArg != BAD_VAR_NUM)
{
m_mainOper = GT_SWIFT_ERROR_RET;
}
else
#endif // SWIFT_SUPPORT
{
m_mainOper = GT_RETURN;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these changes necessary?
It looks like IfConvertCheckStmts will fail naturally for GT_SWIFT_ERROR_RET, so perhaps no changes in if-conversion are necessary?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, we don't check m_mainOper until after we call IfConvertCheckStmts, and we bail if the terminator node is a GT_SWIFT_ERROR_RET, so it should be safe to undo these changes.

Comment on lines +523 to 527
case GT_SWIFT_ERROR_RET:
LowerNode(node->gtGetOp1());

FALLTHROUGH;
case GT_RETURN:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
caseGT_SWIFT_ERROR_RET:
LowerNode(node->gtGetOp1());
FALLTHROUGH;
caseGT_RETURN:
caseGT_SWIFT_ERROR_RET:
caseGT_RETURN:

Otherwise we invoke lowering of op1 twice.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LowerRet should probably be changed to take GenTreeOp* instead.

tree->SetAllEffectsFlags(tree->AsOp()->gtGetOp1());
tree->SetAllEffectsFlags(retVal);

return tree;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case needs to invoke morph on op1 as well for GT_SWIFT_ERROR_RET (I am not sure why the code returns here in the first place, it probably ought to just fall through after inserting the cast)

Comment threadsrc/coreclr/jit/morph.cpp

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great to me now. Thanks for your diligence in implementing this!

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

This looks great to me now. Thanks for your diligence in implementing this!

Thank you for all the reviews! In case you're curious, here's the updated codegen for ConditionallySetErrorTo21:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rsp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> r12 single-def
; V02 tmp0 [V02 ] ( 1, 1 ) struct ( 8) [rsp+0x10] "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V03 OutArgs [V03 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V04 tmp2 [V04 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
;* V05 tmp3 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp4 [V06 ] ( 3, 3 ) struct (16) [rsp+0x00] do-not-enreg[XS] addr-exposed "Reverse Pinvoke FrameVar"
; V07 tmp5 [V07,T01] ( 2, 4 ) long -> r12 "Single return block SwiftError value"
;* V08 tmp6 [V08 ] ( 0, 0 ) long -> zero-ref "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp7 [V09 ] ( 0, 0 ) long -> zero-ref "field V04.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V10 tmp8 [V10 ] ( 0, 0 ) long -> zero-ref "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
sub rsp, 24
mov r12d, edi
;; size=7 bbWeight=1 PerfScore 0.50
G_M3386_IG02: ;; offset=0x0007
lea rdi, [rsp]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
xor edi, edi
mov eax, 21
test r12d, r12d
mov r12, rax
cmove r12, rdi
lea rdi, [rsp]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
nop
;; size=36 bbWeight=1 PerfScore 4.50
G_M3386_IG03: ;; offset=0x002B
add rsp, 24
ret
;; size=5 bbWeight=1 PerfScore 1.25
; Total bytes of code 48, prolog size 4, PerfScore 6.25, instruction count 14, allocated bytes for code 48 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

Quite the improvement.

@amanasifkhalid
amanasifkhalid merged commit ec76487 into dotnet:mainApr 12, 2024
@amanasifkhalid
amanasifkhalid deleted the swift-error-reg-ret branch April 12, 2024 19:26
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
…return (dotnet#100692)
Follow-up to dotnet#100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 13, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@amanasifkhalid@jakobbotsch
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

JIT: Add GT_SWIFT_ERROR_RET to represent loading error register upon return - #100692

Merged
amanasifkhalid merged 29 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg-ret
Apr 12, 2024
Merged

JIT: Add GT_SWIFT_ERROR_RET to represent loading error register upon return#100692
amanasifkhalid merged 29 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg-ret

Conversation

@amanasifkhalid

Copy link
Copy Markdown
Contributor

Follow-up to #100429. When creating return nodes during importation, if the method has a SwiftError* out parameter, the JIT will create a GT_SWIFT_ERROR_RET node before the regular GT_RETURN node to represent loading the SwiftError pseudolocal's value into the error register. We initially keep this node before the GT_RETURN block so we don't have to teach the JIT that it is a new valid terminator for BBJ_RETURN blocks; during lowering, we move this node to the end of the block so the error register won't get trashed after loading the error value. I found this to be an easier approach than trying to replace GT_RETURN with a binary node that holds the regular and error return values.

Old codegen:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T01] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov qword ptr [rbp-0x10], rdi
;; size=6 bbWeight=0.50 PerfScore 0.62
G_M3386_IG05: ;; offset=0x003C
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x0049
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 80, prolog size 27, PerfScore 16.33, instruction count 25, allocated bytes for code 80 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

New codegen:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 1, 1 ) struct ( 8) [rbp-0x10] must-init "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07 ] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08 ] ( 0, 0 ) long -> zero-ref "field V03.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp6 [V09 ] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov edi, 21
mov r12, rdi
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.38
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov r12, rdi
;; size=5 bbWeight=0.50 PerfScore 0.25
G_M3386_IG05: ;; offset=0x003B
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
nop ;; size=10 bbWeight=1 PerfScore 1.75
G_M3386_IG06: ;; offset=0x0045
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 76, prolog size 27, PerfScore 14.96, instruction count 26, allocated bytes for code 76 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Apr 5, 2024
@jakobbotsch

jakobbotsch commented Apr 5, 2024

Copy link
Copy Markdown
Member

We initially keep this node before the GT_RETURN block so we don't have to teach the JIT that it is a new valid terminator for BBJ_RETURN blocks; during lowering, we move this node to the end of the block so the error register won't get trashed after loading the error value. I found this to be an easier approach than trying to replace GT_RETURN with a binary node that holds the regular and error return values.

I'm not sure it's quite that simple due transforms within the JIT that assume knowledge about GT_RETURN and its semantics, and base transformations on this knowledge. This representation makes it error prone to write transforms around GT_RETURN since you aren't forced to handle this new terminator node. It seems intrinsic to me that the JIT has to be taught that this is a new kind of terminator node because of this.
For example, what happens in a function that has a switch with 50 returns in it? I would expect us to do return merging and end up with IR that doesn't handle the error correctly with resulting potential silent bad codegen.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

For example, what happens in a function that has a switch with 50 returns in it? I would expect us to do return merging and end up with IR that doesn't handle the error correctly with resulting potential silent bad codegen.

I see. In that case, let me try substituting GT_SWIFT_ERROR_RET for GT_RETURN then. In the meantime, let's see how much TP we're trading to get this codegen improvement...

@jakobbotsch

Copy link
Copy Markdown
Member

FWIW I think it's very likely we'll end up similarly having to expand the JIT's set of known and handled terminator nodes as part of async2. It has a similar situation with an async continuation that is returned in a different register than usual. I'm pretty sure the prototype has similar issues around return merging as the one I described above.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

FWIW I think it's very likely we'll end up similarly having to expand the JIT's set of known and handled terminator nodes as part of async2.

This might be too radical, but what if we expanded GT_RETURN to have an optional second argument to handle these scenarios, instead of introducing new nodes for them?

@jakobbotsch

Copy link
Copy Markdown
Member

I'm not sure whether it makes things much simpler -- you still have to audit the interesting uses of GT_RETURN on whether they need special handling of the potential second operand. It also becomes a bit easier to forget to handle the second operand. I like the explicitness of just having new types of nodes. In my experience this kind of explicitness pays off in the long run.

A more uniform representation would be to introduce the ability for the JIT to create new class layouts on the fly. In fact we can already do that for layouts without GC pointers, which would be sufficient for Swift (see typGetBlkLayout). Then you would consider a Swift function with error returns to return a tuple (x, y) where y is the error.
We wouldn't be able to handle something like this efficiently today, but it's sort of on my list in #93105 (the items around supporting FIELD_LIST for returns).

@jakobbotsch

jakobbotsch commented Apr 5, 2024

Copy link
Copy Markdown
Member

I think the uniform representation above would also generalize to GT_CALL with swift error returns where we would similarly create a new layout for the return that defines the error too, allowing us to remove GT_SWIFT_ERROR. But again, this requires a lot of work to handle efficiently -- it's essentially the "allow struct fields of physically promoted call returns to stay in registers" of #93105.
We would also need to generalize the ABI information carried in ReturnTypeDesc so that the frontend can set this up and have the backend automatically handle the details around getting the values out of the corresponding registers (for GT_CALL) or putting them into the right registers (for GT_RETURN).

It's probably a lot of work, but seems like it would be the right long term direction.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

I have a binop implementation of GT_SWIFT_ERROR_RET working locally. There are some quirks that I have to clean up and document before pushing, but the codegen looks slightly better:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 1, 1 ) struct ( 8) [rbp-0x10] must-init "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T02] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
; V08 tmp5 [V08,T01] ( 3, 2 ) long -> rbx "field V03.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp6 [V09,T03] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov ebx, 21
jmp SHORT G_M3386_IG05
;; size=9 bbWeight=0.50 PerfScore 1.25
G_M3386_IG04: ;; offset=0x0033
xor ebx, ebx
;; size=2 bbWeight=0.50 PerfScore 0.12
G_M3386_IG05: ;; offset=0x0035
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, rbx
;; size=12 bbWeight=1 PerfScore 1.75
G_M3386_IG06: ;; offset=0x0041
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 72, prolog size 27, PerfScore 14.71, instruction count 24, allocated bytes for code 72 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

@jakobbotsch PTAL. GT_SWIFT_ERROR_RET now takes the error value as its first operand, and the normal return value as its second operand. I wanted to make the normal return value the first operand to match GT_RETURN, but if the method has a void return type, then it would be possible for the first operand of GT_SWIFT_ERROR_RET to be null -- modeling this in the JIT isn't elegant, so I think it's better to flip the operands, and swap them whenever we can handle GT_RETURN and GT_SWIFT_ERROR_RET nodes simultaneously to reduce code duplication.

@jakobbotsch

Copy link
Copy Markdown
Member

I'll take a closer look tomorrow, but I'm curious why the null op1 didn't work out given that we have some prior art with GenTree::NullOp1Legal. Wasn't that sufficient?

Might be a bit cleaner to introduce something like

GenTree* GetReturnedValue(GenTreeOp* op)
{
assert(op->OperIs(GT_RETURN, GT_RETFILT, GT_SWIFT_ERROR_RET));
return op->OperIs(GT_SWIFT_ERROR_RET) ? op->gtGetOp2() : op->gtGetOp1();
}

Mutating GenTree at that point seems a bit iffy...

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

I'll take a closer look tomorrow, but I'm curious why the null op1 didn't work out given that we have some prior art with GenTree::NullOp1Legal. Wasn't that sufficient?

I initially tried that, but when handling simple operators in a bunch of places, we either assume binops have a first operand, or we assume the lack of a first operand implies the second operand is null as well (like in Compiler::gtSetEvalOrder). NullOp1Legal handles some binops already, but we only call it when handling unops. Perhaps I moved on from trying to match the operand order of GT_RETURN too quickly, though I think your proposed GetReturnedValue would work nicely; right now, I think we'd only need it in a few places.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/gtlist.h Outdated

GTNODE(SWIFT_ERROR , GenTree ,0,0,GTK_LEAF) // Error register value post-Swift call
GTNODE(SWIFT_ERROR , GenTree ,0,0,GTK_LEAF) // Error register value post-Swift call
GTNODE(SWIFT_ERROR_RET , GenTreeOp ,0,1,GTK_BINOP|GTK_NOVALUE) // Returns normal return value, and SwiftError pseudolocal's value in REG_SWIFT_ERROR

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice with a comment that op1 has the error and op2 has the normal return value (or null if method returns void)

Comment threadsrc/coreclr/jit/importer.cpp Outdated
Comment on lines +10887 to +10899
// If this method has a SwiftError* out parameter, we need to set the error register upon returning.
// By placing the GT_SWIFT_ERROR_RET node before the GT_RETURN node,
// we don't have to teach the JIT that GT_SWIFT_ERROR_RET is a valid terminator for BBJ_RETURN blocks.
// During lowering, we will move the GT_SWIFT_ERROR_RET to the end of the block
// so the error register won't get trashed when returning the normal value.
if (lvaSwiftErrorArg != BAD_VAR_NUM)
{
assert(info.compCallConv == CorInfoCallConvExtension::Swift);
assert(lvaSwiftErrorLocal != BAD_VAR_NUM);
GenTree* const swiftErrorNode = gtNewLclFldNode(lvaSwiftErrorLocal, TYP_I_IMPL, 0);
op1 = new (this, GT_SWIFT_ERROR_RET)
GenTreeOp(GT_SWIFT_ERROR_RET, op1->TypeGet(), swiftErrorNode, op1->gtGetOp1());
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is outdated now, right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use op1->SetOper(GT_SWIFT_ERROR_RET) instead here to avoid the new node allocation. Of course that requires some shuffling of the operands, so just allocating a new node is also ok with me, if you like that better feel free to ignore this.

Comment threadsrc/coreclr/jit/lower.cpp Outdated
Comment on lines +4640 to +4648
GenTree* Lowering::LowerSwiftErrorRet(GenTree* swiftErrorRet)
{
assert(swiftErrorRet->OperIs(GT_SWIFT_ERROR_RET));
GenTree* const nextNode = swiftErrorRet->gtNext;
LIR::Range& blockRange = BlockRange();
blockRange.Remove(swiftErrorRet);
blockRange.InsertAtEnd(swiftErrorRet);
return nextNode;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this isn't needed anymore?

Comment on lines +2582 to +2605
for (BasicBlock* block : Blocks())
{
if (block->KindIs(BBJ_RETURN))
{
GenTree* const ret = block->lastNode();
assert(ret->OperIs(GT_RETURN));
ret->SetOperRaw(GT_SWIFT_ERROR_RET);
ret->AsOp()->gtOp2 = ret->AsOp()->gtOp1;

// If this is the merged return block, use the merged return error local as the error operand.
// Else, load the error value from the SwiftError pseudolocal (this will probably get promoted, anyway).
if (block == genReturnBB)
{
assert(genReturnErrorLocal == BAD_VAR_NUM);
genReturnErrorLocal = lvaGrabTemp(true DEBUGARG("Single return block SwiftError value"));
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);
ret->AsOp()->gtOp1 = gtNewLclvNode(genReturnErrorLocal, TYP_I_IMPL);
}
else
{
ret->AsOp()->gtOp1 = gtNewLclFldNode(lvaSwiftErrorLocal, TYP_I_IMPL, 0);
}
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think it would be nice to do the replacement of lvaSwiftErrorArg with lvaSwiftErrorLocal here too.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my plan, though I was hitting asserts around fgNodeThreading being invalid when trying to use any of the GenTree iterators -- LocalsGenTreeList seems like it would be the most efficient one for finding the SwiftError* arg uses. Is there a clean way of doing the IR traversal without breaking any of these iterators' invariants?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point we don't have threaded IR nodes (the comment on fgNodeThreading has some relevant information), but you can use GenTreeVisitor to do it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optReplaceScalarUsesWithConst looks close to what you want to do I think.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optReplaceScalarUsesWithConst looks close to what you want to do I think.

Thanks! GenTreeVisitor works.

Comment threadsrc/coreclr/jit/ifconversion.cpp Outdated
}

case GT_RETURN:
case GT_SWIFT_ERROR_RET:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if-conversion supports GT_RETURN to transform

JTRUE(EQ(x, 0)) -> BB01, BB02
BB01:
RETURN(0)
BB02:
RETURN(1)

into

RETURN(SELECT(EQ(x, 0), 0, 1))

I don't see us being able to support this transform for GT_SWIFT_ERROR_RET, so I think we can't add this support here.

Comment threadsrc/coreclr/jit/morph.cpp Outdated
Comment on lines +14119 to +14121
const DebugInfo& di = lastStmt->GetDebugInfo();
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1(), CHECK_SPILL_NONE, nullptr, di, block);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const DebugInfo& di = lastStmt->GetDebugInfo();
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1(), CHECK_SPILL_NONE, nullptr, di, block);
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1());

(For pAfterStmt == nullptr these last parameters aren't relevant)

Comment on lines +11700 to +11711
case GT_SWIFT_ERROR_RET:
if (tree->gtGetOp2() != nullptr)
{
tree->gtVNPair = vnStore->VNPWithExc(vnStore->VNPForVoid(),
vnStore->VNPExceptionSet(tree->gtGetOp2()->gtVNPair));
}
else
{
tree->gtVNPair = vnStore->VNPForVoid();
}
break;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have the exceptions from the error operand propagated too.

Comment on lines +2597 to +2604
fgWalkResult PreOrderVisit(GenTree** use, GenTree* user)
{
if ((*use)->OperIs(GT_LCL_VAR) && ((*use)->AsLclVarCommon()->GetLclNum() == m_compiler->lvaSwiftErrorArg))
{
*use = m_compiler->gtNewLclVarAddrNode(m_compiler->lvaSwiftErrorLocal, genActualType(*use));
}

return fgWalkResult::WALK_CONTINUE;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we BADCODE here if we see a use of the error parameter that isn't a GT_LCL_VAR? E.g. any store or taking the address of it.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems reasonable to me. Do you know if we have any plans upstream to regulate the usage of the error parameter? I imagine we wouldn't want the JIT to do these checks alone.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. cc @jkoritzinsky@kotlarmilos. Does it seem appropriate to you to put restrictions on what the IL can do with the SwiftError* parameter? Specifically we're interested in disallowing stores and disallowing taking its address.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
{
if (!(*use)->OperIs(GT_LCL_VAR))
{
BADCODE("Expected GT_LCL_VAR of SwiftError* parameter, got other GenTreeLclVarCommon node");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the message should be phrased in terms of IL, not IR constructs

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would something like "Found invalid use of SwiftError* parameter" be too vague?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good to me

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
{
assert(genReturnErrorLocal == BAD_VAR_NUM);
genReturnErrorLocal = lvaGrabTemp(true DEBUGARG("Single return block SwiftError value"));
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);
lvaGetDesc(genReturnErrorLocal)->lvType = TYP_I_IMPL;

Comment threadsrc/coreclr/jit/ifconversion.cpp Outdated
Comment on lines +143 to +152
#ifdef SWIFT_SUPPORT
if (m_comp->lvaSwiftErrorArg != BAD_VAR_NUM)
{
m_mainOper = GT_SWIFT_ERROR_RET;
}
else
#endif // SWIFT_SUPPORT
{
m_mainOper = GT_RETURN;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these changes necessary?
It looks like IfConvertCheckStmts will fail naturally for GT_SWIFT_ERROR_RET, so perhaps no changes in if-conversion are necessary?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, we don't check m_mainOper until after we call IfConvertCheckStmts, and we bail if the terminator node is a GT_SWIFT_ERROR_RET, so it should be safe to undo these changes.

Comment on lines +523 to 527
case GT_SWIFT_ERROR_RET:
LowerNode(node->gtGetOp1());

FALLTHROUGH;
case GT_RETURN:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
caseGT_SWIFT_ERROR_RET:
LowerNode(node->gtGetOp1());
FALLTHROUGH;
caseGT_RETURN:
caseGT_SWIFT_ERROR_RET:
caseGT_RETURN:

Otherwise we invoke lowering of op1 twice.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LowerRet should probably be changed to take GenTreeOp* instead.

tree->SetAllEffectsFlags(tree->AsOp()->gtGetOp1());
tree->SetAllEffectsFlags(retVal);

return tree;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case needs to invoke morph on op1 as well for GT_SWIFT_ERROR_RET (I am not sure why the code returns here in the first place, it probably ought to just fall through after inserting the cast)

Comment threadsrc/coreclr/jit/morph.cpp

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great to me now. Thanks for your diligence in implementing this!

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

This looks great to me now. Thanks for your diligence in implementing this!

Thank you for all the reviews! In case you're curious, here's the updated codegen for ConditionallySetErrorTo21:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rsp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> r12 single-def
; V02 tmp0 [V02 ] ( 1, 1 ) struct ( 8) [rsp+0x10] "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V03 OutArgs [V03 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V04 tmp2 [V04 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
;* V05 tmp3 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp4 [V06 ] ( 3, 3 ) struct (16) [rsp+0x00] do-not-enreg[XS] addr-exposed "Reverse Pinvoke FrameVar"
; V07 tmp5 [V07,T01] ( 2, 4 ) long -> r12 "Single return block SwiftError value"
;* V08 tmp6 [V08 ] ( 0, 0 ) long -> zero-ref "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp7 [V09 ] ( 0, 0 ) long -> zero-ref "field V04.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V10 tmp8 [V10 ] ( 0, 0 ) long -> zero-ref "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
sub rsp, 24
mov r12d, edi
;; size=7 bbWeight=1 PerfScore 0.50
G_M3386_IG02: ;; offset=0x0007
lea rdi, [rsp]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
xor edi, edi
mov eax, 21
test r12d, r12d
mov r12, rax
cmove r12, rdi
lea rdi, [rsp]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
nop
;; size=36 bbWeight=1 PerfScore 4.50
G_M3386_IG03: ;; offset=0x002B
add rsp, 24
ret
;; size=5 bbWeight=1 PerfScore 1.25
; Total bytes of code 48, prolog size 4, PerfScore 6.25, instruction count 14, allocated bytes for code 48 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

Quite the improvement.

@amanasifkhalid
amanasifkhalid merged commit ec76487 into dotnet:mainApr 12, 2024
@amanasifkhalid
amanasifkhalid deleted the swift-error-reg-ret branch April 12, 2024 19:26
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
…return (dotnet#100692)
Follow-up to dotnet#100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 13, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@amanasifkhalid@jakobbotsch
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

JIT: Add GT_SWIFT_ERROR_RET to represent loading error register upon return - #100692

Merged
amanasifkhalid merged 29 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg-ret
Apr 12, 2024
Merged

JIT: Add GT_SWIFT_ERROR_RET to represent loading error register upon return#100692
amanasifkhalid merged 29 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg-ret

Conversation

@amanasifkhalid

Copy link
Copy Markdown
Contributor

Follow-up to #100429. When creating return nodes during importation, if the method has a SwiftError* out parameter, the JIT will create a GT_SWIFT_ERROR_RET node before the regular GT_RETURN node to represent loading the SwiftError pseudolocal's value into the error register. We initially keep this node before the GT_RETURN block so we don't have to teach the JIT that it is a new valid terminator for BBJ_RETURN blocks; during lowering, we move this node to the end of the block so the error register won't get trashed after loading the error value. I found this to be an easier approach than trying to replace GT_RETURN with a binary node that holds the regular and error return values.

Old codegen:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T01] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov qword ptr [rbp-0x10], rdi
;; size=6 bbWeight=0.50 PerfScore 0.62
G_M3386_IG05: ;; offset=0x003C
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x0049
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 80, prolog size 27, PerfScore 16.33, instruction count 25, allocated bytes for code 80 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

New codegen:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 1, 1 ) struct ( 8) [rbp-0x10] must-init "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07 ] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08 ] ( 0, 0 ) long -> zero-ref "field V03.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp6 [V09 ] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov edi, 21
mov r12, rdi
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.38
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov r12, rdi
;; size=5 bbWeight=0.50 PerfScore 0.25
G_M3386_IG05: ;; offset=0x003B
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
nop ;; size=10 bbWeight=1 PerfScore 1.75
G_M3386_IG06: ;; offset=0x0045
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 76, prolog size 27, PerfScore 14.96, instruction count 26, allocated bytes for code 76 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Apr 5, 2024
@jakobbotsch

jakobbotsch commented Apr 5, 2024

Copy link
Copy Markdown
Member

We initially keep this node before the GT_RETURN block so we don't have to teach the JIT that it is a new valid terminator for BBJ_RETURN blocks; during lowering, we move this node to the end of the block so the error register won't get trashed after loading the error value. I found this to be an easier approach than trying to replace GT_RETURN with a binary node that holds the regular and error return values.

I'm not sure it's quite that simple due transforms within the JIT that assume knowledge about GT_RETURN and its semantics, and base transformations on this knowledge. This representation makes it error prone to write transforms around GT_RETURN since you aren't forced to handle this new terminator node. It seems intrinsic to me that the JIT has to be taught that this is a new kind of terminator node because of this.
For example, what happens in a function that has a switch with 50 returns in it? I would expect us to do return merging and end up with IR that doesn't handle the error correctly with resulting potential silent bad codegen.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

For example, what happens in a function that has a switch with 50 returns in it? I would expect us to do return merging and end up with IR that doesn't handle the error correctly with resulting potential silent bad codegen.

I see. In that case, let me try substituting GT_SWIFT_ERROR_RET for GT_RETURN then. In the meantime, let's see how much TP we're trading to get this codegen improvement...

@jakobbotsch

Copy link
Copy Markdown
Member

FWIW I think it's very likely we'll end up similarly having to expand the JIT's set of known and handled terminator nodes as part of async2. It has a similar situation with an async continuation that is returned in a different register than usual. I'm pretty sure the prototype has similar issues around return merging as the one I described above.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

FWIW I think it's very likely we'll end up similarly having to expand the JIT's set of known and handled terminator nodes as part of async2.

This might be too radical, but what if we expanded GT_RETURN to have an optional second argument to handle these scenarios, instead of introducing new nodes for them?

@jakobbotsch

Copy link
Copy Markdown
Member

I'm not sure whether it makes things much simpler -- you still have to audit the interesting uses of GT_RETURN on whether they need special handling of the potential second operand. It also becomes a bit easier to forget to handle the second operand. I like the explicitness of just having new types of nodes. In my experience this kind of explicitness pays off in the long run.

A more uniform representation would be to introduce the ability for the JIT to create new class layouts on the fly. In fact we can already do that for layouts without GC pointers, which would be sufficient for Swift (see typGetBlkLayout). Then you would consider a Swift function with error returns to return a tuple (x, y) where y is the error.
We wouldn't be able to handle something like this efficiently today, but it's sort of on my list in #93105 (the items around supporting FIELD_LIST for returns).

@jakobbotsch

jakobbotsch commented Apr 5, 2024

Copy link
Copy Markdown
Member

I think the uniform representation above would also generalize to GT_CALL with swift error returns where we would similarly create a new layout for the return that defines the error too, allowing us to remove GT_SWIFT_ERROR. But again, this requires a lot of work to handle efficiently -- it's essentially the "allow struct fields of physically promoted call returns to stay in registers" of #93105.
We would also need to generalize the ABI information carried in ReturnTypeDesc so that the frontend can set this up and have the backend automatically handle the details around getting the values out of the corresponding registers (for GT_CALL) or putting them into the right registers (for GT_RETURN).

It's probably a lot of work, but seems like it would be the right long term direction.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

I have a binop implementation of GT_SWIFT_ERROR_RET working locally. There are some quirks that I have to clean up and document before pushing, but the codegen looks slightly better:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 1, 1 ) struct ( 8) [rbp-0x10] must-init "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T02] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
; V08 tmp5 [V08,T01] ( 3, 2 ) long -> rbx "field V03.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp6 [V09,T03] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov ebx, 21
jmp SHORT G_M3386_IG05
;; size=9 bbWeight=0.50 PerfScore 1.25
G_M3386_IG04: ;; offset=0x0033
xor ebx, ebx
;; size=2 bbWeight=0.50 PerfScore 0.12
G_M3386_IG05: ;; offset=0x0035
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, rbx
;; size=12 bbWeight=1 PerfScore 1.75
G_M3386_IG06: ;; offset=0x0041
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 72, prolog size 27, PerfScore 14.71, instruction count 24, allocated bytes for code 72 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

@jakobbotsch PTAL. GT_SWIFT_ERROR_RET now takes the error value as its first operand, and the normal return value as its second operand. I wanted to make the normal return value the first operand to match GT_RETURN, but if the method has a void return type, then it would be possible for the first operand of GT_SWIFT_ERROR_RET to be null -- modeling this in the JIT isn't elegant, so I think it's better to flip the operands, and swap them whenever we can handle GT_RETURN and GT_SWIFT_ERROR_RET nodes simultaneously to reduce code duplication.

@jakobbotsch

Copy link
Copy Markdown
Member

I'll take a closer look tomorrow, but I'm curious why the null op1 didn't work out given that we have some prior art with GenTree::NullOp1Legal. Wasn't that sufficient?

Might be a bit cleaner to introduce something like

GenTree* GetReturnedValue(GenTreeOp* op)
{
assert(op->OperIs(GT_RETURN, GT_RETFILT, GT_SWIFT_ERROR_RET));
return op->OperIs(GT_SWIFT_ERROR_RET) ? op->gtGetOp2() : op->gtGetOp1();
}

Mutating GenTree at that point seems a bit iffy...

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

I'll take a closer look tomorrow, but I'm curious why the null op1 didn't work out given that we have some prior art with GenTree::NullOp1Legal. Wasn't that sufficient?

I initially tried that, but when handling simple operators in a bunch of places, we either assume binops have a first operand, or we assume the lack of a first operand implies the second operand is null as well (like in Compiler::gtSetEvalOrder). NullOp1Legal handles some binops already, but we only call it when handling unops. Perhaps I moved on from trying to match the operand order of GT_RETURN too quickly, though I think your proposed GetReturnedValue would work nicely; right now, I think we'd only need it in a few places.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/gtlist.h Outdated

GTNODE(SWIFT_ERROR , GenTree ,0,0,GTK_LEAF) // Error register value post-Swift call
GTNODE(SWIFT_ERROR , GenTree ,0,0,GTK_LEAF) // Error register value post-Swift call
GTNODE(SWIFT_ERROR_RET , GenTreeOp ,0,1,GTK_BINOP|GTK_NOVALUE) // Returns normal return value, and SwiftError pseudolocal's value in REG_SWIFT_ERROR

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice with a comment that op1 has the error and op2 has the normal return value (or null if method returns void)

Comment threadsrc/coreclr/jit/importer.cpp Outdated
Comment on lines +10887 to +10899
// If this method has a SwiftError* out parameter, we need to set the error register upon returning.
// By placing the GT_SWIFT_ERROR_RET node before the GT_RETURN node,
// we don't have to teach the JIT that GT_SWIFT_ERROR_RET is a valid terminator for BBJ_RETURN blocks.
// During lowering, we will move the GT_SWIFT_ERROR_RET to the end of the block
// so the error register won't get trashed when returning the normal value.
if (lvaSwiftErrorArg != BAD_VAR_NUM)
{
assert(info.compCallConv == CorInfoCallConvExtension::Swift);
assert(lvaSwiftErrorLocal != BAD_VAR_NUM);
GenTree* const swiftErrorNode = gtNewLclFldNode(lvaSwiftErrorLocal, TYP_I_IMPL, 0);
op1 = new (this, GT_SWIFT_ERROR_RET)
GenTreeOp(GT_SWIFT_ERROR_RET, op1->TypeGet(), swiftErrorNode, op1->gtGetOp1());
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is outdated now, right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use op1->SetOper(GT_SWIFT_ERROR_RET) instead here to avoid the new node allocation. Of course that requires some shuffling of the operands, so just allocating a new node is also ok with me, if you like that better feel free to ignore this.

Comment threadsrc/coreclr/jit/lower.cpp Outdated
Comment on lines +4640 to +4648
GenTree* Lowering::LowerSwiftErrorRet(GenTree* swiftErrorRet)
{
assert(swiftErrorRet->OperIs(GT_SWIFT_ERROR_RET));
GenTree* const nextNode = swiftErrorRet->gtNext;
LIR::Range& blockRange = BlockRange();
blockRange.Remove(swiftErrorRet);
blockRange.InsertAtEnd(swiftErrorRet);
return nextNode;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this isn't needed anymore?

Comment on lines +2582 to +2605
for (BasicBlock* block : Blocks())
{
if (block->KindIs(BBJ_RETURN))
{
GenTree* const ret = block->lastNode();
assert(ret->OperIs(GT_RETURN));
ret->SetOperRaw(GT_SWIFT_ERROR_RET);
ret->AsOp()->gtOp2 = ret->AsOp()->gtOp1;

// If this is the merged return block, use the merged return error local as the error operand.
// Else, load the error value from the SwiftError pseudolocal (this will probably get promoted, anyway).
if (block == genReturnBB)
{
assert(genReturnErrorLocal == BAD_VAR_NUM);
genReturnErrorLocal = lvaGrabTemp(true DEBUGARG("Single return block SwiftError value"));
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);
ret->AsOp()->gtOp1 = gtNewLclvNode(genReturnErrorLocal, TYP_I_IMPL);
}
else
{
ret->AsOp()->gtOp1 = gtNewLclFldNode(lvaSwiftErrorLocal, TYP_I_IMPL, 0);
}
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think it would be nice to do the replacement of lvaSwiftErrorArg with lvaSwiftErrorLocal here too.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my plan, though I was hitting asserts around fgNodeThreading being invalid when trying to use any of the GenTree iterators -- LocalsGenTreeList seems like it would be the most efficient one for finding the SwiftError* arg uses. Is there a clean way of doing the IR traversal without breaking any of these iterators' invariants?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point we don't have threaded IR nodes (the comment on fgNodeThreading has some relevant information), but you can use GenTreeVisitor to do it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optReplaceScalarUsesWithConst looks close to what you want to do I think.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optReplaceScalarUsesWithConst looks close to what you want to do I think.

Thanks! GenTreeVisitor works.

Comment threadsrc/coreclr/jit/ifconversion.cpp Outdated
}

case GT_RETURN:
case GT_SWIFT_ERROR_RET:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if-conversion supports GT_RETURN to transform

JTRUE(EQ(x, 0)) -> BB01, BB02
BB01:
RETURN(0)
BB02:
RETURN(1)

into

RETURN(SELECT(EQ(x, 0), 0, 1))

I don't see us being able to support this transform for GT_SWIFT_ERROR_RET, so I think we can't add this support here.

Comment threadsrc/coreclr/jit/morph.cpp Outdated
Comment on lines +14119 to +14121
const DebugInfo& di = lastStmt->GetDebugInfo();
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1(), CHECK_SPILL_NONE, nullptr, di, block);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const DebugInfo& di = lastStmt->GetDebugInfo();
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1(), CHECK_SPILL_NONE, nullptr, di, block);
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1());

(For pAfterStmt == nullptr these last parameters aren't relevant)

Comment on lines +11700 to +11711
case GT_SWIFT_ERROR_RET:
if (tree->gtGetOp2() != nullptr)
{
tree->gtVNPair = vnStore->VNPWithExc(vnStore->VNPForVoid(),
vnStore->VNPExceptionSet(tree->gtGetOp2()->gtVNPair));
}
else
{
tree->gtVNPair = vnStore->VNPForVoid();
}
break;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have the exceptions from the error operand propagated too.

Comment on lines +2597 to +2604
fgWalkResult PreOrderVisit(GenTree** use, GenTree* user)
{
if ((*use)->OperIs(GT_LCL_VAR) && ((*use)->AsLclVarCommon()->GetLclNum() == m_compiler->lvaSwiftErrorArg))
{
*use = m_compiler->gtNewLclVarAddrNode(m_compiler->lvaSwiftErrorLocal, genActualType(*use));
}

return fgWalkResult::WALK_CONTINUE;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we BADCODE here if we see a use of the error parameter that isn't a GT_LCL_VAR? E.g. any store or taking the address of it.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems reasonable to me. Do you know if we have any plans upstream to regulate the usage of the error parameter? I imagine we wouldn't want the JIT to do these checks alone.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. cc @jkoritzinsky@kotlarmilos. Does it seem appropriate to you to put restrictions on what the IL can do with the SwiftError* parameter? Specifically we're interested in disallowing stores and disallowing taking its address.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
{
if (!(*use)->OperIs(GT_LCL_VAR))
{
BADCODE("Expected GT_LCL_VAR of SwiftError* parameter, got other GenTreeLclVarCommon node");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the message should be phrased in terms of IL, not IR constructs

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would something like "Found invalid use of SwiftError* parameter" be too vague?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good to me

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
{
assert(genReturnErrorLocal == BAD_VAR_NUM);
genReturnErrorLocal = lvaGrabTemp(true DEBUGARG("Single return block SwiftError value"));
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);
lvaGetDesc(genReturnErrorLocal)->lvType = TYP_I_IMPL;

Comment threadsrc/coreclr/jit/ifconversion.cpp Outdated
Comment on lines +143 to +152
#ifdef SWIFT_SUPPORT
if (m_comp->lvaSwiftErrorArg != BAD_VAR_NUM)
{
m_mainOper = GT_SWIFT_ERROR_RET;
}
else
#endif // SWIFT_SUPPORT
{
m_mainOper = GT_RETURN;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these changes necessary?
It looks like IfConvertCheckStmts will fail naturally for GT_SWIFT_ERROR_RET, so perhaps no changes in if-conversion are necessary?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, we don't check m_mainOper until after we call IfConvertCheckStmts, and we bail if the terminator node is a GT_SWIFT_ERROR_RET, so it should be safe to undo these changes.

Comment on lines +523 to 527
case GT_SWIFT_ERROR_RET:
LowerNode(node->gtGetOp1());

FALLTHROUGH;
case GT_RETURN:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
caseGT_SWIFT_ERROR_RET:
LowerNode(node->gtGetOp1());
FALLTHROUGH;
caseGT_RETURN:
caseGT_SWIFT_ERROR_RET:
caseGT_RETURN:

Otherwise we invoke lowering of op1 twice.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LowerRet should probably be changed to take GenTreeOp* instead.

tree->SetAllEffectsFlags(tree->AsOp()->gtGetOp1());
tree->SetAllEffectsFlags(retVal);

return tree;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case needs to invoke morph on op1 as well for GT_SWIFT_ERROR_RET (I am not sure why the code returns here in the first place, it probably ought to just fall through after inserting the cast)

Comment threadsrc/coreclr/jit/morph.cpp

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great to me now. Thanks for your diligence in implementing this!

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

This looks great to me now. Thanks for your diligence in implementing this!

Thank you for all the reviews! In case you're curious, here's the updated codegen for ConditionallySetErrorTo21:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rsp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> r12 single-def
; V02 tmp0 [V02 ] ( 1, 1 ) struct ( 8) [rsp+0x10] "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V03 OutArgs [V03 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V04 tmp2 [V04 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
;* V05 tmp3 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp4 [V06 ] ( 3, 3 ) struct (16) [rsp+0x00] do-not-enreg[XS] addr-exposed "Reverse Pinvoke FrameVar"
; V07 tmp5 [V07,T01] ( 2, 4 ) long -> r12 "Single return block SwiftError value"
;* V08 tmp6 [V08 ] ( 0, 0 ) long -> zero-ref "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp7 [V09 ] ( 0, 0 ) long -> zero-ref "field V04.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V10 tmp8 [V10 ] ( 0, 0 ) long -> zero-ref "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
sub rsp, 24
mov r12d, edi
;; size=7 bbWeight=1 PerfScore 0.50
G_M3386_IG02: ;; offset=0x0007
lea rdi, [rsp]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
xor edi, edi
mov eax, 21
test r12d, r12d
mov r12, rax
cmove r12, rdi
lea rdi, [rsp]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
nop
;; size=36 bbWeight=1 PerfScore 4.50
G_M3386_IG03: ;; offset=0x002B
add rsp, 24
ret
;; size=5 bbWeight=1 PerfScore 1.25
; Total bytes of code 48, prolog size 4, PerfScore 6.25, instruction count 14, allocated bytes for code 48 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

Quite the improvement.

@amanasifkhalid
amanasifkhalid merged commit ec76487 into dotnet:mainApr 12, 2024
@amanasifkhalid
amanasifkhalid deleted the swift-error-reg-ret branch April 12, 2024 19:26
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
…return (dotnet#100692)
Follow-up to dotnet#100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 13, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@amanasifkhalid@jakobbotsch
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

JIT: Add GT_SWIFT_ERROR_RET to represent loading error register upon return - #100692

Merged
amanasifkhalid merged 29 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg-ret
Apr 12, 2024
Merged

JIT: Add GT_SWIFT_ERROR_RET to represent loading error register upon return#100692
amanasifkhalid merged 29 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg-ret

Conversation

@amanasifkhalid

Copy link
Copy Markdown
Contributor

Follow-up to #100429. When creating return nodes during importation, if the method has a SwiftError* out parameter, the JIT will create a GT_SWIFT_ERROR_RET node before the regular GT_RETURN node to represent loading the SwiftError pseudolocal's value into the error register. We initially keep this node before the GT_RETURN block so we don't have to teach the JIT that it is a new valid terminator for BBJ_RETURN blocks; during lowering, we move this node to the end of the block so the error register won't get trashed after loading the error value. I found this to be an easier approach than trying to replace GT_RETURN with a binary node that holds the regular and error return values.

Old codegen:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T01] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov qword ptr [rbp-0x10], rdi
;; size=6 bbWeight=0.50 PerfScore 0.62
G_M3386_IG05: ;; offset=0x003C
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x0049
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 80, prolog size 27, PerfScore 16.33, instruction count 25, allocated bytes for code 80 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

New codegen:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 1, 1 ) struct ( 8) [rbp-0x10] must-init "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07 ] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08 ] ( 0, 0 ) long -> zero-ref "field V03.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp6 [V09 ] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov edi, 21
mov r12, rdi
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.38
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov r12, rdi
;; size=5 bbWeight=0.50 PerfScore 0.25
G_M3386_IG05: ;; offset=0x003B
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
nop ;; size=10 bbWeight=1 PerfScore 1.75
G_M3386_IG06: ;; offset=0x0045
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 76, prolog size 27, PerfScore 14.96, instruction count 26, allocated bytes for code 76 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Apr 5, 2024
@jakobbotsch

jakobbotsch commented Apr 5, 2024

Copy link
Copy Markdown
Member

We initially keep this node before the GT_RETURN block so we don't have to teach the JIT that it is a new valid terminator for BBJ_RETURN blocks; during lowering, we move this node to the end of the block so the error register won't get trashed after loading the error value. I found this to be an easier approach than trying to replace GT_RETURN with a binary node that holds the regular and error return values.

I'm not sure it's quite that simple due transforms within the JIT that assume knowledge about GT_RETURN and its semantics, and base transformations on this knowledge. This representation makes it error prone to write transforms around GT_RETURN since you aren't forced to handle this new terminator node. It seems intrinsic to me that the JIT has to be taught that this is a new kind of terminator node because of this.
For example, what happens in a function that has a switch with 50 returns in it? I would expect us to do return merging and end up with IR that doesn't handle the error correctly with resulting potential silent bad codegen.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

For example, what happens in a function that has a switch with 50 returns in it? I would expect us to do return merging and end up with IR that doesn't handle the error correctly with resulting potential silent bad codegen.

I see. In that case, let me try substituting GT_SWIFT_ERROR_RET for GT_RETURN then. In the meantime, let's see how much TP we're trading to get this codegen improvement...

@jakobbotsch

Copy link
Copy Markdown
Member

FWIW I think it's very likely we'll end up similarly having to expand the JIT's set of known and handled terminator nodes as part of async2. It has a similar situation with an async continuation that is returned in a different register than usual. I'm pretty sure the prototype has similar issues around return merging as the one I described above.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

FWIW I think it's very likely we'll end up similarly having to expand the JIT's set of known and handled terminator nodes as part of async2.

This might be too radical, but what if we expanded GT_RETURN to have an optional second argument to handle these scenarios, instead of introducing new nodes for them?

@jakobbotsch

Copy link
Copy Markdown
Member

I'm not sure whether it makes things much simpler -- you still have to audit the interesting uses of GT_RETURN on whether they need special handling of the potential second operand. It also becomes a bit easier to forget to handle the second operand. I like the explicitness of just having new types of nodes. In my experience this kind of explicitness pays off in the long run.

A more uniform representation would be to introduce the ability for the JIT to create new class layouts on the fly. In fact we can already do that for layouts without GC pointers, which would be sufficient for Swift (see typGetBlkLayout). Then you would consider a Swift function with error returns to return a tuple (x, y) where y is the error.
We wouldn't be able to handle something like this efficiently today, but it's sort of on my list in #93105 (the items around supporting FIELD_LIST for returns).

@jakobbotsch

jakobbotsch commented Apr 5, 2024

Copy link
Copy Markdown
Member

I think the uniform representation above would also generalize to GT_CALL with swift error returns where we would similarly create a new layout for the return that defines the error too, allowing us to remove GT_SWIFT_ERROR. But again, this requires a lot of work to handle efficiently -- it's essentially the "allow struct fields of physically promoted call returns to stay in registers" of #93105.
We would also need to generalize the ABI information carried in ReturnTypeDesc so that the frontend can set this up and have the backend automatically handle the details around getting the values out of the corresponding registers (for GT_CALL) or putting them into the right registers (for GT_RETURN).

It's probably a lot of work, but seems like it would be the right long term direction.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

I have a binop implementation of GT_SWIFT_ERROR_RET working locally. There are some quirks that I have to clean up and document before pushing, but the codegen looks slightly better:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 1, 1 ) struct ( 8) [rbp-0x10] must-init "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T02] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
; V08 tmp5 [V08,T01] ( 3, 2 ) long -> rbx "field V03.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp6 [V09,T03] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov ebx, 21
jmp SHORT G_M3386_IG05
;; size=9 bbWeight=0.50 PerfScore 1.25
G_M3386_IG04: ;; offset=0x0033
xor ebx, ebx
;; size=2 bbWeight=0.50 PerfScore 0.12
G_M3386_IG05: ;; offset=0x0035
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, rbx
;; size=12 bbWeight=1 PerfScore 1.75
G_M3386_IG06: ;; offset=0x0041
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 72, prolog size 27, PerfScore 14.71, instruction count 24, allocated bytes for code 72 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

@jakobbotsch PTAL. GT_SWIFT_ERROR_RET now takes the error value as its first operand, and the normal return value as its second operand. I wanted to make the normal return value the first operand to match GT_RETURN, but if the method has a void return type, then it would be possible for the first operand of GT_SWIFT_ERROR_RET to be null -- modeling this in the JIT isn't elegant, so I think it's better to flip the operands, and swap them whenever we can handle GT_RETURN and GT_SWIFT_ERROR_RET nodes simultaneously to reduce code duplication.

@jakobbotsch

Copy link
Copy Markdown
Member

I'll take a closer look tomorrow, but I'm curious why the null op1 didn't work out given that we have some prior art with GenTree::NullOp1Legal. Wasn't that sufficient?

Might be a bit cleaner to introduce something like

GenTree* GetReturnedValue(GenTreeOp* op)
{
assert(op->OperIs(GT_RETURN, GT_RETFILT, GT_SWIFT_ERROR_RET));
return op->OperIs(GT_SWIFT_ERROR_RET) ? op->gtGetOp2() : op->gtGetOp1();
}

Mutating GenTree at that point seems a bit iffy...

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

I'll take a closer look tomorrow, but I'm curious why the null op1 didn't work out given that we have some prior art with GenTree::NullOp1Legal. Wasn't that sufficient?

I initially tried that, but when handling simple operators in a bunch of places, we either assume binops have a first operand, or we assume the lack of a first operand implies the second operand is null as well (like in Compiler::gtSetEvalOrder). NullOp1Legal handles some binops already, but we only call it when handling unops. Perhaps I moved on from trying to match the operand order of GT_RETURN too quickly, though I think your proposed GetReturnedValue would work nicely; right now, I think we'd only need it in a few places.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/gtlist.h Outdated

GTNODE(SWIFT_ERROR , GenTree ,0,0,GTK_LEAF) // Error register value post-Swift call
GTNODE(SWIFT_ERROR , GenTree ,0,0,GTK_LEAF) // Error register value post-Swift call
GTNODE(SWIFT_ERROR_RET , GenTreeOp ,0,1,GTK_BINOP|GTK_NOVALUE) // Returns normal return value, and SwiftError pseudolocal's value in REG_SWIFT_ERROR

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice with a comment that op1 has the error and op2 has the normal return value (or null if method returns void)

Comment threadsrc/coreclr/jit/importer.cpp Outdated
Comment on lines +10887 to +10899
// If this method has a SwiftError* out parameter, we need to set the error register upon returning.
// By placing the GT_SWIFT_ERROR_RET node before the GT_RETURN node,
// we don't have to teach the JIT that GT_SWIFT_ERROR_RET is a valid terminator for BBJ_RETURN blocks.
// During lowering, we will move the GT_SWIFT_ERROR_RET to the end of the block
// so the error register won't get trashed when returning the normal value.
if (lvaSwiftErrorArg != BAD_VAR_NUM)
{
assert(info.compCallConv == CorInfoCallConvExtension::Swift);
assert(lvaSwiftErrorLocal != BAD_VAR_NUM);
GenTree* const swiftErrorNode = gtNewLclFldNode(lvaSwiftErrorLocal, TYP_I_IMPL, 0);
op1 = new (this, GT_SWIFT_ERROR_RET)
GenTreeOp(GT_SWIFT_ERROR_RET, op1->TypeGet(), swiftErrorNode, op1->gtGetOp1());
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is outdated now, right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use op1->SetOper(GT_SWIFT_ERROR_RET) instead here to avoid the new node allocation. Of course that requires some shuffling of the operands, so just allocating a new node is also ok with me, if you like that better feel free to ignore this.

Comment threadsrc/coreclr/jit/lower.cpp Outdated
Comment on lines +4640 to +4648
GenTree* Lowering::LowerSwiftErrorRet(GenTree* swiftErrorRet)
{
assert(swiftErrorRet->OperIs(GT_SWIFT_ERROR_RET));
GenTree* const nextNode = swiftErrorRet->gtNext;
LIR::Range& blockRange = BlockRange();
blockRange.Remove(swiftErrorRet);
blockRange.InsertAtEnd(swiftErrorRet);
return nextNode;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this isn't needed anymore?

Comment on lines +2582 to +2605
for (BasicBlock* block : Blocks())
{
if (block->KindIs(BBJ_RETURN))
{
GenTree* const ret = block->lastNode();
assert(ret->OperIs(GT_RETURN));
ret->SetOperRaw(GT_SWIFT_ERROR_RET);
ret->AsOp()->gtOp2 = ret->AsOp()->gtOp1;

// If this is the merged return block, use the merged return error local as the error operand.
// Else, load the error value from the SwiftError pseudolocal (this will probably get promoted, anyway).
if (block == genReturnBB)
{
assert(genReturnErrorLocal == BAD_VAR_NUM);
genReturnErrorLocal = lvaGrabTemp(true DEBUGARG("Single return block SwiftError value"));
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);
ret->AsOp()->gtOp1 = gtNewLclvNode(genReturnErrorLocal, TYP_I_IMPL);
}
else
{
ret->AsOp()->gtOp1 = gtNewLclFldNode(lvaSwiftErrorLocal, TYP_I_IMPL, 0);
}
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think it would be nice to do the replacement of lvaSwiftErrorArg with lvaSwiftErrorLocal here too.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my plan, though I was hitting asserts around fgNodeThreading being invalid when trying to use any of the GenTree iterators -- LocalsGenTreeList seems like it would be the most efficient one for finding the SwiftError* arg uses. Is there a clean way of doing the IR traversal without breaking any of these iterators' invariants?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point we don't have threaded IR nodes (the comment on fgNodeThreading has some relevant information), but you can use GenTreeVisitor to do it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optReplaceScalarUsesWithConst looks close to what you want to do I think.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optReplaceScalarUsesWithConst looks close to what you want to do I think.

Thanks! GenTreeVisitor works.

Comment threadsrc/coreclr/jit/ifconversion.cpp Outdated
}

case GT_RETURN:
case GT_SWIFT_ERROR_RET:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if-conversion supports GT_RETURN to transform

JTRUE(EQ(x, 0)) -> BB01, BB02
BB01:
RETURN(0)
BB02:
RETURN(1)

into

RETURN(SELECT(EQ(x, 0), 0, 1))

I don't see us being able to support this transform for GT_SWIFT_ERROR_RET, so I think we can't add this support here.

Comment threadsrc/coreclr/jit/morph.cpp Outdated
Comment on lines +14119 to +14121
const DebugInfo& di = lastStmt->GetDebugInfo();
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1(), CHECK_SPILL_NONE, nullptr, di, block);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const DebugInfo& di = lastStmt->GetDebugInfo();
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1(), CHECK_SPILL_NONE, nullptr, di, block);
GenTree* swiftErrorStore =
gtNewTempStore(genReturnErrorLocal, ret->gtGetOp1());

(For pAfterStmt == nullptr these last parameters aren't relevant)

Comment on lines +11700 to +11711
case GT_SWIFT_ERROR_RET:
if (tree->gtGetOp2() != nullptr)
{
tree->gtVNPair = vnStore->VNPWithExc(vnStore->VNPForVoid(),
vnStore->VNPExceptionSet(tree->gtGetOp2()->gtVNPair));
}
else
{
tree->gtVNPair = vnStore->VNPForVoid();
}
break;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have the exceptions from the error operand propagated too.

Comment on lines +2597 to +2604
fgWalkResult PreOrderVisit(GenTree** use, GenTree* user)
{
if ((*use)->OperIs(GT_LCL_VAR) && ((*use)->AsLclVarCommon()->GetLclNum() == m_compiler->lvaSwiftErrorArg))
{
*use = m_compiler->gtNewLclVarAddrNode(m_compiler->lvaSwiftErrorLocal, genActualType(*use));
}

return fgWalkResult::WALK_CONTINUE;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we BADCODE here if we see a use of the error parameter that isn't a GT_LCL_VAR? E.g. any store or taking the address of it.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems reasonable to me. Do you know if we have any plans upstream to regulate the usage of the error parameter? I imagine we wouldn't want the JIT to do these checks alone.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. cc @jkoritzinsky@kotlarmilos. Does it seem appropriate to you to put restrictions on what the IL can do with the SwiftError* parameter? Specifically we're interested in disallowing stores and disallowing taking its address.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
{
if (!(*use)->OperIs(GT_LCL_VAR))
{
BADCODE("Expected GT_LCL_VAR of SwiftError* parameter, got other GenTreeLclVarCommon node");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the message should be phrased in terms of IL, not IR constructs

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would something like "Found invalid use of SwiftError* parameter" be too vague?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good to me

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
{
assert(genReturnErrorLocal == BAD_VAR_NUM);
genReturnErrorLocal = lvaGrabTemp(true DEBUGARG("Single return block SwiftError value"));
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
lvaGetDesc(genReturnErrorLocal)->lvType = genActualType(TYP_I_IMPL);
lvaGetDesc(genReturnErrorLocal)->lvType = TYP_I_IMPL;

Comment threadsrc/coreclr/jit/ifconversion.cpp Outdated
Comment on lines +143 to +152
#ifdef SWIFT_SUPPORT
if (m_comp->lvaSwiftErrorArg != BAD_VAR_NUM)
{
m_mainOper = GT_SWIFT_ERROR_RET;
}
else
#endif // SWIFT_SUPPORT
{
m_mainOper = GT_RETURN;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these changes necessary?
It looks like IfConvertCheckStmts will fail naturally for GT_SWIFT_ERROR_RET, so perhaps no changes in if-conversion are necessary?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, we don't check m_mainOper until after we call IfConvertCheckStmts, and we bail if the terminator node is a GT_SWIFT_ERROR_RET, so it should be safe to undo these changes.

Comment on lines +523 to 527
case GT_SWIFT_ERROR_RET:
LowerNode(node->gtGetOp1());

FALLTHROUGH;
case GT_RETURN:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
caseGT_SWIFT_ERROR_RET:
LowerNode(node->gtGetOp1());
FALLTHROUGH;
caseGT_RETURN:
caseGT_SWIFT_ERROR_RET:
caseGT_RETURN:

Otherwise we invoke lowering of op1 twice.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LowerRet should probably be changed to take GenTreeOp* instead.

tree->SetAllEffectsFlags(tree->AsOp()->gtGetOp1());
tree->SetAllEffectsFlags(retVal);

return tree;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case needs to invoke morph on op1 as well for GT_SWIFT_ERROR_RET (I am not sure why the code returns here in the first place, it probably ought to just fall through after inserting the cast)

Comment threadsrc/coreclr/jit/morph.cpp

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great to me now. Thanks for your diligence in implementing this!

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

This looks great to me now. Thanks for your diligence in implementing this!

Thank you for all the reviews! In case you're curious, here's the updated codegen for ConditionallySetErrorTo21:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rsp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> r12 single-def
; V02 tmp0 [V02 ] ( 1, 1 ) struct ( 8) [rsp+0x10] "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V03 OutArgs [V03 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V04 tmp2 [V04 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
;* V05 tmp3 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp4 [V06 ] ( 3, 3 ) struct (16) [rsp+0x00] do-not-enreg[XS] addr-exposed "Reverse Pinvoke FrameVar"
; V07 tmp5 [V07,T01] ( 2, 4 ) long -> r12 "Single return block SwiftError value"
;* V08 tmp6 [V08 ] ( 0, 0 ) long -> zero-ref "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V09 tmp7 [V09 ] ( 0, 0 ) long -> zero-ref "field V04.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V10 tmp8 [V10 ] ( 0, 0 ) long -> zero-ref "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
sub rsp, 24
mov r12d, edi
;; size=7 bbWeight=1 PerfScore 0.50
G_M3386_IG02: ;; offset=0x0007
lea rdi, [rsp]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
xor edi, edi
mov eax, 21
test r12d, r12d
mov r12, rax
cmove r12, rdi
lea rdi, [rsp]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
nop
;; size=36 bbWeight=1 PerfScore 4.50
G_M3386_IG03: ;; offset=0x002B
add rsp, 24
ret
;; size=5 bbWeight=1 PerfScore 1.25
; Total bytes of code 48, prolog size 4, PerfScore 6.25, instruction count 14, allocated bytes for code 48 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

Quite the improvement.

@amanasifkhalid
amanasifkhalid merged commit ec76487 into dotnet:mainApr 12, 2024
@amanasifkhalid
amanasifkhalid deleted the swift-error-reg-ret branch April 12, 2024 19:26
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
…return (dotnet#100692)
Follow-up to dotnet#100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 13, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@amanasifkhalid@jakobbotsch