SIMD-код работает медленнее, чем скалярный код

elma и elmc оба unsigned long массивы. Таковы res1 и res2.

unsigned long simdstore[2];  
__m128i *p, simda, simdb, simdc;  
p = (__m128i *) simdstore;  

for (i = 0; i < _polylen; i++)  
{
    u1 = (elma[i] >> l) & 15;  
    u2 = (elmc[i] >> l) & 15;  
    for (k = 0; k < 20; k++)  
    {
        //res1[i + k] ^= _mulpre1[u1][k];  
        //res2[i + k] ^= _mulpre2[u2][k];               

        simda = _mm_set_epi64x (_mulpre2[u2][k], _mulpre1[u1][k]);  
        simdb = _mm_set_epi64x (res2[i + k], res1[i + k]);  
        simdc = _mm_xor_si128 (simda, simdb);  
        _mm_store_si128 (p, simdc);  
        res1[i + k] = simdstore[0];  
        res2[i + k] = simdstore[1];                     
    }     
}  

В цикл for включены как не-simd, так и simd-версия XOR элементов. Первые две строки во втором цикле for выполняют явную операцию XOR, а остальные реализуют simd-версию той же операции.

Этот цикл вызывается извне сотни раз, поэтому оптимизация этого цикла поможет сократить общее время вычислений.

Проблема в том, что код simd работает во много раз медленнее, чем скалярный код.

РЕДАКТИРОВАТЬ: выполнено частичное развертывание

__m128i *p1, *p2, *p3, *p4;  
p1 = (__m128i *) simdstore1;  
p2 = (__m128i *) simdstore2;  
p3 = (__m128i *) simdstore3;  
p4 = (__m128i *) simdstore4;  

for (i = 0; i < 20; i++)  
{
    u1 = (elma[i] >> l) & 15;  
    u2 = (elmc[i] >> l) & 15;  
    for (k = 0; k < 20; k = k + 4)  
    {
        simda1  = _mm_set_epi64x (_mulpre2[u2][k], _mulpre1[u1][k]);  
        simda2  = _mm_set_epi64x (_mulpre2[u2][k + 1], _mulpre1[u1][k + 1]);  
        simda3  = _mm_set_epi64x (_mulpre2[u2][k + 2], _mulpre1[u1][k + 2]);  
        simda4  = _mm_set_epi64x (_mulpre2[u2][k + 3], _mulpre1[u1][k + 3]);  

        simdb1  = _mm_set_epi64x (res2[i + k], res1[i + k]);  
        simdb2  = _mm_set_epi64x (res2[i + k + 1], res1[i + k + 1]);  
        simdb3  = _mm_set_epi64x (res2[i + k + 2], res1[i + k + 2]);  
        simdb4  = _mm_set_epi64x (res2[i + k + 3], res1[i + k + 3]);  

        simdc1  = _mm_xor_si128 (simda1, simdb1);  
        simdc2  = _mm_xor_si128 (simda2, simdb2);  
        simdc3  = _mm_xor_si128 (simda3, simdb3);  
        simdc4  = _mm_xor_si128 (simda4, simdb4);  

        _mm_store_si128 (p1, simdc1);  
        _mm_store_si128 (p2, simdc2);  
        _mm_store_si128 (p3, simdc3);  
        _mm_store_si128 (p4, simdc4);  

        res1[i + k]= simdstore1[0];  
        res2[i + k]= simdstore1[1]; 
        res1[i + k + 1]= simdstore2[0];  
        res2[i + k + 1]= simdstore2[1];   
        res1[i + k + 2]= simdstore3[0];  
        res2[i + k + 2]= simdstore3[1]; 
        res1[i + k + 3]= simdstore4[0];  
        res2[i + k + 3]= simdstore4[1];   
    }  
}  

Но результат не сильно изменится; он по-прежнему занимает в два раза больше времени, чем скалярный код.


