包装起来很昂贵 System.Numerics.VectorX - 为什么?
Expensive to wrap System.Numerics.VectorX - why?
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;
}
它被 JIT 化为:
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
包装在结构中(类似于第一个示例):
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 source 中进行了一些挖掘之后,我发现 System.Numerics [=69] 实际上并没有什么特别之处=].我只需要将 System.Numerics.JitIntrinsic
属性添加到我的方法中。然后 JIT 将用它自己的实现替换我的实现。 JitIntrinsic
是私人的?没问题,只需复制+粘贴即可。最初的问题仍然存在(即使我现在有解决方法)。
问题仅来自于 Vector4 包含 4 个长整型而 DirectX Vector4 包含 4 个浮点数这一事实。在每种情况下,仅传递向量以添加 X 会使代码复杂得多,因为即使未更改,也必须复制 W、Y 和 Z。向量在每个 "new SomeWrapper(v)" 期间和最后一次在函数外复制,以影响变量的结果。
优化结构代码非常棘手。使用 struct 可以节省堆分配时间,但由于有多个副本,代码会变得更长。
有两件事可以帮助您:
1) 不使用包装器,但扩展方法避免复制到包装器中。
2) 不要将新向量分配给 return 值,但尽可能使用其中之一(优化代码但无助于使类型不变,就像其他算术类型一样,因此请格外小心使用) .
样本:
struct Vector
{
public long X;
public long Y;
}
static class VectorExtension
{
public static void AddToMe(this Vector v, long x, long y)
{
v.X += x;
v.Y += y;
}
public static void AddToMe(this Vector v, Vector v2)
{
v.X += v2.X;
v.Y += v2.Y;
}
}
包装 Numerics.Vector 时性能不佳是一个编译器问题,修复已于 2017 年 1 月 20 日提交给 master:
https://github.com/dotnet/coreclr/issues/7508
我不知道传播在这个项目上是如何工作的,但似乎修复将是 2.0.0 release 的一部分。
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;
}
它被 JIT 化为:
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
包装在结构中(类似于第一个示例):
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 source 中进行了一些挖掘之后,我发现 System.Numerics [=69] 实际上并没有什么特别之处=].我只需要将 System.Numerics.JitIntrinsic
属性添加到我的方法中。然后 JIT 将用它自己的实现替换我的实现。 JitIntrinsic
是私人的?没问题,只需复制+粘贴即可。最初的问题仍然存在(即使我现在有解决方法)。
问题仅来自于 Vector4 包含 4 个长整型而 DirectX Vector4 包含 4 个浮点数这一事实。在每种情况下,仅传递向量以添加 X 会使代码复杂得多,因为即使未更改,也必须复制 W、Y 和 Z。向量在每个 "new SomeWrapper(v)" 期间和最后一次在函数外复制,以影响变量的结果。
优化结构代码非常棘手。使用 struct 可以节省堆分配时间,但由于有多个副本,代码会变得更长。
有两件事可以帮助您:
1) 不使用包装器,但扩展方法避免复制到包装器中。
2) 不要将新向量分配给 return 值,但尽可能使用其中之一(优化代码但无助于使类型不变,就像其他算术类型一样,因此请格外小心使用) .
样本:
struct Vector
{
public long X;
public long Y;
}
static class VectorExtension
{
public static void AddToMe(this Vector v, long x, long y)
{
v.X += x;
v.Y += y;
}
public static void AddToMe(this Vector v, Vector v2)
{
v.X += v2.X;
v.Y += v2.Y;
}
}
包装 Numerics.Vector 时性能不佳是一个编译器问题,修复已于 2017 年 1 月 20 日提交给 master:
https://github.com/dotnet/coreclr/issues/7508
我不知道传播在这个项目上是如何工作的,但似乎修复将是 2.0.0 release 的一部分。