Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit e08df99

Browse files
committed
Avoid creating illegal byref pointers
Byref pointers need to point within their "host" object -- thus the alternate name "interior pointers". If the JIT creates and reports a pointer as a "byref", but it points outside the host object, and a GC occurs that moves the host object, the byref pointer will not be updated. If a subsequent calculation puts the byref "back" into the host object, it will actually be pointing to garbage, since the host object has moved. This occurred on ARM with array index calculations, in particular because ARM doesn't have a single-instruction "base + scale*index + offset" addressing mode. Thus, we were generating, for the jaggedarr_cs_do test case, `ProcessJagged3DArray()` function: ``` // r0 = array object, r6 = computed index offset. We mark r4 as a byref. add r4, r0, r6 // r4 - 32 is the offset of the object we care about. Then we load the array element. // In this case, the loaded element is a gcref, so r4 becomes a gcref. ldr r4, [r4-32] ``` We get this math because the user code uses `a[i - 10]`, which is essentially `a + (i - 10) * 4 + 8` for element size 4. This is optimized to `a + i * 4 - 32`. In the above code, `r6` is `i * 4`. In this case, after the first instruction, `r4` can point beyond the array. If a GC happens, `r4` isn't updated, and the second instruction loads garbage. There are two fixes: 1. Change array morphing in `fgMorphArrayIndex()` to rearrange the array index IR node creation to only create a byref pointer that is precise, and no "intermediate" byref pointers that don't represent the actual array element address being computed. 2. Change `fgMoveOpsLeft()` to prevent the left-weighted reassociation optimization `[byref]+ (ref, [int]+ (int, int)) => [byref]+ ([byref]+ (ref, int), int)`. This optimization creates "incorrect" byrefs that don't necessarily point within the host object. These fixes are all-platform. Fixes #17517. There are many, many diffs. They, perhaps surprisingly, overwhelmingly positive. For AMD64 SuperPMI, the diffs are a 7.6% size win for 5194 functions! This appears to be due to less code cloning, and sometimes better optimization. For ARM32 ngen-based desktop asm diffs, it is a 0.30% improvement across all framework assemblies. A lot of the diffs seem to be because we CSE the entire array address offset expression, not just the index expression.
1 parent 4eb8b37 commit e08df99

File tree

1 file changed

+30
-6
lines changed

1 file changed

+30
-6
lines changed

src/jit/morph.cpp

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5810,6 +5810,21 @@ void Compiler::fgMoveOpsLeft(GenTree* tree)
58105810
break;
58115811
}
58125812

5813+
// Don't split up a byref calculation and create a new byref. E.g.,
5814+
// [byref]+ (ref, [int]+ (int, int)) => [byref]+ ([byref]+ (ref, int), int).
5815+
// Doing this transformation could create a situation where the first
5816+
// addition (that is, [byref]+ (ref, int) ) creates a byref pointer that
5817+
// no longer points within the ref object. If a GC happens, the byref won't
5818+
// get updated. This can happen, for instance, if one of the int components
5819+
// is negative. It also requires the address generation be in a fully-interruptible
5820+
// code region.
5821+
//
5822+
if (varTypeIsGC(op1->TypeGet()) && op2->TypeGet() == TYP_I_IMPL)
5823+
{
5824+
noway_assert(varTypeIsGC(tree->TypeGet()) && (oper == GT_ADD));
5825+
break;
5826+
}
5827+
58135828
/* Change "(x op (y op z))" to "(x op y) op z" */
58145829
/* ie. "(op1 op (ad1 op ad2))" to "(op1 op ad1) op ad2" */
58155830

@@ -6186,7 +6201,7 @@ GenTree* Compiler::fgMorphArrayIndex(GenTree* tree)
61866201

61876202
// Create the "addr" which is "*(arrRef + ((index * elemSize) + elemOffs))"
61886203

6189-
GenTree* addr;
6204+
GenTree* scaledIndex;
61906205

61916206
#ifdef _TARGET_64BIT_
61926207
// Widen 'index' on 64-bit targets
@@ -6217,22 +6232,31 @@ GenTree* Compiler::fgMorphArrayIndex(GenTree* tree)
62176232
size->gtFlags |= GTF_DONT_CSE;
62186233

62196234
/* Multiply by the array element size */
6220-
addr = gtNewOperNode(GT_MUL, TYP_I_IMPL, index, size);
6235+
scaledIndex = gtNewOperNode(GT_MUL, TYP_I_IMPL, index, size);
62216236
}
62226237
else
62236238
{
6224-
addr = index;
6239+
scaledIndex = index;
62256240
}
62266241

6227-
/* Add the object ref to the element's offset */
6242+
// Be careful to only create the byref pointer when the full index expression is added to the array reference.
6243+
// We don't want to create a partial byref address expression that doesn't include the full index offset:
6244+
// a byref must point within the containing object. It is dangerous (especially when optimizations come into
6245+
// play) to create a "partial" byref that doesn't point exactly to the correct object; there is risk that
6246+
// the partial byref will not point within the object, and thus not get updated correctly during a GC.
6247+
// This is mostly a risk in fully-interruptible code regions.
62286248

6229-
addr = gtNewOperNode(GT_ADD, TYP_BYREF, arrRef, addr);
6249+
GenTree* addr;
62306250

62316251
/* Add the first element's offset */
62326252

62336253
GenTree* cns = gtNewIconNode(elemOffs, TYP_I_IMPL);
62346254

6235-
addr = gtNewOperNode(GT_ADD, TYP_BYREF, addr, cns);
6255+
addr = gtNewOperNode(GT_ADD, TYP_I_IMPL, scaledIndex, cns);
6256+
6257+
/* Add the object ref to the element's offset */
6258+
6259+
addr = gtNewOperNode(GT_ADD, TYP_BYREF, arrRef, addr);
62366260

62376261
#if SMALL_TREE_NODES
62386262
assert((tree->gtDebugFlags & GTF_DEBUG_NODE_LARGE) || GenTree::s_gtNodeSizes[GT_IND] == TREE_NODE_SZ_SMALL);

0 commit comments

Comments
 (0)