Показатель 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, и они будут лучше оптимизировать вызов функции.