Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/coreclr/jit/compiler.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -1991,13 +1991,23 @@ struct NaturalLoopIterInfo
// length of an invariant array.
bool HasArrayLengthLimit : 1;

// Whether the consumer must emit its own runtime entry guard for this loop.
// Set when AnalyzeIteration could not prove statically that the loop
// condition [IterVar TestOper Limit] holds on entry (so the analysis
// invariants only hold conditionally). The consumer must insert a runtime
// test equivalent to that condition on the path that reaches the analyzed
// loop body. Only set when AnalyzeIteration is called with
// allowMissingBaseCase=true.
bool NeedsZeroTripGuard : 1;

NaturalLoopIterInfo()
: ExitedOnTrue(false)
, HasConstInit(false)
, HasConstLimit(false)
, HasSimdLimit(false)
, HasInvariantLocalLimit(false)
, HasArrayLengthLimit(false)
, NeedsZeroTripGuard(false)
{
}

Expand DownExpand Up@@ -2191,7 +2201,7 @@ class FlowGraphNaturalLoop
BasicBlock* GetLexicallyTopMostBlock();
BasicBlock* GetLexicallyBottomMostBlock();

bool AnalyzeIteration(NaturalLoopIterInfo* info);
bool AnalyzeIteration(NaturalLoopIterInfo* info, bool allowMissingBaseCase = false);

bool HasDef(unsigned lclNum);

Expand Down
60 changes: 49 additions & 11 deletions src/coreclr/jit/flowgraph.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -5566,19 +5566,31 @@ GenTreeLclVarCommon* FlowGraphNaturalLoop::FindDef(unsigned lclNum)
// the loop.
//
// Parameters:
// info - [out] Loop information
// info - [out] Loop information
// allowMissingBaseCase - If true, succeed even when we cannot prove that the
// loop condition [IterVar TestOper Limit] holds on
// entry, provided the limit form is one we know how
// to materialize at runtime. The caller is then
// responsible for emitting a runtime entry guard
// equivalent to that condition when
// info->NeedsZeroTripGuard is set on return.
// Defaults to false; existing callers retain the
// stronger guarantees described below.
Comment thread
AndyAyersMS marked this conversation as resolved.
//
// Returns:
// True if the structure was analyzed and we can make guarantees about it;
// otherwise false.
//
// Remarks:
// On a true return, the function guarantees that the loop invariant is true
// and maintained at all points within the loop, except possibly right after
// the update of the iterator variable (NaturalLoopIterInfo::IterTree). The
// function guarantees that the test (NaturalLoopIterInfo::TestTree) occurs
// immediately after the update, so no IR in the loop is executed without the
// loop invariant being true, except for the test.
// On a true return with allowMissingBaseCase == false (or with the flag set
// but info->NeedsZeroTripGuard == false), the function guarantees that the
// loop invariant is true and maintained at all points within the loop,
// except possibly right after the update of the iterator variable
// (NaturalLoopIterInfo::IterTree). optExtractTestIncr permits the IV update
// to be separated from the loop test by other statements, but it rejects
// any candidate where an intervening statement references the iterator
// variable, so no IR in the loop is executed observing the post-update
// value of the iterator except the test itself.
//
// The loop invariant is defined as the expression obtained by
// [info->IterVar] [info->TestOper()] [info->Limit()]. Note that
Expand All@@ -5599,7 +5611,19 @@ GenTreeLclVarCommon* FlowGraphNaturalLoop::FindDef(unsigned lclNum)
// In some cases we also know the initial value on entry to the loop; see
// ::HasConstInit and ::ConstInitValue.
//
bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)
// When allowMissingBaseCase is true and the function would otherwise fail
// because the loop condition [IterVar TestOper Limit] cannot be proven to
// hold on entry (no suitable preheader BBJ_COND guard and no constant
// init/limit pair that proves the base case), the function may instead
// succeed with info->NeedsZeroTripGuard set. In that mode the above
// invariant only holds conditionally: it is the caller's obligation to
// insert a runtime test equivalent to [IterVar TestOper() Limit] on the
// path that reaches the analyzed loop body. Loop cloning uses this to emit
// the guard as an extra cloning condition on the fast-path version. Note
// that this includes do/while-style loops that are guaranteed to execute
// at least once but whose loop condition may be false on entry.
//
bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info, bool allowMissingBaseCase)
{
Comment thread
AndyAyersMS marked this conversation as resolved.
JITDUMP("Analyzing iteration for " FMT_LP " with header " FMT_BB "\n", m_index, m_header->bbNum);

Expand All@@ -5613,7 +5637,8 @@ bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)

GenTree* test = nullptr;

info->IterVar = BAD_VAR_NUM;
info->IterVar = BAD_VAR_NUM;
info->NeedsZeroTripGuard = false;

for (FlowEdge* exitEdge : ExitEdges())
{
Expand DownExpand Up@@ -5700,8 +5725,21 @@ bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)

if (!CheckLoopConditionBaseCase(preheader, info))
{
JITDUMP(" Loop condition may not be true on the first iteration\n");
return false;
// If the caller can emit its own runtime guard for the case where the
// loop body might not execute on the first iteration, allow the loop
// through provided we have enough symbolic info about init and limit
// to express the guard.
if (allowMissingBaseCase && (info->HasConstLimit || info->HasInvariantLocalLimit || info->HasArrayLengthLimit))
{
JITDUMP(" Loop condition may not be true on the first iteration; deferring to caller "
"(NeedsZeroTripGuard)\n");
info->NeedsZeroTripGuard = true;
}
else
{
JITDUMP(" Loop condition may not be true on the first iteration\n");
return false;
}
Comment thread
AndyAyersMS marked this conversation as resolved.
}

#ifdef DEBUG
Expand Down
104 changes: 92 additions & 12 deletions src/coreclr/jit/loopcloning.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1227,6 +1227,95 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
return false;
}

// If the loop limit is an array length, compute the underlying ArrIndex
// and queue the deref check once up front. Both the optional zero-trip
// guard below and the regular limit conditions further down reuse this
// single ArrIndex to avoid duplicating the deref entry and allocation.
//
ArrIndex* limitArrIndex = nullptr;
if (iterInfo->HasArrayLengthLimit)
{
limitArrIndex = new (getAllocator(CMK_LoopClone)) ArrIndex(getAllocator(CMK_LoopClone));
if (!iterInfo->ArrLenLimit(this, limitArrIndex))
{
JITDUMP("> ArrLen not matching\n");
return false;
}

LC_Array array(LC_Array::Jagged, limitArrIndex, LC_Array::None);
context->EnsureArrayDerefs(loop->GetIndex())->Push(array);
}

// If AnalyzeIteration could not prove the loop condition holds on entry,
// emit an explicit runtime entry guard as one of the cloning conditions.
// The fast path is then only entered when "init TestOper limit" holds.
if (iterInfo->NeedsZeroTripGuard)
{
LC_Ident initIdent;
if (iterInfo->HasConstInit)
{
if (iterInfo->ConstInitValue < 0)
{
JITDUMP("> NeedsZeroTripGuard: init %d is invalid\n", iterInfo->ConstInitValue);
return false;
}
initIdent = LC_Ident::CreateConst(static_cast<unsigned>(iterInfo->ConstInitValue));
}
else
{
// Init is unknown statically; use the IV local as it stands at the
// preheader (the analysis already verified the local is not
// address-exposed and has no extraneous defs inside the loop, so
// reading it in the preheader gives the entry value).
const unsigned initLcl = iterInfo->IterVar;
if (!genActualTypeIsInt(lvaGetDesc(initLcl)))
{
JITDUMP("> NeedsZeroTripGuard: iter var V%02u not compatible with TYP_INT\n", initLcl);
return false;
}
initIdent = LC_Ident::CreateVar(initLcl);
}

LC_Ident limitIdent;
if (iterInfo->HasConstLimit)
{
int limit = iterInfo->ConstLimit();
if (limit < 0)
{
JITDUMP("> NeedsZeroTripGuard: limit %d is invalid\n", limit);
return false;
}
limitIdent = LC_Ident::CreateConst(static_cast<unsigned>(limit));
}
else if (iterInfo->HasInvariantLocalLimit)
{
const unsigned limitLcl = iterInfo->VarLimit();
if (!genActualTypeIsInt(lvaGetDesc(limitLcl)))
{
JITDUMP("> NeedsZeroTripGuard: limit var V%02u not compatible with TYP_INT\n", limitLcl);
return false;
}
limitIdent = LC_Ident::CreateVar(limitLcl);
}
else if (iterInfo->HasArrayLengthLimit)
{
assert(limitArrIndex != nullptr);
limitIdent = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}
Comment thread
AndyAyersMS marked this conversation as resolved.
else
{
JITDUMP("> NeedsZeroTripGuard: undetected limit\n");
return false;
}

// TestOper() returns the stays-in-loop relop in IV-on-lhs form, already
// adjusted for IsReversed and ExitedOnTrue.
LC_Condition zeroTrip(iterInfo->TestOper(), LC_Expr(initIdent), LC_Expr(limitIdent),
iterInfo->TestTree->IsUnsigned());
context->EnsureConditions(loop->GetIndex())->Push(zeroTrip);
JITDUMP("Added zero-trip guard cloning condition\n");
}

LC_Ident ident;
// Init conditions
if (iterInfo->HasConstInit)
Expand DownExpand Up@@ -1311,17 +1400,8 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}
else if (iterInfo->HasArrayLengthLimit)
{
ArrIndex* index = new (getAllocator(CMK_LoopClone)) ArrIndex(getAllocator(CMK_LoopClone));
if (!iterInfo->ArrLenLimit(this, index))
{
JITDUMP("> ArrLen not matching\n");
return false;
}
ident = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, index, LC_Array::ArrLen));

