Expensive to wrap System.Numerics.VectorX - why?
TL;DR: Why is wrapping the System.Numerics.Vectors type expensive, and is there anything I can do about it?
Рассмотрим следующий фрагмент кода:
[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;
}
This will JIT into (x64):
00007FFDA3F94500 lea rax,[rcx+rdx]
00007FFDA3F94504 ret
and 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
Now, if I wrap this in a struct, eg
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;
}
the JITted result is still exactly the same as when using the native types directly (the AddThem
и SomeWrapper
overloaded operator and constructor are all inlined). Как и ожидалось.
Now, if I try this with the SIMD-enabled types, eg System.Numerics.Vector4
:
[MethodImpl(MethodImplOptions.NoInlining)]
private static Vector4 GetIt(Vector4 a, Vector4 b)
{
var x = AddThem(a, b);
return x;
}
это СОЗДАНО в:
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;
}
мой код теперь JITted в гораздо больше:
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
это личное? Нет проблем, просто скопируйте + вставьте. Оригинальный вопрос все еще остается, хотя (даже если у меня теперь есть обходной путь).
2 ответа
Низкая производительность при переносе Numerics.Vector была проблемой компилятора, и исправление было исправлено 20 января 2017 года:
https://github.com/dotnet/coreclr/issues/7508
Я не знаю, как именно распространение работает в этом проекте, но похоже, что исправление будет частью версии 2.0.0.
Проблема заключается в том, что Vector4 содержит 4 long, а DirectX Vector4 содержит 4 Float. В каждом случае передача векторов только для добавления Xs делает код намного более сложным, потому что W, Y и Z должны быть скопированы, даже если они не изменены. Векторы копируются во время каждого "нового SomeWrapper(v)" и вне функции в последний раз, чтобы повлиять на результат для переменной.
Оптимизация структурного кода очень сложна. С помощью struct вы экономите время выделения кучи, но из-за нескольких копий код становится длиннее.
Вам могут помочь две вещи:
1) Не используйте обертки, но методы расширения избегают копирования в обертку.
2) Не выделяйте новые векторы для возвращаемых значений, но используйте один из них, когда это возможно (оптимизируйте код, но не помогайте сделать тип инвариантным, как и другие арифметические типы, поэтому используйте с крайней осторожностью).
Образец:
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;
}
}