person anup    schedule 09.12.2010    source источник
comment
Выровняйте res1 / res2, поменяйте регистры и напишите прямо в них. Избавьтесь от simdstore.   -  person EboMike    schedule 09.12.2010
comment
res1 / res2 - это длинные массивы без знака, которые я использовал в своем коде. Я не уверен, что вы имеете в виду, говоря выровнять их по 16 байтам и писать им напрямую.   -  person anup    schedule 09.12.2010
comment
Я имею в виду, убедитесь, что их начальный адрес выровнен по 16 байтам, а затем напишите им напрямую, используя _mm_store_si128. (Очевидно, вам придется взять два результата и объединить их в один, переключив регистр вектора.) Запись в память и последующее чтение из нее вызовет срыв.   -  person EboMike    schedule 09.12.2010


Ответы (4)


Отказ от ответственности: я работаю с PowerPC, так что то, что я говорю здесь, может быть полной чушью. Но вы задерживаете свой векторный конвейер, так как пытаетесь сразу получить доступ к своим результатам.

Лучше всего оставить все в своем векторном конвейере. Как только вы выполняете какое-либо преобразование из вектора в int или float или сохраняете результат в памяти, вы застреваете.

Наилучший режим работы с SSE или VMX: загрузка, обработка, сохранение. Загрузите данные в свои векторные регистры, выполните всю векторную обработку, а затем сохраните их в памяти.

Я бы порекомендовал: зарезервировать несколько регистров __m128i, несколько раз развернуть цикл, а затем сохранить его.

РЕДАКТИРОВАТЬ: Кроме того, если вы развернете и выровняете res1 и res2 на 16 байтов, вы можете сохранить свои результаты непосредственно в памяти, не проходя через это косвенное обращение к simdstore, которое, вероятно, является LHS и другим киоском.

РЕДАКТИРОВАТЬ: Забыл очевидное. Если ваша полилена обычно большая, не забывайте выполнять предварительную выборку данных из кеша на каждой итерации.

person EboMike    schedule 09.12.2010
comment
Развертывание цикла хорошо на PowerPC, но не так хорошо на современных процессорах x86, где меньше регистров, с которыми можно поиграть, а сам ЦП будет выполнять некоторую развертку для небольших циклов. - person Paul R; 09.12.2010
comment
Полезно знать, спасибо! В этом конкретном случае, я думаю, имеет смысл развернуть хотя бы один раз, чтобы построить вывод в векторных регистрах (поскольку вам нужно взять два последовательных результата и объединить их вместе в один) - я думаю, одна из самых больших проблем здесь это simdstore - ›переадресация res1 / res2. - person EboMike; 09.12.2010
comment
@Paul R По моему опыту, развертывание циклов в x86 действительно полезно. Прошло некоторое время с тех пор, как я профилировал его для 32-разрядной версии, но, безусловно, на x86-64 она полезна (в 64-разрядной версии регистров намного больше); Я постоянно получаю от этого ускорение. В зависимости от случая это дает оптимизатору возможность действительно переупорядочить вещи более оптимальным образом, не говоря уже о сокращении переменных счетчика или других операций, которые выполняются в каждом цикле, которые могут быть исключены путем развертывания. - person Apriori; 08.02.2014
comment
@ Богато: разворачивание цикла, безусловно, может помочь в некоторых случаях, но в других оно может быть контрпродуктивным, особенно если вы развернете больше, чем размер кэша uop детектора потока циклов. Вам также необходимо решить, позволите ли вы компилятору разворачивать циклы за вас или собираетесь делать это вручную. Итог: раскрутка - не та панацея, которой когда-то была, например. на старых процессорах x86 или других архитектурах, таких как PowerPC. - person Paul R; 08.02.2014

Здесь вы делаете очень мало вычислений относительно количества выполняемых загрузок и сохранений, поэтому в результате вы не видите небольшой выгоды от SIMD. В этом случае вам, вероятно, будет более полезно использовать скалярный код, особенно если у вас есть процессор x86-64, который можно использовать в 64-битном режиме. Это уменьшит количество загрузок и сохранений, которые в настоящее время являются доминирующим фактором вашей производительности.

(Примечание: вам, вероятно, не следует разворачивать цикл, особенно если вы используете Core 2 или новее.)

person Paul R    schedule 09.12.2010

То, как выглядит ваш код, res1 и res2 кажутся полностью независимыми векторами. Тем не менее, вы смешиваете их в одном регистре, чтобы искоренить их.

Я бы использовал разные регистры примерно так (все векторы должны быть выровнены).

__m128i x0, x1, x2, x3;  
for (i = 0; i < _polylen; i++)  
{  

    u1 = (elma[i] >> l) & 15;  
    u2 = (elmc[i] >> l) & 15;  
    for (k = 0; k < 20; k+=2)  
    {     
        //res1[i + k] ^= _mulpre1[u1][k];
        x0= _mm_load_si128(&_mulpre1[u1][k]);
        x1= _mm_load_si128(&res1[i + k]);
        x0= _mm_xor_si128 (x0, x1);
        _mm_store_si128 (&res1[i + k], x0);
        //res2[i + k] ^= _mulpre2[u2][k];               
        x2= _mm_load_si128(&_mulpre2[u2][k]);
        x3= _mm_load_si128(&res2[i + k]);
        x2= _mm_xor_si128 (x2, x3);
        _mm_store_si128 (&res2[i + k], x2);
   }     
}  

Обратите внимание, что я использую только 4 регистра. Вы можете вручную развернуть, чтобы использовать все 8 регистров в x86 или больше в x86_64

person renick    schedule 09.12.2010
comment
Убедитесь, что вы правильно выровняли res1 и res2. Кстати, я думал, что res1 получает первые 8 байтов регистра, а res2 получает остальные 8 байтов? Итак, вам нужно сделать две итерации и изменить результаты. - person EboMike; 09.12.2010
comment
@EboMike прав. res1 получает первые 8 байтов, а res2 - остальные. @renick, то, как вы его реализовали, не дает никаких дополнительных преимуществ SIMD, потому что вы использовали два отдельных xors, и это просто эквивалентно скалярному коду. - person anup; 09.12.2010
comment
@renick mulpre и res имеют ширину 64 бита, поэтому вставка их в регистры SIMD может быть и xoring их не так полезны ... - person anup; 09.12.2010
comment
@anup Если что-то не так, я должен удалить его. В противном случае обратите внимание, что на каждую итерацию цикла приходится 4 операции xor с 8 инструкциями, так что это не должно быть слишком потрепанным. - person renick; 09.12.2010
comment
@renick: Это не так! @anup: Он вычисляет 2 элемента за раз для каждого массива отдельно. Он использует преимущества SIMD: 2 вызова XOR на итерацию цикла выполняют в общей сложности 4 операции. И это будет быстрее, чем ваш подход к смешиванию массивов res1[] и res2[], потому что он избегает промежуточного хранилища для simdstore. - person j_random_hacker; 09.12.2010
comment
@anup, @EboMike: На каждой итерации цикла он вычисляет 2 элемента для res1[], затем 2 элемента для res2[]. Это нормально, потому что между соседними элементами одного и того же массива нет зависимостей. Это быстрый и надежный код. - person j_random_hacker; 09.12.2010
comment
@anup: Обратите внимание, что эта версия использует k+=2 вместо k++ в цикле - она ​​выполняет вдвое меньше итераций. - person caf; 09.12.2010
comment
Кстати, создание вашего магазина с использованием _mm_stream_ps обычно заканчивается намного быстрее, поскольку вы не вводите новую строку кеша при записи. Вы кэшируете только прочитанные данные. Записи действительно не нуждаются в кешировании, и вы, вероятно, можете получить значительное ускорение ... - person Goz; 10.12.2010

Я тоже не эксперт по SIMD, но похоже, что вы также могли бы извлечь выгоду из предварительной выборки данных в сочетании с упомянутым прекращением EboMike. Также может помочь, если вы объедините res1 и res2 в один выровненный массив (структур в зависимости от того, что еще использует), тогда вам не понадобится дополнительное копирование, вы можете работать непосредственно с ним.

person Necrolis    schedule 09.12.2010