// Ensure that this array must be dereference-able, before executing the actual condition.
LC_Array array(LC_Array::Jagged, index, LC_Array::None);
context->EnsureArrayDerefs(loop->GetIndex())->Push(array);
assert(limitArrIndex != nullptr);
ident = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}
else
{
Expand DownExpand Up@@ -2993,7 +3073,7 @@ bool Compiler::optObtainLoopCloningOpts(LoopCloneContext* context)
{
JITDUMP("Considering loop " FMT_LP " to clone for optimizations.\n", loop->GetIndex());
NaturalLoopIterInfo iterInfo;
if (loop->AnalyzeIteration(&iterInfo))
if (loop->AnalyzeIteration(&iterInfo, /* allowMissingBaseCase */ true))
{
Comment thread
AndyAyersMS marked this conversation as resolved.
context->SetLoopIterInfo(loop->GetIndex(), new (this, CMK_LoopClone) NaturalLoopIterInfo(iterInfo));
}
Expand Down
88 changes: 76 additions & 12 deletions src/coreclr/jit/optimizer.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -356,8 +356,9 @@ bool Compiler::optExtractTestIncr(BasicBlock* cond, GenTree** ppTest, GenTree**
assert(ppTest != nullptr);
assert(ppIncr != nullptr);

// Check if last two statements in the loop body are the increment of the iterator
// and the loop termination test.
// The loop termination test is expected to be the last statement of the exiting
// block. The increment of the iterator is expected somewhere earlier in the
// same block; we scan backward from the test to find an IV-shaped update.
noway_assert(cond->firstStmt() != nullptr);
Statement* testStmt = cond->lastStmt();
noway_assert(testStmt != nullptr && testStmt->GetNextStmt() == nullptr);
Expand All@@ -368,24 +369,87 @@ bool Compiler::optExtractTestIncr(BasicBlock* cond, GenTree** ppTest, GenTree**
testStmt = newTestStmt;
}

// Check if we have the incr stmt before the test stmt, if we don't,
// check if incr is part of the loop "header".
Statement* incrStmt = testStmt->GetPrevStmt();

// If we've added profile instrumentation, we may need to skip past a BB counter update.
// Walk backward from the test statement looking for a candidate IV increment
// of the form 'v = v op c'. For each such candidate, verify it is suitable:
// - the iterator local is not address-exposed (our intervening-read check
// would not see indirect accesses, and AnalyzeIteration rejects
// address-exposed IVs anyway);
// - the test actually reads the iterator (otherwise this is an unrelated
// update that happens to be IV-shaped);
// - no statement strictly between the candidate and the test reads the
// iterator. Such statements would execute with the post-increment value,
// violating the AnalyzeIteration invariant that no loop body IR observes
// the post-increment value except the test. Stores are not checked here:
// AnalyzeIteration's VisitDefs already rejects any def of iterVar in the
// loop other than the picked increment. If the test block is in a try
// region, treat any intervening statement that may throw as if it were
// an intervening read, since an EH handler could observe the
// post-increment value.
// On any rejection, continue scanning backward; only fail when no candidate
// in the block satisfies all checks. A budget bounds the combined work of
// the outer scan and the inner intervening-read scan to avoid pathological
// O(N^2) behavior in blocks with many IV-shaped statements.
//
if (opts.jitFlags->IsSet(JitFlags::JIT_FLAG_BBINSTR) && (incrStmt != nullptr) &&
incrStmt->GetRootNode()->IsBlockProfileUpdate())
{
incrStmt = incrStmt->GetPrevStmt();
Statement* incrStmt = nullptr;
unsigned iterVar = BAD_VAR_NUM;
Statement* firstStmt = cond->firstStmt();
const bool condInTry = cond->hasTryIndex();
unsigned int budget = 100;
if (testStmt != firstStmt)
{
for (Statement* s = testStmt->GetPrevStmt();; s = s->GetPrevStmt())
{
if (budget-- == 0)
{
JITDUMP("optExtractTestIncr: budget exhausted in " FMT_BB "\n", cond->bbNum);
return false;
}
Comment thread
AndyAyersMS marked this conversation as resolved.

unsigned candVar = optIsLoopIncrTree(s->GetRootNode());
if (candVar != BAD_VAR_NUM)
{
if (!lvaGetDesc(candVar)->IsAddressExposed() && gtTreeHasLocalRead(testStmt->GetRootNode(), candVar))
{
bool intermediateUse = false;
for (Statement* t = s->GetNextStmt(); t != testStmt; t = t->GetNextStmt())
{
assert(t != nullptr);
if (budget-- == 0)
{
JITDUMP("optExtractTestIncr: budget exhausted in " FMT_BB "\n", cond->bbNum);
return false;
}
GenTree* root = t->GetRootNode();
if (gtTreeHasLocalRead(root, candVar) || (condInTry && ((root->gtFlags & GTF_EXCEPT) != 0)))
{
intermediateUse = true;
break;
}
}

if (!intermediateUse)
{
incrStmt = s;
iterVar = candVar;
break;
}
}
}

if (s == firstStmt)
{
break;
}
}
Comment thread
AndyAyersMS marked this conversation as resolved.
}

if (incrStmt == nullptr || (optIsLoopIncrTree(incrStmt->GetRootNode()) == BAD_VAR_NUM))
if (incrStmt == nullptr)
{
return false;
}

assert(testStmt != incrStmt);
assert(iterVar != BAD_VAR_NUM);

*ppTest = testStmt->GetRootNode();
*ppIncr = incrStmt->GetRootNode();
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
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;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} 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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/coreclr/jit/compiler.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -1991,13 +1991,23 @@ struct NaturalLoopIterInfo
// length of an invariant array.
bool HasArrayLengthLimit : 1;

// Whether the consumer must emit its own runtime entry guard for this loop.
// Set when AnalyzeIteration could not prove statically that the loop
// condition [IterVar TestOper Limit] holds on entry (so the analysis
// invariants only hold conditionally). The consumer must insert a runtime
// test equivalent to that condition on the path that reaches the analyzed
// loop body. Only set when AnalyzeIteration is called with
// allowMissingBaseCase=true.
bool NeedsZeroTripGuard : 1;

NaturalLoopIterInfo()
: ExitedOnTrue(false)
, HasConstInit(false)
, HasConstLimit(false)
, HasSimdLimit(false)
, HasInvariantLocalLimit(false)
, HasArrayLengthLimit(false)
, NeedsZeroTripGuard(false)
{
}

Expand DownExpand Up@@ -2191,7 +2201,7 @@ class FlowGraphNaturalLoop
BasicBlock* GetLexicallyTopMostBlock();
BasicBlock* GetLexicallyBottomMostBlock();

bool AnalyzeIteration(NaturalLoopIterInfo* info);
bool AnalyzeIteration(NaturalLoopIterInfo* info, bool allowMissingBaseCase = false);

bool HasDef(unsigned lclNum);

Expand Down
60 changes: 49 additions & 11 deletions src/coreclr/jit/flowgraph.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -5566,19 +5566,31 @@ GenTreeLclVarCommon* FlowGraphNaturalLoop::FindDef(unsigned lclNum)
// the loop.
//
// Parameters:
// info - [out] Loop information
// info - [out] Loop information
// allowMissingBaseCase - If true, succeed even when we cannot prove that the
// loop condition [IterVar TestOper Limit] holds on
// entry, provided the limit form is one we know how
// to materialize at runtime. The caller is then
// responsible for emitting a runtime entry guard
// equivalent to that condition when
// info->NeedsZeroTripGuard is set on return.
// Defaults to false; existing callers retain the
// stronger guarantees described below.
Comment thread
AndyAyersMS marked this conversation as resolved.
//
// Returns:
// True if the structure was analyzed and we can make guarantees about it;
// otherwise false.
//
// Remarks:
// On a true return, the function guarantees that the loop invariant is true
// and maintained at all points within the loop, except possibly right after
// the update of the iterator variable (NaturalLoopIterInfo::IterTree). The
// function guarantees that the test (NaturalLoopIterInfo::TestTree) occurs
// immediately after the update, so no IR in the loop is executed without the
// loop invariant being true, except for the test.
// On a true return with allowMissingBaseCase == false (or with the flag set
// but info->NeedsZeroTripGuard == false), the function guarantees that the
// loop invariant is true and maintained at all points within the loop,
// except possibly right after the update of the iterator variable
// (NaturalLoopIterInfo::IterTree). optExtractTestIncr permits the IV update
// to be separated from the loop test by other statements, but it rejects
// any candidate where an intervening statement references the iterator
// variable, so no IR in the loop is executed observing the post-update
// value of the iterator except the test itself.
//
// The loop invariant is defined as the expression obtained by
// [info->IterVar] [info->TestOper()] [info->Limit()]. Note that
Expand All@@ -5599,7 +5611,19 @@ GenTreeLclVarCommon* FlowGraphNaturalLoop::FindDef(unsigned lclNum)
// In some cases we also know the initial value on entry to the loop; see
// ::HasConstInit and ::ConstInitValue.
//
bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)
// When allowMissingBaseCase is true and the function would otherwise fail
// because the loop condition [IterVar TestOper Limit] cannot be proven to
// hold on entry (no suitable preheader BBJ_COND guard and no constant
// init/limit pair that proves the base case), the function may instead
// succeed with info->NeedsZeroTripGuard set. In that mode the above
// invariant only holds conditionally: it is the caller's obligation to
// insert a runtime test equivalent to [IterVar TestOper() Limit] on the
// path that reaches the analyzed loop body. Loop cloning uses this to emit
// the guard as an extra cloning condition on the fast-path version. Note
// that this includes do/while-style loops that are guaranteed to execute
// at least once but whose loop condition may be false on entry.
//
bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info, bool allowMissingBaseCase)
{
Comment thread
AndyAyersMS marked this conversation as resolved.
JITDUMP("Analyzing iteration for " FMT_LP " with header " FMT_BB "\n", m_index, m_header->bbNum);

Expand All@@ -5613,7 +5637,8 @@ bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)

GenTree* test = nullptr;

info->IterVar = BAD_VAR_NUM;
info->IterVar = BAD_VAR_NUM;
info->NeedsZeroTripGuard = false;

for (FlowEdge* exitEdge : ExitEdges())
{
Expand DownExpand Up@@ -5700,8 +5725,21 @@ bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)

if (!CheckLoopConditionBaseCase(preheader, info))
{
JITDUMP(" Loop condition may not be true on the first iteration\n");
return false;
// If the caller can emit its own runtime guard for the case where the
// loop body might not execute on the first iteration, allow the loop
// through provided we have enough symbolic info about init and limit
// to express the guard.
if (allowMissingBaseCase && (info->HasConstLimit || info->HasInvariantLocalLimit || info->HasArrayLengthLimit))
{
JITDUMP(" Loop condition may not be true on the first iteration; deferring to caller "
"(NeedsZeroTripGuard)\n");
info->NeedsZeroTripGuard = true;
}
else
{
JITDUMP(" Loop condition may not be true on the first iteration\n");
return false;
}
Comment thread
AndyAyersMS marked this conversation as resolved.
}

#ifdef DEBUG
Expand Down
104 changes: 92 additions & 12 deletions src/coreclr/jit/loopcloning.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1227,6 +1227,95 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
return false;
}

// If the loop limit is an array length, compute the underlying ArrIndex
// and queue the deref check once up front. Both the optional zero-trip
// guard below and the regular limit conditions further down reuse this
// single ArrIndex to avoid duplicating the deref entry and allocation.
//
ArrIndex* limitArrIndex = nullptr;
if (iterInfo->HasArrayLengthLimit)
{
limitArrIndex = new (getAllocator(CMK_LoopClone)) ArrIndex(getAllocator(CMK_LoopClone));
if (!iterInfo->ArrLenLimit(this, limitArrIndex))
{
JITDUMP("> ArrLen not matching\n");
return false;
}

LC_Array array(LC_Array::Jagged, limitArrIndex, LC_Array::None);
context->EnsureArrayDerefs(loop->GetIndex())->Push(array);
}

// If AnalyzeIteration could not prove the loop condition holds on entry,
// emit an explicit runtime entry guard as one of the cloning conditions.
// The fast path is then only entered when "init TestOper limit" holds.
if (iterInfo->NeedsZeroTripGuard)
{
LC_Ident initIdent;
if (iterInfo->HasConstInit)
{
if (iterInfo->ConstInitValue < 0)
{
JITDUMP("> NeedsZeroTripGuard: init %d is invalid\n", iterInfo->ConstInitValue);
return false;
}
initIdent = LC_Ident::CreateConst(static_cast<unsigned>(iterInfo->ConstInitValue));
}
else
{
// Init is unknown statically; use the IV local as it stands at the
// preheader (the analysis already verified the local is not
// address-exposed and has no extraneous defs inside the loop, so
// reading it in the preheader gives the entry value).
const unsigned initLcl = iterInfo->IterVar;
if (!genActualTypeIsInt(lvaGetDesc(initLcl)))
{
JITDUMP("> NeedsZeroTripGuard: iter var V%02u not compatible with TYP_INT\n", initLcl);
return false;
}
initIdent = LC_Ident::CreateVar(initLcl);
}

LC_Ident limitIdent;
if (iterInfo->HasConstLimit)
{
int limit = iterInfo->ConstLimit();
if (limit < 0)
{
JITDUMP("> NeedsZeroTripGuard: limit %d is invalid\n", limit);
return false;
}
limitIdent = LC_Ident::CreateConst(static_cast<unsigned>(limit));
}
else if (iterInfo->HasInvariantLocalLimit)
{
const unsigned limitLcl = iterInfo->VarLimit();
if (!genActualTypeIsInt(lvaGetDesc(limitLcl)))
{
JITDUMP("> NeedsZeroTripGuard: limit var V%02u not compatible with TYP_INT\n", limitLcl);
return false;
}
limitIdent = LC_Ident::CreateVar(limitLcl);
}
else if (iterInfo->HasArrayLengthLimit)
{
assert(limitArrIndex != nullptr);
limitIdent = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}
Comment thread
AndyAyersMS marked this conversation as resolved.
else
{
JITDUMP("> NeedsZeroTripGuard: undetected limit\n");
return false;
}

// TestOper() returns the stays-in-loop relop in IV-on-lhs form, already
// adjusted for IsReversed and ExitedOnTrue.
LC_Condition zeroTrip(iterInfo->TestOper(), LC_Expr(initIdent), LC_Expr(limitIdent),
iterInfo->TestTree->IsUnsigned());
context->EnsureConditions(loop->GetIndex())->Push(zeroTrip);
JITDUMP("Added zero-trip guard cloning condition\n");
}

LC_Ident ident;
// Init conditions
if (iterInfo->HasConstInit)
Expand DownExpand Up@@ -1311,17 +1400,8 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}
else if (iterInfo->HasArrayLengthLimit)
{
ArrIndex* index = new (getAllocator(CMK_LoopClone)) ArrIndex(getAllocator(CMK_LoopClone));
if (!iterInfo->ArrLenLimit(this, index))
{
JITDUMP("> ArrLen not matching\n");
return false;
}
ident = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, index, LC_Array::ArrLen));

