SIMD код работает медленнее скалярного кода - PullRequest
4 голосов
/ 09 декабря 2010

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 включены версии XOR не-simd и simd-элементов.Первые две строки во втором цикле 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];   
    }  
}  

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

Ответы [ 4 ]

6 голосов
/ 09 декабря 2010

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

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

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

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

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

4 голосов
/ 09 декабря 2010

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

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

4 голосов
/ 09 декабря 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

2 голосов
/ 09 декабря 2010

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

...