Показатель SSE векторной оболочки по сравнению с голым __m128

Я нашел интересную о подводных лодках SIMD, в которой говорится, что достичь "чистого" __m128 с типами обертки. Ну, я был настроен скептически, поэтому я загрузил файлы проекта и сфабриковал сопоставимый тестовый пример.

Оказалось (для моего удивления), что версия обертки значительно медленнее. Поскольку я не хочу говорить о простом воздухе, тестовые примеры следующие:

В первом случае Vec4 является простым псевдонимом типа __m128 с некоторыми операторами:

#include <xmmintrin.h>
#include <emmintrin.h>

using Vec4 = __m128;

inline __m128 VLoad(float f)
{
    return _mm_set_ps(f, f, f, f);
};

inline Vec4& operator+=(Vec4 &va, Vec4 vb)
{
    return (va = _mm_add_ps(va, vb));
};

inline Vec4& operator*=(Vec4 &va, Vec4 vb)
{
    return (va = _mm_mul_ps(va, vb));
};

inline Vec4 operator+(Vec4 va, Vec4 vb)
{
    return _mm_add_ps(va, vb);
};

inline Vec4 operator-(Vec4 va, Vec4 vb)
{
    return _mm_sub_ps(va, vb);
};

inline Vec4 operator*(Vec4 va, Vec4 vb)
{
    return _mm_mul_ps(va, vb);
};

Во втором случае Vec4 - это облегченная обертка вокруг __m128. Это не полная обертка, просто короткий эскиз, который охватывает проблему. Операторы обертывают точно такие же внутренности, единственное различие (поскольку выравнивание по 16 байт не может быть применено к аргументам), они берут Vec4 как const ссылку:

#include <xmmintrin.h>
#include <emmintrin.h>

struct Vec4
{
    __m128 simd;

    inline Vec4() = default;
    inline Vec4(const Vec4&) = default;
    inline Vec4& operator=(const Vec4&) = default;

    inline Vec4(__m128 s)
        : simd(s)
    {}

    inline operator __m128() const
    {
        return simd;
    }

    inline operator __m128&()
    {
        return simd;
    }
};

inline __m128 VLoad(float f)
{
    return _mm_set_ps(f, f, f, f);
};

inline Vec4 VAdd(const Vec4 &va, const Vec4 &vb)
{
    return _mm_add_ps(va, vb);
    // return _mm_add_ps(va.simd, vb.simd); // doesn't make difference
};

inline Vec4 VSub(const Vec4 &va, const Vec4 &vb)
{
    return _mm_sub_ps(va, vb);
    // return _mm_sub_ps(va.simd, vb.simd); // doesn't make difference
};

inline Vec4 VMul(const Vec4 &va, const Vec4 &vb)
{
    return _mm_mul_ps(va, vb);
    // return _mm_mul_ps(va.simd, vb.simd); // doesn't make difference
};

И вот тестовое ядро ​​, которое производит разную производительность с разными версиями Vec4:

#include <xmmintrin.h>
#include <emmintrin.h>

struct EQSTATE
{
    // Filter #1 (Low band)

    Vec4  lf;       // Frequency
    Vec4  f1p0;     // Poles ...
    Vec4  f1p1;     
    Vec4  f1p2;
    Vec4  f1p3;

    // Filter #2 (High band)

    Vec4  hf;       // Frequency
    Vec4  f2p0;     // Poles ...
    Vec4  f2p1;
    Vec4  f2p2;
    Vec4  f2p3;

    // Sample history buffer

    Vec4  sdm1;     // Sample data minus 1
    Vec4  sdm2;     //                   2
    Vec4  sdm3;     //                   3

    // Gain Controls

    Vec4  lg;       // low  gain
    Vec4  mg;       // mid  gain
    Vec4  hg;       // high gain

};  

static float vsaf = (1.0f / 4294967295.0f);   // Very small amount (Denormal Fix)
static Vec4 vsa = VLoad(vsaf);

