Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JIT doesn't eliminate bounds checks sometimes #11623

Closed
EgorBo opened this issue Dec 6, 2018 · 7 comments · Fixed by #40180 or #43568
Closed

JIT doesn't eliminate bounds checks sometimes #11623

EgorBo opened this issue Dec 6, 2018 · 7 comments · Fixed by #40180 or #43568
Labels
area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI JitUntriaged CLR JIT issues needing additional triage optimization
Milestone

Comments

@EgorBo
Copy link
Member

EgorBo commented Dec 6, 2018

During dotnet/coreclr#21336 I noticed that JIT doesn't eliminate bounds checks in some cases despite the (uint) pattern, e.g.:

public void WriteBytes(Span<byte> destination)
{
    if ((uint)destination.Length >= 5)
    {
        destination[0] = 1;
        destination[1] = 2;
        destination[2] = 3;
        destination[3] = 4;
        destination[4] = 5;
    }
}

Output:

WriteBytes(System.Span`1)
  | cmp     edx,0
  | jbe     00007ffd`190b6d11
  | mov     byte ptr [rax],1
  | cmp     edx,1
  | jbe     00007ffd`190b6d11
  | mov     byte ptr [rax+1],2
  | cmp     edx,2
  | jbe     00007ffd`190b6d11
  | mov     byte ptr [rax+2],3
  | cmp     edx,3
  | jbe     00007ffd`190b6d11
  | mov     byte ptr [rax+3],4
  | cmp     edx,4
  | jbe     00007ffd`190b6d11
  | mov     byte ptr [rax+4],5
  | add     rsp,28h

but if I change the condition a bit - it might help:
if ((uint)destination.Length >= 5) - remain
if ((uint)destination.Length > 4) - eliminated!
if ((uint)destination.Length == 5) - eliminated!
if (5 <= (uint)destination.Length) - remain

Is it a known issue/feature-request?

TieredJit is disabled
.NET Core 3.0.0-preview-27122-01 (CoreCLR 4.6.27121.03, CoreFX 4.7.18.57103), 64bit RyuJIT

category:cq
theme:range-check
skill-level:intermediate
cost:small

@mikedn
Copy link
Contributor

mikedn commented Dec 6, 2018

JIT doesn't eliminate bounds checks in some cases despite the (uint) pattern,

The unsigned compare shouldn't be needed in this case because both the constant and the array length are obviously positive. It's only needed when the index is a variable, basically it helps the JIT figure out that the variable is positive. Ideally the JIT should recognize something like (index >= 0 && index < a.Length) but it doesn't do that today.

Is it a known issue/feature-request?

I think I've seen this issue reported in the past, I'll have to check. Anyway, it doesn't hurt to have an additional example.

@mikedn
Copy link
Contributor

mikedn commented Dec 6, 2018

So, something like if (4 < (uint)destination.Length) results in range check elimination but the similar signed comparison doesn't do that. It turns out that the reason why the unsigned comparison works is exactly the "hack" I added to the JIT to deal with if ((uint)i < (uint)destination.Length).

I don't think that at the time I considered the case of constant indices. Or I did but ignored it because it didn't seem common. I'll try to extend the existing logic and see if there are other hits in the framework to make this worthwhile.

@GSPP
Copy link

GSPP commented Jan 19, 2019

I thought I'd test a few variants of this pattern:

        void Test_Array_Constant(int[] array)
        {
            //signed:
            if (array.Length >= 5) array[4] = 0x1001;
            if (array.Length > 4) array[4] = 0x1002;
            if (array.Length == 5) array[4] = 0x1003;
            if (5 <= array.Length) array[4] = 0x1004;
            if (4 < array.Length) array[4] = 0x1005;

            //unsigned:
            if ((uint)array.Length >= (uint)5) array[4] = 0x1001;
            if ((uint)array.Length > (uint)4) array[4] = 0x1002; //range check eliminated
            if ((uint)array.Length == (uint)5) array[4] = 0x1003;
            if ((uint)5 <= (uint)array.Length) array[4] = 0x1004; //range check eliminated
            if ((uint)4 < (uint)array.Length) array[4] = 0x1005;
        }

        void Test_Array_Span(Span<int> array)
        {
            //signed:
            if (array.Length >= 5) array[4] = 0x1001;
            if (array.Length > 4) array[4] = 0x1002;
            if (array.Length == 5) array[4] = 0x1003; //range check eliminated (difference compared to array case)
            if (5 <= array.Length) array[4] = 0x1004;
            if (4 < array.Length) array[4] = 0x1005;

            //unsigned:
            if ((uint)array.Length >= (uint)5) array[4] = 0x1001;
            if ((uint)array.Length > (uint)4) array[4] = 0x1002; //range check eliminated
            if ((uint)array.Length == (uint)5) array[4] = 0x1003; //range check eliminated (difference compared to array case)
            if ((uint)5 <= (uint)array.Length) array[4] = 0x1004; //range check eliminated
            if ((uint)4 < (uint)array.Length) array[4] = 0x1005;
        }

Interestingly, the span version manages to eliminate more range checks. Also, using unsigned helps with some cases.

@msftgits msftgits transferred this issue from dotnet/coreclr Jan 31, 2020
@msftgits msftgits added this to the Future milestone Jan 31, 2020
@am11
Copy link
Member

am11 commented Oct 6, 2020

I built the the latest master (30e643e) on macOS and tested with COMPlus_JitDisasm=WriteBytes COMPlus_TieredCompilation=0, it seems that some cases @EgorBo mentioned in top post are still not optimized:

expression is bound check eliminated
* if ((uint)destination.Length >= 5) no
if ((uint)destination.Length > 4) yes
if ((uint)destination.Length <= 5) no
if ((uint)destination.Length < 4) no
if ((uint)destination.Length == 5) yes
* if (5 <= (uint)destination.Length) no
if (4 < (uint)destination.Length) yes
if (5 >= (uint)destination.Length) no
if (4 > (uint)destination.Length) no
if (5 == (uint)destination.Length) yes
if (destination.Length >= 5) yes
if (destination.Length > 4) yes
if (destination.Length <= 5) no
if (destination.Length < 4) no
if (destination.Length == 5) yes
if (5 <= destination.Length) yes
if (4 < destination.Length) yes
if (5 >= destination.Length) no
if (4 > destination.Length) no
if (5 == destination.Length) yes

disasm of > 4:

G_M58786_IG03:
       488B442408           mov      rax, bword ptr [rsp+08H]
       C60001               mov      byte  ptr [rax], 1
       C6400102             mov      byte  ptr [rax+1], 2
       C6400203             mov      byte  ptr [rax+2], 3
       C6400304             mov      byte  ptr [rax+3], 4
       C6400405             mov      byte  ptr [rax+4], 5
                                                ;; bbWeight=0.50 PerfScore 3.00

vs. >= 5:

G_M58786_IG03:
       85C0                 test     eax, eax
       7431                 je       SHORT G_M58786_IG05
       488B7DF0             mov      rdi, bword ptr [rbp-10H]
       C60701               mov      byte  ptr [rdi], 1
       83F801               cmp      eax, 1
       7625                 jbe      SHORT G_M58786_IG05
       C6470102             mov      byte  ptr [rdi+1], 2
       83F802               cmp      eax, 2
       761C                 jbe      SHORT G_M58786_IG05
       C6470203             mov      byte  ptr [rdi+2], 3
       83F803               cmp      eax, 3
       7613                 jbe      SHORT G_M58786_IG05
       C6470304             mov      byte  ptr [rdi+3], 4
       83F804               cmp      eax, 4
       760A                 jbe      SHORT G_M58786_IG05
       C6470405             mov      byte  ptr [rdi+4], 5
                                                ;; bbWeight=0.50 PerfScore 6.12

@nathan-moore
Copy link
Contributor

nathan-moore commented Oct 7, 2020

@am11 I was mostly focused on the cases like (5 < destination.Length) without the cast, as I figured it would be more common/natural. I can see about this scenario if you disagree.

Unsigned comparisons are currently largely ignored by this area of assertion prop, barring the assertion that leads to the removal of the above checks.

@am11
Copy link
Member

am11 commented Oct 7, 2020

@nathan-moore, this is a great news. I have updated the table with cases without casts.

I have marked two cases with leading asterisk (*) which are interesting to fix because this pattern is common in the existing consumer code; as so far, this is considered as a (tricky) way to elide away the bound checks. The (uint)X >= # pattern can also be found in this repo with e.g. git grep "(uint).*>=" :/*.cs. We can cleanup some of those casts, which have been rendered redundant by your change.

@briansull
Copy link
Contributor

briansull commented Oct 16, 2020

Reopening this issue to address the remaining cases which aren't handled:

We should at least address these two common cases:

expression bounds check not eliminated

  • if ((uint)destination.Length >= 5)
  • if (5 <= (uint)destination.Length)

@BruceForstall BruceForstall added the JitUntriaged CLR JIT issues needing additional triage label Oct 28, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Mar 12, 2021
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 JitUntriaged CLR JIT issues needing additional triage optimization
Projects
None yet
8 participants