// Ensure that this array must be dereference-able, before executing the actual condition.
LC_Array array(LC_Array::Jagged, index, LC_Array::None);
context->EnsureArrayDerefs(loop->GetIndex())->Push(array);
assert(limitArrIndex != nullptr);
ident = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}
else
{
Expand DownExpand Up@@ -2993,7 +3073,7 @@ bool Compiler::optObtainLoopCloningOpts(LoopCloneContext* context)
{
JITDUMP("Considering loop " FMT_LP " to clone for optimizations.\n", loop->GetIndex());
NaturalLoopIterInfo iterInfo;
if (loop->AnalyzeIteration(&iterInfo))
if (loop->AnalyzeIteration(&iterInfo, /* allowMissingBaseCase */ true))
{
Comment thread
AndyAyersMS marked this conversation as resolved.
context->SetLoopIterInfo(loop->GetIndex(), new (this, CMK_LoopClone) NaturalLoopIterInfo(iterInfo));
}
Expand Down
88 changes: 76 additions & 12 deletions src/coreclr/jit/optimizer.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -356,8 +356,9 @@ bool Compiler::optExtractTestIncr(BasicBlock* cond, GenTree** ppTest, GenTree**
assert(ppTest != nullptr);
assert(ppIncr != nullptr);

// Check if last two statements in the loop body are the increment of the iterator
// and the loop termination test.
// The loop termination test is expected to be the last statement of the exiting
// block. The increment of the iterator is expected somewhere earlier in the
// same block; we scan backward from the test to find an IV-shaped update.
noway_assert(cond->firstStmt() != nullptr);
Statement* testStmt = cond->lastStmt();
noway_assert(testStmt != nullptr && testStmt->GetNextStmt() == nullptr);
Expand All@@ -368,24 +369,87 @@ bool Compiler::optExtractTestIncr(BasicBlock* cond, GenTree** ppTest, GenTree**
testStmt = newTestStmt;
}

// Check if we have the incr stmt before the test stmt, if we don't,
// check if incr is part of the loop "header".
Statement* incrStmt = testStmt->GetPrevStmt();

// If we've added profile instrumentation, we may need to skip past a BB counter update.
// Walk backward from the test statement looking for a candidate IV increment
// of the form 'v = v op c'. For each such candidate, verify it is suitable:
// - the iterator local is not address-exposed (our intervening-read check
// would not see indirect accesses, and AnalyzeIteration rejects
// address-exposed IVs anyway);
// - the test actually reads the iterator (otherwise this is an unrelated
// update that happens to be IV-shaped);
// - no statement strictly between the candidate and the test reads the
// iterator. Such statements would execute with the post-increment value,
// violating the AnalyzeIteration invariant that no loop body IR observes
// the post-increment value except the test. Stores are not checked here:
// AnalyzeIteration's VisitDefs already rejects any def of iterVar in the
// loop other than the picked increment. If the test block is in a try
// region, treat any intervening statement that may throw as if it were
// an intervening read, since an EH handler could observe the
// post-increment value.
// On any rejection, continue scanning backward; only fail when no candidate
// in the block satisfies all checks. A budget bounds the combined work of
// the outer scan and the inner intervening-read scan to avoid pathological
// O(N^2) behavior in blocks with many IV-shaped statements.
//
if (opts.jitFlags->IsSet(JitFlags::JIT_FLAG_BBINSTR) && (incrStmt != nullptr) &&
incrStmt->GetRootNode()->IsBlockProfileUpdate())
{
incrStmt = incrStmt->GetPrevStmt();
Statement* incrStmt = nullptr;
unsigned iterVar = BAD_VAR_NUM;
Statement* firstStmt = cond->firstStmt();
const bool condInTry = cond->hasTryIndex();
unsigned int budget = 100;
if (testStmt != firstStmt)
{
for (Statement* s = testStmt->GetPrevStmt();; s = s->GetPrevStmt())
{
if (budget-- == 0)
{
JITDUMP("optExtractTestIncr: budget exhausted in " FMT_BB "\n", cond->bbNum);
return false;
}
Comment thread
AndyAyersMS marked this conversation as resolved.

unsigned candVar = optIsLoopIncrTree(s->GetRootNode());
if (candVar != BAD_VAR_NUM)
{
if (!lvaGetDesc(candVar)->IsAddressExposed() && gtTreeHasLocalRead(testStmt->GetRootNode(), candVar))
{
bool intermediateUse = false;
for (Statement* t = s->GetNextStmt(); t != testStmt; t = t->GetNextStmt())
{
assert(t != nullptr);
if (budget-- == 0)
{
JITDUMP("optExtractTestIncr: budget exhausted in " FMT_BB "\n", cond->bbNum);
return false;
}
GenTree* root = t->GetRootNode();
if (gtTreeHasLocalRead(root, candVar) || (condInTry && ((root->gtFlags & GTF_EXCEPT) != 0)))
{
intermediateUse = true;
break;
}
}

if (!intermediateUse)
{
incrStmt = s;
iterVar = candVar;
break;
}
}
}

if (s == firstStmt)
{
break;
}
}
Comment thread
AndyAyersMS marked this conversation as resolved.
}

if (incrStmt == nullptr || (optIsLoopIncrTree(incrStmt->GetRootNode()) == BAD_VAR_NUM))
if (incrStmt == nullptr)
{
return false;
}

assert(testStmt != incrStmt);
assert(iterVar != BAD_VAR_NUM);

*ppTest = testStmt->GetRootNode();
*ppIncr = incrStmt->GetRootNode();
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/coreclr/jit/compiler.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -1991,13 +1991,23 @@ struct NaturalLoopIterInfo
// length of an invariant array.
bool HasArrayLengthLimit : 1;

// Whether the consumer must emit its own runtime entry guard for this loop.
// Set when AnalyzeIteration could not prove statically that the loop
// condition [IterVar TestOper Limit] holds on entry (so the analysis
// invariants only hold conditionally). The consumer must insert a runtime
// test equivalent to that condition on the path that reaches the analyzed
// loop body. Only set when AnalyzeIteration is called with
// allowMissingBaseCase=true.
bool NeedsZeroTripGuard : 1;

NaturalLoopIterInfo()
: ExitedOnTrue(false)
, HasConstInit(false)
, HasConstLimit(false)
, HasSimdLimit(false)
, HasInvariantLocalLimit(false)
, HasArrayLengthLimit(false)
, NeedsZeroTripGuard(false)
{
}

Expand DownExpand Up@@ -2191,7 +2201,7 @@ class FlowGraphNaturalLoop
BasicBlock* GetLexicallyTopMostBlock();
BasicBlock* GetLexicallyBottomMostBlock();

bool AnalyzeIteration(NaturalLoopIterInfo* info);
bool AnalyzeIteration(NaturalLoopIterInfo* info, bool allowMissingBaseCase = false);

bool HasDef(unsigned lclNum);

Expand Down
60 changes: 49 additions & 11 deletions src/coreclr/jit/flowgraph.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -5566,19 +5566,31 @@ GenTreeLclVarCommon* FlowGraphNaturalLoop::FindDef(unsigned lclNum)
// the loop.
//
// Parameters:
// info - [out] Loop information
// info - [out] Loop information
// allowMissingBaseCase - If true, succeed even when we cannot prove that the
// loop condition [IterVar TestOper Limit] holds on
// entry, provided the limit form is one we know how
// to materialize at runtime. The caller is then
// responsible for emitting a runtime entry guard
// equivalent to that condition when
// info->NeedsZeroTripGuard is set on return.
// Defaults to false; existing callers retain the
// stronger guarantees described below.
Comment thread
AndyAyersMS marked this conversation as resolved.
//
// Returns:
// True if the structure was analyzed and we can make guarantees about it;
// otherwise false.
//
// Remarks:
// On a true return, the function guarantees that the loop invariant is true
// and maintained at all points within the loop, except possibly right after
// the update of the iterator variable (NaturalLoopIterInfo::IterTree). The
// function guarantees that the test (NaturalLoopIterInfo::TestTree) occurs
// immediately after the update, so no IR in the loop is executed without the
// loop invariant being true, except for the test.
// On a true return with allowMissingBaseCase == false (or with the flag set
// but info->NeedsZeroTripGuard == false), the function guarantees that the
// loop invariant is true and maintained at all points within the loop,
// except possibly right after the update of the iterator variable
// (NaturalLoopIterInfo::IterTree). optExtractTestIncr permits the IV update
// to be separated from the loop test by other statements, but it rejects
// any candidate where an intervening statement references the iterator
// variable, so no IR in the loop is executed observing the post-update
// value of the iterator except the test itself.
//
// The loop invariant is defined as the expression obtained by
// [info->IterVar] [info->TestOper()] [info->Limit()]. Note that
Expand All@@ -5599,7 +5611,19 @@ GenTreeLclVarCommon* FlowGraphNaturalLoop::FindDef(unsigned lclNum)
// In some cases we also know the initial value on entry to the loop; see
// ::HasConstInit and ::ConstInitValue.
//
bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)
// When allowMissingBaseCase is true and the function would otherwise fail
// because the loop condition [IterVar TestOper Limit] cannot be proven to
// hold on entry (no suitable preheader BBJ_COND guard and no constant
// init/limit pair that proves the base case), the function may instead
// succeed with info->NeedsZeroTripGuard set. In that mode the above
// invariant only holds conditionally: it is the caller's obligation to
// insert a runtime test equivalent to [IterVar TestOper() Limit] on the
// path that reaches the analyzed loop body. Loop cloning uses this to emit
// the guard as an extra cloning condition on the fast-path version. Note
// that this includes do/while-style loops that are guaranteed to execute
// at least once but whose loop condition may be false on entry.
//
bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info, bool allowMissingBaseCase)
{
Comment thread
AndyAyersMS marked this conversation as resolved.
JITDUMP("Analyzing iteration for " FMT_LP " with header " FMT_BB "\n", m_index, m_header->bbNum);

Expand All@@ -5613,7 +5637,8 @@ bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)

GenTree* test = nullptr;

info->IterVar = BAD_VAR_NUM;
info->IterVar = BAD_VAR_NUM;
info->NeedsZeroTripGuard = false;

for (FlowEdge* exitEdge : ExitEdges())
{
Expand DownExpand Up@@ -5700,8 +5725,21 @@ bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)

if (!CheckLoopConditionBaseCase(preheader, info))
{
JITDUMP(" Loop condition may not be true on the first iteration\n");
return false;
// If the caller can emit its own runtime guard for the case where the
// loop body might not execute on the first iteration, allow the loop
// through provided we have enough symbolic info about init and limit
// to express the guard.
if (allowMissingBaseCase && (info->HasConstLimit || info->HasInvariantLocalLimit || info->HasArrayLengthLimit))
{
JITDUMP(" Loop condition may not be true on the first iteration; deferring to caller "
"(NeedsZeroTripGuard)\n");
info->NeedsZeroTripGuard = true;
}
else
{
JITDUMP(" Loop condition may not be true on the first iteration\n");
return false;
}
Comment thread
AndyAyersMS marked this conversation as resolved.
}

#ifdef DEBUG
Expand Down
104 changes: 92 additions & 12 deletions src/coreclr/jit/loopcloning.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1227,6 +1227,95 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
return false;
}

// If the loop limit is an array length, compute the underlying ArrIndex
// and queue the deref check once up front. Both the optional zero-trip
// guard below and the regular limit conditions further down reuse this
// single ArrIndex to avoid duplicating the deref entry and allocation.
//
ArrIndex* limitArrIndex = nullptr;
if (iterInfo->HasArrayLengthLimit)
{
limitArrIndex = new (getAllocator(CMK_LoopClone)) ArrIndex(getAllocator(CMK_LoopClone));
if (!iterInfo->ArrLenLimit(this, limitArrIndex))
{
JITDUMP("> ArrLen not matching\n");
return false;
}

LC_Array array(LC_Array::Jagged, limitArrIndex, LC_Array::None);
context->EnsureArrayDerefs(loop->GetIndex())->Push(array);
}

// If AnalyzeIteration could not prove the loop condition holds on entry,
// emit an explicit runtime entry guard as one of the cloning conditions.
// The fast path is then only entered when "init TestOper limit" holds.
if (iterInfo->NeedsZeroTripGuard)
{
LC_Ident initIdent;
if (iterInfo->HasConstInit)
{
if (iterInfo->ConstInitValue < 0)
{
JITDUMP("> NeedsZeroTripGuard: init %d is invalid\n", iterInfo->ConstInitValue);
return false;
}
initIdent = LC_Ident::CreateConst(static_cast<unsigned>(iterInfo->ConstInitValue));
}
else
{
// Init is unknown statically; use the IV local as it stands at the
// preheader (the analysis already verified the local is not
// address-exposed and has no extraneous defs inside the loop, so
// reading it in the preheader gives the entry value).
const unsigned initLcl = iterInfo->IterVar;
if (!genActualTypeIsInt(lvaGetDesc(initLcl)))
{
JITDUMP("> NeedsZeroTripGuard: iter var V%02u not compatible with TYP_INT\n", initLcl);
return false;
}
initIdent = LC_Ident::CreateVar(initLcl);
}

LC_Ident limitIdent;
if (iterInfo->HasConstLimit)
{
int limit = iterInfo->ConstLimit();
if (limit < 0)
{
JITDUMP("> NeedsZeroTripGuard: limit %d is invalid\n", limit);
return false;
}
limitIdent = LC_Ident::CreateConst(static_cast<unsigned>(limit));
}
else if (iterInfo->HasInvariantLocalLimit)
{
const unsigned limitLcl = iterInfo->VarLimit();
if (!genActualTypeIsInt(lvaGetDesc(limitLcl)))
{
JITDUMP("> NeedsZeroTripGuard: limit var V%02u not compatible with TYP_INT\n", limitLcl);
return false;
}
limitIdent = LC_Ident::CreateVar(limitLcl);
}
else if (iterInfo->HasArrayLengthLimit)
{
assert(limitArrIndex != nullptr);
limitIdent = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}
Comment thread
AndyAyersMS marked this conversation as resolved.
else
{
JITDUMP("> NeedsZeroTripGuard: undetected limit\n");
return false;
}

// TestOper() returns the stays-in-loop relop in IV-on-lhs form, already
// adjusted for IsReversed and ExitedOnTrue.
LC_Condition zeroTrip(iterInfo->TestOper(), LC_Expr(initIdent), LC_Expr(limitIdent),
iterInfo->TestTree->IsUnsigned());
context->EnsureConditions(loop->GetIndex())->Push(zeroTrip);
JITDUMP("Added zero-trip guard cloning condition\n");
}

LC_Ident ident;
// Init conditions
if (iterInfo->HasConstInit)
Expand DownExpand Up@@ -1311,17 +1400,8 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}
else if (iterInfo->HasArrayLengthLimit)
{
ArrIndex* index = new (getAllocator(CMK_LoopClone)) ArrIndex(getAllocator(CMK_LoopClone));
if (!iterInfo->ArrLenLimit(this, index))
{
JITDUMP("> ArrLen not matching\n");
return false;
}
ident = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, index, LC_Array::ArrLen));

