Skip to content

Commit 388edb6

Browse files
authored
JIT: Add a (disabled) prototype for a generalized promotion pass (#83388)
Introduce a "physical" promotion pass that generalizes the existing promotion. More specifically, it does not have restrictions on field count and it can handle arbitrary recursive promotion. The pass is physical in the sense that it does not rely on any field metadata for structs. Instead, it works in two separate passes over the IR: 1. In the first pass we find and analyze how unpromoted struct locals are accessed. For example, for a simple program like: ``` public static void Main() { S s = default; Call(s, s.C); Console.WriteLine(s.B + s.C); } [MethodImpl(MethodImplOptions.NoInlining)] private static void Call(S s, byte b) { } private struct S { public byte A, B, C, D, E; } ``` we see IR like: ``` ***** BB01 STMT00000 ( 0x000[E-] ... 0x003 ) [000003] IA--------- ▌ ASG struct (init) [000001] D------N--- ├──▌ LCL_VAR struct<Program+S, 5> V00 loc0 [000002] ----------- └──▌ CNS_INT int 0 ***** BB01 STMT00001 ( 0x008[E-] ... 0x026 ) [000008] --C-G------ ▌ CALL void Program:Call(Program+S,ubyte) [000004] ----------- arg0 ├──▌ LCL_VAR struct<Program+S, 5> V00 loc0 [000007] ----------- arg1 └──▌ LCL_FLD ubyte V00 loc0 [+2] ***** BB01 STMT00002 ( 0x014[E-] ... ??? ) [000016] --C-G------ ▌ CALL void System.Console:WriteLine(int) [000015] ----------- arg0 └──▌ ADD int [000011] ----------- ├──▌ LCL_FLD ubyte V00 loc0 [+1] [000014] ----------- └──▌ LCL_FLD ubyte V00 loc0 [+2] ``` and the analysis produces ``` Accesses for V00 [000..005) #: (2, 200) # assigned from: (0, 0) # assigned to: (1, 100) # as call arg: (1, 100) # as implicit by-ref call arg: (1, 100) # as on-stack call arg: (0, 0) # as retbuf: (0, 0) # as returned value: (0, 0) ubyte @ 001 #: (1, 100) # assigned from: (0, 0) # assigned to: (0, 0) # as call arg: (0, 0) # as implicit by-ref call arg: (0, 0) # as on-stack call arg: (0, 0) # as retbuf: (0, 0) # as returned value: (0, 0) ubyte @ 002 #: (2, 200) # assigned from: (0, 0) # assigned to: (0, 0) # as call arg: (1, 100) # as implicit by-ref call arg: (0, 0) # as on-stack call arg: (0, 0) # as retbuf: (0, 0) # as returned value: (0, 0) ``` Here the pairs are (#ref counts, wtd ref counts). Based on this accounting, the analysis estimates the profitability of replacing some of the accessed parts of the struct with a local. This may be costly because overlapping struct accesses (e.g. passing the whole struct as an argument) may require more expensive codegen after promotion. And of course, creating new locals introduces more register pressure. Currently the profitability analysis is very crude. In this case the logic decides that promotion is not worth it: ``` Evaluating access ubyte @ 001 Single write-back cost: 5 Write backs: 100 Read backs: 100 Cost with: 1350 Cost without: 650 Disqualifying replacement Evaluating access ubyte @ 002 Single write-back cost: 5 Write backs: 100 Read backs: 100 Cost with: 1700 Cost without: 1300 Disqualifying replacement ``` 2. In the second pass the field accesses are replaced with new locals for the profitable cases. For overlapping accesses that currently involves writing back replacements to the struct local first. For arguments/OSR locals, it involves reading them back from the struct first. In the above case we can override the profitability analysis with stress mode STRESS_PHYSICAL_PROMOTION_COST and we get: ``` Evaluating access ubyte @ 001 Single write-back cost: 5 Write backs: 100 Read backs: 100 Cost with: 1350 Cost without: 650 Promoting replacement due to stress lvaGrabTemp returning 2 (V02 tmp1) (a long lifetime temp) called for V00.[001..002). Evaluating access ubyte @ 002 Single write-back cost: 5 Write backs: 100 Read backs: 100 Cost with: 1700 Cost without: 1300 Promoting replacement due to stress lvaGrabTemp returning 3 (V03 tmp2) (a long lifetime temp) called for V00.[002..003). V00 promoted with 2 replacements [001..002) promoted as ubyte V02 [002..003) promoted as ubyte V03 ... ***** BB01 STMT00000 ( 0x000[E-] ... 0x003 ) [000003] IA--------- ▌ ASG struct (init) [000001] D------N--- ├──▌ LCL_VAR struct<Program+S, 5> V00 loc0 [000002] ----------- └──▌ CNS_INT int 0 ***** BB01 STMT00001 ( 0x008[E-] ... 0x026 ) [000008] -ACXG------ ▌ CALL void Program:Call(Program+S,ubyte) [000004] ----------- arg0 ├──▌ LCL_VAR struct<Program+S, 5> V00 loc0 [000022] -A--------- arg1 └──▌ COMMA ubyte [000021] -A--------- ├──▌ ASG ubyte [000019] D------N--- │ ├──▌ LCL_VAR ubyte V03 tmp2 [000020] ----------- │ └──▌ LCL_FLD ubyte V00 loc0 [+2] [000018] ----------- └──▌ LCL_VAR ubyte V03 tmp2 ***** BB01 STMT00002 ( 0x014[E-] ... ??? ) [000016] -ACXG------ ▌ CALL void System.Console:WriteLine(int) [000015] -A--------- arg0 └──▌ ADD int [000027] -A--------- ├──▌ COMMA ubyte [000026] -A--------- │ ├──▌ ASG ubyte [000024] D------N--- │ │ ├──▌ LCL_VAR ubyte V02 tmp1 [000025] ----------- │ │ └──▌ LCL_FLD ubyte V00 loc0 [+1] [000023] ----------- │ └──▌ LCL_VAR ubyte V02 tmp1 [000028] ----------- └──▌ LCL_VAR ubyte V03 tmp2 ``` The pass still only has rudimentary support and is missing many basic CQ optimization optimizations. For example, it does not make use of any liveness yet and it does not have any decomposition support for assignments. Yet, it already shows good potential in user benchmarks. I have listed some follow-up improvements in #76928. This PR is adding the pass but it is disabled by default. It can be enabled by setting DOTNET_JitStressModeNames=STRESS_PHYSICAL_PROMOTION. There are two new scenarios added to jit-experimental that enables it, to be used for testing purposes.
1 parent 62e026e commit 388edb6

16 files changed

+1442
-41
lines changed

eng/pipelines/common/templates/runtimes/run-test-job.yml

+2
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,8 @@ jobs:
583583
- jitpartialcompilation
584584
- jitpartialcompilation_pgo
585585
- jitobjectstackallocation
586+
- jitgeneralizedpromotion
587+
- jitgeneralizedpromotion_full
586588

587589
${{ if in(parameters.testGroup, 'jit-cfg') }}:
588590
scenarios:

src/coreclr/jit/CMakeLists.txt

+2
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ set( JIT_SOURCES
158158
optimizer.cpp
159159
patchpoint.cpp
160160
phase.cpp
161+
promotion.cpp
161162
rangecheck.cpp
162163
rationalize.cpp
163164
redundantbranchopts.cpp
@@ -348,6 +349,7 @@ set( JIT_HEADERS
348349
objectalloc.h
349350
opcode.h
350351
phase.h
352+
promotion.h
351353
rangecheck.h
352354
rationalize.h
353355
regalloc.h

src/coreclr/jit/assertionprop.cpp

+16
Original file line numberDiff line numberDiff line change
@@ -1256,6 +1256,22 @@ AssertionIndex Compiler::optCreateAssertion(GenTree* op1,
12561256
goto DONE_ASSERTION; // Don't make an assertion
12571257
}
12581258

1259+
// We process locals when we see the LCL_VAR node instead
1260+
// of at its actual use point (its parent). That opens us
1261+
// up to problems in a case like the following, assuming we
1262+
// allowed creating an assertion like V10 = V35:
1263+
//
1264+
// └──▌ ADD int
1265+
// ├──▌ LCL_VAR int V10 tmp6 -> copy propagated to [V35 tmp31]
1266+
// └──▌ COMMA int
1267+
// ├──▌ ASG int
1268+
// │ ├──▌ LCL_VAR int V35 tmp31
1269+
// │ └──▌ LCL_FLD int V03 loc1 [+4]
1270+
if (lclVar2->lvRedefinedInEmbeddedStatement)
1271+
{
1272+
goto DONE_ASSERTION; // Don't make an assertion
1273+
}
1274+
12591275
assertion.op2.kind = O2K_LCLVAR_COPY;
12601276
assertion.op2.vn = optConservativeNormalVN(op2);
12611277
assertion.op2.lcl.lclNum = lclNum2;

src/coreclr/jit/compiler.cpp

+4
Original file line numberDiff line numberDiff line change
@@ -4745,6 +4745,10 @@ void Compiler::compCompile(void** methodCodePtr, uint32_t* methodCodeSize, JitFl
47454745
//
47464746
DoPhase(this, PHASE_EARLY_LIVENESS, &Compiler::fgEarlyLiveness);
47474747

4748+
// Promote struct locals based on primitive access patterns
4749+
//
4750+
DoPhase(this, PHASE_PHYSICAL_PROMOTION, &Compiler::PhysicalPromotion);
4751+
47484752
// Run a simple forward substitution pass.
47494753
//
47504754
DoPhase(this, PHASE_FWD_SUB, &Compiler::fgForwardSub);

src/coreclr/jit/compiler.h

+12-2
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,8 @@ class LclVarDsc
661661

662662
unsigned char lvIsOSRExposedLocal : 1; // OSR local that was address exposed in Tier0
663663

664+
unsigned char lvRedefinedInEmbeddedStatement : 1; // Local has redefinitions inside embedded statements that
665+
// disqualify it from local copy prop.
664666
private:
665667
unsigned char lvIsNeverNegative : 1; // The local is known to be never negative
666668

@@ -2030,6 +2032,9 @@ class Compiler
20302032
friend class CallArgs;
20312033
friend class IndirectCallTransformer;
20322034
friend class ProfileSynthesis;
2035+
friend class LocalsUseVisitor;
2036+
friend class Promotion;
2037+
friend class ReplaceVisitor;
20332038

20342039
#ifdef FEATURE_HW_INTRINSICS
20352040
friend struct HWIntrinsicInfo;
@@ -2449,7 +2454,7 @@ class Compiler
24492454
GenTree* gtNewOperNode(genTreeOps oper, var_types type, GenTree* op1);
24502455

24512456
// For binary opers.
2452-
GenTree* gtNewOperNode(genTreeOps oper, var_types type, GenTree* op1, GenTree* op2);
2457+
GenTreeOp* gtNewOperNode(genTreeOps oper, var_types type, GenTree* op1, GenTree* op2);
24532458

24542459
GenTreeCC* gtNewCC(genTreeOps oper, var_types type, GenCondition cond);
24552460
GenTreeOpCC* gtNewOperCC(genTreeOps oper, var_types type, GenCondition cond, GenTree* op1, GenTree* op2);
@@ -5740,9 +5745,9 @@ class Compiler
57405745
private:
57415746
void fgInsertStmtNearEnd(BasicBlock* block, Statement* stmt);
57425747
void fgInsertStmtAtBeg(BasicBlock* block, Statement* stmt);
5743-
void fgInsertStmtAfter(BasicBlock* block, Statement* insertionPoint, Statement* stmt);
57445748

57455749
public:
5750+
void fgInsertStmtAfter(BasicBlock* block, Statement* insertionPoint, Statement* stmt);
57465751
void fgInsertStmtBefore(BasicBlock* block, Statement* insertionPoint, Statement* stmt);
57475752

57485753
private:
@@ -6078,6 +6083,8 @@ class Compiler
60786083
PhaseStatus fgMarkAddressExposedLocals();
60796084
void fgSequenceLocals(Statement* stmt);
60806085

6086+
PhaseStatus PhysicalPromotion();
6087+
60816088
PhaseStatus fgForwardSub();
60826089
bool fgForwardSubBlock(BasicBlock* block);
60836090
bool fgForwardSubStatement(Statement* statement);
@@ -9720,6 +9727,9 @@ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
97209727
STRESS_MODE(SSA_INFO) /* Select lower thresholds for "complex" SSA num encoding */ \
97219728
STRESS_MODE(SPLIT_TREES_RANDOMLY) /* Split all statements at a random tree */ \
97229729
STRESS_MODE(SPLIT_TREES_REMOVE_COMMAS) /* Remove all GT_COMMA nodes */ \
9730+
STRESS_MODE(NO_OLD_PROMOTION) /* Do not use old promotion */ \
9731+
STRESS_MODE(PHYSICAL_PROMOTION) /* Use physical promotion */ \
9732+
STRESS_MODE(PHYSICAL_PROMOTION_COST) \
97239733
\
97249734
/* After COUNT_VARN, stress level 2 does all of these all the time */ \
97259735
\

src/coreclr/jit/compmemkind.h

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ CompMemKindMacro(LoopHoist)
5050
CompMemKindMacro(Unknown)
5151
CompMemKindMacro(RangeCheck)
5252
CompMemKindMacro(CopyProp)
53+
CompMemKindMacro(Promotion)
5354
CompMemKindMacro(SideEffects)
5455
CompMemKindMacro(ObjectAllocator)
5556
CompMemKindMacro(VariableLiveRanges)

src/coreclr/jit/compphases.h

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ CompPhaseNameMacro(PHASE_UPDATE_FINALLY_FLAGS, "Update finally target flag
4343
CompPhaseNameMacro(PHASE_EARLY_UPDATE_FLOW_GRAPH, "Update flow graph early pass", false, -1, false)
4444
CompPhaseNameMacro(PHASE_STR_ADRLCL, "Morph - Structs/AddrExp", false, -1, false)
4545
CompPhaseNameMacro(PHASE_EARLY_LIVENESS, "Early liveness", false, -1, false)
46+
CompPhaseNameMacro(PHASE_PHYSICAL_PROMOTION, "Physical promotion", false, -1, false)
4647
CompPhaseNameMacro(PHASE_FWD_SUB, "Forward Substitution", false, -1, false)
4748
CompPhaseNameMacro(PHASE_MORPH_IMPBYREF, "Morph - ByRefs", false, -1, false)
4849
CompPhaseNameMacro(PHASE_PROMOTE_STRUCTS, "Morph - Promote Structs", false, -1, false)

src/coreclr/jit/fgopt.cpp

+37-30
Original file line numberDiff line numberDiff line change
@@ -6774,45 +6774,52 @@ PhaseStatus Compiler::fgTailMerge()
67746774
//
67756775
for (BasicBlock* const predBlock : block->PredBlocks())
67766776
{
6777-
if ((predBlock->GetUniqueSucc() == block) && BasicBlock::sameEHRegion(block, predBlock))
6777+
if (predBlock->GetUniqueSucc() != block)
67786778
{
6779-
Statement* lastStmt = predBlock->lastStmt();
6779+
continue;
6780+
}
67806781

6781-
// Block might be empty.
6782-
//
6783-
if (lastStmt == nullptr)
6784-
{
6785-
continue;
6786-
}
6782+
if (!BasicBlock::sameEHRegion(block, predBlock))
6783+
{
6784+
continue;
6785+
}
67876786

6788-
// Walk back past any GT_NOPs.
6789-
//
6790-
Statement* const firstStmt = predBlock->firstStmt();
6791-
while (lastStmt->GetRootNode()->OperIs(GT_NOP))
6792-
{
6793-
if (lastStmt == firstStmt)
6794-
{
6795-
// predBlock is evidently all GT_NOP.
6796-
//
6797-
lastStmt = nullptr;
6798-
break;
6799-
}
6787+
Statement* lastStmt = predBlock->lastStmt();
68006788

6801-
lastStmt = lastStmt->GetPrevStmt();
6802-
}
6789+
// Block might be empty.
6790+
//
6791+
if (lastStmt == nullptr)
6792+
{
6793+
continue;
6794+
}
68036795

6804-
// Block might be effectively empty.
6805-
//
6806-
if (lastStmt == nullptr)
6796+
// Walk back past any GT_NOPs.
6797+
//
6798+
Statement* const firstStmt = predBlock->firstStmt();
6799+
while (lastStmt->GetRootNode()->OperIs(GT_NOP))
6800+
{
6801+
if (lastStmt == firstStmt)
68076802
{
6808-
continue;
6803+
// predBlock is evidently all GT_NOP.
6804+
//
6805+
lastStmt = nullptr;
6806+
break;
68096807
}
68106808

6811-
// We don't expect to see PHIs but watch for them anyways.
6812-
//
6813-
assert(!lastStmt->IsPhiDefnStmt());
6814-
predInfo.Emplace(predBlock, lastStmt);
6809+
lastStmt = lastStmt->GetPrevStmt();
6810+
}
6811+
6812+
// Block might be effectively empty.
6813+
//
6814+
if (lastStmt == nullptr)
6815+
{
6816+
continue;
68156817
}
6818+
6819+
// We don't expect to see PHIs but watch for them anyways.
6820+
//
6821+
assert(!lastStmt->IsPhiDefnStmt());
6822+
predInfo.Emplace(predBlock, lastStmt);
68166823
}
68176824

68186825
// Are there enough preds to make it interesting?

src/coreclr/jit/fgstmt.cpp

+5-1
Original file line numberDiff line numberDiff line change
@@ -386,11 +386,15 @@ Statement* Compiler::fgNewStmtFromTree(GenTree* tree, BasicBlock* block, const D
386386
{
387387
Statement* stmt = gtNewStmt(tree, di);
388388

389-
if (fgNodeThreading != NodeThreading::None)
389+
if (fgNodeThreading == NodeThreading::AllTrees)
390390
{
391391
gtSetStmtInfo(stmt);
392392
fgSetStmtSeq(stmt);
393393
}
394+
else if (fgNodeThreading == NodeThreading::AllLocals)
395+
{
396+
fgSequenceLocals(stmt);
397+
}
394398

395399
#if DEBUG
396400
if (block != nullptr)

src/coreclr/jit/gentree.cpp

+32-5
Original file line numberDiff line numberDiff line change
@@ -7012,7 +7012,7 @@ void GenTree::SetVtableForOper(genTreeOps oper)
70127012
}
70137013
#endif // DEBUGGABLE_GENTREE
70147014

7015-
GenTree* Compiler::gtNewOperNode(genTreeOps oper, var_types type, GenTree* op1, GenTree* op2)
7015+
GenTreeOp* Compiler::gtNewOperNode(genTreeOps oper, var_types type, GenTree* op1, GenTree* op2)
70167016
{
70177017
assert(op1 != nullptr);
70187018
assert(op2 != nullptr);
@@ -7021,7 +7021,7 @@ GenTree* Compiler::gtNewOperNode(genTreeOps oper, var_types type, GenTree* op1,
70217021
// should call the appropriate constructor for the extended type.
70227022
assert(!GenTree::IsExOp(GenTree::OperKind(oper)));
70237023

7024-
GenTree* node = new (this, oper) GenTreeOp(oper, type, op1, op2);
7024+
GenTreeOp* node = new (this, oper) GenTreeOp(oper, type, op1, op2);
70257025

70267026
return node;
70277027
}
@@ -8397,7 +8397,7 @@ GenTree* Compiler::gtClone(GenTree* tree, bool complexOK)
83978397
return nullptr;
83988398
}
83998399

8400-
if (tree->gtOper == GT_FIELD)
8400+
if (tree->OperIs(GT_FIELD))
84018401
{
84028402
GenTree* objp = nullptr;
84038403

@@ -16263,6 +16263,34 @@ bool Compiler::gtSplitTree(
1626316263
return false;
1626416264
}
1626516265

16266+
bool IsValue(const UseInfo& useInf)
16267+
{
16268+
GenTree* node = (*useInf.Use)->gtEffectiveVal();
16269+
if (!node->IsValue())
16270+
{
16271+
return false;
16272+
}
16273+
16274+
if (node->OperIs(GT_ASG))
16275+
{
16276+
return false;
16277+
}
16278+
16279+
GenTree* user = useInf.User;
16280+
16281+
if (user == nullptr)
16282+
{
16283+
return false;
16284+
}
16285+
16286+
if (user->OperIs(GT_COMMA) && (&user->AsOp()->gtOp1 == useInf.Use))
16287+
{
16288+
return false;
16289+
}
16290+
16291+
return true;
16292+
}
16293+
1626616294
void SplitOutUse(const UseInfo& useInf, bool userIsReturned)
1626716295
{
1626816296
GenTree** use = useInf.Use;
@@ -16328,8 +16356,7 @@ bool Compiler::gtSplitTree(
1632816356
}
1632916357

1633016358
Statement* stmt = nullptr;
16331-
if (!(*use)->IsValue() || (*use)->gtEffectiveVal()->OperIs(GT_ASG) || (user == nullptr) ||
16332-
(user->OperIs(GT_COMMA) && (user->gtGetOp1() == *use)))
16359+
if (!IsValue(useInf))
1633316360
{
1633416361
GenTree* sideEffects = nullptr;
1633516362
m_compiler->gtExtractSideEffList(*use, &sideEffects);

src/coreclr/jit/jitconfigvalues.h

+1
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,7 @@ CONFIG_STRING(JitEnableVNBasedDeadStoreRemovalRange, W("JitEnableVNBasedDeadStor
431431
CONFIG_STRING(JitEnableEarlyLivenessRange, W("JitEnableEarlyLivenessRange"))
432432
CONFIG_STRING(JitOnlyOptimizeRange,
433433
W("JitOnlyOptimizeRange")) // If set, all methods that do _not_ match are forced into MinOpts
434+
CONFIG_STRING(JitEnablePhysicalPromotionRange, W("JitEnablePhysicalPromotionRange"))
434435

435436
CONFIG_INTEGER(JitDoSsa, W("JitDoSsa"), 1) // Perform Static Single Assignment (SSA) numbering on the variables
436437
CONFIG_INTEGER(JitDoValueNumber, W("JitDoValueNumber"), 1) // Perform value numbering on method expressions

src/coreclr/jit/jitstd/vector.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -733,7 +733,7 @@ void vector<T, Allocator>::insert_elements_helper(iterator iter, size_type size,
733733

734734
ensure_capacity(m_nSize + size);
735735

736-
for (int src = m_nSize - 1, dst = m_nSize + size - 1; src >= (int) pos; --src, --dst)
736+
for (int src = (int)(m_nSize - 1), dst = (int)(m_nSize + size - 1); src >= (int) pos; --src, --dst)
737737
{
738738
m_pArray[dst] = m_pArray[src];
739739
}

src/coreclr/jit/morph.cpp

+10-2
Original file line numberDiff line numberDiff line change
@@ -3328,7 +3328,7 @@ GenTreeCall* Compiler::fgMorphArgs(GenTreeCall* call)
33283328

33293329
if (argLclNum != BAD_VAR_NUM)
33303330
{
3331-
argObj->ChangeType(argVarDsc->TypeGet());
3331+
argx->ChangeType(argVarDsc->TypeGet());
33323332
argObj->SetOper(GT_LCL_VAR);
33333333
argObj->AsLclVar()->SetLclNum(argLclNum);
33343334
}
@@ -3346,7 +3346,7 @@ GenTreeCall* Compiler::fgMorphArgs(GenTreeCall* call)
33463346
{
33473347
// TODO-CQ: perform this transformation in lowering instead of here and
33483348
// avoid marking enregisterable structs DNER.
3349-
argObj->ChangeType(structBaseType);
3349+
argx->ChangeType(structBaseType);
33503350
if (argObj->OperIs(GT_LCL_VAR))
33513351
{
33523352
argObj->SetOper(GT_LCL_FLD);
@@ -14751,6 +14751,14 @@ PhaseStatus Compiler::fgPromoteStructs()
1475114751
return PhaseStatus::MODIFIED_NOTHING;
1475214752
}
1475314753

14754+
#ifdef DEBUG
14755+
if (compStressCompile(STRESS_NO_OLD_PROMOTION, 10))
14756+
{
14757+
JITDUMP(" skipping due to stress\n");
14758+
return PhaseStatus::MODIFIED_NOTHING;
14759+
}
14760+
#endif
14761+
1475414762
#if 0
1475514763
// The code in this #if has been useful in debugging struct promotion issues, by
1475614764
// enabling selective enablement of the struct promotion optimization according to

0 commit comments

Comments
 (0)