Skip to content

Commit 6d18b06

Browse files
Fix loop cloning of array of struct with array field (#67130)
Loop cloning needs to parse what morph creates from GT_INDEX nodes to determine if there are array accesses with bounds checks that could potentially be optimized. For jagged array access, this can be a "comma chain" of bounds checks and array element address expressions. For a case where an array of structs had a struct field, such as `ValueTuple<int[], int>[]`, cloning was confusing the expression `a[i].Item1[j]` for the jagged array access `a[i][j]`. The fix here is to keep track of the type of the `GT_INDEX` node that is being morphed, in the `GT_BOUNDS_CHECK` node that is created for it. (This is the only thing cloning parses, to avoid the need to parse the very complex trees morph can create.) This type is then checked when parsing the "comma chain" trees. If a non-`TYP_REF` is found (such as a `TYP_STRUCT` in the above example), no more levels of array indexing are considered. (`TYP_REF` is what an array object would have, for a jagged array.) Fixes #66254.
1 parent cdfd731 commit 6d18b06

File tree

8 files changed

+284
-30
lines changed

8 files changed

+284
-30
lines changed

src/coreclr/jit/compiler.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8088,8 +8088,9 @@ class Compiler
80888088
};
80898089

80908090
bool optIsStackLocalInvariant(unsigned loopNum, unsigned lclNum);
8091-
bool optExtractArrIndex(GenTree* tree, ArrIndex* result, unsigned lhsNum);
8092-
bool optReconstructArrIndex(GenTree* tree, ArrIndex* result, unsigned lhsNum);
8091+
bool optExtractArrIndex(GenTree* tree, ArrIndex* result, unsigned lhsNum, bool* topLevelIsFinal);
8092+
bool optReconstructArrIndexHelp(GenTree* tree, ArrIndex* result, unsigned lhsNum, bool* topLevelIsFinal);
8093+
bool optReconstructArrIndex(GenTree* tree, ArrIndex* result);
80938094
bool optIdentifyLoopOptInfo(unsigned loopNum, LoopCloneContext* context);
80948095
static fgWalkPreFn optCanOptimizeByLoopCloningVisitor;
80958096
fgWalkResult optCanOptimizeByLoopCloning(GenTree* tree, LoopCloneVisitorInfo* info);

src/coreclr/jit/compiler.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3556,7 +3556,7 @@ inline bool Compiler::LoopDsc::lpArrLenLimit(Compiler* comp, ArrIndex* index) co
35563556
// We have a[i].length, extract a[i] pattern.
35573557
else if (limit->AsArrLen()->ArrRef()->gtOper == GT_COMMA)
35583558
{
3559-
return comp->optReconstructArrIndex(limit->AsArrLen()->ArrRef(), index, BAD_VAR_NUM);
3559+
return comp->optReconstructArrIndex(limit->AsArrLen()->ArrRef(), index);
35603560
}
35613561
return false;
35623562
}