// Ensure that this array must be dereference-able, before executing the actual condition.
LC_Array array(LC_Array::Jagged, index, LC_Array::None);
context->EnsureArrayDerefs(loop->GetIndex())->Push(array);
assert(limitArrIndex != nullptr);
ident = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}
else
{
Expand DownExpand Up@@ -2993,7 +3073,7 @@ bool Compiler::optObtainLoopCloningOpts(LoopCloneContext* context)
{
JITDUMP("Considering loop " FMT_LP " to clone for optimizations.\n", loop->GetIndex());
NaturalLoopIterInfo iterInfo;
if (loop->AnalyzeIteration(&iterInfo))
if (loop->AnalyzeIteration(&iterInfo, /* allowMissingBaseCase */ true))
{
Comment thread
AndyAyersMS marked this conversation as resolved.
context->SetLoopIterInfo(loop->GetIndex(), new (this, CMK_LoopClone) NaturalLoopIterInfo(iterInfo));
}
Expand Down
88 changes: 76 additions & 12 deletions src/coreclr/jit/optimizer.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -356,8 +356,9 @@ bool Compiler::optExtractTestIncr(BasicBlock* cond, GenTree** ppTest, GenTree**
assert(ppTest != nullptr);
assert(ppIncr != nullptr);

// Check if last two statements in the loop body are the increment of the iterator
// and the loop termination test.
// The loop termination test is expected to be the last statement of the exiting
// block. The increment of the iterator is expected somewhere earlier in the
// same block; we scan backward from the test to find an IV-shaped update.
noway_assert(cond->firstStmt() != nullptr);
Statement* testStmt = cond->lastStmt();
noway_assert(testStmt != nullptr && testStmt->GetNextStmt() == nullptr);
Expand All@@ -368,24 +369,87 @@ bool Compiler::optExtractTestIncr(BasicBlock* cond, GenTree** ppTest, GenTree**
testStmt = newTestStmt;
}

// Check if we have the incr stmt before the test stmt, if we don't,
// check if incr is part of the loop "header".
Statement* incrStmt = testStmt->GetPrevStmt();

// If we've added profile instrumentation, we may need to skip past a BB counter update.
// Walk backward from the test statement looking for a candidate IV increment
// of the form 'v = v op c'. For each such candidate, verify it is suitable:
// - the iterator local is not address-exposed (our intervening-read check
// would not see indirect accesses, and AnalyzeIteration rejects
// address-exposed IVs anyway);
// - the test actually reads the iterator (otherwise this is an unrelated
// update that happens to be IV-shaped);
// - no statement strictly between the candidate and the test reads the
// iterator. Such statements would execute with the post-increment value,
// violating the AnalyzeIteration invariant that no loop body IR observes
// the post-increment value except the test. Stores are not checked here:
// AnalyzeIteration's VisitDefs already rejects any def of iterVar in the
// loop other than the picked increment. If the test block is in a try
// region, treat any intervening statement that may throw as if it were
// an intervening read, since an EH handler could observe the
// post-increment value.
// On any rejection, continue scanning backward; only fail when no candidate
// in the block satisfies all checks. A budget bounds the combined work of
// the outer scan and the inner intervening-read scan to avoid pathological
// O(N^2) behavior in blocks with many IV-shaped statements.
//
if (opts.jitFlags->IsSet(JitFlags::JIT_FLAG_BBINSTR) && (incrStmt != nullptr) &&
incrStmt->GetRootNode()->IsBlockProfileUpdate())
{
incrStmt = incrStmt->GetPrevStmt();
Statement* incrStmt = nullptr;
unsigned iterVar = BAD_VAR_NUM;
Statement* firstStmt = cond->firstStmt();
const bool condInTry = cond->hasTryIndex();
unsigned int budget = 100;
if (testStmt != firstStmt)
{
for (Statement* s = testStmt->GetPrevStmt();; s = s->GetPrevStmt())
{
if (budget-- == 0)
{
JITDUMP("optExtractTestIncr: budget exhausted in " FMT_BB "\n", cond->bbNum);
return false;
}
Comment thread
AndyAyersMS marked this conversation as resolved.

unsigned candVar = optIsLoopIncrTree(s->GetRootNode());
if (candVar != BAD_VAR_NUM)
{
if (!lvaGetDesc(candVar)->IsAddressExposed() && gtTreeHasLocalRead(testStmt->GetRootNode(), candVar))
{
bool intermediateUse = false;
for (Statement* t = s->GetNextStmt(); t != testStmt; t = t->GetNextStmt())
{
assert(t != nullptr);
if (budget-- == 0)
{
JITDUMP("optExtractTestIncr: budget exhausted in " FMT_BB "\n", cond->bbNum);
return false;
}
GenTree* root = t->GetRootNode();
if (gtTreeHasLocalRead(root, candVar) || (condInTry && ((root->gtFlags & GTF_EXCEPT) != 0)))
{
intermediateUse = true;
break;
}
}

if (!intermediateUse)
{
incrStmt = s;
iterVar = candVar;
break;
}
}
}

if (s == firstStmt)
{
break;
}
}
Comment thread
AndyAyersMS marked this conversation as resolved.
}

if (incrStmt == nullptr || (optIsLoopIncrTree(incrStmt->GetRootNode()) == BAD_VAR_NUM))
if (incrStmt == nullptr)
{
return false;
}

assert(testStmt != incrStmt);
assert(iterVar != BAD_VAR_NUM);

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/coreclr/jit/compiler.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -1991,13 +1991,23 @@ struct NaturalLoopIterInfo
// length of an invariant array.
bool HasArrayLengthLimit : 1;

// Whether the consumer must emit its own runtime entry guard for this loop.
// Set when AnalyzeIteration could not prove statically that the loop
// condition [IterVar TestOper Limit] holds on entry (so the analysis
// invariants only hold conditionally). The consumer must insert a runtime
// test equivalent to that condition on the path that reaches the analyzed
// loop body. Only set when AnalyzeIteration is called with
// allowMissingBaseCase=true.
bool NeedsZeroTripGuard : 1;

NaturalLoopIterInfo()
: ExitedOnTrue(false)
, HasConstInit(false)
, HasConstLimit(false)
, HasSimdLimit(false)
, HasInvariantLocalLimit(false)
, HasArrayLengthLimit(false)
, NeedsZeroTripGuard(false)
{
}

Expand DownExpand Up@@ -2191,7 +2201,7 @@ class FlowGraphNaturalLoop
BasicBlock* GetLexicallyTopMostBlock();
BasicBlock* GetLexicallyBottomMostBlock();

bool AnalyzeIteration(NaturalLoopIterInfo* info);
bool AnalyzeIteration(NaturalLoopIterInfo* info, bool allowMissingBaseCase = false);

bool HasDef(unsigned lclNum);

Expand Down
60 changes: 49 additions & 11 deletions src/coreclr/jit/flowgraph.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -5566,19 +5566,31 @@ GenTreeLclVarCommon* FlowGraphNaturalLoop::FindDef(unsigned lclNum)
// the loop.
//
// Parameters:
// info - [out] Loop information
// info - [out] Loop information
// allowMissingBaseCase - If true, succeed even when we cannot prove that the
// loop condition [IterVar TestOper Limit] holds on
// entry, provided the limit form is one we know how
// to materialize at runtime. The caller is then
// responsible for emitting a runtime entry guard
// equivalent to that condition when
// info->NeedsZeroTripGuard is set on return.
// Defaults to false; existing callers retain the
// stronger guarantees described below.
Comment thread
AndyAyersMS marked this conversation as resolved.
//
// Returns:
// True if the structure was analyzed and we can make guarantees about it;
// otherwise false.
//
// Remarks:
// On a true return, the function guarantees that the loop invariant is true
// and maintained at all points within the loop, except possibly right after
// the update of the iterator variable (NaturalLoopIterInfo::IterTree). The
// function guarantees that the test (NaturalLoopIterInfo::TestTree) occurs
// immediately after the update, so no IR in the loop is executed without the
// loop invariant being true, except for the test.
// On a true return with allowMissingBaseCase == false (or with the flag set
// but info->NeedsZeroTripGuard == false), the function guarantees that the
// loop invariant is true and maintained at all points within the loop,
// except possibly right after the update of the iterator variable
// (NaturalLoopIterInfo::IterTree). optExtractTestIncr permits the IV update
// to be separated from the loop test by other statements, but it rejects
// any candidate where an intervening statement references the iterator
// variable, so no IR in the loop is executed observing the post-update
// value of the iterator except the test itself.
//
// The loop invariant is defined as the expression obtained by
// [info->IterVar] [info->TestOper()] [info->Limit()]. Note that
Expand All@@ -5599,7 +5611,19 @@ GenTreeLclVarCommon* FlowGraphNaturalLoop::FindDef(unsigned lclNum)
// In some cases we also know the initial value on entry to the loop; see
// ::HasConstInit and ::ConstInitValue.
//
bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)
// When allowMissingBaseCase is true and the function would otherwise fail
// because the loop condition [IterVar TestOper Limit] cannot be proven to
// hold on entry (no suitable preheader BBJ_COND guard and no constant
// init/limit pair that proves the base case), the function may instead
// succeed with info->NeedsZeroTripGuard set. In that mode the above
// invariant only holds conditionally: it is the caller's obligation to
// insert a runtime test equivalent to [IterVar TestOper() Limit] on the
// path that reaches the analyzed loop body. Loop cloning uses this to emit
// the guard as an extra cloning condition on the fast-path version. Note
// that this includes do/while-style loops that are guaranteed to execute
// at least once but whose loop condition may be false on entry.
//
bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info, bool allowMissingBaseCase)
{
Comment thread
AndyAyersMS marked this conversation as resolved.
JITDUMP("Analyzing iteration for " FMT_LP " with header " FMT_BB "\n", m_index, m_header->bbNum);

Expand All@@ -5613,7 +5637,8 @@ bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)

GenTree* test = nullptr;

info->IterVar = BAD_VAR_NUM;
info->IterVar = BAD_VAR_NUM;
info->NeedsZeroTripGuard = false;

for (FlowEdge* exitEdge : ExitEdges())
{
Expand DownExpand Up@@ -5700,8 +5725,21 @@ bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)

if (!CheckLoopConditionBaseCase(preheader, info))
{
JITDUMP(" Loop condition may not be true on the first iteration\n");
return false;
// If the caller can emit its own runtime guard for the case where the
// loop body might not execute on the first iteration, allow the loop
// through provided we have enough symbolic info about init and limit
// to express the guard.
if (allowMissingBaseCase && (info->HasConstLimit || info->HasInvariantLocalLimit || info->HasArrayLengthLimit))
{
JITDUMP(" Loop condition may not be true on the first iteration; deferring to caller "
"(NeedsZeroTripGuard)\n");
info->NeedsZeroTripGuard = true;
}
else
{
JITDUMP(" Loop condition may not be true on the first iteration\n");
return false;
}
Comment thread
AndyAyersMS marked this conversation as resolved.
}

#ifdef DEBUG
Expand Down
104 changes: 92 additions & 12 deletions src/coreclr/jit/loopcloning.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1227,6 +1227,95 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
return false;
}

// If the loop limit is an array length, compute the underlying ArrIndex
// and queue the deref check once up front. Both the optional zero-trip
// guard below and the regular limit conditions further down reuse this
// single ArrIndex to avoid duplicating the deref entry and allocation.
//
ArrIndex* limitArrIndex = nullptr;
if (iterInfo->HasArrayLengthLimit)
{
limitArrIndex = new (getAllocator(CMK_LoopClone)) ArrIndex(getAllocator(CMK_LoopClone));
if (!iterInfo->ArrLenLimit(this, limitArrIndex))
{
JITDUMP("> ArrLen not matching\n");
return false;
}

LC_Array array(LC_Array::Jagged, limitArrIndex, LC_Array::None);
context->EnsureArrayDerefs(loop->GetIndex())->Push(array);
}

// If AnalyzeIteration could not prove the loop condition holds on entry,
// emit an explicit runtime entry guard as one of the cloning conditions.
// The fast path is then only entered when "init TestOper limit" holds.
if (iterInfo->NeedsZeroTripGuard)
{
LC_Ident initIdent;
if (iterInfo->HasConstInit)
{
if (iterInfo->ConstInitValue < 0)
{
JITDUMP("> NeedsZeroTripGuard: init %d is invalid\n", iterInfo->ConstInitValue);
return false;
}
initIdent = LC_Ident::CreateConst(static_cast<unsigned>(iterInfo->ConstInitValue));
}
else
{
// Init is unknown statically; use the IV local as it stands at the
// preheader (the analysis already verified the local is not
// address-exposed and has no extraneous defs inside the loop, so
// reading it in the preheader gives the entry value).
const unsigned initLcl = iterInfo->IterVar;
if (!genActualTypeIsInt(lvaGetDesc(initLcl)))
{
JITDUMP("> NeedsZeroTripGuard: iter var V%02u not compatible with TYP_INT\n", initLcl);
return false;
}
initIdent = LC_Ident::CreateVar(initLcl);
}

LC_Ident limitIdent;
if (iterInfo->HasConstLimit)
{
int limit = iterInfo->ConstLimit();
if (limit < 0)
{
JITDUMP("> NeedsZeroTripGuard: limit %d is invalid\n", limit);
return false;
}
limitIdent = LC_Ident::CreateConst(static_cast<unsigned>(limit));
}
else if (iterInfo->HasInvariantLocalLimit)
{
const unsigned limitLcl = iterInfo->VarLimit();
if (!genActualTypeIsInt(lvaGetDesc(limitLcl)))
{
JITDUMP("> NeedsZeroTripGuard: limit var V%02u not compatible with TYP_INT\n", limitLcl);
return false;
}
limitIdent = LC_Ident::CreateVar(limitLcl);
}
else if (iterInfo->HasArrayLengthLimit)
{
assert(limitArrIndex != nullptr);
limitIdent = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}
Comment thread
AndyAyersMS marked this conversation as resolved.
else
{
JITDUMP("> NeedsZeroTripGuard: undetected limit\n");
return false;
}

// TestOper() returns the stays-in-loop relop in IV-on-lhs form, already
// adjusted for IsReversed and ExitedOnTrue.
LC_Condition zeroTrip(iterInfo->TestOper(), LC_Expr(initIdent), LC_Expr(limitIdent),
iterInfo->TestTree->IsUnsigned());
context->EnsureConditions(loop->GetIndex())->Push(zeroTrip);
JITDUMP("Added zero-trip guard cloning condition\n");
}

LC_Ident ident;
// Init conditions
if (iterInfo->HasConstInit)
Expand DownExpand Up@@ -1311,17 +1400,8 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}
else if (iterInfo->HasArrayLengthLimit)
{
ArrIndex* index = new (getAllocator(CMK_LoopClone)) ArrIndex(getAllocator(CMK_LoopClone));
if (!iterInfo->ArrLenLimit(this, index))
{
JITDUMP("> ArrLen not matching\n");
return false;
}
ident = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, index, LC_Array::ArrLen));

// Ensure that this array must be dereference-able, before executing the actual condition.
LC_Array array(LC_Array::Jagged, index, LC_Array::None);
context->EnsureArrayDerefs(loop->GetIndex())->Push(array);
assert(limitArrIndex != nullptr);
ident = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}
else
{
Expand DownExpand Up@@ -2993,7 +3073,7 @@ bool Compiler::optObtainLoopCloningOpts(LoopCloneContext* context)
{
JITDUMP("Considering loop " FMT_LP " to clone for optimizations.\n", loop->GetIndex());
NaturalLoopIterInfo iterInfo;
if (loop->AnalyzeIteration(&iterInfo))
if (loop->AnalyzeIteration(&iterInfo, /* allowMissingBaseCase */ true))
{
Comment thread
AndyAyersMS marked this conversation as resolved.
context->SetLoopIterInfo(loop->GetIndex(), new (this, CMK_LoopClone) NaturalLoopIterInfo(iterInfo));
}
Expand Down
88 changes: 76 additions & 12 deletions src/coreclr/jit/optimizer.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -356,8 +356,9 @@ bool Compiler::optExtractTestIncr(BasicBlock* cond, GenTree** ppTest, GenTree**
assert(ppTest != nullptr);
assert(ppIncr != nullptr);

// Check if last two statements in the loop body are the increment of the iterator
// and the loop termination test.
// The loop termination test is expected to be the last statement of the exiting
// block. The increment of the iterator is expected somewhere earlier in the
// same block; we scan backward from the test to find an IV-shaped update.
noway_assert(cond->firstStmt() != nullptr);
Statement* testStmt = cond->lastStmt();
noway_assert(testStmt != nullptr && testStmt->GetNextStmt() == nullptr);
Expand All@@ -368,24 +369,87 @@ bool Compiler::optExtractTestIncr(BasicBlock* cond, GenTree** ppTest, GenTree**
testStmt = newTestStmt;
}

// Check if we have the incr stmt before the test stmt, if we don't,
// check if incr is part of the loop "header".
Statement* incrStmt = testStmt->GetPrevStmt();

// If we've added profile instrumentation, we may need to skip past a BB counter update.
// Walk backward from the test statement looking for a candidate IV increment
// of the form 'v = v op c'. For each such candidate, verify it is suitable:
// - the iterator local is not address-exposed (our intervening-read check
// would not see indirect accesses, and AnalyzeIteration rejects
// address-exposed IVs anyway);
// - the test actually reads the iterator (otherwise this is an unrelated
// update that happens to be IV-shaped);
// - no statement strictly between the candidate and the test reads the
// iterator. Such statements would execute with the post-increment value,
// violating the AnalyzeIteration invariant that no loop body IR observes
// the post-increment value except the test. Stores are not checked here:
// AnalyzeIteration's VisitDefs already rejects any def of iterVar in the
// loop other than the picked increment. If the test block is in a try
// region, treat any intervening statement that may throw as if it were
// an intervening read, since an EH handler could observe the
// post-increment value.
// On any rejection, continue scanning backward; only fail when no candidate
// in the block satisfies all checks. A budget bounds the combined work of
// the outer scan and the inner intervening-read scan to avoid pathological
// O(N^2) behavior in blocks with many IV-shaped statements.
//
if (opts.jitFlags->IsSet(JitFlags::JIT_FLAG_BBINSTR) && (incrStmt != nullptr) &&
incrStmt->GetRootNode()->IsBlockProfileUpdate())
{
incrStmt = incrStmt->GetPrevStmt();
Statement* incrStmt = nullptr;
unsigned iterVar = BAD_VAR_NUM;
Statement* firstStmt = cond->firstStmt();
const bool condInTry = cond->hasTryIndex();
unsigned int budget = 100;
if (testStmt != firstStmt)
{
for (Statement* s = testStmt->GetPrevStmt();; s = s->GetPrevStmt())
{
if (budget-- == 0)
{
JITDUMP("optExtractTestIncr: budget exhausted in " FMT_BB "\n", cond->bbNum);
return false;
}
Comment thread
AndyAyersMS marked this conversation as resolved.

unsigned candVar = optIsLoopIncrTree(s->GetRootNode());
if (candVar != BAD_VAR_NUM)
{
if (!lvaGetDesc(candVar)->IsAddressExposed() && gtTreeHasLocalRead(testStmt->GetRootNode(), candVar))
{
bool intermediateUse = false;
for (Statement* t = s->GetNextStmt(); t != testStmt; t = t->GetNextStmt())
{
assert(t != nullptr);
if (budget-- == 0)
{
JITDUMP("optExtractTestIncr: budget exhausted in " FMT_BB "\n", cond->bbNum);
return false;
}
GenTree* root = t->GetRootNode();
if (gtTreeHasLocalRead(root, candVar) || (condInTry && ((root->gtFlags & GTF_EXCEPT) != 0)))
{
intermediateUse = true;
break;
}
}

if (!intermediateUse)
{
incrStmt = s;
iterVar = candVar;
break;
}
}
}

if (s == firstStmt)
{
break;
}
}
Comment thread
AndyAyersMS marked this conversation as resolved.
}

if (incrStmt == nullptr || (optIsLoopIncrTree(incrStmt->GetRootNode()) == BAD_VAR_NUM))
if (incrStmt == nullptr)
{
return false;
}

assert(testStmt != incrStmt);
assert(iterVar != BAD_VAR_NUM);

*ppTest = testStmt->GetRootNode();
*ppIncr = incrStmt->GetRootNode();
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } 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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/coreclr/jit/compiler.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -1991,13 +1991,23 @@ struct NaturalLoopIterInfo
// length of an invariant array.
bool HasArrayLengthLimit : 1;

// Whether the consumer must emit its own runtime entry guard for this loop.
// Set when AnalyzeIteration could not prove statically that the loop
// condition [IterVar TestOper Limit] holds on entry (so the analysis
// invariants only hold conditionally). The consumer must insert a runtime
// test equivalent to that condition on the path that reaches the analyzed
// loop body. Only set when AnalyzeIteration is called with
// allowMissingBaseCase=true.
bool NeedsZeroTripGuard : 1;

NaturalLoopIterInfo()
: ExitedOnTrue(false)
, HasConstInit(false)
, HasConstLimit(false)
, HasSimdLimit(false)
, HasInvariantLocalLimit(false)
, HasArrayLengthLimit(false)
, NeedsZeroTripGuard(false)
{
}

Expand DownExpand Up@@ -2191,7 +2201,7 @@ class FlowGraphNaturalLoop
BasicBlock* GetLexicallyTopMostBlock();
BasicBlock* GetLexicallyBottomMostBlock();

bool AnalyzeIteration(NaturalLoopIterInfo* info);
bool AnalyzeIteration(NaturalLoopIterInfo* info, bool allowMissingBaseCase = false);

bool HasDef(unsigned lclNum);

Expand Down
60 changes: 49 additions & 11 deletions src/coreclr/jit/flowgraph.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -5566,19 +5566,31 @@ GenTreeLclVarCommon* FlowGraphNaturalLoop::FindDef(unsigned lclNum)
// the loop.
//
// Parameters:
// info - [out] Loop information
// info - [out] Loop information
// allowMissingBaseCase - If true, succeed even when we cannot prove that the
// loop condition [IterVar TestOper Limit] holds on
// entry, provided the limit form is one we know how
// to materialize at runtime. The caller is then
// responsible for emitting a runtime entry guard
// equivalent to that condition when
// info->NeedsZeroTripGuard is set on return.
// Defaults to false; existing callers retain the
// stronger guarantees described below.
Comment thread
AndyAyersMS marked this conversation as resolved.
//
// Returns:
// True if the structure was analyzed and we can make guarantees about it;
// otherwise false.
//
// Remarks:
// On a true return, the function guarantees that the loop invariant is true
// and maintained at all points within the loop, except possibly right after
// the update of the iterator variable (NaturalLoopIterInfo::IterTree). The
// function guarantees that the test (NaturalLoopIterInfo::TestTree) occurs
// immediately after the update, so no IR in the loop is executed without the
// loop invariant being true, except for the test.
// On a true return with allowMissingBaseCase == false (or with the flag set
// but info->NeedsZeroTripGuard == false), the function guarantees that the
// loop invariant is true and maintained at all points within the loop,
// except possibly right after the update of the iterator variable
// (NaturalLoopIterInfo::IterTree). optExtractTestIncr permits the IV update
// to be separated from the loop test by other statements, but it rejects
// any candidate where an intervening statement references the iterator
// variable, so no IR in the loop is executed observing the post-update
// value of the iterator except the test itself.
//
// The loop invariant is defined as the expression obtained by
// [info->IterVar] [info->TestOper()] [info->Limit()]. Note that
Expand All@@ -5599,7 +5611,19 @@ GenTreeLclVarCommon* FlowGraphNaturalLoop::FindDef(unsigned lclNum)
// In some cases we also know the initial value on entry to the loop; see
// ::HasConstInit and ::ConstInitValue.
//
bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)
// When allowMissingBaseCase is true and the function would otherwise fail
// because the loop condition [IterVar TestOper Limit] cannot be proven to
// hold on entry (no suitable preheader BBJ_COND guard and no constant
// init/limit pair that proves the base case), the function may instead
// succeed with info->NeedsZeroTripGuard set. In that mode the above
// invariant only holds conditionally: it is the caller's obligation to
// insert a runtime test equivalent to [IterVar TestOper() Limit] on the
// path that reaches the analyzed loop body. Loop cloning uses this to emit
// the guard as an extra cloning condition on the fast-path version. Note
// that this includes do/while-style loops that are guaranteed to execute
// at least once but whose loop condition may be false on entry.
//
bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info, bool allowMissingBaseCase)
{
Comment thread
AndyAyersMS marked this conversation as resolved.
JITDUMP("Analyzing iteration for " FMT_LP " with header " FMT_BB "\n", m_index, m_header->bbNum);

Expand All@@ -5613,7 +5637,8 @@ bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)

GenTree* test = nullptr;

info->IterVar = BAD_VAR_NUM;
info->IterVar = BAD_VAR_NUM;
info->NeedsZeroTripGuard = false;

for (FlowEdge* exitEdge : ExitEdges())
{
Expand DownExpand Up@@ -5700,8 +5725,21 @@ bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)

if (!CheckLoopConditionBaseCase(preheader, info))
{
JITDUMP(" Loop condition may not be true on the first iteration\n");
return false;
// If the caller can emit its own runtime guard for the case where the
// loop body might not execute on the first iteration, allow the loop
// through provided we have enough symbolic info about init and limit
// to express the guard.
if (allowMissingBaseCase && (info->HasConstLimit || info->HasInvariantLocalLimit || info->HasArrayLengthLimit))
{
JITDUMP(" Loop condition may not be true on the first iteration; deferring to caller "
"(NeedsZeroTripGuard)\n");
info->NeedsZeroTripGuard = true;
}
else
{
JITDUMP(" Loop condition may not be true on the first iteration\n");
return false;
}
Comment thread
AndyAyersMS marked this conversation as resolved.
}

#ifdef DEBUG
Expand Down
104 changes: 92 additions & 12 deletions src/coreclr/jit/loopcloning.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1227,6 +1227,95 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
return false;
}

// If the loop limit is an array length, compute the underlying ArrIndex
// and queue the deref check once up front. Both the optional zero-trip
// guard below and the regular limit conditions further down reuse this
// single ArrIndex to avoid duplicating the deref entry and allocation.
//
ArrIndex* limitArrIndex = nullptr;
if (iterInfo->HasArrayLengthLimit)
{
limitArrIndex = new (getAllocator(CMK_LoopClone)) ArrIndex(getAllocator(CMK_LoopClone));
if (!iterInfo->ArrLenLimit(this, limitArrIndex))
{
JITDUMP("> ArrLen not matching\n");
return false;
}

LC_Array array(LC_Array::Jagged, limitArrIndex, LC_Array::None);
context->EnsureArrayDerefs(loop->GetIndex())->Push(array);
}

// If AnalyzeIteration could not prove the loop condition holds on entry,
// emit an explicit runtime entry guard as one of the cloning conditions.
// The fast path is then only entered when "init TestOper limit" holds.
if (iterInfo->NeedsZeroTripGuard)
{
LC_Ident initIdent;
if (iterInfo->HasConstInit)
{
if (iterInfo->ConstInitValue < 0)
{
JITDUMP("> NeedsZeroTripGuard: init %d is invalid\n", iterInfo->ConstInitValue);
return false;
}
initIdent = LC_Ident::CreateConst(static_cast<unsigned>(iterInfo->ConstInitValue));
}
else
{
// Init is unknown statically; use the IV local as it stands at the
// preheader (the analysis already verified the local is not
// address-exposed and has no extraneous defs inside the loop, so
// reading it in the preheader gives the entry value).
const unsigned initLcl = iterInfo->IterVar;
if (!genActualTypeIsInt(lvaGetDesc(initLcl)))
{
JITDUMP("> NeedsZeroTripGuard: iter var V%02u not compatible with TYP_INT\n", initLcl);
return false;
}
initIdent = LC_Ident::CreateVar(initLcl);
}

LC_Ident limitIdent;
if (iterInfo->HasConstLimit)
{
int limit = iterInfo->ConstLimit();
if (limit < 0)
{
JITDUMP("> NeedsZeroTripGuard: limit %d is invalid\n", limit);
return false;
}
limitIdent = LC_Ident::CreateConst(static_cast<unsigned>(limit));
}
else if (iterInfo->HasInvariantLocalLimit)
{
const unsigned limitLcl = iterInfo->VarLimit();
if (!genActualTypeIsInt(lvaGetDesc(limitLcl)))
{
JITDUMP("> NeedsZeroTripGuard: limit var V%02u not compatible with TYP_INT\n", limitLcl);
return false;
}
limitIdent = LC_Ident::CreateVar(limitLcl);
}
else if (iterInfo->HasArrayLengthLimit)
{
assert(limitArrIndex != nullptr);
limitIdent = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}
Comment thread
AndyAyersMS marked this conversation as resolved.
else
{
JITDUMP("> NeedsZeroTripGuard: undetected limit\n");
return false;
}

// TestOper() returns the stays-in-loop relop in IV-on-lhs form, already
// adjusted for IsReversed and ExitedOnTrue.
LC_Condition zeroTrip(iterInfo->TestOper(), LC_Expr(initIdent), LC_Expr(limitIdent),
iterInfo->TestTree->IsUnsigned());
context->EnsureConditions(loop->GetIndex())->Push(zeroTrip);
JITDUMP("Added zero-trip guard cloning condition\n");
}

LC_Ident ident;
// Init conditions
if (iterInfo->HasConstInit)
Expand DownExpand Up@@ -1311,17 +1400,8 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}
else if (iterInfo->HasArrayLengthLimit)
{
ArrIndex* index = new (getAllocator(CMK_LoopClone)) ArrIndex(getAllocator(CMK_LoopClone));
if (!iterInfo->ArrLenLimit(this, index))
{
JITDUMP("> ArrLen not matching\n");
return false;
}
ident = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, index, LC_Array::ArrLen));

