Использование стека с внутренними функциями MMX и Microsoft C ++

У меня есть встроенный цикл ассемблера, который кумулятивно добавляет элементы из массива данных int32 с инструкциями MMX. В частности, он использует тот факт, что регистры MMX могут вмещать 16 int32 для параллельного вычисления 16 различных совокупных сумм.

Теперь я хотел бы преобразовать этот фрагмент кода во встроенные функции MMX, но я боюсь, что буду страдать от потери производительности, потому что нельзя явно указать компилятору использовать 8 регистров MMX для хранения 16 независимых сумм.

Может ли кто-нибудь прокомментировать это и, возможно, предложить решение о том, как преобразовать фрагмент кода ниже для использования встроенных функций?

== встроенный ассемблер (только часть цикла) ==

paddd   mm0, [esi+edx+8*0]  ; add first & second pair of int32 elements
paddd   mm1, [esi+edx+8*1]  ; add third & fourth pair of int32 elements ...
paddd   mm2, [esi+edx+8*2]
paddd   mm3, [esi+edx+8*3]
paddd   mm4, [esi+edx+8*4]
paddd   mm5, [esi+edx+8*5]
paddd   mm6, [esi+edx+8*6]
paddd   mm7, [esi+edx+8*7]  ; add 15th & 16th pair of int32 elements
  • esi указывает на начало массива данных
  • edx предоставляет смещение в массиве данных для текущей итерации цикла
  • массив данных устроен так, что элементы для 16 независимых сумм чередуются.

person ARF    schedule 23.05.2010    source источник


Ответы (1)


VS2010 выполняет достойную работу по оптимизации эквивалентного кода с использованием встроенных функций. В большинстве случаев он компилирует внутренний:

sum = _mm_add_pi32(sum, *(__m64 *) &intArray[i + offset]);

во что-то вроде:

movq  mm0, mmword ptr [eax+8*offset]
paddd mm1, mm0

Это не так лаконично, как ваш padd mm1, [esi+edx+8*offset], но, возможно, довольно близко. Время выполнения, вероятно, зависит от выборки из памяти.

Загвоздка в том, что VS, похоже, любит добавлять регистры MMX только к другим регистрам MMX. Вышеуказанная схема работает только для первых 7 сумм. 8-я сумма требует временного сохранения некоторого регистра в памяти.

Вот полная программа и соответствующая ей скомпилированная сборка (релизная сборка):

#include <stdio.h>
#include <stdlib.h>
#include <xmmintrin.h>

void addWithInterleavedIntrinsics(int *interleaved, int count)
{
    // sum up the numbers
    __m64 sum0 = _mm_setzero_si64(), sum1 = _mm_setzero_si64(),
          sum2 = _mm_setzero_si64(), sum3 = _mm_setzero_si64(),
          sum4 = _mm_setzero_si64(), sum5 = _mm_setzero_si64(),
          sum6 = _mm_setzero_si64(), sum7 = _mm_setzero_si64();

    for (int i = 0; i < 16 * count; i += 16) {
        sum0 = _mm_add_pi32(sum0, *(__m64 *) &interleaved[i]);
        sum1 = _mm_add_pi32(sum1, *(__m64 *) &interleaved[i + 2]);
        sum2 = _mm_add_pi32(sum2, *(__m64 *) &interleaved[i + 4]);
        sum3 = _mm_add_pi32(sum3, *(__m64 *) &interleaved[i + 6]);
        sum4 = _mm_add_pi32(sum4, *(__m64 *) &interleaved[i + 8]);
        sum5 = _mm_add_pi32(sum5, *(__m64 *) &interleaved[i + 10]);
        sum6 = _mm_add_pi32(sum6, *(__m64 *) &interleaved[i + 12]);
        sum7 = _mm_add_pi32(sum7, *(__m64 *) &interleaved[i + 14]);
    }

    // reset the MMX/floating-point state
    _mm_empty();

    // write out the sums; we have to do something with the sums so that
    // the optimizer doesn't just decide to avoid computing them.
    printf("%.8x %.8x\n", ((int *) &sum0)[0], ((int *) &sum0)[1]);
    printf("%.8x %.8x\n", ((int *) &sum1)[0], ((int *) &sum1)[1]);
    printf("%.8x %.8x\n", ((int *) &sum2)[0], ((int *) &sum2)[1]);
    printf("%.8x %.8x\n", ((int *) &sum3)[0], ((int *) &sum3)[1]);
    printf("%.8x %.8x\n", ((int *) &sum4)[0], ((int *) &sum4)[1]);
    printf("%.8x %.8x\n", ((int *) &sum5)[0], ((int *) &sum5)[1]);
    printf("%.8x %.8x\n", ((int *) &sum6)[0], ((int *) &sum6)[1]);
    printf("%.8x %.8x\n", ((int *) &sum7)[0], ((int *) &sum7)[1]);
}

