Skip to content

Conversation

@EgorBo
Copy link
Member

@EgorBo EgorBo commented Nov 14, 2021

@stephentoub noticed that JIT doesn't handle well range checks for arrays/spans of known lengths, e.g.:

static ReadOnlySpan<byte> span => new byte[] { 1, 2, 3, 4 };

byte Test1(int i)
{
    if ((uint)i < (uint)span.Length) // span.Length is folded into '4'
        return span[i];
    return 0;
}



const String MyStr = "test";

char Test2(int i)
{
    if ((uint)i < (uint)MyStr.Length) // MyStr.Length is folded into '4'
        return MyStr[i];
    return ' ';
}

For both Test1 and Test2 we always emit range checks no matter how conditions are reversed and how many uint casts are added 🙁. Part of the problem that both span.Length and MyStr.Length are folded into constants.

It turns out assertprop doesn't emit any assertions for those JTRUE. However, it does emit them for patterns like this:

if (i >= 0 && i < cns)
GenTreeNode creates assertion:
N004 (  5,  5) [000003] ------------              *  JTRUE     void  
In BB01 New Global Constant Assertion: ($140,$40) Const_Loop_Bnd {LE($80, $40)} is not  {IntCns 0}, index = #01
GenTreeNode creates assertion:
N004 (  5,  5) [000003] ------------              *  JTRUE     void  
In BB01 New Global Constant Assertion: ($140,$40) Const_Loop_Bnd {LE($80, $40)} is  {IntCns 0}, index = #02
GenTreeNode creates assertion:
N004 (  5,  5) [000016] ------------              *  JTRUE     void  
In BB02 New Global Constant Assertion: ($141,$40) Const_Loop_Bnd {GE($80, $42)} is not  {IntCns 0}, index = #03
GenTreeNode creates assertion:
N004 (  5,  5) [000016] ------------              *  JTRUE     void  
In BB02 New Global Constant Assertion: ($141,$40) Const_Loop_Bnd {GE($80, $42)} is  {IntCns 0}, index = #04
GenTreeNode creates assertion:
N003 (  6,  9) [000028] ---X--------              *  ARR_BOUNDS_CHECK_Rng void   $1c2
In BB03 New Global ArrBnds  Assertion: ($0,$0) [idx: {InitVal($40)};len: {IntCns 4}] in range , index = #05

then later these assertions are merged correctly in rangecheck.cpp into

The range after edge merging:<1, 3>
   Computed Range [000023] => <1, 3>    // it means i is in [1..3] range.
}

but then we just give up on it due to DoesOverflow returning true for index node (which is a LCL_VAR argument).

If my fix is correct, I then will change optCreateJTrueBoundsAssertion to emit exactly the same assertions for

if ((uint)i < (uint)cns)  // === "(i >= 0 && i < cns)"

and that will fix range checks for if ((uint)i < span.Length) patterns too.

cc @dotnet/jit-contrib @SingleAccretion

the only spmi diff is:

-23 (-34.33% of base) : 33026.dasm - ManagedTests.DynamicCSharp.Conformance.dynamic.context.operate.regclass.regclass007a.regclass007a.Test:TestMethod(ManagedTests.DynamicCSharp.Conformance.dynamic.context.operate.regclass.regclassoperate.regclassoperate.MyClass[]):int:this

but I assume they will appear after the next step (in a separate PR)

@ghost ghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Nov 14, 2021
@ghost
Copy link

ghost commented Nov 14, 2021

Tagging subscribers to this area: @JulieLeeMSFT
See info in area-owners.md if you want to be subscribed.

Issue Details

@stephentoub noticed that JIT doesn't handle well range checks for arrays/spans of known lengths, e.g.:

static ReadOnlySpan<byte> span => new byte[] { 1, 2, 3, 4 };

byte Test1(int i)
{
    if ((uint)i < (uint)span.Length) // span.Length is folded into '4'
        return span[i];
    return 0;
}



const String MyStr = "test";

char Test2(int i)
{
    if ((uint)i < (uint)MyStr.Length) // MyStr.Length is folded into '4'
        return MyStr[i];
    return ' ';
}

For both Test1 and Test2 we always emit range checks no matter how conditions are reversed and how many uint casts are added 🙁. Part of the problem that both span.Length and MyStr.Length are folded into constants.

It turns out assertprop doesn't emit any assertions for those JTRUE. However, it does emit them for patterns like this:

if (i >= 0 && i < cns)
GenTreeNode creates assertion:
N004 (  5,  5) [000003] ------------              *  JTRUE     void  
In BB01 New Global Constant Assertion: ($140,$40) Const_Loop_Bnd {LE($80, $40)} is not  {IntCns 0}, index = #01
GenTreeNode creates assertion:
N004 (  5,  5) [000003] ------------              *  JTRUE     void  
In BB01 New Global Constant Assertion: ($140,$40) Const_Loop_Bnd {LE($80, $40)} is  {IntCns 0}, index = #02
GenTreeNode creates assertion:
N004 (  5,  5) [000016] ------------              *  JTRUE     void  
In BB02 New Global Constant Assertion: ($141,$40) Const_Loop_Bnd {GE($80, $42)} is not  {IntCns 0}, index = #03
GenTreeNode creates assertion:
N004 (  5,  5) [000016] ------------              *  JTRUE     void  
In BB02 New Global Constant Assertion: ($141,$40) Const_Loop_Bnd {GE($80, $42)} is  {IntCns 0}, index = #04
GenTreeNode creates assertion:
N003 (  6,  9) [000028] ---X--------              *  ARR_BOUNDS_CHECK_Rng void   $1c2
In BB03 New Global ArrBnds  Assertion: ($0,$0) [idx: {InitVal($40)};len: {IntCns 4}] in range , index = #05