// Ensure that this array must be dereference-able, before executing the actual condition.
LC_Array array(LC_Array::Jagged, index, LC_Array::None);
context->EnsureArrayDerefs(loop->GetIndex())->Push(array);
assert(limitArrIndex != nullptr);
ident = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}
else
{
Expand DownExpand Up@@ -2993,7 +3073,7 @@ bool Compiler::optObtainLoopCloningOpts(LoopCloneContext* context)
{
JITDUMP("Considering loop " FMT_LP " to clone for optimizations.\n", loop->GetIndex());
NaturalLoopIterInfo iterInfo;
if (loop->AnalyzeIteration(&iterInfo))
if (loop->AnalyzeIteration(&iterInfo, /* allowMissingBaseCase */ true))
{
Comment thread
AndyAyersMS marked this conversation as resolved.
context->SetLoopIterInfo(loop->GetIndex(), new (this, CMK_LoopClone) NaturalLoopIterInfo(iterInfo));
}
Expand Down
88 changes: 76 additions & 12 deletions src/coreclr/jit/optimizer.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -356,8 +356,9 @@ bool Compiler::optExtractTestIncr(BasicBlock* cond, GenTree** ppTest, GenTree**
assert(ppTest != nullptr);
assert(ppIncr != nullptr);

// Check if last two statements in the loop body are the increment of the iterator
// and the loop termination test.
// The loop termination test is expected to be the last statement of the exiting
// block. The increment of the iterator is expected somewhere earlier in the
// same block; we scan backward from the test to find an IV-shaped update.
noway_assert(cond->firstStmt() != nullptr);
Statement* testStmt = cond->lastStmt();
noway_assert(testStmt != nullptr && testStmt->GetNextStmt() == nullptr);
Expand All@@ -368,24 +369,87 @@ bool Compiler::optExtractTestIncr(BasicBlock* cond, GenTree** ppTest, GenTree**
testStmt = newTestStmt;
}

// Check if we have the incr stmt before the test stmt, if we don't,
// check if incr is part of the loop "header".
Statement* incrStmt = testStmt->GetPrevStmt();

// If we've added profile instrumentation, we may need to skip past a BB counter update.
// Walk backward from the test statement looking for a candidate IV increment
// of the form 'v = v op c'. For each such candidate, verify it is suitable:
// - the iterator local is not address-exposed (our intervening-read check
// would not see indirect accesses, and AnalyzeIteration rejects
// address-exposed IVs anyway);
// - the test actually reads the iterator (otherwise this is an unrelated
// update that happens to be IV-shaped);
// - no statement strictly between the candidate and the test reads the
// iterator. Such statements would execute with the post-increment value,
// violating the AnalyzeIteration invariant that no loop body IR observes
// the post-increment value except the test. Stores are not checked here:
// AnalyzeIteration's VisitDefs already rejects any def of iterVar in the
// loop other than the picked increment. If the test block is in a try
// region, treat any intervening statement that may throw as if it were
// an intervening read, since an EH handler could observe the
// post-increment value.
// On any rejection, continue scanning backward; only fail when no candidate
// in the block satisfies all checks. A budget bounds the combined work of
// the outer scan and the inner intervening-read scan to avoid pathological
// O(N^2) behavior in blocks with many IV-shaped statements.
//
if (opts.jitFlags->IsSet(JitFlags::JIT_FLAG_BBINSTR) && (incrStmt != nullptr) &&
incrStmt->GetRootNode()->IsBlockProfileUpdate())
{
incrStmt = incrStmt->GetPrevStmt();
Statement* incrStmt = nullptr;
unsigned iterVar = BAD_VAR_NUM;
Statement* firstStmt = cond->firstStmt();
const bool condInTry = cond->hasTryIndex();
unsigned int budget = 100;
if (testStmt != firstStmt)
{
for (Statement* s = testStmt->GetPrevStmt();; s = s->GetPrevStmt())
{
if (budget-- == 0)
{
JITDUMP("optExtractTestIncr: budget exhausted in " FMT_BB "\n", cond->bbNum);
return false;
}
Comment thread
AndyAyersMS marked this conversation as resolved.

unsigned candVar = optIsLoopIncrTree(s->GetRootNode());
if (candVar != BAD_VAR_NUM)
{
if (!lvaGetDesc(candVar)->IsAddressExposed() && gtTreeHasLocalRead(testStmt->GetRootNode(), candVar))
{
bool intermediateUse = false;
for (Statement* t = s->GetNextStmt(); t != testStmt; t = t->GetNextStmt())
{
assert(t != nullptr);
if (budget-- == 0)
{
JITDUMP("optExtractTestIncr: budget exhausted in " FMT_BB "\n", cond->bbNum);
return false;
}
GenTree* root = t->GetRootNode();
if (gtTreeHasLocalRead(root, candVar) || (condInTry && ((root->gtFlags & GTF_EXCEPT) != 0)))
{
intermediateUse = true;
break;
}
}

if (!intermediateUse)
{
incrStmt = s;
iterVar = candVar;
break;
}
}
}

if (s == firstStmt)
{
break;
}
}
Comment thread
AndyAyersMS marked this conversation as resolved.
}

if (incrStmt == nullptr || (optIsLoopIncrTree(incrStmt->GetRootNode()) == BAD_VAR_NUM))
if (incrStmt == nullptr)
{
return false;
}

assert(testStmt != incrStmt);
assert(iterVar != BAD_VAR_NUM);

*ppTest = testStmt->GetRootNode();
*ppIncr = incrStmt->GetRootNode();
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/coreclr/jit/compiler.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -1991,13 +1991,23 @@ struct NaturalLoopIterInfo
// length of an invariant array.
bool HasArrayLengthLimit : 1;

// Whether the consumer must emit its own runtime entry guard for this loop.
// Set when AnalyzeIteration could not prove statically that the loop
// condition [IterVar TestOper Limit] holds on entry (so the analysis
// invariants only hold conditionally). The consumer must insert a runtime
// test equivalent to that condition on the path that reaches the analyzed
// loop body. Only set when AnalyzeIteration is called with
// allowMissingBaseCase=true.
bool NeedsZeroTripGuard : 1;

NaturalLoopIterInfo()
: ExitedOnTrue(false)
, HasConstInit(false)
, HasConstLimit(false)
, HasSimdLimit(false)
, HasInvariantLocalLimit(false)
, HasArrayLengthLimit(false)
, NeedsZeroTripGuard(false)
{
}

Expand DownExpand Up@@ -2191,7 +2201,7 @@ class FlowGraphNaturalLoop
BasicBlock* GetLexicallyTopMostBlock();
BasicBlock* GetLexicallyBottomMostBlock();

bool AnalyzeIteration(NaturalLoopIterInfo* info);
bool AnalyzeIteration(NaturalLoopIterInfo* info, bool allowMissingBaseCase = false);

bool HasDef(unsigned lclNum);

Expand Down
60 changes: 49 additions & 11 deletions src/coreclr/jit/flowgraph.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -5566,19 +5566,31 @@ GenTreeLclVarCommon* FlowGraphNaturalLoop::FindDef(unsigned lclNum)
// the loop.
//
// Parameters:
// info - [out] Loop information
// info - [out] Loop information
// allowMissingBaseCase - If true, succeed even when we cannot prove that the
// loop condition [IterVar TestOper Limit] holds on
// entry, provided the limit form is one we know how
// to materialize at runtime. The caller is then
// responsible for emitting a runtime entry guard
// equivalent to that condition when
// info->NeedsZeroTripGuard is set on return.
// Defaults to false; existing callers retain the
// stronger guarantees described below.
Comment thread
AndyAyersMS marked this conversation as resolved.
//
// Returns:
// True if the structure was analyzed and we can make guarantees about it;
// otherwise false.
//
// Remarks:
// On a true return, the function guarantees that the loop invariant is true
// and maintained at all points within the loop, except possibly right after
// the update of the iterator variable (NaturalLoopIterInfo::IterTree). The
// function guarantees that the test (NaturalLoopIterInfo::TestTree) occurs
// immediately after the update, so no IR in the loop is executed without the
// loop invariant being true, except for the test.
// On a true return with allowMissingBaseCase == false (or with the flag set
// but info->NeedsZeroTripGuard == false), the function guarantees that the
// loop invariant is true and maintained at all points within the loop,
// except possibly right after the update of the iterator variable
// (NaturalLoopIterInfo::IterTree). optExtractTestIncr permits the IV update
// to be separated from the loop test by other statements, but it rejects
// any candidate where an intervening statement references the iterator
// variable, so no IR in the loop is executed observing the post-update
// value of the iterator except the test itself.
//
// The loop invariant is defined as the expression obtained by
// [info->IterVar] [info->TestOper()] [info->Limit()]. Note that
Expand All@@ -5599,7 +5611,19 @@ GenTreeLclVarCommon* FlowGraphNaturalLoop::FindDef(unsigned lclNum)
// In some cases we also know the initial value on entry to the loop; see
// ::HasConstInit and ::ConstInitValue.
//
bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)
// When allowMissingBaseCase is true and the function would otherwise fail
// because the loop condition [IterVar TestOper Limit] cannot be proven to
// hold on entry (no suitable preheader BBJ_COND guard and no constant
// init/limit pair that proves the base case), the function may instead
// succeed with info->NeedsZeroTripGuard set. In that mode the above
// invariant only holds conditionally: it is the caller's obligation to
// insert a runtime test equivalent to [IterVar TestOper() Limit] on the
// path that reaches the analyzed loop body. Loop cloning uses this to emit
// the guard as an extra cloning condition on the fast-path version. Note
// that this includes do/while-style loops that are guaranteed to execute
// at least once but whose loop condition may be false on entry.
//
bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info, bool allowMissingBaseCase)
{
Comment thread
AndyAyersMS marked this conversation as resolved.
JITDUMP("Analyzing iteration for " FMT_LP " with header " FMT_BB "\n", m_index, m_header->bbNum);

Expand All@@ -5613,7 +5637,8 @@ bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)

GenTree* test = nullptr;

info->IterVar = BAD_VAR_NUM;
info->IterVar = BAD_VAR_NUM;
info->NeedsZeroTripGuard = false;

for (FlowEdge* exitEdge : ExitEdges())
{
Expand DownExpand Up@@ -5700,8 +5725,21 @@ bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)

if (!CheckLoopConditionBaseCase(preheader, info))
{
JITDUMP(" Loop condition may not be true on the first iteration\n");
return false;
// If the caller can emit its own runtime guard for the case where the
// loop body might not execute on the first iteration, allow the loop
// through provided we have enough symbolic info about init and limit
// to express the guard.
if (allowMissingBaseCase && (info->HasConstLimit || info->HasInvariantLocalLimit || info->HasArrayLengthLimit))
{
JITDUMP(" Loop condition may not be true on the first iteration; deferring to caller "
"(NeedsZeroTripGuard)\n");
info->NeedsZeroTripGuard = true;
}
else
{
JITDUMP(" Loop condition may not be true on the first iteration\n");
return false;
}
Comment thread
AndyAyersMS marked this conversation as resolved.
}

#ifdef DEBUG
Expand Down
104 changes: 92 additions & 12 deletions src/coreclr/jit/loopcloning.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1227,6 +1227,95 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
return false;
}

// If the loop limit is an array length, compute the underlying ArrIndex
// and queue the deref check once up front. Both the optional zero-trip
// guard below and the regular limit conditions further down reuse this
// single ArrIndex to avoid duplicating the deref entry and allocation.
//
ArrIndex* limitArrIndex = nullptr;
if (iterInfo->HasArrayLengthLimit)
{
limitArrIndex = new (getAllocator(CMK_LoopClone)) ArrIndex(getAllocator(CMK_LoopClone));
if (!iterInfo->ArrLenLimit(this, limitArrIndex))
{
JITDUMP("> ArrLen not matching\n");
return false;
}

LC_Array array(LC_Array::Jagged, limitArrIndex, LC_Array::None);
context->EnsureArrayDerefs(loop->GetIndex())->Push(array);
}

// If AnalyzeIteration could not prove the loop condition holds on entry,
// emit an explicit runtime entry guard as one of the cloning conditions.
// The fast path is then only entered when "init TestOper limit" holds.
if (iterInfo->NeedsZeroTripGuard)
{
LC_Ident initIdent;
if (iterInfo->HasConstInit)
{
if (iterInfo->ConstInitValue < 0)
{
JITDUMP("> NeedsZeroTripGuard: init %d is invalid\n", iterInfo->ConstInitValue);
return false;
}
initIdent = LC_Ident::CreateConst(static_cast<unsigned>(iterInfo->ConstInitValue));
}
else
{
// Init is unknown statically; use the IV local as it stands at the
// preheader (the analysis already verified the local is not
// address-exposed and has no extraneous defs inside the loop, so
// reading it in the preheader gives the entry value).
const unsigned initLcl = iterInfo->IterVar;
if (!genActualTypeIsInt(lvaGetDesc(initLcl)))
{
JITDUMP("> NeedsZeroTripGuard: iter var V%02u not compatible with TYP_INT\n", initLcl);
return false;
}
initIdent = LC_Ident::CreateVar(initLcl);
}

LC_Ident limitIdent;
if (iterInfo->HasConstLimit)
{
int limit = iterInfo->ConstLimit();
if (limit < 0)
{
JITDUMP("> NeedsZeroTripGuard: limit %d is invalid\n", limit);
return false;
}
limitIdent = LC_Ident::CreateConst(static_cast<unsigned>(limit));
}
else if (iterInfo->HasInvariantLocalLimit)
{
const unsigned limitLcl = iterInfo->VarLimit();
if (!genActualTypeIsInt(lvaGetDesc(limitLcl)))
{
JITDUMP("> NeedsZeroTripGuard: limit var V%02u not compatible with TYP_INT\n", limitLcl);
return false;
}
limitIdent = LC_Ident::CreateVar(limitLcl);
}
else if (iterInfo->HasArrayLengthLimit)
{
assert(limitArrIndex != nullptr);
limitIdent = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}
Comment thread
AndyAyersMS marked this conversation as resolved.
else
{
JITDUMP("> NeedsZeroTripGuard: undetected limit\n");
return false;
}

// TestOper() returns the stays-in-loop relop in IV-on-lhs form, already
// adjusted for IsReversed and ExitedOnTrue.
LC_Condition zeroTrip(iterInfo->TestOper(), LC_Expr(initIdent), LC_Expr(limitIdent),
iterInfo->TestTree->IsUnsigned());
context->EnsureConditions(loop->GetIndex())->Push(zeroTrip);
JITDUMP("Added zero-trip guard cloning condition\n");
}

LC_Ident ident;
// Init conditions
if (iterInfo->HasConstInit)
Expand DownExpand Up@@ -1311,17 +1400,8 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}
else if (iterInfo->HasArrayLengthLimit)
{
ArrIndex* index = new (getAllocator(CMK_LoopClone)) ArrIndex(getAllocator(CMK_LoopClone));
if (!iterInfo->ArrLenLimit(this, index))
{
JITDUMP("> ArrLen not matching\n");
return false;
}
ident = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, index, LC_Array::ArrLen));

// Ensure that this array must be dereference-able, before executing the actual condition.
LC_Array array(LC_Array::Jagged, index, LC_Array::None);
context->EnsureArrayDerefs(loop->GetIndex())->Push(array);
assert(limitArrIndex != nullptr);
ident = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}
else
{
Expand DownExpand Up@@ -2993,7 +3073,7 @@ bool Compiler::optObtainLoopCloningOpts(LoopCloneContext* context)
{
JITDUMP("Considering loop " FMT_LP " to clone for optimizations.\n", loop->GetIndex());
NaturalLoopIterInfo iterInfo;
if (loop->AnalyzeIteration(&iterInfo))
if (loop->AnalyzeIteration(&iterInfo, /* allowMissingBaseCase */ true))
{
Comment thread
AndyAyersMS marked this conversation as resolved.
context->SetLoopIterInfo(loop->GetIndex(), new (this, CMK_LoopClone) NaturalLoopIterInfo(iterInfo));
}
Expand Down
88 changes: 76 additions & 12 deletions src/coreclr/jit/optimizer.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -356,8 +356,9 @@ bool Compiler::optExtractTestIncr(BasicBlock* cond, GenTree** ppTest, GenTree**
assert(ppTest != nullptr);
assert(ppIncr != nullptr);

// Check if last two statements in the loop body are the increment of the iterator
// and the loop termination test.
// The loop termination test is expected to be the last statement of the exiting
// block. The increment of the iterator is expected somewhere earlier in the
// same block; we scan backward from the test to find an IV-shaped update.
noway_assert(cond->firstStmt() != nullptr);
Statement* testStmt = cond->lastStmt();
noway_assert(testStmt != nullptr && testStmt->GetNextStmt() == nullptr);
Expand All@@ -368,24 +369,87 @@ bool Compiler::optExtractTestIncr(BasicBlock* cond, GenTree** ppTest, GenTree**
testStmt = newTestStmt;
}

// Check if we have the incr stmt before the test stmt, if we don't,
// check if incr is part of the loop "header".
Statement* incrStmt = testStmt->GetPrevStmt();

// If we've added profile instrumentation, we may need to skip past a BB counter update.
// Walk backward from the test statement looking for a candidate IV increment
// of the form 'v = v op c'. For each such candidate, verify it is suitable:
// - the iterator local is not address-exposed (our intervening-read check
// would not see indirect accesses, and AnalyzeIteration rejects
// address-exposed IVs anyway);
// - the test actually reads the iterator (otherwise this is an unrelated
// update that happens to be IV-shaped);
// - no statement strictly between the candidate and the test reads the
// iterator. Such statements would execute with the post-increment value,
// violating the AnalyzeIteration invariant that no loop body IR observes
// the post-increment value except the test. Stores are not checked here:
// AnalyzeIteration's VisitDefs already rejects any def of iterVar in the
// loop other than the picked increment. If the test block is in a try
// region, treat any intervening statement that may throw as if it were
// an intervening read, since an EH handler could observe the
// post-increment value.
// On any rejection, continue scanning backward; only fail when no candidate
// in the block satisfies all checks. A budget bounds the combined work of
// the outer scan and the inner intervening-read scan to avoid pathological
// O(N^2) behavior in blocks with many IV-shaped statements.
//
if (opts.jitFlags->IsSet(JitFlags::JIT_FLAG_BBINSTR) && (incrStmt != nullptr) &&
incrStmt->GetRootNode()->IsBlockProfileUpdate())
{
incrStmt = incrStmt->GetPrevStmt();
Statement* incrStmt = nullptr;
unsigned iterVar = BAD_VAR_NUM;
Statement* firstStmt = cond->firstStmt();
const bool condInTry = cond->hasTryIndex();
unsigned int budget = 100;
if (testStmt != firstStmt)
{
for (Statement* s = testStmt->GetPrevStmt();; s = s->GetPrevStmt())
{
if (budget-- == 0)
{
JITDUMP("optExtractTestIncr: budget exhausted in " FMT_BB "\n", cond->bbNum);
return false;
}
Comment thread
AndyAyersMS marked this conversation as resolved.

unsigned candVar = optIsLoopIncrTree(s->GetRootNode());
if (candVar != BAD_VAR_NUM)
{
if (!lvaGetDesc(candVar)->IsAddressExposed() && gtTreeHasLocalRead(testStmt->GetRootNode(), candVar))
{
bool intermediateUse = false;
for (Statement* t = s->GetNextStmt(); t != testStmt; t = t->GetNextStmt())
{
assert(t != nullptr);
if (budget-- == 0)
{
JITDUMP("optExtractTestIncr: budget exhausted in " FMT_BB "\n", cond->bbNum);
return false;
}
GenTree* root = t->GetRootNode();
if (gtTreeHasLocalRead(root, candVar) || (condInTry && ((root->gtFlags & GTF_EXCEPT) != 0)))
{
intermediateUse = true;
break;
}
}

if (!intermediateUse)
{
incrStmt = s;
iterVar = candVar;
break;
}
}
}

if (s == firstStmt)
{
break;
}
}
Comment thread
AndyAyersMS marked this conversation as resolved.
}

if (incrStmt == nullptr || (optIsLoopIncrTree(incrStmt->GetRootNode()) == BAD_VAR_NUM))
if (incrStmt == nullptr)
{
return false;
}

assert(testStmt != incrStmt);
assert(iterVar != BAD_VAR_NUM);

*ppTest = testStmt->GetRootNode();
*ppIncr = incrStmt->GetRootNode();
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/coreclr/jit/compiler.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -1991,13 +1991,23 @@ struct NaturalLoopIterInfo
// length of an invariant array.
bool HasArrayLengthLimit : 1;

// Whether the consumer must emit its own runtime entry guard for this loop.
// Set when AnalyzeIteration could not prove statically that the loop
// condition [IterVar TestOper Limit] holds on entry (so the analysis
// invariants only hold conditionally). The consumer must insert a runtime
// test equivalent to that condition on the path that reaches the analyzed
// loop body. Only set when AnalyzeIteration is called with
// allowMissingBaseCase=true.
bool NeedsZeroTripGuard : 1;

NaturalLoopIterInfo()
: ExitedOnTrue(false)
, HasConstInit(false)
, HasConstLimit(false)
, HasSimdLimit(false)
, HasInvariantLocalLimit(false)
, HasArrayLengthLimit(false)
, NeedsZeroTripGuard(false)
{
}

Expand DownExpand Up@@ -2191,7 +2201,7 @@ class FlowGraphNaturalLoop
BasicBlock* GetLexicallyTopMostBlock();
BasicBlock* GetLexicallyBottomMostBlock();

bool AnalyzeIteration(NaturalLoopIterInfo* info);
bool AnalyzeIteration(NaturalLoopIterInfo* info, bool allowMissingBaseCase = false);

bool HasDef(unsigned lclNum);

Expand Down
60 changes: 49 additions & 11 deletions src/coreclr/jit/flowgraph.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -5566,19 +5566,31 @@ GenTreeLclVarCommon* FlowGraphNaturalLoop::FindDef(unsigned lclNum)
// the loop.
//
// Parameters:
// info - [out] Loop information
// info - [out] Loop information
// allowMissingBaseCase - If true, succeed even when we cannot prove that the
// loop condition [IterVar TestOper Limit] holds on
// entry, provided the limit form is one we know how
// to materialize at runtime. The caller is then
// responsible for emitting a runtime entry guard
// equivalent to that condition when
// info->NeedsZeroTripGuard is set on return.
// Defaults to false; existing callers retain the
// stronger guarantees described below.
Comment thread
AndyAyersMS marked this conversation as resolved.
//
// Returns:
// True if the structure was analyzed and we can make guarantees about it;
// otherwise false.
//
// Remarks:
// On a true return, the function guarantees that the loop invariant is true
// and maintained at all points within the loop, except possibly right after
// the update of the iterator variable (NaturalLoopIterInfo::IterTree). The
// function guarantees that the test (NaturalLoopIterInfo::TestTree) occurs
// immediately after the update, so no IR in the loop is executed without the
// loop invariant being true, except for the test.
// On a true return with allowMissingBaseCase == false (or with the flag set
// but info->NeedsZeroTripGuard == false), the function guarantees that the
// loop invariant is true and maintained at all points within the loop,
// except possibly right after the update of the iterator variable
// (NaturalLoopIterInfo::IterTree). optExtractTestIncr permits the IV update
// to be separated from the loop test by other statements, but it rejects
// any candidate where an intervening statement references the iterator
// variable, so no IR in the loop is executed observing the post-update
// value of the iterator except the test itself.
//
// The loop invariant is defined as the expression obtained by
// [info->IterVar] [info->TestOper()] [info->Limit()]. Note that
Expand All@@ -5599,7 +5611,19 @@ GenTreeLclVarCommon* FlowGraphNaturalLoop::FindDef(unsigned lclNum)
// In some cases we also know the initial value on entry to the loop; see
// ::HasConstInit and ::ConstInitValue.
//
bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)
// When allowMissingBaseCase is true and the function would otherwise fail
// because the loop condition [IterVar TestOper Limit] cannot be proven to
// hold on entry (no suitable preheader BBJ_COND guard and no constant
// init/limit pair that proves the base case), the function may instead
// succeed with info->NeedsZeroTripGuard set. In that mode the above
// invariant only holds conditionally: it is the caller's obligation to
// insert a runtime test equivalent to [IterVar TestOper() Limit] on the
// path that reaches the analyzed loop body. Loop cloning uses this to emit
// the guard as an extra cloning condition on the fast-path version. Note
// that this includes do/while-style loops that are guaranteed to execute
// at least once but whose loop condition may be false on entry.
//
bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info, bool allowMissingBaseCase)
{
Comment thread
AndyAyersMS marked this conversation as resolved.
JITDUMP("Analyzing iteration for " FMT_LP " with header " FMT_BB "\n", m_index, m_header->bbNum);

Expand All@@ -5613,7 +5637,8 @@ bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)

GenTree* test = nullptr;

info->IterVar = BAD_VAR_NUM;
info->IterVar = BAD_VAR_NUM;
info->NeedsZeroTripGuard = false;

for (FlowEdge* exitEdge : ExitEdges())
{
Expand DownExpand Up@@ -5700,8 +5725,21 @@ bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)

if (!CheckLoopConditionBaseCase(preheader, info))
{
JITDUMP(" Loop condition may not be true on the first iteration\n");
return false;
// If the caller can emit its own runtime guard for the case where the
// loop body might not execute on the first iteration, allow the loop
// through provided we have enough symbolic info about init and limit
// to express the guard.
if (allowMissingBaseCase && (info->HasConstLimit || info->HasInvariantLocalLimit || info->HasArrayLengthLimit))
{
JITDUMP(" Loop condition may not be true on the first iteration; deferring to caller "
"(NeedsZeroTripGuard)\n");
info->NeedsZeroTripGuard = true;
}
else
{
JITDUMP(" Loop condition may not be true on the first iteration\n");
return false;
}
Comment thread
AndyAyersMS marked this conversation as resolved.
}

#ifdef DEBUG
Expand Down
104 changes: 92 additions & 12 deletions src/coreclr/jit/loopcloning.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1227,6 +1227,95 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
return false;
}

// If the loop limit is an array length, compute the underlying ArrIndex
// and queue the deref check once up front. Both the optional zero-trip
// guard below and the regular limit conditions further down reuse this
// single ArrIndex to avoid duplicating the deref entry and allocation.
//
ArrIndex* limitArrIndex = nullptr;
if (iterInfo->HasArrayLengthLimit)
{
limitArrIndex = new (getAllocator(CMK_LoopClone)) ArrIndex(getAllocator(CMK_LoopClone));
if (!iterInfo->ArrLenLimit(this, limitArrIndex))
{
JITDUMP("> ArrLen not matching\n");
return false;
}

LC_Array array(LC_Array::Jagged, limitArrIndex, LC_Array::None);
context->EnsureArrayDerefs(loop->GetIndex())->Push(array);
}

// If AnalyzeIteration could not prove the loop condition holds on entry,
// emit an explicit runtime entry guard as one of the cloning conditions.
// The fast path is then only entered when "init TestOper limit" holds.
if (iterInfo->NeedsZeroTripGuard)
{
LC_Ident initIdent;
if (iterInfo->HasConstInit)
{
if (iterInfo->ConstInitValue < 0)
{
JITDUMP("> NeedsZeroTripGuard: init %d is invalid\n", iterInfo->ConstInitValue);
return false;
}
initIdent = LC_Ident::CreateConst(static_cast<unsigned>(iterInfo->ConstInitValue));
}
else
{
// Init is unknown statically; use the IV local as it stands at the
// preheader (the analysis already verified the local is not
// address-exposed and has no extraneous defs inside the loop, so
// reading it in the preheader gives the entry value).
const unsigned initLcl = iterInfo->IterVar;
if (!genActualTypeIsInt(lvaGetDesc(initLcl)))
{
JITDUMP("> NeedsZeroTripGuard: iter var V%02u not compatible with TYP_INT\n", initLcl);
return false;
}
initIdent = LC_Ident::CreateVar(initLcl);
}

LC_Ident limitIdent;
if (iterInfo->HasConstLimit)
{
int limit = iterInfo->ConstLimit();
if (limit < 0)
{
JITDUMP("> NeedsZeroTripGuard: limit %d is invalid\n", limit);
return false;
}
limitIdent = LC_Ident::CreateConst(static_cast<unsigned>(limit));
}
else if (iterInfo->HasInvariantLocalLimit)
{
const unsigned limitLcl = iterInfo->VarLimit();
if (!genActualTypeIsInt(lvaGetDesc(limitLcl)))
{
JITDUMP("> NeedsZeroTripGuard: limit var V%02u not compatible with TYP_INT\n", limitLcl);
return false;
}
limitIdent = LC_Ident::CreateVar(limitLcl);
}
else if (iterInfo->HasArrayLengthLimit)
{
assert(limitArrIndex != nullptr);
limitIdent = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}
Comment thread
AndyAyersMS marked this conversation as resolved.
else
{
JITDUMP("> NeedsZeroTripGuard: undetected limit\n");
return false;
}

// TestOper() returns the stays-in-loop relop in IV-on-lhs form, already
// adjusted for IsReversed and ExitedOnTrue.
LC_Condition zeroTrip(iterInfo->TestOper(), LC_Expr(initIdent), LC_Expr(limitIdent),
iterInfo->TestTree->IsUnsigned());
context->EnsureConditions(loop->GetIndex())->Push(zeroTrip);
JITDUMP("Added zero-trip guard cloning condition\n");
}

LC_Ident ident;
// Init conditions
if (iterInfo->HasConstInit)
Expand DownExpand Up@@ -1311,17 +1400,8 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}
else if (iterInfo->HasArrayLengthLimit)
{
ArrIndex* index = new (getAllocator(CMK_LoopClone)) ArrIndex(getAllocator(CMK_LoopClone));
if (!iterInfo->ArrLenLimit(this, index))
{
JITDUMP("> ArrLen not matching\n");
return false;
}
ident = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, index, LC_Array::ArrLen));

