Подтвердить что ты не робот

Дорогой обернуть System.Numerics.VectorX - почему?

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;
}

он помещается в:

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.

EDIT: Итак, после некоторого поиска в источнике coreclr, я узнал, что на самом деле ничего особенного в Системе нет. Нумерические классы. Мне просто нужно добавить атрибут System.Numerics.JitIntrinsic к моим методам. Затем JIT заменит мою реализацию своей собственной. JitIntrinsic является закрытым? Нет проблем, просто скопируйте + вставьте его. Исходный вопрос по-прежнему остается (даже если у меня есть обходное решение).

4b9b3361

Ответ 1

Плохая производительность при упаковке Numerics.Vector была проблемой компилятора, и исправление было зафиксировано 20 января 2017 года:

https://github.com/dotnet/coreclr/issues/7508

Я не знаю, как распространение работает именно в этом проекте, но похоже, что исправление будет частью 2.0.0 release.

Ответ 2

Проблема возникает только из-за того, что Vector4 содержит 4 longs, а DirectX Vector4 содержит 4 Floats. В каждом случае прохождение векторов только для добавления 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;
    }
}