Оптимизация 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. Это также даст компилятору больше возможностей для оптимального планирования инструкций в цикле.