Runtime: [Intrinsic] CompareEqual does not use the memory source instruction

Created on 29 Jan 2019  路  9Comments  路  Source: dotnet/runtime

I was trying to optimize SequenceCompareTo beyond the point where @benaadams pushed it and stumbled upon various JIT issues controlling the output.

From this code a few are showcased.

Avx2.CompareEqual(LoadVector256(ref first, (IntPtr)lengthAndOffset), LoadVector256(ref second, (IntPtr)lengthAndOffset))

It generates:

movsxd      rdi,esi      
vmovupd     ymm0,ymmword ptr [r11+rdi]  # allocation of usable register should prevent the previous instruction
movsxd      rdi,esi      # useless move
vmovupd     ymm1,ymmword ptr [r10+rdi]  
vpcmpeqb    ymm0,ymm0,ymm1   

I doubt that the movsxd creates a false dependency, but it could use cpu registers which will impact the loop. Probably the worst setback is that is:

vmovupd     ymm1,ymmword ptr [r10+rdi]  
vpcmpeqb    ymm0,ymm0,ymm1   

which could become instead:

vpcmpeqb    ymm0, ymm0, ymmword ptr [r10+rdi]  

using the memory version instead we avoid a data dependency and also using extra physical registers.

Removing the movs and the compare encoding the code should look like:

movsxd      rdi,esi   # outside the loop if really necessary
Loop:
vmovupd     ymm0,ymmword ptr [r11+rdi]
vpcmpeqb    ymm0, ymm0, ymmword ptr [r10+rdi] 
vpmovmskb   edi,ymm0  
...
add       rdi, -32
jna         Loop

category:cq
theme:hardware-intrinsics
skill-level:expert
cost:medium

area-CodeGen-coreclr optimization

Most helpful comment

Yeah, basically this happens pretty much every time you have something like call(call1, call2, ... callN), in theory it could be imported as a single expression tree but the JIT has issues with getting the order right and insists in splitting the expression into multiple trees. In this case:

***** BB01, stmt 1
     (  6,  5) [000016] ------------              *  STMT      void  (IL   ???...  ???)
N006 (  6,  5) [000047] -A-XG---R---              \--*  ASG       simd32 (copy) $VN.Void
N005 (  1,  1) [000046] D------N----                 +--*  LCL_VAR   simd32 V04 tmp1         d:1
N004 (  6,  5) [000044] U--XG-------                 \--*  IND       simd32 <l:$241, c:$240>
N003 (  3,  3) [000041] ------------                    \--*  ADD       byref  $140
N001 (  1,  1) [000000] ------------                       +--*  LCL_VAR   byref  V00 arg0         u:1 (last use) $80
N002 (  1,  1) [000001] ------------                       \--*  LCL_VAR   long   V02 arg2         u:1 $c0

***** BB01, stmt 2
     (  6,  5) [000022] ------------              *  STMT      void  (IL   ???...  ???)
N006 (  6,  5) [000068] -A-XG---R---              \--*  ASG       simd32 (copy) $VN.Void
N005 (  1,  1) [000067] D------N----                 +--*  LCL_VAR   simd32 V05 tmp2         d:1
N004 (  6,  5) [000065] U--XG-------                 \--*  IND       simd32 <l:$243, c:$242>
N003 (  3,  3) [000062] ------------                    \--*  ADD       byref  $141
N001 (  1,  1) [000007] ------------                       +--*  LCL_VAR   byref  V01 arg1         u:1 (last use) $81
N002 (  1,  1) [000008] ------------                       \--*  LCL_VAR   long   V02 arg2         u:1 (last use) $c0

***** BB01, stmt 3
     (  5,  5) [000027] ------------              *  STMT      void  (IL   ???...  ???)
N005 (  5,  5) [000026] ------------              \--*  RETURN    int    $2c0
N004 (  4,  4) [000025] ------------                 \--*  HWIntrinsic int    ubyte MoveMask $281
N003 (  3,  3) [000024] ------------                    \--*  HWIntrinsic simd32 ubyte CompareEqual $280
N001 (  1,  1) [000017] ------------                       +--*  LCL_VAR   simd32 V04 tmp1         u:1 (last use) $1c2
N002 (  1,  1) [000023] ------------                       \--*  LCL_VAR   simd32 V05 tmp2         u:1 (last use) $1c5

I'll see if I can tweak my forward substitution experiment to handle this case (but obviously even if I do that it will be too late for 3.0). Though it would be better if the JIT stops splitting trees like that. It just results in more variables and more work for the downstream phases.

All 9 comments

The double movsxd could probably be avoided by casting once:

IntPtr offset = (IntPtr)lengthAndOffset;
Avx2.CompareEqual(LoadVector256(ref first, offset), LoadVector256(ref second, offset)

Not sure about the other things though.

@redknightlois, if you could get this down to a simple repro and/or provide the JIT dump, it would make it easier to determine why this isn't meeting the IsContainableOp checks in lowering (see https://github.com/dotnet/coreclr/blob/master/src/jit/lowerxarch.cpp#L2450).

Avx2.CompareEqual is a commutative simple SIMD intrinsic that requires the VEX encoding, so it should be getting marked as:

  • SupportsAlignedSimdLoads = false
  • SupportsUnalignedSimdLoads = true
  • SupportsGeneralLoads = true

The second operand should not be a HWIntrinsic, so it should come down to isContainableMemoryOp (https://github.com/dotnet/coreclr/blob/85c61f151c04c1d3cec782532b17969795f07a93/src/jit/lsrabuild.cpp#L683)

@tannergooding Probably not simple, but the repro :)
https://gist.github.com/redknightlois/0c2a301006f817e41c364666a422ca63

@redknightlois - can you confirm whether dotnet/coreclr#22944 addressed this?

It does not, you can check it on the code generated at the large loop on:
https://github.com/dotnet/coreclr/blob/52aff202cd382c233d903d432da06deffaa21868/src/System.Private.CoreLib/shared/System/SpanHelpers.Byte.cs#L1406

vmovupd     ymm0,ymmword ptr [rcx+r11]  
vmovupd     ymm1,ymmword ptr [r8+r11]  
vpcmpeqb    ymm0,ymm0,ymm1  
vpmovmskb   r10d,ymm0  

Should emit instead:

vmovupd     ymm0,ymmword ptr [rcx+r11]  
vpcmpeqb    ymm0,ymm0,ymmword ptr [r8+r11]  
vpmovmskb   r10d,ymm0  

Checked on: 3.0.0-preview6-27622-10

Those 2 LoadVector256 calls are likely to lead to a "broken" expression tree, that would require forward substitution or other approach to make it work.

It looks to only be a problem around the LoadVector256(ref byte start, IntPtr offset) helper. Pinning and using Avx.LoadVector256(byte* address) correctly folds the second load into the pcmpeqb instruction.

00007FFC4D465EDA  vmovdqu     ymm0,ymmword ptr [rax+rcx]  
00007FFC4D465EDF  vpcmpeqb    ymm0,ymm0,ymmword ptr [rdx+rcx]  
00007FFC4D465EE4  vpmovmskb   r8d,ymm0

Yeah, basically this happens pretty much every time you have something like call(call1, call2, ... callN), in theory it could be imported as a single expression tree but the JIT has issues with getting the order right and insists in splitting the expression into multiple trees. In this case:

***** BB01, stmt 1
     (  6,  5) [000016] ------------              *  STMT      void  (IL   ???...  ???)
N006 (  6,  5) [000047] -A-XG---R---              \--*  ASG       simd32 (copy) $VN.Void
N005 (  1,  1) [000046] D------N----                 +--*  LCL_VAR   simd32 V04 tmp1         d:1
N004 (  6,  5) [000044] U--XG-------                 \--*  IND       simd32 <l:$241, c:$240>
N003 (  3,  3) [000041] ------------                    \--*  ADD       byref  $140
N001 (  1,  1) [000000] ------------                       +--*  LCL_VAR   byref  V00 arg0         u:1 (last use) $80
N002 (  1,  1) [000001] ------------                       \--*  LCL_VAR   long   V02 arg2         u:1 $c0

***** BB01, stmt 2
     (  6,  5) [000022] ------------              *  STMT      void  (IL   ???...  ???)
N006 (  6,  5) [000068] -A-XG---R---              \--*  ASG       simd32 (copy) $VN.Void
N005 (  1,  1) [000067] D------N----                 +--*  LCL_VAR   simd32 V05 tmp2         d:1
N004 (  6,  5) [000065] U--XG-------                 \--*  IND       simd32 <l:$243, c:$242>
N003 (  3,  3) [000062] ------------                    \--*  ADD       byref  $141
N001 (  1,  1) [000007] ------------                       +--*  LCL_VAR   byref  V01 arg1         u:1 (last use) $81
N002 (  1,  1) [000008] ------------                       \--*  LCL_VAR   long   V02 arg2         u:1 (last use) $c0

***** BB01, stmt 3
     (  5,  5) [000027] ------------              *  STMT      void  (IL   ???...  ???)
N005 (  5,  5) [000026] ------------              \--*  RETURN    int    $2c0
N004 (  4,  4) [000025] ------------                 \--*  HWIntrinsic int    ubyte MoveMask $281
N003 (  3,  3) [000024] ------------                    \--*  HWIntrinsic simd32 ubyte CompareEqual $280
N001 (  1,  1) [000017] ------------                       +--*  LCL_VAR   simd32 V04 tmp1         u:1 (last use) $1c2
N002 (  1,  1) [000023] ------------                       \--*  LCL_VAR   simd32 V05 tmp2         u:1 (last use) $1c5

I'll see if I can tweak my forward substitution experiment to handle this case (but obviously even if I do that it will be too late for 3.0). Though it would be better if the JIT stops splitting trees like that. It just results in more variables and more work for the downstream phases.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

omariom picture omariom  路  3Comments

GitAntoinee picture GitAntoinee  路  3Comments

EgorBo picture EgorBo  路  3Comments

aggieben picture aggieben  路  3Comments

jkotas picture jkotas  路  3Comments