then later these assertions are merged correctly in rangecheck.cpp into

The range after edge merging:<1, 3>
   Computed Range [000023] => <1, 3>
}

but then we just give up on it due to DoesOverflow returning true for index node (which is a LCL_VAR argument).

If my fix is correct, I then will change assertprop to emit exactly the same assertions for

if (i >= 0 && i < cns)

and

if ((uint)i < (uint)cns)

cc @dotnet/jit-contrib @SingleAccretion

Author: EgorBo
Assignees: -
Labels:

area-CodeGen-coreclr

Milestone: -

@EgorBo
Copy link
Member Author

EgorBo commented Nov 14, 2021

Example:

static ReadOnlySpan<byte> span => new byte[] { 1, 2, 3, 4 };
static byte Test1(int i)
{
    if (i > 0 && i < span.Length)
        return span[i];

    return 0;
}


const String MyStr = "hello";
char Test2(int i)
{
    if (i > 0 && i < MyStr.Length)
        return MyStr[i];

    return ' ';
}

Codegen diff (Main vs This PR): https://www.diffchecker.com/CeU2AlYV

@EgorBo
Copy link
Member Author

EgorBo commented Dec 2, 2021

@dotnet/jit-contrib PTAL, a simple change.
I have a follow up pr in plans to remove more bound checks on top of it.

@EgorBo
Copy link
Member Author

EgorBo commented Dec 9, 2021

@BruceForstall @AndyAyersMS could some of you take a look please as it looks like you were the only jit team members who touched this area?

Once this is merged I'll file a one more PR on top of this one to finally fix bound checks for scenarios like:

static ReadOnlySpan<byte> span => new byte[] { 1, 2, 3, 4 };

byte Test1(int i)
{
    if ((uint)i < (uint)span.Length)
        return span[i];

@BruceForstall
Copy link
Contributor

@BruceForstall @AndyAyersMS could some of you take a look please as it looks like you were the only jit team members who touched this area?

Sorry, I don't have any experience with this code.

However:

then we just give up on it due to DoesOverflow returning true for index node (which is a LCL_VAR argument).

Why does this happen, and why is the fix not fixing that?

It looks like we give up because it looks for the SSA def assignment of the index var, but probably doesn't find one (?) because it's an argument, so assumes it might overflow. Is that correct? It seems like if the lcl_var def is an argument, then it should return false for DoesOverflow?

Comment on lines 1180 to 1185
assert(binop->OperIs(GT_ADD, GT_MUL, GT_LSH));

GenTree* op1 = binop->gtGetOp1();
GenTree* op2 = binop->gtGetOp2();

JITDUMP("[RangeCheck::IsBinOpMonotonicallyIncreasing] [%06d], [%06d]\n", Compiler::dspTreeID(op1),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, it is possible to get there for defs that aren't just FIRST_SSA_NUM ones, but also non-LCL_VAR ones. I think it would be safer to check that what we are seeing here is really the def that comes from outside the method:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SingleAccretion not sure I understand, can you elaborate?

Copy link
Contributor

@SingleAccretion SingleAccretion Dec 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Sorry for the delay :( ).

The !m_pCompiler->lvaIsParameter(lcl->GetLclNum()); check permits us to say that for any SSA definition (1st, 2nd, 3rd, etc) of a parameter, we assert it will not overflow.

However, that seems like a dangerous assumption, because we only know that is the case for the first definition (one that comes from outside the method), so I was suggesting the check be strengthened to this:

if (ssaDef == nullptr)
{
    // Parameter definitions that come from outside the method could not have overflown.
    if ((lcl->GetSsaNum() == SsaConfig::FIRST_SSA_NUM) && m_pCompiler->lvaIsParameter(lcl->GetLclNum()))
    {
        return false;
    }

    return true;
}

I believe it is the case today we will not end up here with ssaDef == nullptr and lcl->GetSsaNum() != SsaConfig::FIRST_SSA_NUM, but that's a fragile (and non-obvious) invariant to rely on.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also assert in the return false clause that the ssa def comes from fgFirstBB.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for educating me!

@EgorBo
Copy link
Member Author

EgorBo commented Dec 14, 2021

Why does this happen, and why is the fix not fixing that?

@BruceForstall good question, it's now addressed, thanks!

@EgorBo EgorBo merged commit 6be1c8d into dotnet:main Dec 14, 2021
@ghost ghost locked as resolved and limited conversation to collaborators Jan 14, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants