Skip to content

Cannot convert efficiently a System.Numerics.Vector4 to a Vector3聽#86220

Closed

Description

@xoofx

Hey JIT compiler friends, 馃槉

Vector3 is quite convenient and popular to manipulate e.g position/speed, used for storage and calculations. The problem comes that loading/storing is requiring several instructions to store the elements separately (unlike for a Vector4).

One technique that is usually used is to load a Vector4 and downcasting it to a Vector3, this operation should effectively zero out the .w component of the SIMD register. Usually, the load is made on safe data boundaries were you know that loading w from memory is ok (even with garbage) and is safe (e.g no page fault access).

The problem is that while upcasting from a Vector3 to a Vector4 has a proper intrinsics, downcasting doesn't and generates always stack spilling. I haven't found a way to workaround this, so I usually have to replace all Vector3 to Vector4, which is not ideal.

For example the following code (on sharplab.io)

    private static void TestVector4ToVector3v1(Span<Vector4> span, out Vector3 output) {
        Vector3 result = Vector3.Zero;
        for(int i = 0; i < span.Length; i++) {
            result += span[i].AsVector128().AsVector3();
        }

        output = result;
    }

will generate the following code:

C.TestVector4ToVector3v1(System.Span`1<System.Numerics.Vector4>, System.Numerics.Vector3 ByRef)
    L0000: sub rsp, 0x18
    L0004: vzeroupper
    L0007: mov rax, [rcx]
    L000a: mov ecx, [rcx+8]
    L000d: vxorps xmm0, xmm0, xmm0
    L0012: xor r8d, r8d
    L0015: test ecx, ecx
    L0017: jle short L004e
    L0019: nop [rax]
    L0020: mov r9d, r8d
    L0023: shl r9, 4
    L0027: vmovupd xmm1, [rax+r9]
    <<<<<<<<<<<<<< stack spilling and reload - begin
    L002d: vmovapd [rsp], xmm1
    L0032: vmovss xmm1, [rsp+8]
    L0038: vmovsd xmm2, [rsp]
    <<<<<<<<<<<<<< stack spilling and reload - end
    L003d: vshufps xmm2, xmm2, xmm1, 0x44
    L0042: vaddps xmm0, xmm0, xmm2
    L0046: inc r8d
    L0049: cmp r8d, ecx
    L004c: jl short L0020
    L004e: vmovsd [rdx], xmm0
    L0052: vpshufd xmm1, xmm0, 2
    L0057: vmovss [rdx+8], xmm1
    L005c: add rsp, 0x18
    L0060: ret

Trying to workaround it via the following doesn't work either which generates a code similar to the code above. See the sharplab link above. For which I was more surprised, as the Vector3(float, float, float) constructor is marked as an intrinsic...

    private static void TestVector4ToVector3v2(Span<Vector4> span, out Vector3 output) {
        Vector3 result = Vector3.Zero;
        for(int i = 0; i < span.Length; i++) {
            var v4 = span[i];
            var v3 = new Vector3(v4.X, v4.Y, v4.Z);
            result += v3;
        }

        output = result;
    }    

Instead, a downcast should be able to generate a similar code to what we could get with an upcast by setting directly to 0.0f the .w component like this:

    L0027: vxorps xmm0, xmm0, xmm0
    L002b: vinsertps xmm0, xmm1, xmm0, 0x30

Maybe I have missed something in the API that is providing such conversion but I failed to find it... 馃

Would it possible to optimize this conversion as proposed here?

Thanks!

(Edit: consequently, this applies to any downcast e.g so to Vector2 as well, or from Vector3 to Vector2)

Activity

  1. ghost added
    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
    on May 14, 2023
  2. ghost added
    untriagedNew issue has not been triaged by the area owner
    on May 14, 2023
  3. EgorBo commented on May 14, 2023

    @EgorBo
    Member

    I can reproduce your problem on .NET 7.0, but .NET 8.0 looks fine?: Main (Windows-x64):

    ; Method Program:TestVector4ToVector3v1(System.Span`1[System.Numerics.Vector4],byref)
    G_M29886_IG01:  ;; offset=0000H
           vzeroupper 
    G_M29886_IG02:  ;; offset=0003H
           mov      rax, bword ptr [rcx]
           mov      ecx, dword ptr [rcx+08H]
           vxorps   xmm0, xmm0, xmm0
           xor      r8d, r8d
           test     ecx, ecx
           jle      SHORT G_M29886_IG04
           align    [12 bytes for IG03]
    G_M29886_IG03:  ;; offset=0020H
           mov      r9d, r8d
           shl      r9, 4
           vmovups  xmm1, xmmword ptr [rax+r9]
           vaddps   xmm0, xmm0, xmm1
           inc      r8d
           cmp      r8d, ecx
           jl       SHORT G_M29886_IG03
    G_M29886_IG04:  ;; offset=0039H
           vmovsd   qword ptr [rdx], xmm0
           vextractps dword ptr [rdx+08H], xmm0, 2
    G_M29886_IG05:  ;; offset=0044H
           ret      
    ; Total bytes of code: 69
    
    
    ; Method Program:TestVector4ToVector3v2(System.Span`1[System.Numerics.Vector4],byref)
    G_M22013_IG01:  ;; offset=0000H
           vzeroupper 
    G_M22013_IG02:  ;; offset=0003H
           mov      rax, bword ptr [rcx]
           mov      ecx, dword ptr [rcx+08H]
           vxorps   xmm0, xmm0, xmm0
           xor      r8d, r8d
           test     ecx, ecx
           jle      SHORT G_M22013_IG04
           align    [12 bytes for IG03]
    G_M22013_IG03:  ;; offset=0020H
           mov      r9d, r8d
           shl      r9, 4
           vmovups  xmm1, xmmword ptr [rax+r9]
           vaddps   xmm0, xmm0, xmm1
           inc      r8d
           cmp      r8d, ecx
           jl       SHORT G_M22013_IG03
    G_M22013_IG04:  ;; offset=0039H
           vmovsd   qword ptr [rdx], xmm0
           vextractps dword ptr [rdx+08H], xmm0, 2
    G_M22013_IG05:  ;; offset=0044H
           ret      
    ; Total bytes of code: 69
  4. EgorBo commented on May 14, 2023

    @EgorBo
    Member

    Also, sounds like we should provide a public API to cast to/from Vector3/Vector4?

    Usually, the load is made on safe data boundaries were you know that loading w from memory is ok (even with garbage)

    Btw, we hit a case where a garbage value in the 4th component could affect performance for Vector3 operations recently 馃檪

  5. xoofx commented on May 14, 2023

    @xoofx
    MemberAuthor

    Oh, interesting! But I'm a bit worried about the non-zeroing of .w, is the compiler that smart to see that .w is not used at all? What happens if we start to do a result += v3.Length() for example?

  6. EgorBo commented on May 14, 2023

    @EgorBo
    Member

    But I'm a bit worried about the non-zeroing of .w

    Looks like a bug

  7. EgorBo commented on May 14, 2023

    @EgorBo
    Member

    But I'm a bit worried about the non-zeroing of .w

    Looks like a bug

    Although, maybe it's not?

        private static float TestVector4ToVector3v1(Span<Vector4> span)
        {
            float result = 0.0f;
            for (int i = 0; i < span.Length; i++)
            {
                result += span[i].AsVector128().AsVector3().Length();
            }
            return result;
        }

    emits:

    ; Method Program:TestVector4ToVector3v1(System.Span`1[System.Numerics.Vector4]):float
    G_M47058_IG01:  ;; offset=0000H
           vzeroupper 
    						;; size=3 bbWeight=1 PerfScore 1.00
    
    G_M47058_IG02:  ;; offset=0003H
           mov      rax, bword ptr [rcx]
           mov      edx, dword ptr [rcx+08H]
           vxorps   xmm0, xmm0, xmm0
           xor      ecx, ecx
           test     edx, edx
           jle      SHORT G_M47058_IG04
           align    [0 bytes for IG03]
    						;; size=16 bbWeight=1 PerfScore 5.83
    
    G_M47058_IG03:  ;; offset=0013H
           mov      r8d, ecx
           shl      r8, 4
           vmovups  xmm1, xmmword ptr [rax+r8]
           vdpps    xmm1, xmm1, xmm1, 127
           vsqrtss  xmm1, xmm1, xmm1
           vaddss   xmm0, xmm1, xmm0
           inc      ecx
           cmp      ecx, edx
           jl       SHORT G_M47058_IG03
    						;; size=33 bbWeight=4 PerfScore 129.00
    
    G_M47058_IG04:  ;; offset=0034H
           ret      
    						;; size=1 bbWeight=1 PerfScore 1.00
    ; Total bytes of code: 53

    where vdpps will ignore the 4th component?

  8. xoofx commented on May 14, 2023

    @xoofx
    MemberAuthor

    Hm... it seems ok from the doc:

    Conditionally multiply the packed single-precision (32-bit) floating-point elements in a and b using the high 4 bits in imm8, sum the four products, and conditionally store the sum in dst using the low 4 bits of imm8.

    so 127 is 0b_0111_1111, high bits is 0111 for the multiplication, which is correct. The 1111 for the lower bits suggests that the resulting of the dot of xyz would be stored in the full Vector4 register, but in that case it is not used, the target is a float...

    Maybe could you try with result += Vector3.Normalize(span[i].AsVector128().AsVector3()); instead? (with result Vector3)

  9. EgorBo commented on May 14, 2023

    @EgorBo
    Member
    private static void TestVector4ToVector3v1(Span<Vector4> span, out Vector3 output)
    {
        Vector3 result = Vector3.Zero;
        for (int i = 0; i < span.Length; i++)
        {
            result += Vector3.Normalize(span[i].AsVector128().AsVector3());
        }
        output = result;
    }
    ; Method Program:TestVector4ToVector3v1(System.Span`1[System.Numerics.Vector4],byref)
    G_M29886_IG01:  ;; offset=0000H
           vzeroupper 
    						;; size=3 bbWeight=1 PerfScore 1.00
    
    G_M29886_IG02:  ;; offset=0003H
           mov      rax, bword ptr [rcx]
           mov      ecx, dword ptr [rcx+08H]
           vxorps   xmm0, xmm0, xmm0
           xor      r8d, r8d
           test     ecx, ecx
           jle      SHORT G_M29886_IG04
           align    [0 bytes for IG03]
    						;; size=17 bbWeight=1 PerfScore 5.83
    
    G_M29886_IG03:  ;; offset=0014H
           mov      r9d, r8d
           shl      r9, 4
           vmovups  xmm1, xmmword ptr [rax+r9]
           vdpps    xmm2, xmm1, xmm1, 127
           vsqrtps  xmm2, xmm2
           vdivps   xmm1, xmm1, xmm2
           vaddps   xmm0, xmm0, xmm1
           inc      r8d
           cmp      r8d, ecx
           jl       SHORT G_M29886_IG03
    						;; size=39 bbWeight=4 PerfScore 169.00
    
    G_M29886_IG04:  ;; offset=003BH
           vmovsd   qword ptr [rdx], xmm0
           vextractps dword ptr [rdx+08H], xmm0, 2
    						;; size=11 bbWeight=1 PerfScore 6.00
    
    G_M29886_IG05:  ;; offset=0046H
           ret      
    						;; size=1 bbWeight=1 PerfScore 1.00
    ; Total bytes of code: 71

    Presumably, also fine?

  10. xoofx commented on May 14, 2023

    @xoofx
    MemberAuthor

    Presumably, also fine?

    Hm somehow, yes, 馃檪... xmm1 has a .wthat is not used (but garbage), so still, the lack of zeroing is really concerning. It means that .w can carry any garbage and if the .w is e.g a denormal, it could potentially trash the performance of the calculation... no?

  11. EgorBo commented on May 14, 2023

    @EgorBo
    Member
  12. added this to the Future milestone on May 22, 2023
  13. ghost removed
    untriagedNew issue has not been triaged by the area owner
    on May 22, 2023
  14. JulieLeeMSFT commented on May 22, 2023

    @JulieLeeMSFT
    Member

    Will look into it in the Future release. Need to come up with a benchmark.

  15. tannergooding commented on Feb 5, 2024

    @tannergooding
    Member

    I fixed this quite a while back.

    vector3.AsVector128()

    Register to register generates:

    vinsertps xmm0, xmm0, xmm0, 56

    Memory to register generates the following, noting the two vinsertps should be getting folded together, but they aren't. I'd have to look and see what's blocking that (maybe a simple type mismatch between the TYP_SIMD12 and TYP_SIMD16):

    vmovsd   xmm0, qword ptr [rdx]
    vinsertps xmm0, xmm0, dword ptr [rdx+0x08], 40
    vinsertps xmm0, xmm0, xmm0, 56

    Register to memory generates:

    vinsertps xmm0, xmm0, xmm0, 56
    movaps [rdx], xmm0

    vector128.AsVector3()

    Register to register we just generate the following, noting that if the source and destination register are the same, its a nop:

    movaps xmm0, xmm1

    Memory to register generates:

    vmovsd   xmm0, qword ptr [rdx]
    vinsertps xmm0, xmm0, dword ptr [rdx+0x08], 40

    Register to memory generates:

    vmovsd   qword ptr [rcx], xmm0
    vextractps dword ptr [rcx+0x08], xmm0, 2

    So in all cases, except for a single minor bug, we're doing the most optimal thing. The few instructions, like Dot and Sum are aware of the need to mask off the additional element and do so if required.

  16. locked and limited conversation to collaborators on Mar 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMItenet-performancePerformance related issue

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions