Оптимизация C-кода с помощью SSE-intrinsics
Я некоторое время боролся с производительностью сетевого кодирования в приложении, которое я разрабатываю (см. Optimzing ГСП-код, повышение производительности сетевого кодирования-кодирование и распределение OpenCL). Теперь я достаточно близок к достижению приемлемой производительности. Это текущее состояние самого внутреннего цикла (где тратится >99% времени выполнения):
while(elementIterations-- >0)
{
unsigned int firstMessageField = *(currentMessageGaloisFieldsArray++);
unsigned int secondMessageField = *(currentMessageGaloisFieldsArray++);
__m128i valuesToMultiply = _mm_set_epi32(0, secondMessageField, 0, firstMessageField);
__m128i mulitpliedHalves = _mm_mul_epu32(valuesToMultiply, fragmentCoefficentVector);
}
у вас есть какие-либо предложения о том, как дальше оптимизировать это? Я понимаю, что это трудно сделать без контекста, но любая помощь приветствуется!
2 ответов
теперь, когда я проснулся, вот мой ответ:
в вашем исходном коде узкое место почти наверняка _mm_set_epi32
. Этот единственный встроенный компилируется в этот беспорядок в вашей сборке:
633415EC xor edi,edi
633415EE movd xmm3,edi
...
633415F6 xor ebx,ebx
633415F8 movd xmm4,edi
633415FC movd xmm5,ebx
63341600 movd xmm0,esi
...
6334160B punpckldq xmm5,xmm3
6334160F punpckldq xmm0,xmm4
...
63341618 punpckldq xmm0,xmm5
что это? 9 Инструкции?!?!?! чисто накладные расходы...
еще одно место, которое кажется странным, заключается в том, что компилятор не объединил добавления и загрузки:
movdqa xmm3,xmmword ptr [ecx-10h]
paddq xmm0,xmm3
должны были быть объединены в:
paddq xmm0,xmmword ptr [ecx-10h]
Я не уверен, что у компилятора умер мозг, или у него действительно была законная причина для этого... В любом случае, это мелочь по сравнению с _mm_set_epi32
.
отказ от ответственности: код, который я представлю здесь, нарушает строгое сглаживание. Но для достижения максимальной производительности часто требуются нестандартные совместимые методы.
Решение 1: Нет Векторизации
это решение предполагает allZero
на самом деле все нули.
петля На самом деле проще, чем кажется. Поскольку арифметики не так много, может быть, лучше просто не векторизовать:
// Test Data
unsigned __int32 fragmentCoefficentVector = 1000000000;
__declspec(align(16)) int currentMessageGaloisFieldsArray_[8] = {10,11,12,13,14,15,16,17};
int *currentMessageGaloisFieldsArray = currentMessageGaloisFieldsArray_;
__m128i currentUnModdedGaloisFieldFragments_[8];
__m128i *currentUnModdedGaloisFieldFragments = currentUnModdedGaloisFieldFragments_;
memset(currentUnModdedGaloisFieldFragments,0,8 * sizeof(__m128i));
int elementIterations = 4;
// The Loop
while (elementIterations > 0){
elementIterations -= 1;
// Default 32 x 32 -> 64-bit multiply code
unsigned __int64 r0 = currentMessageGaloisFieldsArray[0] * (unsigned __int64)fragmentCoefficentVector;
unsigned __int64 r1 = currentMessageGaloisFieldsArray[1] * (unsigned __int64)fragmentCoefficentVector;
// Use this for Visual Studio. VS doesn't know how to optimize 32 x 32 -> 64-bit multiply
// unsigned __int64 r0 = __emulu(currentMessageGaloisFieldsArray[0], fragmentCoefficentVector);
// unsigned __int64 r1 = __emulu(currentMessageGaloisFieldsArray[1], fragmentCoefficentVector);
((__int64*)currentUnModdedGaloisFieldFragments)[0] += r0 & 0x00000000ffffffff;
((__int64*)currentUnModdedGaloisFieldFragments)[1] += r0 >> 32;
((__int64*)currentUnModdedGaloisFieldFragments)[2] += r1 & 0x00000000ffffffff;
((__int64*)currentUnModdedGaloisFieldFragments)[3] += r1 >> 32;
currentMessageGaloisFieldsArray += 2;
currentUnModdedGaloisFieldFragments += 2;
}
который компилируется к этому на x64:
$LL4@main:
mov ecx, DWORD PTR [rbx]
mov rax, r11
add r9, 32 ; 00000020H
add rbx, 8
mul rcx
mov ecx, DWORD PTR [rbx-4]
mov r8, rax
mov rax, r11
mul rcx
mov ecx, r8d
shr r8, 32 ; 00000020H
add QWORD PTR [r9-48], rcx
add QWORD PTR [r9-40], r8
mov ecx, eax
shr rax, 32 ; 00000020H
add QWORD PTR [r9-24], rax
add QWORD PTR [r9-32], rcx
dec r10
jne SHORT $LL4@main
и это на x86:
$LL4@main:
mov eax, DWORD PTR [esi]
mul DWORD PTR _fragmentCoefficentVector$[esp+224]
mov ebx, eax
mov eax, DWORD PTR [esi+4]
mov DWORD PTR _r0463[esp+228], edx
mul DWORD PTR _fragmentCoefficentVector$[esp+224]
add DWORD PTR [ecx-16], ebx
mov ebx, DWORD PTR _r0463[esp+228]
adc DWORD PTR [ecx-12], edi
add DWORD PTR [ecx-8], ebx
adc DWORD PTR [ecx-4], edi
add DWORD PTR [ecx], eax
adc DWORD PTR [ecx+4], edi
add DWORD PTR [ecx+8], edx
adc DWORD PTR [ecx+12], edi
add esi, 8
add ecx, 32 ; 00000020H
dec DWORD PTR tv150[esp+224]
jne SHORT $LL4@main
возможно, что оба они уже быстрее, чем ваш исходный код (SSE)... на x64, разворачивая его сделает его даже лучше.
решение 2: SSE2 Integer Shuffle
это решение разворачивает цикл до 2 итераций:
// Test Data
__m128i allZero = _mm_setzero_si128();
__m128i fragmentCoefficentVector = _mm_set1_epi32(1000000000);
__declspec(align(16)) int currentMessageGaloisFieldsArray_[8] = {10,11,12,13,14,15,16,17};
int *currentMessageGaloisFieldsArray = currentMessageGaloisFieldsArray_;
__m128i currentUnModdedGaloisFieldFragments_[8];
__m128i *currentUnModdedGaloisFieldFragments = currentUnModdedGaloisFieldFragments_;
memset(currentUnModdedGaloisFieldFragments,0,8 * sizeof(__m128i));
int elementIterations = 4;
// The Loop
while(elementIterations > 1){
elementIterations -= 2;
// Load 4 elements. If needed use unaligned load instead.
// messageField = {a, b, c, d}
__m128i messageField = _mm_load_si128((__m128i*)currentMessageGaloisFieldsArray);
// Get into this form:
// values0 = {a, x, b, x}
// values1 = {c, x, d, x}
__m128i values0 = _mm_shuffle_epi32(messageField,216);
__m128i values1 = _mm_shuffle_epi32(messageField,114);
// Multiply by "fragmentCoefficentVector"
values0 = _mm_mul_epu32(values0, fragmentCoefficentVector);
values1 = _mm_mul_epu32(values1, fragmentCoefficentVector);
__m128i halves0 = _mm_unpacklo_epi32(values0, allZero);
__m128i halves1 = _mm_unpackhi_epi32(values0, allZero);
__m128i halves2 = _mm_unpacklo_epi32(values1, allZero);
__m128i halves3 = _mm_unpackhi_epi32(values1, allZero);
halves0 = _mm_add_epi64(halves0, currentUnModdedGaloisFieldFragments[0]);
halves1 = _mm_add_epi64(halves1, currentUnModdedGaloisFieldFragments[1]);
halves2 = _mm_add_epi64(halves2, currentUnModdedGaloisFieldFragments[2]);
halves3 = _mm_add_epi64(halves3, currentUnModdedGaloisFieldFragments[3]);
currentUnModdedGaloisFieldFragments[0] = halves0;
currentUnModdedGaloisFieldFragments[1] = halves1;
currentUnModdedGaloisFieldFragments[2] = halves2;
currentUnModdedGaloisFieldFragments[3] = halves3;
currentMessageGaloisFieldsArray += 4;
currentUnModdedGaloisFieldFragments += 4;
}
который компилируется в это (x86): (x64 не слишком отличается)
$LL4@main:
movdqa xmm1, XMMWORD PTR [esi]
pshufd xmm0, xmm1, 216 ; 000000d8H
pmuludq xmm0, xmm3
movdqa xmm4, xmm0
punpckhdq xmm0, xmm2
paddq xmm0, XMMWORD PTR [eax-16]
pshufd xmm1, xmm1, 114 ; 00000072H
movdqa XMMWORD PTR [eax-16], xmm0
pmuludq xmm1, xmm3
movdqa xmm0, xmm1
punpckldq xmm4, xmm2
paddq xmm4, XMMWORD PTR [eax-32]
punpckldq xmm0, xmm2
paddq xmm0, XMMWORD PTR [eax]
punpckhdq xmm1, xmm2
paddq xmm1, XMMWORD PTR [eax+16]
movdqa XMMWORD PTR [eax-32], xmm4
movdqa XMMWORD PTR [eax], xmm0
movdqa XMMWORD PTR [eax+16], xmm1
add esi, 16 ; 00000010H
add eax, 64 ; 00000040H
dec ecx
jne SHORT $LL4@main
только немного больше, чем векторизованную версию для двух итераций. Это использует очень мало регистров, поэтому вы можете развернуть это даже на архитектуры x86.
объяснениями:
- как упоминал Павел R, развертывание до двух итераций позволяет объединить начальную нагрузку в одну нагрузку SSE. Это также имеет преимущество получения ваших данных в регистрах SSE.
- поскольку данные начинаются в регистрах SSE,
_mm_set_epi32
(который компилируется примерно в ~9 инструкций в исходном коде) может быть заменен одним_mm_shuffle_epi32
.
Я предлагаю вам развернуть цикл в 2 раза, чтобы вы могли загрузить 4 значения messageField, используя один _mm_load_XXX, а затем распаковать эти четыре значения в две векторные пары и обработать их в соответствии с текущим циклом. Таким образом, у вас не будет много беспорядочного кода, генерируемого компилятором для _mm_set_epi32, и все ваши нагрузки и магазины будут 128-битными загрузками/магазинами SSE. Это также даст компилятору больше возможностей для оптимального планирования инструкций в цикле.