Vec4 TestEQ(EQSTATE* es, Vec4& sample)
{
    // Locals

    Vec4  l,m,h;      // Low / Mid / High - Sample Values

    // Filter #1 (lowpass)

    es->f1p0  += (es->lf * (sample   - es->f1p0)) + vsa;
    //es->f1p0 = VAdd(es->f1p0, VAdd(VMul(es->lf, VSub(sample, es->f1p0)), vsa));

    es->f1p1  += (es->lf * (es->f1p0 - es->f1p1));
    //es->f1p1 = VAdd(es->f1p1, VMul(es->lf, VSub(es->f1p0, es->f1p1)));

    es->f1p2  += (es->lf * (es->f1p1 - es->f1p2));
    //es->f1p2 = VAdd(es->f1p2, VMul(es->lf, VSub(es->f1p1, es->f1p2)));

    es->f1p3  += (es->lf * (es->f1p2 - es->f1p3));
    //es->f1p3 = VAdd(es->f1p3, VMul(es->lf, VSub(es->f1p2, es->f1p3)));

    l          = es->f1p3;

    // Filter #2 (highpass)

    es->f2p0  += (es->hf * (sample   - es->f2p0)) + vsa;
    //es->f2p0 = VAdd(es->f2p0, VAdd(VMul(es->hf, VSub(sample, es->f2p0)), vsa));

    es->f2p1  += (es->hf * (es->f2p0 - es->f2p1));
    //es->f2p1 = VAdd(es->f2p1, VMul(es->hf, VSub(es->f2p0, es->f2p1)));

    es->f2p2  += (es->hf * (es->f2p1 - es->f2p2));
    //es->f2p2 = VAdd(es->f2p2, VMul(es->hf, VSub(es->f2p1, es->f2p2)));

    es->f2p3  += (es->hf * (es->f2p2 - es->f2p3));
    //es->f2p3 = VAdd(es->f2p3, VMul(es->hf, VSub(es->f2p2, es->f2p3)));

    h          = es->sdm3 - es->f2p3;
    //h = VSub(es->sdm3, es->f2p3);

    // Calculate midrange (signal - (low + high))

    m          = es->sdm3 - (h + l);
    //m = VSub(es->sdm3, VAdd(h, l));

    // Scale, Combine and store

    l         *= es->lg;
    m         *= es->mg;
    h         *= es->hg;

    //l = VMul(l, es->lg);
    //m = VMul(m, es->mg);
    //h = VMul(h, es->hg);

    // Shuffle history buffer 

    es->sdm3   = es->sdm2;
    es->sdm2   = es->sdm1;
    es->sdm1   = sample;                

    // Return result

    return(l + m + h);
    //return(VAdd(l, VAdd(m, h)));
}

//make these as globals to enforce the function call;
static Vec4 sample[1024], result[1024];
static EQSTATE es;

#include <chrono>
#include <iostream>

int main()
{
    auto t0 = std::chrono::high_resolution_clock::now();

    for (int ii=0; ii<1024; ii++)
    {
        result[ii] = TestEQ(&es, sample[ii]);
    }

    auto t1 = std::chrono::high_resolution_clock::now();
    auto t = std::chrono::duration_cast<std::chrono::nanoseconds>(t1 - t0).count();
    std::cout << "timing: " << t << '\n';

    std::cin.get();

    return 0;
}

Ссылка на рабочий код


MSVC 2015 сгенерирована сборка для 1-й версии:

;   COMDAT [email protected]@[email protected]@[email protected]@[email protected]@Z
_TEXT   SEGMENT
[email protected]@[email protected]@[email protected]@[email protected]@Z PROC      ; TestEQ, COMDAT
; _es$dead$ = ecx
; _sample$ = edx
    vmovaps xmm0, XMMWORD PTR [edx]
    vsubps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+16
    vmovaps xmm2, XMMWORD PTR [email protected]@[email protected]@A
    vmulps  xmm0, xmm0, xmm2
    vaddps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A
    vaddps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+16
    vmovaps XMMWORD PTR [email protected]@[email protected]@A+16, xmm0
    vsubps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+32
    vmulps  xmm0, xmm0, xmm2
    vaddps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+32
    vmovaps XMMWORD PTR [email protected]@[email protected]@A+32, xmm0
    vsubps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+48
    vmulps  xmm0, xmm0, xmm2
    vaddps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+48
    vmovaps XMMWORD PTR [email protected]@[email protected]@A+48, xmm0
    vsubps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+64
    vmulps  xmm0, xmm0, xmm2
    vaddps  xmm4, xmm0, XMMWORD PTR [email protected]@[email protected]@A+64
    vmovaps xmm2, XMMWORD PTR [email protected]@[email protected]@A+80
    vmovaps xmm1, XMMWORD PTR [email protected]@[email protected]@A+192
    vmovaps XMMWORD PTR [email protected]@[email protected]@A+64, xmm4
    vmovaps xmm0, XMMWORD PTR [edx]
    vsubps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+96
    vmulps  xmm0, xmm0, xmm2
    vaddps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A
    vaddps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+96
    vmovaps XMMWORD PTR [email protected]@[email protected]@A+96, xmm0
    vsubps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+112
    vmulps  xmm0, xmm0, xmm2
    vaddps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+112
    vmovaps XMMWORD PTR [email protected]@[email protected]@A+112, xmm0
    vsubps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+128
    vmulps  xmm0, xmm0, xmm2
    vaddps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+128
    vmovaps XMMWORD PTR [email protected]@[email protected]@A+128, xmm0
    vsubps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+144
    vmulps  xmm0, xmm0, xmm2
    vaddps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+144
    vsubps  xmm2, xmm1, xmm0
    vmovaps XMMWORD PTR [email protected]@[email protected]@A+144, xmm0
    vmovaps xmm0, XMMWORD PTR [email protected]@[email protected]@A+176
    vmovaps XMMWORD PTR [email protected]@[email protected]@A+192, xmm0
    vmovaps xmm0, XMMWORD PTR [email protected]@[email protected]@A+160
    vmovaps XMMWORD PTR [email protected]@[email protected]@A+176, xmm0
    vmovaps xmm0, XMMWORD PTR [edx]
    vmovaps XMMWORD PTR [email protected]@[email protected]@A+160, xmm0
    vaddps  xmm0, xmm4, xmm2
    vsubps  xmm0, xmm1, xmm0
    vmulps  xmm1, xmm0, XMMWORD PTR [email protected]@[email protected]@A+224
    vmulps  xmm0, xmm2, XMMWORD PTR [email protected]@[email protected]@A+240
    vaddps  xmm1, xmm1, xmm0
    vmulps  xmm0, xmm4, XMMWORD PTR [email protected]@[email protected]@A+208
    vaddps  xmm0, xmm1, xmm0
    ret 0
[email protected]@[email protected]@[email protected]@[email protected]@Z ENDP      ; TestEQ

Сгенерированная сборка MSVC 2015 для 2-й версии:

[email protected]@[email protected]@@[email protected]@[email protected]@Z PROC ; TestEQ, COMDAT
; ___$ReturnUdt$ = ecx
; _es$dead$ = edx
    push    ebx
    mov ebx, esp
    sub esp, 8
    and esp, -8                 ; fffffff8H
    add esp, 4
    push    ebp
    mov ebp, DWORD PTR [ebx+4]
    mov eax, DWORD PTR _sample$[ebx]
    vmovaps xmm2, XMMWORD PTR [email protected]@[email protected]@A
    vmovaps xmm1, XMMWORD PTR [email protected]@[email protected]@A+192
    mov DWORD PTR [esp+4], ebp
    vmovaps xmm0, XMMWORD PTR [eax]
    vsubps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+16
    vmulps  xmm0, xmm0, xmm2
    vaddps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@@A
    vaddps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+16
    vmovaps XMMWORD PTR [email protected]@[email protected]@A+16, xmm0
    vsubps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+32
    vmulps  xmm0, xmm0, xmm2
    vaddps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+32
    vmovaps XMMWORD PTR [email protected]@[email protected]@A+32, xmm0
    vsubps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+48
    vmulps  xmm0, xmm0, xmm2
    vaddps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+48
    vmovaps XMMWORD PTR [email protected]@[email protected]@A+48, xmm0
    vsubps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+64
    vmulps  xmm0, xmm0, xmm2
    vaddps  xmm4, xmm0, XMMWORD PTR [email protected]@[email protected]@A+64
    vmovaps xmm2, XMMWORD PTR [email protected]@[email protected]@A+80
    vmovaps XMMWORD PTR [email protected]@[email protected]@A+64, xmm4
    vmovaps xmm0, XMMWORD PTR [eax]
    vsubps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+96
    vmulps  xmm0, xmm0, xmm2
    vaddps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@@A
    vaddps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+96
    vmovaps XMMWORD PTR [email protected]@[email protected]@A+96, xmm0
    vsubps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+112
    vmulps  xmm0, xmm0, xmm2
    vaddps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+112
    vmovaps XMMWORD PTR [email protected]@[email protected]@A+112, xmm0
    vsubps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+128
    vmulps  xmm0, xmm0, xmm2
    vaddps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+128
    vmovaps XMMWORD PTR [email protected]@[email protected]@A+128, xmm0
    vsubps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+144
    vmulps  xmm0, xmm0, xmm2
    vaddps  xmm0, xmm0, XMMWORD PTR [email protected]@[email protected]@A+144
    vsubps  xmm2, xmm1, xmm0
    vmovaps XMMWORD PTR [email protected]@[email protected]@A+144, xmm0
    vaddps  xmm0, xmm2, xmm4
    vsubps  xmm0, xmm1, xmm0
    vmulps  xmm1, xmm0, XMMWORD PTR [email protected]@[email protected]@A+224
    vmovdqu xmm0, XMMWORD PTR [email protected]@[email protected]@A+176
    vmovdqu XMMWORD PTR [email protected]@[email protected]@A+192, xmm0
    vmovdqu xmm0, XMMWORD PTR [email protected]@[email protected]@A+160
    vmovdqu XMMWORD PTR [email protected]@[email protected]@A+176, xmm0
    vmovdqu xmm0, XMMWORD PTR [eax]
    vmovdqu XMMWORD PTR [email protected]@[email protected]@A+160, xmm0
    vmulps  xmm0, xmm4, XMMWORD PTR [email protected]@[email protected]@A+208
    vaddps  xmm1, xmm0, xmm1
    vmulps  xmm0, xmm2, XMMWORD PTR [email protected]@[email protected]@A+240
    vaddps  xmm0, xmm1, xmm0
    vmovaps XMMWORD PTR [ecx], xmm0
    mov eax, ecx
    pop ebp
    mov esp, ebx
    pop ebx
    ret 0
[email protected]@[email protected]@@[email protected]@[email protected]@Z ENDP ; TestEQ

Произведенная сборка 2-й версии значительно длиннее и медленнее. Это не связано с Visual Studio, поскольку Clang 3.8 дает похожие результаты.


Clang 3.8 сгенерированная сборка для 1-й версии:

