Repository navigation
Cannot convert efficiently a System.Numerics.Vector4 to a Vector3聽#86220
Description
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
EgorBo commentedon May 14, 2023
I can reproduce your problem on .NET 7.0, but .NET 8.0 looks fine?: Main (Windows-x64):
EgorBo commentedon May 14, 2023
Also, sounds like we should provide a public API to cast to/from Vector3/Vector4?
Btw, we hit a case where a garbage value in the 4th component could affect performance for Vector3 operations recently 馃檪
xoofx commentedon May 14, 2023
Oh, interesting! But I'm a bit worried about the non-zeroing of
.w, is the compiler that smart to see that.wis not used at all? What happens if we start to do aresult += v3.Length()for example?EgorBo commentedon May 14, 2023
Looks like a bug
EgorBo commentedon May 14, 2023
Although, maybe it's not?
emits:
where
vdppswill ignore the 4th component?xoofx commentedon May 14, 2023
Hm... it seems ok from the doc:
so 127 is
0b_0111_1111, high bits is0111for the multiplication, which is correct. The1111for 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)EgorBo commentedon May 14, 2023
Presumably, also fine?
xoofx commentedon May 14, 2023
Hm somehow, yes, 馃檪... xmm1 has a
.wthat is not used (but garbage), so still, the lack of zeroing is really concerning. It means that.wcan carry any garbage and if the.wis e.g a denormal, it could potentially trash the performance of the calculation... no?EgorBo commentedon May 14, 2023
cc @tannergooding
JulieLeeMSFT commentedon May 22, 2023
Will look into it in the Future release. Need to come up with a benchmark.
tannergooding commentedon Feb 5, 2024
I fixed this quite a while back.
vector3.AsVector128()Register to register generates:
Memory to register generates the following, noting the two
vinsertpsshould 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 theTYP_SIMD12andTYP_SIMD16):Register to memory generates:
vector128.AsVector3()Register to register we just generate the following, noting that if the source and destination register are the same, its a nop:
Memory to register generates:
Register to memory generates:
So in all cases, except for a single minor bug, we're doing the most optimal thing. The few instructions, like
DotandSumare aware of the need to mask off the additional element and do so if required.