Использование стека с внутренними компонентами MMX и Microsoft C ++ - PullRequest
1 голос
/ 24 мая 2010

У меня есть встроенный цикл ассемблера, который кумулятивно добавляет элементы из массива данных 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 независимых сумм чередуются.

1 Ответ

2 голосов
/ 07 июня 2010

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 сумм. Восьмая сумма требует, чтобы какой-то регистр был временно сохранен в памяти.

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

#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 году.

...