"[email protected]@[email protected]@[email protected]@AA[email protected]@Z": # @"\[email protected]@[email protected]@[email protected]@[email protected]@Z"
Lfunc_begin0:
Ltmp0:
# BB#0:                                 # %entry
    movl    8(%esp), %eax
    movl    4(%esp), %ecx
    vmovaps _vsa, %xmm0
    vmovaps (%ecx), %xmm1
    vmovaps 16(%ecx), %xmm2
    vmovaps (%eax), %xmm3
    vsubps  %xmm2, %xmm3, %xmm3
    vmulps  %xmm3, %xmm1, %xmm3
    vaddps  %xmm3, %xmm0, %xmm3
    vaddps  %xmm3, %xmm2, %xmm2
    vmovaps %xmm2, 16(%ecx)
    vmovaps 32(%ecx), %xmm3
    vsubps  %xmm3, %xmm2, %xmm2
    vmulps  %xmm2, %xmm1, %xmm2
    vaddps  %xmm2, %xmm3, %xmm2
    vmovaps %xmm2, 32(%ecx)
    vmovaps 48(%ecx), %xmm3
    vsubps  %xmm3, %xmm2, %xmm2
    vmulps  %xmm2, %xmm1, %xmm2
    vaddps  %xmm2, %xmm3, %xmm2
    vmovaps %xmm2, 48(%ecx)
    vmovaps 64(%ecx), %xmm3
    vsubps  %xmm3, %xmm2, %xmm2
    vmulps  %xmm2, %xmm1, %xmm1
    vaddps  %xmm1, %xmm3, %xmm1
    vmovaps %xmm1, 64(%ecx)
    vmovaps 80(%ecx), %xmm2
    vmovaps 96(%ecx), %xmm3
    vmovaps (%eax), %xmm4
    vsubps  %xmm3, %xmm4, %xmm4
    vmulps  %xmm4, %xmm2, %xmm4
    vaddps  %xmm4, %xmm0, %xmm0
    vaddps  %xmm0, %xmm3, %xmm0
    vmovaps %xmm0, 96(%ecx)
    vmovaps 112(%ecx), %xmm3
    vsubps  %xmm3, %xmm0, %xmm0
    vmulps  %xmm0, %xmm2, %xmm0
    vaddps  %xmm0, %xmm3, %xmm0
    vmovaps %xmm0, 112(%ecx)
    vmovaps 128(%ecx), %xmm3
    vsubps  %xmm3, %xmm0, %xmm0
    vmulps  %xmm0, %xmm2, %xmm0
    vaddps  %xmm0, %xmm3, %xmm0
    vmovaps %xmm0, 128(%ecx)
    vmovaps 144(%ecx), %xmm3
    vsubps  %xmm3, %xmm0, %xmm0
    vmulps  %xmm0, %xmm2, %xmm0
    vaddps  %xmm0, %xmm3, %xmm0
    vmovaps %xmm0, 144(%ecx)
    vmovaps 192(%ecx), %xmm2
    vsubps  %xmm0, %xmm2, %xmm0
    vaddps  %xmm0, %xmm1, %xmm3
    vsubps  %xmm3, %xmm2, %xmm2
    vmulps  208(%ecx), %xmm1, %xmm1
    vmulps  224(%ecx), %xmm2, %xmm2
    vmulps  240(%ecx), %xmm0, %xmm0
    vmovaps 176(%ecx), %xmm3
    vmovaps %xmm3, 192(%ecx)
    vmovaps 160(%ecx), %xmm3
    vmovaps %xmm3, 176(%ecx)
    vmovaps (%eax), %xmm3
    vmovaps %xmm3, 160(%ecx)
    vaddps  %xmm2, %xmm0, %xmm0
    vaddps  %xmm0, %xmm1, %xmm0
    retl
Lfunc_end0:

Clang 3.8 сгенерированная сборка для 2-й версии:

"[email protected]@[email protected]@[email protected]@[email protected]@Z": # @"\[email protected]@[email protected]@[email protected]@[email protected]@Z"
Lfunc_begin0:
Ltmp0:
# BB#0:                                 # %entry
    movl    12(%esp), %ecx
    movl    8(%esp), %edx
    vmovaps (%edx), %xmm0
    vmovaps 16(%edx), %xmm1
    vmovaps (%ecx), %xmm2
    vsubps  %xmm1, %xmm2, %xmm2
    vmulps  %xmm0, %xmm2, %xmm2
    vaddps  _vsa, %xmm2, %xmm2
    vaddps  %xmm2, %xmm1, %xmm1
    vmovaps %xmm1, 16(%edx)
    vmovaps 32(%edx), %xmm2
    vsubps  %xmm2, %xmm1, %xmm1
    vmulps  %xmm0, %xmm1, %xmm1
    vaddps  %xmm1, %xmm2, %xmm1
    vmovaps %xmm1, 32(%edx)
    vmovaps 48(%edx), %xmm2
    vsubps  %xmm2, %xmm1, %xmm1
    vmulps  %xmm0, %xmm1, %xmm1
    vaddps  %xmm1, %xmm2, %xmm1
    vmovaps %xmm1, 48(%edx)
    vmovaps 64(%edx), %xmm2
    vsubps  %xmm2, %xmm1, %xmm1
    vmulps  %xmm0, %xmm1, %xmm0
    vaddps  %xmm0, %xmm2, %xmm0
    vmovaps %xmm0, 64(%edx)
    vmovaps 80(%edx), %xmm1
    vmovaps 96(%edx), %xmm2
    vmovaps (%ecx), %xmm3
    vsubps  %xmm2, %xmm3, %xmm3
    vmulps  %xmm1, %xmm3, %xmm3
    vaddps  _vsa, %xmm3, %xmm3
    vaddps  %xmm3, %xmm2, %xmm2
    vmovaps %xmm2, 96(%edx)
    vmovaps 112(%edx), %xmm3
    vsubps  %xmm3, %xmm2, %xmm2
    vmulps  %xmm1, %xmm2, %xmm2
    vaddps  %xmm2, %xmm3, %xmm2
    vmovaps %xmm2, 112(%edx)
    vmovaps 128(%edx), %xmm3
    vsubps  %xmm3, %xmm2, %xmm2
    vmulps  %xmm1, %xmm2, %xmm2
    vaddps  %xmm2, %xmm3, %xmm2
    vmovaps %xmm2, 128(%edx)
    vmovaps 144(%edx), %xmm3
    vsubps  %xmm3, %xmm2, %xmm2
    vmulps  %xmm1, %xmm2, %xmm1
    vaddps  %xmm1, %xmm3, %xmm1
    vmovaps %xmm1, 144(%edx)
    vmovaps 192(%edx), %xmm2
    vsubps  %xmm1, %xmm2, %xmm1
    vaddps  %xmm1, %xmm0, %xmm3
    vsubps  %xmm3, %xmm2, %xmm2
    vmulps  208(%edx), %xmm0, %xmm0
    vmulps  224(%edx), %xmm2, %xmm2
    movl    4(%esp), %eax
    vmulps  240(%edx), %xmm1, %xmm1
    vmovaps 176(%edx), %xmm3
    vmovaps %xmm3, 192(%edx)
    vmovaps 160(%edx), %xmm3
    vmovaps %xmm3, 176(%edx)
    vmovaps (%ecx), %xmm3
    vmovaps %xmm3, 160(%edx)
    vaddps  %xmm2, %xmm0, %xmm0
    vaddps  %xmm0, %xmm1, %xmm0
    vmovaps %xmm0, (%eax)
    retl
