TL; DR:为什么包装System.Numerics.Vectors类型很昂贵,有什么我可以做的吗?
考虑以下代码:
[MethodImpl(MethodImplOptions.NoInlining)] private static long GetIt(long a, long b) { var x = AddThem(a, b); return x; } private static long AddThem(long a, long b) { return a + b; }
这将JIT转换为(x64):
00007FFDA3F94500 lea rax,[rcx+rdx] 00007FFDA3F94504 ret
和x86:
00EB2E20 push ebp 00EB2E21 mov ebp,esp 00EB2E23 mov eax,dword ptr [ebp+10h] 00EB2E26 mov edx,dword ptr [ebp+14h] 00EB2E29 add eax,dword ptr [ebp+8] 00EB2E2C adc edx,dword ptr [ebp+0Ch] 00EB2E2F pop ebp 00EB2E30 ret 10h
现在,如果我将它包装在一个结构中,例如
public struct SomeWrapper { public long X; public SomeWrapper(long X) { this.X = X; } public static SomeWrapper operator +(SomeWrapper a, SomeWrapper b) { return new SomeWrapper(a.X + b.X); } }
并改变GetIt
,例如
private static long GetIt(long a, long b) { var x = AddThem(new SomeWrapper(a), new SomeWrapper(b)).X; return x; } private static SomeWrapper AddThem(SomeWrapper a, SomeWrapper b) { return a + b; }
JITted结果仍然与直接使用本机类型时完全相同(AddThem
并且SomeWrapper
重载的运算符和构造函数都是内联的).正如所料.
现在,如果我尝试使用支持SIMD的类型,例如System.Numerics.Vector4
:
[MethodImpl(MethodImplOptions.NoInlining)] private static Vector4 GetIt(Vector4 a, Vector4 b) { var x = AddThem(a, b); return x; }
它被JITted成:
00007FFDA3F94640 vmovupd xmm0,xmmword ptr [rdx] 00007FFDA3F94645 vmovupd xmm1,xmmword ptr [r8] 00007FFDA3F9464A vaddps xmm0,xmm0,xmm1 00007FFDA3F9464F vmovupd xmmword ptr [rcx],xmm0 00007FFDA3F94654 ret
但是,如果我Vector4
在一个struct中包装(类似于第一个例子):
public struct SomeWrapper { public Vector4 X; [MethodImpl(MethodImplOptions.AggressiveInlining)] public SomeWrapper(Vector4 X) { this.X = X; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static SomeWrapper operator+(SomeWrapper a, SomeWrapper b) { return new SomeWrapper(a.X + b.X); } } [MethodImpl(MethodImplOptions.NoInlining)] private static Vector4 GetIt(Vector4 a, Vector4 b) { var x = AddThem(new SomeWrapper(a), new SomeWrapper(b)).X; return x; }
我的代码现在被JIT更多了:
00007FFDA3F84A02 sub rsp,0B8h 00007FFDA3F84A09 mov rsi,rcx 00007FFDA3F84A0C lea rdi,[rsp+10h] 00007FFDA3F84A11 mov ecx,1Ch 00007FFDA3F84A16 xor eax,eax 00007FFDA3F84A18 rep stos dword ptr [rdi] 00007FFDA3F84A1A mov rcx,rsi 00007FFDA3F84A1D vmovupd xmm0,xmmword ptr [rdx] 00007FFDA3F84A22 vmovupd xmmword ptr [rsp+60h],xmm0 00007FFDA3F84A29 vmovupd xmm0,xmmword ptr [rsp+60h] 00007FFDA3F84A30 lea rax,[rsp+90h] 00007FFDA3F84A38 vmovupd xmmword ptr [rax],xmm0 00007FFDA3F84A3D vmovupd xmm0,xmmword ptr [r8] 00007FFDA3F84A42 vmovupd xmmword ptr [rsp+50h],xmm0 00007FFDA3F84A49 vmovupd xmm0,xmmword ptr [rsp+50h] 00007FFDA3F84A50 lea rax,[rsp+80h] 00007FFDA3F84A58 vmovupd xmmword ptr [rax],xmm0 00007FFDA3F84A5D vmovdqu xmm0,xmmword ptr [rsp+90h] 00007FFDA3F84A67 vmovdqu xmmword ptr [rsp+40h],xmm0 00007FFDA3F84A6E vmovdqu xmm0,xmmword ptr [rsp+80h] 00007FFDA3F84A78 vmovdqu xmmword ptr [rsp+30h],xmm0 00007FFDA3F84A7F vmovdqu xmm0,xmmword ptr [rsp+40h] 00007FFDA3F84A86 vmovdqu xmmword ptr [rsp+20h],xmm0 00007FFDA3F84A8D vmovdqu xmm0,xmmword ptr [rsp+30h] 00007FFDA3F84A94 vmovdqu xmmword ptr [rsp+10h],xmm0 00007FFDA3F84A9B vmovups xmm0,xmmword ptr [rsp+20h] 00007FFDA3F84AA2 vmovups xmm1,xmmword ptr [rsp+10h] 00007FFDA3F84AA9 vaddps xmm0,xmm0,xmm1 00007FFDA3F84AAE lea rax,[rsp] 00007FFDA3F84AB2 vmovupd xmmword ptr [rax],xmm0 00007FFDA3F84AB7 vmovdqu xmm0,xmmword ptr [rsp] 00007FFDA3F84ABD vmovdqu xmmword ptr [rsp+70h],xmm0 00007FFDA3F84AC4 vmovups xmm0,xmmword ptr [rsp+70h] 00007FFDA3F84ACB vmovupd xmmword ptr [rsp+0A0h],xmm0 00007FFDA3F84AD5 vmovupd xmm0,xmmword ptr [rsp+0A0h] 00007FFDA3F84ADF vmovupd xmmword ptr [rcx],xmm0 00007FFDA3F84AE4 add rsp,0B8h 00007FFDA3F84AEB pop rsi 00007FFDA3F84AEC pop rdi 00007FFDA3F84AED ret
看起来JIT现在由于某种原因决定它不能只使用寄存器,而是使用临时变量,但我无法理解为什么.首先我认为它可能是一个对齐问题,但后来我无法理解为什么它首先加载到xmm0然后决定往返内存.
这里发生了什么?更重要的是,我可以解决它吗?
我想这样包装结构的原因是我有很多使用API的遗留代码,其实现将受益于某些SIMD优点.
编辑:所以,在coreclr源代码中进行了一些挖掘后,我发现System.Numerics类实际上并没什么特别之处.我只需要将System.Numerics.JitIntrinsic
属性添加到我的方法中.然后JIT将用自己的实现替换我的实现.JitIntrinsic
是私人的?没问题,只需复制+粘贴即可.最初的问题仍然存在(即使我现在有一个解决方法).