-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Revamp the generation of runtime division checks on ARM64
Fixes #64795 This patch introduces a new compilation phase that passes over the GenTrees looking for GT_DIV/GT_UDIV nodes on integral types, and morphs the code to introduce the necessary conformance checks (overflow/divide-by-zero) early on in the compilation pipeline. Currently these are added during the Emit phase, meaning optimizations don't run on any code introduced. The aim is to allow the compiler to make decisions on code position and instruction selection for these checks. For example on ARM64 this enables certain scenarios to choose the cbz instruction over cmp/beq, can lead to more compact code. It also allows some of the comparisons in the checks to be hoisted out of loops.
- Loading branch information
1 parent
4de59c4
commit e22295b
Showing
12 changed files
with
382 additions
and
97 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,270 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
#include "jitpch.h" | ||
#include "compiler.h" | ||
#include "gentree.h" | ||
#ifdef _MSC_VER | ||
#pragma hdrstop | ||
#endif | ||
|
||
jitstd::vector<unsigned>& Compiler::getVisitedDivNodes() | ||
{ | ||
if (visitedDivNodes == nullptr) | ||
{ | ||
visitedDivNodes = new (getAllocator(CMK_Unknown)) jitstd::vector<unsigned>(getAllocator(CMK_Unknown)); | ||
} | ||
return *visitedDivNodes; | ||
} | ||
|
||
jitstd::vector<Compiler::ThrowHelper>& Compiler::getThrowHelperSet() | ||
{ | ||
if (throwHelperSet == nullptr) | ||
{ | ||
throwHelperSet = new (getAllocator(CMK_Unknown)) jitstd::vector<ThrowHelper>(getAllocator(CMK_Unknown)); | ||
} | ||
return *throwHelperSet; | ||
} | ||
|
||
void Compiler::fgInsertConditionalThrowException(Compiler::TreeCoord& code, SpecialCodeKind codeKind) | ||
{ | ||
// Create a new basic block, splitting before the code containing the division. | ||
Statement* newStmt = nullptr; | ||
GenTree** splitNodeUse = nullptr; | ||
JITDUMP("[%06d] contains the division\n", code.tree->gtTreeID); | ||
BasicBlock* divBlock = fgSplitBlockBeforeTree(code.block, code.stmt, code.tree, &newStmt, &splitNodeUse); | ||
GenTree* tree = *splitNodeUse; | ||
JITDUMP("[%06d] contains the division after split\n", tree->gtTreeID); | ||
BasicBlock* checkBlock = code.block; | ||
|
||
// The checking block is added after the original block that has been split. This is for convenience of | ||
// adding the checking statement to an empty basic block. | ||
checkBlock = fgNewBBafter(BBJ_ALWAYS, code.block, true); | ||
fgRedirectTargetEdge(code.block, checkBlock); | ||
|
||
GenTree* dividend = tree->gtGetOp1(); | ||
GenTree* divisor = tree->gtGetOp2(); | ||
assert(varTypeIsIntegral(divisor) && varTypeIsIntegral(dividend)); | ||
|
||
GenTree* divisorCopy = nullptr; | ||
GenTree* dividendCopy = nullptr; | ||
|
||
GenTree* condition = nullptr; | ||
CorInfoHelpFunc helper = CORINFO_HELP_UNDEF; | ||
switch (codeKind) | ||
{ | ||
case SCK_DIV_BY_ZERO: | ||
{ | ||
assert(codeKind == SCK_DIV_BY_ZERO); | ||
impCloneExpr(divisor, &divisorCopy, CHECK_SPILL_NONE, nullptr DEBUGARG("cloned for runtime check")); | ||
|
||
// (divisor == 0) | ||
condition = gtNewOperNode(GT_EQ, TYP_INT, divisorCopy, gtNewIconNode(0, genActualType(divisorCopy))); | ||
|
||
helper = CORINFO_HELP_THROWDIVZERO; | ||
break; | ||
} | ||
case SCK_OVERFLOW: | ||
{ | ||
impCloneExpr(divisor, &divisorCopy, CHECK_SPILL_NONE, nullptr DEBUGARG("cloned for runtime check")); | ||
impCloneExpr(dividend, ÷ndCopy, CHECK_SPILL_NONE, nullptr DEBUGARG("cloned for runtime check")); | ||
|
||
// (dividend < 0 && divisor == -1) | ||
GenTreeOp* const divisorIsMinusOne = | ||
gtNewOperNode(GT_EQ, TYP_INT, divisorCopy, gtNewIconNode(-1, genActualType(divisorCopy))); | ||
GenTreeOp* const dividendIsNegative = | ||
gtNewOperNode(GT_LT, TYP_INT, dividendCopy, gtNewIconNode(0, genActualType(dividendCopy))); | ||
GenTreeOp* const combinedTest = gtNewOperNode(GT_AND, TYP_INT, divisorIsMinusOne, dividendIsNegative); | ||
condition = gtNewOperNode(GT_EQ, TYP_INT, combinedTest, gtNewTrue()); | ||
|
||
helper = CORINFO_HELP_OVERFLOW; | ||
break; | ||
} | ||
default: | ||
noway_assert(!"unexpected exception code kind"); | ||
} | ||
assert(condition != nullptr); | ||
assert(helper != CORINFO_HELP_UNDEF); | ||
|
||
// Add another block near the checking block, this will be the throw block. | ||
// This block is explicitly placed after the checking block to produce debuggable code | ||
// when optimizations aren't enabled. | ||
BasicBlock* throwBlock = nullptr; | ||
|
||
// Search for an already created throw block. We don't do this for debug code | ||
// so the throw code will stay in-line. | ||
if (!opts.compDbgCode) | ||
{ | ||
for (const ThrowHelper& th : getThrowHelperSet()) | ||
{ | ||
if (th.For(helper, divBlock)) | ||
{ | ||
throwBlock = th.Block(); | ||
break; | ||
} | ||
} | ||
} | ||
|
||
if (throwBlock == nullptr) | ||
{ | ||
// Insert a throw block close by, in the same EH region. | ||
throwBlock = fgNewBBafter(BBJ_THROW, checkBlock, true); | ||
|
||
// Add exception raising code to the throw block. | ||
GenTreeCall* const thrw = gtNewHelperCallNode(helper, TYP_VOID); | ||
Statement* throwStmt = gtNewStmt(thrw); | ||
fgInsertStmtAtEnd(throwBlock, throwStmt); | ||
assert(throwBlock->lastStmt() == throwStmt); | ||
|
||
if (!opts.compDbgCode) | ||
getThrowHelperSet().push_back(ThrowHelper(helper, throwBlock)); | ||
} | ||
|
||
// Add the compare and jump to the end of the checking block. | ||
condition->gtFlags |= GTF_RELOP_JMP_USED; | ||
GenTree* const cndJmp = gtNewOperNode(GT_JTRUE, TYP_VOID, condition); | ||
Statement* jmpStmt = gtNewStmt(cndJmp); | ||
fgInsertStmtAtEnd(checkBlock, jmpStmt); | ||
assert(checkBlock->lastStmt() == jmpStmt); | ||
|
||
// Set the true edge to the exception block, false edge to the block where division occurs. | ||
FlowEdge* trueEdge = fgAddRefPred(throwBlock, checkBlock); | ||
FlowEdge* falseEdge = fgAddRefPred(divBlock, checkBlock); | ||
|
||
// The exception case is considered unlikely in comparison to normal control flow. | ||
trueEdge->setLikelihood(0.01); | ||
falseEdge->setLikelihood(0.99); | ||
|
||
checkBlock->SetCond(trueEdge, falseEdge); | ||
|
||
JITDUMP(FMT_BB " contains the division\n", divBlock->bbNum); | ||
JITDUMP(FMT_BB " contains the check\n", checkBlock->bbNum); | ||
JITDUMP(FMT_BB " contains the throw\n", throwBlock->bbNum); | ||
|
||
code.block = divBlock; | ||
code.stmt = divBlock->firstStmt(); | ||
code.tree = tree; | ||
} | ||
|
||
bool Compiler::fgInsertDivisionChecks(BasicBlock* block, Statement* stmt, GenTree* tree) | ||
{ | ||
assert(tree->OperIs(GT_DIV, GT_UDIV)); | ||
|
||
// Check we haven't processed this DIV before. | ||
bool found = false; | ||
for (const unsigned& id : getVisitedDivNodes()) | ||
{ | ||
if (tree->gtTreeID == id) | ||
{ | ||
found = true; | ||
break; | ||
} | ||
} | ||
if (found) | ||
return false; | ||
|
||
// Only integral divisions will throw | ||
if (!(varTypeIsIntegral(tree->gtGetOp1()) && varTypeIsIntegral(tree->gtGetOp2()))) | ||
{ | ||
return false; | ||
} | ||
|
||
bool modified = false; | ||
|
||
TreeCoord divCode(block, stmt, tree); | ||
|
||
// Check for division by -1 when the dividend is signed. | ||
// (NegativeVal / -1) => OverflowException | ||
if ((divCode.tree->OperExceptions(this) & ExceptionSetFlags::ArithmeticException) != ExceptionSetFlags::None) | ||
{ | ||
assert(tree->OperIs(GT_DIV)); | ||
JITDUMP(FMT_BB " " FMT_STMT " needs a signed overflow check\n", divCode.block->bbNum, divCode.stmt->GetID()); | ||
fgInsertConditionalThrowException(divCode, SCK_OVERFLOW); | ||
modified = true; | ||
} | ||
|
||
// Check for division by 0 - both unsigned and signed are affected. | ||
// (AnyVal / 0) => DivideByZeroException | ||
if ((divCode.tree->OperExceptions(this) & ExceptionSetFlags::DivideByZeroException) != ExceptionSetFlags::None) | ||
{ | ||
JITDUMP(FMT_BB " " FMT_STMT " needs a divide by zero check\n", divCode.block->bbNum, divCode.stmt->GetID()); | ||
fgInsertConditionalThrowException(divCode, SCK_DIV_BY_ZERO); | ||
modified = true; | ||
} | ||
|
||
JITDUMP("Marking [%06d] as visited.\n", divCode.tree->gtTreeID); | ||
getVisitedDivNodes().push_back(divCode.tree->gtTreeID); | ||
|
||
return modified; | ||
} | ||
|
||
PhaseStatus Compiler::fgInsertArithmeticExceptions() | ||
{ | ||
#if defined(TARGET_ARM64) | ||
struct Visitor : GenTreeVisitor<Visitor> | ||
{ | ||
enum | ||
{ | ||
DoPreOrder = true, | ||
}; | ||
|
||
Visitor(Compiler* comp, BasicBlock* block, Statement* stmt) | ||
: GenTreeVisitor(comp) | ||
, m_block(block) | ||
, m_stmt(stmt) | ||
{ | ||
} | ||
|
||
fgWalkResult PreOrderVisit(GenTree** use, GenTree* user) | ||
{ | ||
if (!(*use)->OperIs(GT_DIV, GT_UDIV)) | ||
{ | ||
return WALK_CONTINUE; | ||
} | ||
|
||
modified = m_compiler->fgInsertDivisionChecks(m_block, m_stmt, *use); | ||
|
||
// If checks were inserted, the tree was split and the graph was modified, | ||
// so traversal needs to start again. | ||
if (modified) | ||
{ | ||
return WALK_ABORT; | ||
} | ||
|
||
return WALK_CONTINUE; | ||
} | ||
|
||
BasicBlock* m_block; | ||
Statement* m_stmt; | ||
public: | ||
bool modified = false; | ||
}; | ||
|
||
bool lastModified = false; | ||
bool anyModified = false; | ||
do | ||
{ | ||
lastModified = false; | ||
for (BasicBlock* const block : Blocks()) | ||
{ | ||
for (Statement* const stmt : block->Statements()) | ||
{ | ||
GenTree* tree = stmt->GetRootNode(); | ||
Visitor visitor(this, block, stmt); | ||
visitor.WalkTree(&tree, nullptr); | ||
lastModified = visitor.modified; | ||
anyModified |= lastModified; | ||
|
||
if (lastModified) | ||
break; | ||
} | ||
if (lastModified) | ||
break; | ||
} | ||
} while (lastModified); | ||
|
||
return anyModified ? PhaseStatus::MODIFIED_EVERYTHING : PhaseStatus::MODIFIED_NOTHING; | ||
#else | ||
return PhaseStatus::MODIFIED_NOTHING; | ||
#endif | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.