-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
Originally discovered in #104731 (comment)
When a delegate references collectible code (dynamic method or collectible assembly), and being the only thing keeping the code alive, the code may be collected before invoked.
Consider the following method:
[MethodImpl(MethodImplOptions.NoInlining)]
static void Test(Action<int> action, int counts)
{
int sum = 0;
for (int i = 0; i < counts; i++)
{
sum += i;
}
action(sum);
}
Current codegen on arm64:
; Assembly listing for method CSPlayground.Program:Test(System.Action`1[int],int) (FullOpts)
; Emitting BLENDED_CODE for generic ARM64 - Windows
; FullOpts code
; optimized code
; fp based frame
; fully interruptible
; No PGO data
; invoked as altjit
; Final local variable assignments
;
; V00 arg0 [V00,T03] ( 4, 4 ) ref -> x2 class-hnd single-def <System.Action`1[int]>
; V01 arg1 [V01,T02] ( 4, 7 ) int -> x1 single-def
; V02 loc0 [V02,T01] ( 4, 10 ) int -> x0
; V03 loc1 [V03,T00] ( 5, 17 ) int -> x3
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [sp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;
; Lcl frame size = 0
G_M16562_IG01: ; bbWeight=1, gcrefRegs=0000 {}, byrefRegs=0000 {}, byref, nogc <-- Prolog IG
stp fp, lr, [sp, #-0x10]!
mov fp, sp
mov x2, x0
; gcrRegs +[x2]
;; size=12 bbWeight=1 PerfScore 2.00
G_M16562_IG02: ; bbWeight=1, gcrefRegs=0004 {x2}, byrefRegs=0000 {}, byref, isz, align
mov w0, wzr
mov w3, wzr
cmp w1, #0
ble G_M16562_IG04
align [4 bytes for IG03]
align [0 bytes]
align [0 bytes]
align [0 bytes]
;; size=20 bbWeight=1 PerfScore 3.00
G_M16562_IG03: ; bbWeight=4, gcrefRegs=0004 {x2}, byrefRegs=0000 {}, loop=IG03, byref, isz
add w0, w0, w3
add w3, w3, #1
cmp w3, w1
blt G_M16562_IG03
;; size=16 bbWeight=4 PerfScore 10.00
G_M16562_IG04: ; bbWeight=1, gcrefRegs=0004 {x2}, byrefRegs=0000 {}, byref
mov w1, w0
ldr x0, [x2, #0x08]
; gcrRegs +[x0]
ldr x2, [x2, #0x18]
; gcrRegs -[x2]
blr x2
; gcrRegs -[x0]
; gcr arg pop 0
;; size=16 bbWeight=1 PerfScore 7.50
G_M16562_IG05: ; bbWeight=1, epilog, nogc, extend
ldp fp, lr, [sp], #0x10
ret lr
;; size=8 bbWeight=1 PerfScore 2.00
; Total bytes of code 72, prolog size 12, PerfScore 24.50, instruction count 21, allocated bytes for code 72 (MethodHash=da6bbf4d) for method CSPlayground.Program:Test(System.Action`1[int],int) (FullOpts)
The ldr x2, [x2, #0x18]
instruction loads the function pointer from the delegate object. Now no register holds the delegate object, and GC is not aware of the function pointer in x2
. If GC happens before the next blr x2
instruction, it may collect the delegate object and associated code.
The loop in front of the invocation makes the method fully interruptible, and thus GC can happen at any instruction except prolog/epilog.
All RISC architectures should be suffered from this issue. In xarch, loading the function pointer can be contained in the call instruction like call [rax+0x18]
, so GC won't lose the track.