// Ensure that this array must be dereference-able, before executing the actual condition.
LC_Array array(LC_Array::Jagged, index, LC_Array::None);
context->EnsureArrayDerefs(loop->GetIndex())->Push(array);
assert(limitArrIndex != nullptr);
ident = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}
else
{
Expand DownExpand Up@@ -2993,7 +3073,7 @@ bool Compiler::optObtainLoopCloningOpts(LoopCloneContext* context)
{
JITDUMP("Considering loop " FMT_LP " to clone for optimizations.\n", loop->GetIndex());
NaturalLoopIterInfo iterInfo;
if (loop->AnalyzeIteration(&iterInfo))
if (loop->AnalyzeIteration(&iterInfo, /* allowMissingBaseCase */ true))
{
Comment thread
AndyAyersMS marked this conversation as resolved.
context->SetLoopIterInfo(loop->GetIndex(), new (this, CMK_LoopClone) NaturalLoopIterInfo(iterInfo));
}
Expand Down
88 changes: 76 additions & 12 deletions src/coreclr/jit/optimizer.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -356,8 +356,9 @@ bool Compiler::optExtractTestIncr(BasicBlock* cond, GenTree** ppTest, GenTree**
assert(ppTest != nullptr);
assert(ppIncr != nullptr);

// Check if last two statements in the loop body are the increment of the iterator
// and the loop termination test.
// The loop termination test is expected to be the last statement of the exiting
// block. The increment of the iterator is expected somewhere earlier in the
// same block; we scan backward from the test to find an IV-shaped update.
noway_assert(cond->firstStmt() != nullptr);
Statement* testStmt = cond->lastStmt();
noway_assert(testStmt != nullptr && testStmt->GetNextStmt() == nullptr);
Expand All@@ -368,24 +369,87 @@ bool Compiler::optExtractTestIncr(BasicBlock* cond, GenTree** ppTest, GenTree**
testStmt = newTestStmt;
}

// Check if we have the incr stmt before the test stmt, if we don't,
// check if incr is part of the loop "header".
Statement* incrStmt = testStmt->GetPrevStmt();

// If we've added profile instrumentation, we may need to skip past a BB counter update.
// Walk backward from the test statement looking for a candidate IV increment
// of the form 'v = v op c'. For each such candidate, verify it is suitable:
// - the iterator local is not address-exposed (our intervening-read check
// would not see indirect accesses, and AnalyzeIteration rejects
// address-exposed IVs anyway);
// - the test actually reads the iterator (otherwise this is an unrelated
// update that happens to be IV-shaped);
// - no statement strictly between the candidate and the test reads the
// iterator. Such statements would execute with the post-increment value,
// violating the AnalyzeIteration invariant that no loop body IR observes
// the post-increment value except the test. Stores are not checked here:
// AnalyzeIteration's VisitDefs already rejects any def of iterVar in the
// loop other than the picked increment. If the test block is in a try
// region, treat any intervening statement that may throw as if it were
// an intervening read, since an EH handler could observe the
// post-increment value.
// On any rejection, continue scanning backward; only fail when no candidate
// in the block satisfies all checks. A budget bounds the combined work of
// the outer scan and the inner intervening-read scan to avoid pathological
// O(N^2) behavior in blocks with many IV-shaped statements.
//
if (opts.jitFlags->IsSet(JitFlags::JIT_FLAG_BBINSTR) && (incrStmt != nullptr) &&
incrStmt->GetRootNode()->IsBlockProfileUpdate())
{
incrStmt = incrStmt->GetPrevStmt();
Statement* incrStmt = nullptr;
unsigned iterVar = BAD_VAR_NUM;
Statement* firstStmt = cond->firstStmt();
const bool condInTry = cond->hasTryIndex();
unsigned int budget = 100;
if (testStmt != firstStmt)
{
for (Statement* s = testStmt->GetPrevStmt();; s = s->GetPrevStmt())
{
if (budget-- == 0)
{
JITDUMP("optExtractTestIncr: budget exhausted in " FMT_BB "\n", cond->bbNum);
return false;
}
Comment thread
AndyAyersMS marked this conversation as resolved.

unsigned candVar = optIsLoopIncrTree(s->GetRootNode());
if (candVar != BAD_VAR_NUM)
{
if (!lvaGetDesc(candVar)->IsAddressExposed() && gtTreeHasLocalRead(testStmt->GetRootNode(), candVar))
{
bool intermediateUse = false;
for (Statement* t = s->GetNextStmt(); t != testStmt; t = t->GetNextStmt())
{
assert(t != nullptr);
if (budget-- == 0)
{
JITDUMP("optExtractTestIncr: budget exhausted in " FMT_BB "\n", cond->bbNum);
return false;
}
GenTree* root = t->GetRootNode();
if (gtTreeHasLocalRead(root, candVar) || (condInTry && ((root->gtFlags & GTF_EXCEPT) != 0)))
{
intermediateUse = true;
break;
}
}

if (!intermediateUse)
{
incrStmt = s;
iterVar = candVar;
break;
}
}
}

if (s == firstStmt)
{
break;
}
}
Comment thread
AndyAyersMS marked this conversation as resolved.
}

if (incrStmt == nullptr || (optIsLoopIncrTree(incrStmt->GetRootNode()) == BAD_VAR_NUM))
if (incrStmt == nullptr)
{
return false;
}

assert(testStmt != incrStmt);
assert(iterVar != BAD_VAR_NUM);

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/coreclr/jit/compiler.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -1991,13 +1991,23 @@ struct NaturalLoopIterInfo
// length of an invariant array.
bool HasArrayLengthLimit : 1;

// Whether the consumer must emit its own runtime entry guard for this loop.
// Set when AnalyzeIteration could not prove statically that the loop
// condition [IterVar TestOper Limit] holds on entry (so the analysis
// invariants only hold conditionally). The consumer must insert a runtime
// test equivalent to that condition on the path that reaches the analyzed
// loop body. Only set when AnalyzeIteration is called with
// allowMissingBaseCase=true.
bool NeedsZeroTripGuard : 1;

NaturalLoopIterInfo()
: ExitedOnTrue(false)
, HasConstInit(false)
, HasConstLimit(false)
, HasSimdLimit(false)
, HasInvariantLocalLimit(false)
, HasArrayLengthLimit(false)
, NeedsZeroTripGuard(false)
{
}

Expand DownExpand Up@@ -2191,7 +2201,7 @@ class FlowGraphNaturalLoop
BasicBlock* GetLexicallyTopMostBlock();
BasicBlock* GetLexicallyBottomMostBlock();

bool AnalyzeIteration(NaturalLoopIterInfo* info);
bool AnalyzeIteration(NaturalLoopIterInfo* info, bool allowMissingBaseCase = false);

bool HasDef(unsigned lclNum);

Expand Down
60 changes: 49 additions & 11 deletions src/coreclr/jit/flowgraph.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -5566,19 +5566,31 @@ GenTreeLclVarCommon* FlowGraphNaturalLoop::FindDef(unsigned lclNum)
// the loop.
//
// Parameters:
// info - [out] Loop information
// info - [out] Loop information
// allowMissingBaseCase - If true, succeed even when we cannot prove that the
// loop condition [IterVar TestOper Limit] holds on
// entry, provided the limit form is one we know how
// to materialize at runtime. The caller is then
// responsible for emitting a runtime entry guard
// equivalent to that condition when
// info->NeedsZeroTripGuard is set on return.
// Defaults to false; existing callers retain the
// stronger guarantees described below.
Comment thread
AndyAyersMS marked this conversation as resolved.
//
// Returns:
// True if the structure was analyzed and we can make guarantees about it;
// otherwise false.
//
// Remarks:
// On a true return, the function guarantees that the loop invariant is true
// and maintained at all points within the loop, except possibly right after
// the update of the iterator variable (NaturalLoopIterInfo::IterTree). The
// function guarantees that the test (NaturalLoopIterInfo::TestTree) occurs
// immediately after the update, so no IR in the loop is executed without the
// loop invariant being true, except for the test.
// On a true return with allowMissingBaseCase == false (or with the flag set
// but info->NeedsZeroTripGuard == false), the function guarantees that the
// loop invariant is true and maintained at all points within the loop,
// except possibly right after the update of the iterator variable
// (NaturalLoopIterInfo::IterTree). optExtractTestIncr permits the IV update
// to be separated from the loop test by other statements, but it rejects
// any candidate where an intervening statement references the iterator
// variable, so no IR in the loop is executed observing the post-update
// value of the iterator except the test itself.
//
// The loop invariant is defined as the expression obtained by
// [info->IterVar] [info->TestOper()] [info->Limit()]. Note that
Expand All@@ -5599,7 +5611,19 @@ GenTreeLclVarCommon* FlowGraphNaturalLoop::FindDef(unsigned lclNum)
// In some cases we also know the initial value on entry to the loop; see
// ::HasConstInit and ::ConstInitValue.
//
bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)
// When allowMissingBaseCase is true and the function would otherwise fail
// because the loop condition [IterVar TestOper Limit] cannot be proven to
// hold on entry (no suitable preheader BBJ_COND guard and no constant
// init/limit pair that proves the base case), the function may instead
// succeed with info->NeedsZeroTripGuard set. In that mode the above
// invariant only holds conditionally: it is the caller's obligation to
// insert a runtime test equivalent to [IterVar TestOper() Limit] on the
// path that reaches the analyzed loop body. Loop cloning uses this to emit
// the guard as an extra cloning condition on the fast-path version. Note
// that this includes do/while-style loops that are guaranteed to execute
// at least once but whose loop condition may be false on entry.
//
bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info, bool allowMissingBaseCase)
{
Comment thread
AndyAyersMS marked this conversation as resolved.
JITDUMP("Analyzing iteration for " FMT_LP " with header " FMT_BB "\n", m_index, m_header->bbNum);

Expand All@@ -5613,7 +5637,8 @@ bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)

GenTree* test = nullptr;

info->IterVar = BAD_VAR_NUM;
info->IterVar = BAD_VAR_NUM;
info->NeedsZeroTripGuard = false;

for (FlowEdge* exitEdge : ExitEdges())
{
Expand DownExpand Up@@ -5700,8 +5725,21 @@ bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)

if (!CheckLoopConditionBaseCase(preheader, info))
{
JITDUMP(" Loop condition may not be true on the first iteration\n");
return false;
// If the caller can emit its own runtime guard for the case where the
// loop body might not execute on the first iteration, allow the loop
// through provided we have enough symbolic info about init and limit
// to express the guard.
if (allowMissingBaseCase && (info->HasConstLimit || info->HasInvariantLocalLimit || info->HasArrayLengthLimit))
{
JITDUMP(" Loop condition may not be true on the first iteration; deferring to caller "
"(NeedsZeroTripGuard)\n");
info->NeedsZeroTripGuard = true;
}
else
{
JITDUMP(" Loop condition may not be true on the first iteration\n");
return false;
}
Comment thread
AndyAyersMS marked this conversation as resolved.
}

#ifdef DEBUG
Expand Down
104 changes: 92 additions & 12 deletions src/coreclr/jit/loopcloning.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1227,6 +1227,95 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
return false;
}

// If the loop limit is an array length, compute the underlying ArrIndex
// and queue the deref check once up front. Both the optional zero-trip
// guard below and the regular limit conditions further down reuse this
// single ArrIndex to avoid duplicating the deref entry and allocation.
//
ArrIndex* limitArrIndex = nullptr;
if (iterInfo->HasArrayLengthLimit)
{
limitArrIndex = new (getAllocator(CMK_LoopClone)) ArrIndex(getAllocator(CMK_LoopClone));
if (!iterInfo->ArrLenLimit(this, limitArrIndex))
{
JITDUMP("> ArrLen not matching\n");
return false;
}

LC_Array array(LC_Array::Jagged, limitArrIndex, LC_Array::None);
context->EnsureArrayDerefs(loop->GetIndex())->Push(array);
}

// If AnalyzeIteration could not prove the loop condition holds on entry,
// emit an explicit runtime entry guard as one of the cloning conditions.
// The fast path is then only entered when "init TestOper limit" holds.
if (iterInfo->NeedsZeroTripGuard)
{
LC_Ident initIdent;
if (iterInfo->HasConstInit)
{
if (iterInfo->ConstInitValue < 0)
{
JITDUMP("> NeedsZeroTripGuard: init %d is invalid\n", iterInfo->ConstInitValue);
return false;
}
initIdent = LC_Ident::CreateConst(static_cast<unsigned>(iterInfo->ConstInitValue));
}
else
{
// Init is unknown statically; use the IV local as it stands at the
// preheader (the analysis already verified the local is not
// address-exposed and has no extraneous defs inside the loop, so
// reading it in the preheader gives the entry value).
const unsigned initLcl = iterInfo->IterVar;
if (!genActualTypeIsInt(lvaGetDesc(initLcl)))
{
JITDUMP("> NeedsZeroTripGuard: iter var V%02u not compatible with TYP_INT\n", initLcl);
return false;
}
initIdent = LC_Ident::CreateVar(initLcl);
}

LC_Ident limitIdent;
if (iterInfo->HasConstLimit)
{
int limit = iterInfo->ConstLimit();
if (limit < 0)
{
JITDUMP("> NeedsZeroTripGuard: limit %d is invalid\n", limit);
return false;
}
limitIdent = LC_Ident::CreateConst(static_cast<unsigned>(limit));
}
else if (iterInfo->HasInvariantLocalLimit)
{
const unsigned limitLcl = iterInfo->VarLimit();
if (!genActualTypeIsInt(lvaGetDesc(limitLcl)))
{
JITDUMP("> NeedsZeroTripGuard: limit var V%02u not compatible with TYP_INT\n", limitLcl);
return false;
}
limitIdent = LC_Ident::CreateVar(limitLcl);
}
else if (iterInfo->HasArrayLengthLimit)
{
assert(limitArrIndex != nullptr);
limitIdent = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}
Comment thread
AndyAyersMS marked this conversation as resolved.
else
{
JITDUMP("> NeedsZeroTripGuard: undetected limit\n");
return false;
}

// TestOper() returns the stays-in-loop relop in IV-on-lhs form, already
// adjusted for IsReversed and ExitedOnTrue.
LC_Condition zeroTrip(iterInfo->TestOper(), LC_Expr(initIdent), LC_Expr(limitIdent),
iterInfo->TestTree->IsUnsigned());
context->EnsureConditions(loop->GetIndex())->Push(zeroTrip);
JITDUMP("Added zero-trip guard cloning condition\n");
}

LC_Ident ident;
// Init conditions
if (iterInfo->HasConstInit)
Expand DownExpand Up@@ -1311,17 +1400,8 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}
else if (iterInfo->HasArrayLengthLimit)
{
ArrIndex* index = new (getAllocator(CMK_LoopClone)) ArrIndex(getAllocator(CMK_LoopClone));
if (!iterInfo->ArrLenLimit(this, index))
{
JITDUMP("> ArrLen not matching\n");
return false;
}
ident = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, index, LC_Array::ArrLen));

// Ensure that this array must be dereference-able, before executing the actual condition.
LC_Array array(LC_Array::Jagged, index, LC_Array::None);
context->EnsureArrayDerefs(loop->GetIndex())->Push(array);
assert(limitArrIndex != nullptr);
ident = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}
else
{
Expand DownExpand Up@@ -2993,7 +3073,7 @@ bool Compiler::optObtainLoopCloningOpts(LoopCloneContext* context)
{
JITDUMP("Considering loop " FMT_LP " to clone for optimizations.\n", loop->GetIndex());
NaturalLoopIterInfo iterInfo;
if (loop->AnalyzeIteration(&iterInfo))
if (loop->AnalyzeIteration(&iterInfo, /* allowMissingBaseCase */ true))
{
Comment thread
AndyAyersMS marked this conversation as resolved.
context->SetLoopIterInfo(loop->GetIndex(), new (this, CMK_LoopClone) NaturalLoopIterInfo(iterInfo));
}
Expand Down
88 changes: 76 additions & 12 deletions src/coreclr/jit/optimizer.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -356,8 +356,9 @@ bool Compiler::optExtractTestIncr(BasicBlock* cond, GenTree** ppTest, GenTree**
assert(ppTest != nullptr);
assert(ppIncr != nullptr);

// Check if last two statements in the loop body are the increment of the iterator
// and the loop termination test.
// The loop termination test is expected to be the last statement of the exiting
// block. The increment of the iterator is expected somewhere earlier in the
// same block; we scan backward from the test to find an IV-shaped update.
noway_assert(cond->firstStmt() != nullptr);
Statement* testStmt = cond->lastStmt();
noway_assert(testStmt != nullptr && testStmt->GetNextStmt() == nullptr);
Expand All@@ -368,24 +369,87 @@ bool Compiler::optExtractTestIncr(BasicBlock* cond, GenTree** ppTest, GenTree**
testStmt = newTestStmt;
}

// Check if we have the incr stmt before the test stmt, if we don't,
// check if incr is part of the loop "header".
Statement* incrStmt = testStmt->GetPrevStmt();

// If we've added profile instrumentation, we may need to skip past a BB counter update.
// Walk backward from the test statement looking for a candidate IV increment
// of the form 'v = v op c'. For each such candidate, verify it is suitable:
// - the iterator local is not address-exposed (our intervening-read check
// would not see indirect accesses, and AnalyzeIteration rejects
// address-exposed IVs anyway);
// - the test actually reads the iterator (otherwise this is an unrelated
// update that happens to be IV-shaped);
// - no statement strictly between the candidate and the test reads the
// iterator. Such statements would execute with the post-increment value,
// violating the AnalyzeIteration invariant that no loop body IR observes
// the post-increment value except the test. Stores are not checked here:
// AnalyzeIteration's VisitDefs already rejects any def of iterVar in the
// loop other than the picked increment. If the test block is in a try
// region, treat any intervening statement that may throw as if it were
// an intervening read, since an EH handler could observe the
// post-increment value.
// On any rejection, continue scanning backward; only fail when no candidate
// in the block satisfies all checks. A budget bounds the combined work of
// the outer scan and the inner intervening-read scan to avoid pathological
// O(N^2) behavior in blocks with many IV-shaped statements.
//
if (opts.jitFlags->IsSet(JitFlags::JIT_FLAG_BBINSTR) && (incrStmt != nullptr) &&
incrStmt->GetRootNode()->IsBlockProfileUpdate())
{
incrStmt = incrStmt->GetPrevStmt();
Statement* incrStmt = nullptr;
unsigned iterVar = BAD_VAR_NUM;
Statement* firstStmt = cond->firstStmt();
const bool condInTry = cond->hasTryIndex();
unsigned int budget = 100;
if (testStmt != firstStmt)
{
for (Statement* s = testStmt->GetPrevStmt();; s = s->GetPrevStmt())
{
if (budget-- == 0)
{
JITDUMP("optExtractTestIncr: budget exhausted in " FMT_BB "\n", cond->bbNum);
return false;
}
Comment thread
AndyAyersMS marked this conversation as resolved.

unsigned candVar = optIsLoopIncrTree(s->GetRootNode());
if (candVar != BAD_VAR_NUM)
{
if (!lvaGetDesc(candVar)->IsAddressExposed() && gtTreeHasLocalRead(testStmt->GetRootNode(), candVar))
{
bool intermediateUse = false;
for (Statement* t = s->GetNextStmt(); t != testStmt; t = t->GetNextStmt())
{
assert(t != nullptr);
if (budget-- == 0)
{
JITDUMP("optExtractTestIncr: budget exhausted in " FMT_BB "\n", cond->bbNum);
return false;
}
GenTree* root = t->GetRootNode();
if (gtTreeHasLocalRead(root, candVar) || (condInTry && ((root->gtFlags & GTF_EXCEPT) != 0)))
{
intermediateUse = true;
break;
}
}

if (!intermediateUse)
{
incrStmt = s;
iterVar = candVar;
break;
}
}
}

if (s == firstStmt)
{
break;
}
}
Comment thread
AndyAyersMS marked this conversation as resolved.
}

if (incrStmt == nullptr || (optIsLoopIncrTree(incrStmt->GetRootNode()) == BAD_VAR_NUM))
if (incrStmt == nullptr)
{
return false;
}

assert(testStmt != incrStmt);
assert(iterVar != BAD_VAR_NUM);

*ppTest = testStmt->GetRootNode();
*ppIncr = incrStmt->GetRootNode();
Expand Down
Loading