Lfunc_end0:

Хотя количество инструкций одинаково, 1-я версия все же примерно на 50% быстрее.


Я попытался определить причину проблемы без успеха. Есть подозрительные вещи, такие как уродливые инструкции vmovdqu во второй сборке MSVC. Конструкция, оператор назначения копирования и перекрестная ссылка также могут излишне перемещать данные из регистров SSE обратно в память, однако все мои попытки решить или точно определить проблему были неудачными.

Я действительно не думаю, что такая простая оболочка не может достичь той же производительности, что и голый __m128, что может привести к потере служебных данных.

Итак, что там происходит?

Ответы

Ответ 1

Как выяснилось, проблема не в определении пользователя struct Vec4. Он глубоко связан с соглашениями о вызовах x86.

Стандартное соглашение об использовании x86 по умолчанию в Visual С++ - это __cdecl, которое

Вставляет параметры в стек, в обратном порядке (справа налево)

Теперь это проблема, так как Vec4 следует хранить и передавать в регистре XMM. Но посмотрим, что на самом деле происходит.


1-й случай

В первом случае Vec4 является псевдонимом простого типа __m128.

using Vec4 = __m128;
/* ... */
Vec4 TestEQ(EQSTATE* es, Vec4 &sample) { ... }

Сгенерированный заголовок функции TestEQ в сборке -

[email protected]@[email protected]@[email protected]@[email protected]@Z PROC      ; TestEQ, COMDAT
; _es$ = ecx
; _sample$ = edx
...

Ницца.


Второй случай

Во втором случае Vec4 не является псевдонимом __m128, теперь он является определяемым пользователем.

Здесь я исследую компиляцию для платформы x86 и x64.

x86 (32-разрядная компиляция)