void main()
{
    int count        = 10000;
    int *interleaved = new int[16 * count];

    // create some random numbers to add up
    // (note that on VS2010, RAND_MAX is just 32767)
    for (int i = 0; i < 16 * count; ++i) {
        interleaved[i] = rand();
    }

    addWithInterleavedIntrinsics(interleaved, count);
}

Вот сгенерированный ассемблерный код для внутренней части цикла суммы (без его пролога и эпилога). Обратите внимание, как эффективно ведется большая часть сумм в мм1-мм6. Сравните это с mm0, которое используется для прибавления числа к каждой сумме, и с mm7, которое служит последним двум суммам. Версия этой программы с 7 суммами, похоже, не имеет проблемы с mm7.

012D1070  movq        mm7,mmword ptr [esp+18h]  
012D1075  movq        mm0,mmword ptr [eax-10h]  
012D1079  paddd       mm1,mm0  
012D107C  movq        mm0,mmword ptr [eax-8]  
012D1080  paddd       mm2,mm0  
012D1083  movq        mm0,mmword ptr [eax]  
012D1086  paddd       mm3,mm0  
012D1089  movq        mm0,mmword ptr [eax+8]  
012D108D  paddd       mm4,mm0  
012D1090  movq        mm0,mmword ptr [eax+10h]  
012D1094  paddd       mm5,mm0  
012D1097  movq        mm0,mmword ptr [eax+18h]  
012D109B  paddd       mm6,mm0  
012D109E  movq        mm0,mmword ptr [eax+20h]  
012D10A2  paddd       mm7,mm0  
012D10A5  movq        mmword ptr [esp+18h],mm7  
012D10AA  movq        mm0,mmword ptr [esp+10h]  
012D10AF  movq        mm7,mmword ptr [eax+28h]  
012D10B3  add         eax,40h  
012D10B6  dec         ecx  
012D10B7  paddd       mm0,mm7  
012D10BA  movq        mmword ptr [esp+10h],mm0  
012D10BF  jne         main+70h (12D1070h)  

Так что ты можешь сделать?

  1. Профилируйте программы, основанные на 7 и 8 суммах. Выберите тот, который выполняется быстрее.

  2. Профилируйте версию, которая добавляет только один регистр MMX за раз. Он по-прежнему должен иметь возможность использовать тот факт, что современные процессоры извлекать в кеш от 64 до 128 байт за раз. Не очевидно, что версия с 8 суммой будет быстрее, чем версия с 1 суммой. Версия с 1 суммой извлекает точно такой же объем памяти и выполняет такое же количество добавлений MMX. Однако вам нужно будет соответствующим образом чередовать входы.

  3. Если ваше целевое оборудование позволяет это, рассмотрите возможность использования инструкций SSE. Они могут добавлять 4 32-битных значения за раз. SSE доступен в процессорах Intel с Pentium III в 1999 году.

person Oren Trutner    schedule 07.06.2010