Ответ - первый блок:
__m128 ai_v = _mm_loadu_ps(&a[i]);
__m128 two_v = _mm_set1_ps(2);
__m128 ai2_v = _mm_mul_ps(ai_v,two_v);
_mm_storeu_ps(&b[i],ai2_v);
Он уже принимает четыре переменных одновременно.
Вот полная программа с закомментированным эквивалентным разделом кода:
#include <iostream>
int main()
{
int i{0};
float a[10] ={1,2,3,4,5,6,7,8,9,10};
float b[10] ={0,0,0,0,0,0,0,0,0,0};
int n = 10;
int unroll = (n/4)*4;
for (i=0; i<unroll; i+=4) {
//b[i] = a[i]*2;
//b[i+1] = a[i+1]*2;
//b[i+2] = a[i+2]*2;
//b[i+3] = a[i+3]*2;
__m128 ai_v = _mm_loadu_ps(&a[i]);
__m128 two_v = _mm_set1_ps(2);
__m128 ai2_v = _mm_mul_ps(ai_v,two_v);
_mm_storeu_ps(&b[i],ai2_v);
}
for (; i<n; i++) {
b[i] = a[i]*2;
}
for (auto i : a) { std::cout << i << "\t"; }
std::cout << "\n";
for (auto i : b) { std::cout << i << "\t"; }
std::cout << "\n";
return 0;
}
Что касается эффективности; похоже, что сборка в моей системе генерирует movups
инструкции, тогда как код, скрученный вручную, можно заставить использовать movaps
, что должно быть быстрее.
Я использовал следующую программу, чтобы провести несколько тестов:
#include <iostream>
//#define NO_UNROLL
//#define UNROLL
//#define SSE_UNROLL
#define SSE_UNROLL_ALIGNED
int main()
{
const size_t array_size = 100003;
#ifdef SSE_UNROLL_ALIGNED
__declspec(align(16)) int i{0};
__declspec(align(16)) float a[array_size] ={1,2,3,4,5,6,7,8,9,10};
__declspec(align(16)) float b[array_size] ={0,0,0,0,0,0,0,0,0,0};
#endif
#ifndef SSE_UNROLL_ALIGNED
int i{0};
float a[array_size] ={1,2,3,4,5,6,7,8,9,10};
float b[array_size] ={0,0,0,0,0,0,0,0,0,0};
#endif
int n = array_size;
int unroll = (n/4)*4;
for (size_t j{0}; j < 100000; ++j) {
#ifdef NO_UNROLL
for (i=0; i<n; i++) {
b[i] = a[i]*2;
}
#endif
#ifdef UNROLL
for (i=0; i<unroll; i+=4) {
b[i] = a[i]*2;
b[i+1] = a[i+1]*2;
b[i+2] = a[i+2]*2;
b[i+3] = a[i+3]*2;
}
#endif
#ifdef SSE_UNROLL
for (i=0; i<unroll; i+=4) {
__m128 ai_v = _mm_loadu_ps(&a[i]);
__m128 two_v = _mm_set1_ps(2);
__m128 ai2_v = _mm_mul_ps(ai_v,two_v);
_mm_storeu_ps(&b[i],ai2_v);
}
#endif
#ifdef SSE_UNROLL_ALIGNED
for (i=0; i<unroll; i+=4) {
__m128 ai_v = _mm_load_ps(&a[i]);
__m128 two_v = _mm_set1_ps(2);
__m128 ai2_v = _mm_mul_ps(ai_v,two_v);
_mm_store_ps(&b[i],ai2_v);
}
#endif
#ifndef NO_UNROLL
for (; i<n; i++) {
b[i] = a[i]*2;
}
#endif
}
//for (auto i : a) { std::cout << i << "\t"; }
//std::cout << "\n";
//for (auto i : b) { std::cout << i << "\t"; }
//std::cout << "\n";
return 0;
}
Получил следующие результаты (x86):
NO_UNROLL
: 0.994 секунд, SSE не выбран компилятором
UNROLL
: 3,511 секунд, используется movups
SSE_UNROLL
: 3,315 секунды, используется movups
SSE_UNROLL_ALIGNED
: 3,276 секунды, используется movaps
Итак, очевидно, что разворачивание цикла в данном случае не помогло. Даже то, что мы используем более эффективный movaps
, мало помогает.
Но при компиляции в 64 бит (x64) я получил еще более странный результат:
NO_UNROLL
: 1,138 секунды, SSE не выбран компилятором
UNROLL
: 1,409 секунд, SSE не выбран компилятором
SSE_UNROLL
: 1,420 секунды, компилятор по-прежнему не выбрал SSE!
SSE_UNROLL_ALIGNED
: 1,476 секунды, компилятор по-прежнему не выбрал SSE!
Кажется, MSVC видит предложение и генерирует лучшую сборку, хотя и медленнее, чем если бы мы вообще не пробовали какую-либо ручную оптимизацию.
person
wally
schedule
10.03.2016
n/4
отбрасывает остаток. Итак,for(;i<n;i++)
делает последнюю часть. - person wally   schedule 10.03.2016_mm_mul_ps
для умножения на 2? Почему бы не использовать_mm_sll_epi32
или только один_mm_add_ps(ai_v, ai_v)
? Отдельногоtwo2_v
не требуется - person phuclv   schedule 10.03.2016_mm_add_ps
, а неss
. Кроме того, целочисленный сдвиг не даст желаемого результата для данных FP. - person Peter Cordes   schedule 10.03.2016