Так как __cdecl (который является стандартным вызовом для вызова в x86), он не позволяет передавать выровненные значения в функции (которые испускают Error C2719: 'sample': formal parameter with requested alignment of 16 won't be aligned), мы передаем его ссылкой const.

struct Vec4{ __m128 simd; /* ... */ };
/* ... */
Vec4 TestEQ(EQSTATE* es, const Vec4 &sample) { ... }

который генерирует заголовок функции для TestEQ как

[email protected]@[email protected]@[email protected]@[email protected]@Z PROC        ; TestEQ, COMDAT
; ___$ReturnUdt$ = ecx
; _es$ = edx
    push    ebx
    mov ebx, esp
    sub esp, 8
    and esp, -8                 ; fffffff8H
    add esp, 4
    push    ebp
    mov ebp, DWORD PTR [ebx+4]
    mov eax, DWORD PTR _sample$[ebx]
    ...

Это не так просто, как в первом случае. Аргументы переносятся в стек. Есть несколько дополнительных инструкций mov между первыми инструкциями SSE, которые здесь не перечислены. Этих инструкций в целом достаточно, чтобы немного поразить производительность.

x64 (64-разрядная компиляция)

Windows в x64 использует другое соглашение о вызове как часть бинарного интерфейса приложения (64).

Это соглашение пытается сохранить данные в регистрах, если это возможно, таким образом, что данные с плавающей запятой сохраняются в регистрах XMM.

От MSDN Обзор x64 соглашений о вызовах:

Бинарный интерфейс приложения x64 (ABI) - это 4-х быстрый быстрый вызов вызывающего соглашения, с поддержкой стека для этих регистров. Eсть строгое взаимно однозначное соответствие между аргументами в функции и регистры для этих аргументов. Любой аргумент, который не подходит для 8 байты, или не 1, 2, 4 или 8 байтов, должны передаваться по ссылке. (...) Все операции с плавающей запятой выполняются с использованием 16 регистров XMM. Аргументы передаются в регистры RCX, RDX, R8 и R9. Если аргументы float/double, они передаются в XMM0L, XMM1L, XMM2L и XMM3L. 16 аргументы байта передаются по ссылке.

Из Страница Википедии для соглашений о вызовах x86-64

В Windows используется соглашение о вызове Microsoft x64 и pre-boot UEFI (для длинного режима на x86-64). Он использует регистры RCX, RDX, R8, R9 для первых четырех целых или указательных аргументов (в этом порядок), а XMM0, XMM1, XMM2, XMM3 используются для с плавающей запятой аргументы. Дополнительные аргументы помещаются в стек (справа на оставил). Возвращаемые значения Integer (похожие на x86) возвращаются в RAX, если 64 бит или меньше. Возвращаемые значения с плавающей точкой возвращаются в XMM0.

Итак, второй случай в режиме x64 генерирует заголовок функции для TestEQ как

[email protected]@[email protected]@[email protected]@[email protected]@Z PROC        ; TestEQ, COMDAT
; _es$ = ecx
; _sample$ = edx
...

Это точно так же, как в первом случае!


Решение

В режиме x86 представленное поведение должно быть четко зафиксировано.

Наиболее простым решением является функция inline. Хотя это всего лишь подсказка, и компилятор может полностью игнорировать, вы можете сказать компилятору всегда встроить функцию. Однако иногда это нежелательно из-за размера функции или по любой другой причине.

К счастью, Microsoft представила соглашение __vectorcall в Visual Studio 2013 и выше (доступно в режиме x86 и x64). Это очень похоже на стандартное соглашение об использовании Windows x64 по умолчанию, но с более доступными регистрами.

Перепишите второй случай с __vectorcall:

Vec4 __vectorcall TestEQ(EQSTATE* es, const Vec4 &sample) { ... }

Теперь сгенерированный заголовок функции сборки для TestEQ равен

[email protected]@[email protected]@[email protected]@[email protected]@Z PROC        ; TestEQ, COMDAT
; _es$ = ecx
; _sample$ = edx
...

который, наконец, тот же, что и первый случай, и второй случай в x64.

Как указал Питер Кордес, чтобы в полной мере воспользоваться __vectorcall, аргумент Vec4 должен быть передан по значению вместо постоянной ссылки. Для этого переданный тип должен удовлетворять некоторым требованиям, например, он должен быть тривиально копируемым конструктивным (без пользовательских конструкторов копирования) и не должен содержать никакого объединения. Подробнее в комментариях ниже и здесь.

Заключительные слова

Похоже, что MSVC под капотом автоматически применяет соглашение __vectorcall как оптимизацию, когда обнаруживает аргумент __m128. В противном случае используется условное соглашение по умолчанию __cdecl (вы можете изменить это поведение по параметрам компилятора).

Люди сказали мне в комментариях, что они не видели большой разницы между GCC и Clang, сгенерированной сборкой из двух случаев. Это связано с тем, что эти компиляторы с флагом оптимизации -O2 просто встраивают функцию TestEQ в тело тестового цикла (см.). Также возможно, что они будут более умными, чем MSVC, и они будут лучше оптимизировать вызов функции.