src/coreclr/jit/gentree.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7664,6 +7664,7 @@ GenTree* Compiler::gtCloneExpr(
76647664
GenTreeBoundsChk(tree->AsBoundsChk()->GetIndex(), tree->AsBoundsChk()->GetArrayLength(),
76657665
tree->AsBoundsChk()->gtThrowKind);
76667666
copy->AsBoundsChk()->gtIndRngFailBB = tree->AsBoundsChk()->gtIndRngFailBB;
7667+
copy->AsBoundsChk()->gtInxType = tree->AsBoundsChk()->gtInxType;
76677668
break;
76687669

76697670
case GT_LEA:

src/coreclr/jit/gentree.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5876,8 +5876,16 @@ struct GenTreeBoundsChk : public GenTreeOp
58765876
BasicBlock* gtIndRngFailBB; // Basic block to jump to for index-out-of-range
58775877
SpecialCodeKind gtThrowKind; // Kind of throw block to branch to on failure
58785878

5879+
// Store some information about the array element type that was in the GT_INDEX node before morphing.
5880+
// Note that this information is also stored in the m_arrayInfoMap of the morphed IND node (that
5881+
// is marked with GTF_IND_ARR_INDEX), but that can be hard to find.
5882+
var_types gtInxType; // Save the GT_INDEX type
5883+
58795884
GenTreeBoundsChk(GenTree* index, GenTree* length, SpecialCodeKind kind)
5880-
: GenTreeOp(GT_BOUNDS_CHECK, TYP_VOID, index, length), gtIndRngFailBB(nullptr), gtThrowKind(kind)
5885+
: GenTreeOp(GT_BOUNDS_CHECK, TYP_VOID, index, length)
5886+
, gtIndRngFailBB(nullptr)
5887+
, gtThrowKind(kind)
5888+
, gtInxType(TYP_UNKNOWN)
58815889
{
58825890
gtFlags |= GTF_EXCEPT;
58835891
}

src/coreclr/jit/loopcloning.cpp

Lines changed: 155 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2140,9 +2140,10 @@ bool Compiler::optIsStackLocalInvariant(unsigned loopNum, unsigned lclNum)
21402140
// optExtractArrIndex: Try to extract the array index from "tree".
21412141
//
21422142
// Arguments:
2143-
// tree the tree to be checked if it is the array [] operation.
2144-
// result the extracted GT_INDEX information is updated in result.
2145-
// lhsNum for the root level (function is recursive) callers should pass BAD_VAR_NUM.
2143+
// tree the tree to be checked if it is the array [] operation.
2144+
// result the extracted GT_INDEX information is updated in result.
2145+
// lhsNum for the root level (function is recursive) callers should pass BAD_VAR_NUM.
2146+
// topLevelIsFinal OUT: set to `true` if see a non-TYP_REF element type array.
21462147
//
21472148
// Return Value:
21482149
// Returns true if array index can be extracted, else, return false. See assumption about
@@ -2203,7 +2204,7 @@ bool Compiler::optIsStackLocalInvariant(unsigned loopNum, unsigned lclNum)
22032204
// used as an index expression, or array base var is used as the array base. This saves us from parsing
22042205
// all the forms that morph can create, especially for arrays of structs.
22052206
//
2206-
bool Compiler::optExtractArrIndex(GenTree* tree, ArrIndex* result, unsigned lhsNum)
2207+
bool Compiler::optExtractArrIndex(GenTree* tree, ArrIndex* result, unsigned lhsNum, bool* topLevelIsFinal)
22072208
{
22082209
if (tree->gtOper != GT_COMMA)
22092210
{
@@ -2247,37 +2248,31 @@ bool Compiler::optExtractArrIndex(GenTree* tree, ArrIndex* result, unsigned lhsN
22472248
result->useBlock = compCurBB;
22482249
result->rank++;
22492250

2251+
// If the array element type (saved from the GT_INDEX node during morphing) is anything but
2252+
// TYP_REF, then it must the the final level of jagged array.
2253+
assert(arrBndsChk->gtInxType != TYP_VOID);
2254+
*topLevelIsFinal = (arrBndsChk->gtInxType != TYP_REF);
2255+
22502256
return true;
22512257
}
22522258

22532259
//---------------------------------------------------------------------------------------------------------------
2254-
// optReconstructArrIndex: Reconstruct array index.
2260+
// optReconstructArrIndexHelp: Helper function for optReconstructArrIndex. See that function for more details.
22552261
//
22562262
// Arguments:
2257-
// tree the tree to be checked if it is an array [][][] operation.
2258-
// result OUT: the extracted GT_INDEX information.
2259-
// lhsNum for the root level (function is recursive) callers should pass BAD_VAR_NUM.
2263+
// tree the tree to be checked if it is an array [][][] operation.
2264+
// result OUT: the extracted GT_INDEX information.
2265+
// lhsNum var number of array object we're looking for.
2266+
// topLevelIsFinal OUT: set to `true` if we reached a non-TYP_REF element type array.
22602267
//
22612268
// Return Value:
22622269
// Returns true if array index can be extracted, else, return false. "rank" field in
2263-
// "result" contains the array access depth. The "indLcls" fields contain the indices.
2264-
//
2265-
// Operation:
2266-
// Recursively look for a list of array indices. For example, if the tree is
2267-
// V03 = (V05 = V00[V01]), V05[V02]
2268-
// that corresponds to access of V00[V01][V02]. The return value would then be:
2269-
// ArrIndex result { arrLcl: V00, indLcls: [V01, V02], rank: 2 }
2270-
//
2271-
// Note that the array expression is implied by the array bounds check under the COMMA, and the array bounds
2272-
// checks is what is parsed from the morphed tree; the array addressing expression is not parsed.
2273-
//
2274-
// Assumption:
2275-
// The method extracts only if the array base and indices are GT_LCL_VAR.
2270+
// "result" contains the array access depth. The "indLcls" field contains the indices.
22762271
//
2277-
bool Compiler::optReconstructArrIndex(GenTree* tree, ArrIndex* result, unsigned lhsNum)
2272+
bool Compiler::optReconstructArrIndexHelp(GenTree* tree, ArrIndex* result, unsigned lhsNum, bool* topLevelIsFinal)
22782273
{
22792274
// If we can extract "tree" (which is a top level comma) return.
2280-
if (optExtractArrIndex(tree, result, lhsNum))
2275+
if (optExtractArrIndex(tree, result, lhsNum, topLevelIsFinal))
22812276
{
22822277
return true;
22832278
}
@@ -2294,18 +2289,152 @@ bool Compiler::optReconstructArrIndex(GenTree* tree, ArrIndex* result, unsigned
22942289
GenTree* rhs = before->gtGetOp2();
22952290

22962291
// "rhs" should contain an GT_INDEX
2297-
if (!lhs->IsLocal() || !optReconstructArrIndex(rhs, result, lhsNum))
2292+
if (!lhs->IsLocal() || !optReconstructArrIndexHelp(rhs, result, lhsNum, topLevelIsFinal))
22982293
{
22992294
return false;
23002295
}
2296+
2297+
// If rhs represents an array of elements other than arrays (e.g., an array of structs),
2298+
// then we can't go any farther.
2299+
if (*topLevelIsFinal)
2300+
{
2301+
return false;
2302+
}
2303+
23012304
unsigned lhsNum = lhs->AsLclVarCommon()->GetLclNum();
23022305
GenTree* after = tree->gtGetOp2();
23032306
// Pass the "lhsNum", so we can verify if indeed it is used as the array base.
2304-
return optExtractArrIndex(after, result, lhsNum);
2307+
return optExtractArrIndex(after, result, lhsNum, topLevelIsFinal);
23052308
}
23062309
return false;
23072310
}
23082311

2312+
//---------------------------------------------------------------------------------------------------------------
2313+
// optReconstructArrIndex: Reconstruct array index from a post-morph tree.
2314+
//
2315+
// Arguments:
2316+
// tree the tree to be checked if it is an array [][][] operation.
2317+
// result OUT: the extracted GT_INDEX information.
2318+
//
2319+
// Return Value:
2320+
// Returns true if array index can be extracted, else, return false. "rank" field in
2321+
// "result" contains the array access depth. The "indLcls" field contains the indices.
2322+
//
2323+
// Operation:
2324+
// Recursively look for a list of array indices. For example, if the tree is
2325+
// V03 = (V05 = V00[V01]), V05[V02]
2326+
// that corresponds to access of V00[V01][V02]. The return value would then be:
2327+
// ArrIndex result { arrLcl: V00, indLcls: [V01, V02], rank: 2 }
2328+
//
2329+
// Note that the array expression is implied by the array bounds check under the COMMA, and the array bounds
2330+
// checks is what is parsed from the morphed tree; the array addressing expression is not parsed.
2331+
// However, the array bounds checks are not quite sufficient because of the way "morph" alters the trees.
2332+
// Specifically, we normally see a COMMA node with a LHS of the morphed array INDEX expression and RHS
2333+
// of the bounds check. E.g., for int[][], a[i][j] we have a pre-morph tree:
2334+
//
2335+
// \--* INDEX int
2336+
// +--* INDEX ref
2337+
// | +--* LCL_VAR ref V00 loc0
2338+
// | \--* LCL_VAR int V02 loc2
2339+
// \--* LCL_VAR int V03 loc3
2340+
//
2341+
// and post-morph tree:
2342+
//
2343+
// \--* COMMA int
2344+
// +--* ASG ref
2345+
// | +--* LCL_VAR ref V19 tmp12
2346+
// | \--* COMMA ref
2347+
// | +--* BOUNDS_CHECK_Rng void
2348+
// | | +--* LCL_VAR int V02 loc2
2349+
// | | \--* ARR_LENGTH int
2350+
// | | \--* LCL_VAR ref V00 loc0
2351+
// | \--* IND ref
2352+
// | \--* ADD byref
2353+
// | +--* LCL_VAR ref V00 loc0
2354+
// | \--* ADD long
2355+
// | +--* LSH long
2356+
// | | +--* CAST long <- uint
2357+
// | | | \--* LCL_VAR int V02 loc2
2358+
// | | \--* CNS_INT long 3
2359+
// | \--* CNS_INT long 16 Fseq[#FirstElem]
2360+
// \--* COMMA int
2361+
// +--* BOUNDS_CHECK_Rng void
2362+
// | +--* LCL_VAR int V03 loc3
2363+
// | \--* ARR_LENGTH int
2364+
// | \--* LCL_VAR ref V19 tmp12
2365+
// \--* IND int
2366+
// \--* ADD byref
2367+
// +--* LCL_VAR ref V19 tmp12
2368+
// \--* ADD long
2369+
// +--* LSH long
2370+
// | +--* CAST long <- uint
2371+
// | | \--* LCL_VAR int V03 loc3
2372+
// | \--* CNS_INT long 2
2373+
// \--* CNS_INT long 16 Fseq[#FirstElem]
2374+
//
2375+
// However, for an array of structs that contains an array field, e.g. ValueTuple<int[], int>[], expression
2376+
// a[i].Item1[j],
2377+
//
2378+
// \--* INDEX int
2379+
// +--* FIELD ref Item1
2380+
// | \--* ADDR byref
2381+
// | \--* INDEX struct<System.ValueTuple`2[System.Int32[],System.Int32], 16>
2382+
// | +--* LCL_VAR ref V01 loc1
2383+
// | \--* LCL_VAR int V04 loc4
2384+
// \--* LCL_VAR int V06 loc6
2385+
//
2386+
// Morph "hoists" the bounds check above the struct field access:
2387+
//
2388+
// \--* COMMA int
2389+
// +--* ASG ref
2390+
// | +--* LCL_VAR ref V23 tmp16
2391+
// | \--* COMMA ref
2392+
// | +--* BOUNDS_CHECK_Rng void
2393+
// | | +--* LCL_VAR int V04 loc4
2394+
// | | \--* ARR_LENGTH int
2395+
// | | \--* LCL_VAR ref V01 loc1
2396+
// | \--* IND ref
2397+
// | \--* ADDR byref Zero Fseq[Item1]
2398+
// | \--* IND struct<System.ValueTuple`2[System.Int32[],System.Int32], 16>
2399+
// | \--* ADD byref
2400+
// | +--* LCL_VAR ref V01 loc1
2401+
// | \--* ADD long
2402+
// | +--* LSH long
2403+
// | | +--* CAST long <- uint
2404+
// | | | \--* LCL_VAR int V04 loc4
2405+
// | | \--* CNS_INT long 4
2406+
// | \--* CNS_INT long 16 Fseq[#FirstElem]
2407+
// \--* COMMA int
2408+
// +--* BOUNDS_CHECK_Rng void
2409+
// | +--* LCL_VAR int V06 loc6
2410+
// | \--* ARR_LENGTH int
2411+
// | \--* LCL_VAR ref V23 tmp16
2412+
// \--* IND int
2413+
// \--* ADD byref
2414+
// +--* LCL_VAR ref V23 tmp16
2415+
// \--* ADD long
2416+
// +--* LSH long
2417+
// | +--* CAST long <- uint
2418+
// | | \--* LCL_VAR int V06 loc6
2419+
// | \--* CNS_INT long 2
2420+
// \--* CNS_INT long 16 Fseq[#FirstElem]
2421+
//
2422+
// This should not be parsed as a jagged array (e.g., a[i][j]). To ensure that it is not, the type of the
2423+
// GT_INDEX node is stashed in the GT_BOUNDS_CHECK node during morph. If we see a bounds check node where
2424+
// the GT_INDEX was not TYP_REF, then it must be the outermost jagged array level. E.g., if it is
2425+
// TYP_STRUCT, then we have an array of structs, and any further bounds checks must be of one of its fields.
2426+
//
2427+
// It would be much better if we didn't need to parse these trees at all, and did all this work pre-morph.
2428+
//
2429+
// Assumption:
2430+
// The method extracts only if the array base and indices are GT_LCL_VAR.
2431+
//
2432+
bool Compiler::optReconstructArrIndex(GenTree* tree, ArrIndex* result)
2433+
{
2434+
bool topLevelIsFinal = false;
2435+
return optReconstructArrIndexHelp(tree, result, BAD_VAR_NUM, &topLevelIsFinal);
2436+
}
2437+
23092438
//----------------------------------------------------------------------------------------------
23102439
// optCanOptimizeByLoopCloning: Check if the tree can be optimized by loop cloning and if so,
23112440
// identify as potential candidate and update the loop context.
@@ -2329,7 +2458,7 @@ Compiler::fgWalkResult Compiler::optCanOptimizeByLoopCloning(GenTree* tree, Loop
23292458
ArrIndex arrIndex(getAllocator(CMK_LoopClone));
23302459

23312460
// Check if array index can be optimized.
2332-
if (optReconstructArrIndex(tree, &arrIndex, BAD_VAR_NUM))
2461+
if (optReconstructArrIndex(tree, &arrIndex))
23332462
{
23342463
assert(tree->gtOper == GT_COMMA);
23352464

src/coreclr/jit/morph.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5436,6 +5436,7 @@ GenTree* Compiler::fgMorphArrayIndex(GenTree* tree)
54365436
}
54375437

54385438
GenTreeBoundsChk* arrBndsChk = new (this, GT_BOUNDS_CHECK) GenTreeBoundsChk(index, arrLen, SCK_RNGCHK_FAIL);
5439+
arrBndsChk->gtInxType = elemTyp;
54395440

54405441
bndsChk = arrBndsChk;
54415442

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
// Test that loop cloning won't consider a[i].struct_field[j] to be
5+
// a jagged array a[i][j].
6+
7+
using System;
8+
9+
public class Runtime_66254
10+
{
11+
public static void t1()
12+
{
13+
var a = new ValueTuple<int[], int>[]
14+
{
15+
new (new[] { 0 }, 1),
16+
new (new[] { 2 }, 3)
17+
};
18+
19+
for (var i1 = 0; i1 < a.Length; i1++)
20+
{
21+
for (var i2 = 0; i2 < a[i1].Item1.Length; i2++)
22+
{
23+
var elem = a[i1].Item1[i2];
24+
Console.WriteLine(elem);
25+
}
26+
}
27+
}
28+
29+
public static void t2()
30+
{
31+
var a = new ValueTuple<int[], int>[]
32+
{
33+
new (new[] { 0 }, 1),
34+
new (new[] { 2 }, 3)
35+
};
36+
37+
for (var i1 = 0; i1 < a.Length; i1++)
38+
{
39+
var length = a[i1].Item1.Length;
40+
for (var i2 = 0; i2 < length; i2++)
41+
{
42+
var elem = a[i1].Item1[i2];
43+
Console.WriteLine(elem);
44+
}
45+
}
46+
}
47+
48+
public static void t3()
49+
{
50+
var a = new ValueTuple<int, int[]>[]
51+
{
52+
new (1, new[] { 2 }),
53+
new (3, new[] { 4 })
54+
};
55+
56+
for (var i1 = 0; i1 < a.Length; i1++)
57+
{
58+
var length = a[i1].Item2.Length;
59+
for (var i2 = 0; i2 < length; i2++)
60+
{
61+
var elem = a[i1].Item2[i2];
62+
Console.WriteLine(elem);
63+
}
64+
}
65+
}
66+
67+
public static int Main()
68+
{
69+
int result = 100;
70+
71+
try
72+
{
73+
t1();
74+
}
75+
catch (Exception e)
76+
{
77+
Console.WriteLine($"t1 failed: {e}");
78+
result = 101;
79+
}
80+
81+
try
82+
{
83+
t2();
84+
}
85+
catch (Exception e)
86+
{
87+
Console.WriteLine($"t2 failed: {e}");
88+
result = 101;
89+
}
90+
91+
try
92+
{
93+
t3();
94+
}
95+
catch (Exception e)
96+
{
97+
Console.WriteLine($"t3 failed: {e}");
98+
result = 101;
99+
}
100+
101+
Console.WriteLine((result == 100) ? "PASS" : "FAIL");
102+
return result;
103+
}
104+
}

0 commit comments

Comments
 (0)