Оптимизация кода с использованием свойств Intel SSE для векторизации
Я впервые работаю с SSE. Я пытаюсь преобразовать простой кусок кода в более быструю версию, используя встроенную функцию Intel SSE (до SSE4.2). Кажется, я столкнулся с рядом ошибок.
Скалярная версия кода: (простое умножение матриц)
void mm(int n, double *A, double *B, double *C)
{
int i,j,k;
double tmp;
for(i = 0; i < n; i++)
for(j = 0; j < n; j++) {
tmp = 0.0;
for(k = 0; k < n; k++)
tmp += A[n*i+k] *
B[n*k+j];
C[n*i+j] = tmp;
}
}
Это моя версия: я включил #include <ia32intrin.h>
void mm_sse(int n, double *A, double *B, double *C)
{
int i,j,k;
double tmp;
__m128d a_i, b_i, c_i;
for(i = 0; i < n; i++)
for(j = 0; j < n; j++) {
tmp = 0.0;
for(k = 0; k < n; k+=4)
a_i = __mm_load_ps(&A[n*i+k]);
b_i = __mm_load_ps(&B[n*k+j]);
c_i = __mm_load_ps(&C[n*i+j]);
__m128d tmp1 = __mm_mul_ps(a_i,b_i);
__m128d tmp2 = __mm_hadd_ps(tmp1,tmp1);
__m128d tmp3 = __mm_add_ps(tmp2,tmp3);
__mm_store_ps(&C[n*i+j], tmp3);
}
}
Куда я иду с этим не так? Я получаю несколько ошибок, как это:
mm_vec.c(84): ошибка: значение типа "int" нельзя присвоить объекту типа "__m128d" a_i = __mm_load_ps (& A [n * я + k]);
Вот как я собираю: icc -O2 mm_vec.c -o vec
Может кто-нибудь, пожалуйста, помогите мне преобразовать этот код точно. Спасибо!
ОБНОВИТЬ:
По вашим предложениям я внес следующие изменения:
void mm_sse(int n, float *A, float *B, float *C)
{
int i,j,k;
float tmp;
__m128 a_i, b_i, c_i;
for(i = 0; i < n; i++)
for(j = 0; j < n; j++) {
tmp = 0.0;
for(k = 0; k < n; k+=4)
a_i = _mm_load_ps(&A[n*i+k]);
b_i = _mm_load_ps(&B[n*k+j]);
c_i = _mm_load_ps(&C[n*i+j]);
__m128 tmp1 = _mm_mul_ps(a_i,b_i);
__m128 tmp2 = _mm_hadd_ps(tmp1,tmp1);
__m128 tmp3 = _mm_add_ps(tmp2,tmp3);
_mm_store_ps(&C[n*i+j], tmp3);
}
}
Но теперь я, кажется, получаю ошибку сегментации. Я знаю это, возможно, потому что я не обращаюсь к индексам массива должным образом для массива A, B, C. Я очень новичок в этом и не уверен, как поступить с этим.
Пожалуйста, помогите мне определить правильный подход к обработке этого кода.
Ответы
Ответ 1
Ошибка, которую вы видите, связана с тем, что в именах функций слишком много символов подчеркивания, например:
__mm_mul_ps
должен быть:
_mm_mul_ps // Just one underscore up front
поэтому компилятор C предполагает, что он возвращает int
, поскольку он не видел объявления.
Помимо этого, хотя есть и другие проблемы - вы, кажется, смешиваете вызовы с двойными и одиночными вариантами с плавающей точкой одной и той же инструкции.
Например, у вас есть:
__m128d a_i, b_i, c_i;
но вы вызываете:
__mm_load_ps(&A[n*i+k]);
который возвращает __m128
не a __m128d
- вы хотите вызвать:
_mm_load_pd
вместо этого. Аналогично другим инструкциям, если вы хотите, чтобы они работали над парами парных.
Если вы видите необъяснимые ошибки сегментации и код SSE, я бы склонен предположить, что у вас проблемы с выравниванием памяти. Указатели, переданные в SSE-intrinsics (в основном 1), должны быть 16 байт выровнены. Вы можете проверить это с помощью простого утверждения в своем коде или проверить его в отладчике (вы ожидаете, что последняя цифра указателя будет равна 0, если она правильно выровнена).
Если он не выровнен правильно, вам нужно убедиться, что он есть. Для вещей, не выделенных с помощью new
/malloc()
, вы можете сделать это с помощью расширения компилятора (например, с gcc):
float a[16] __attribute__ ((aligned (16)));
Если ваша версия gcc имеет максимальное выравнивание, достаточное для поддержки этого и несколько других предостережений о выравнивании стека. Для динамически распределенного хранилища вы захотите использовать расширение для конкретной платформы, например. posix_memalign
выделить подходящее хранилище:
float *a=NULL;
posix_memalign(&a, __alignof__(__m128), sizeof(float)*16);
(Я думаю, что с С++ 11 могут быть более удобные, переносимые способы сделать это, но я пока не уверен на 100%).
1 Есть несколько инструкций, которые позволяют делать неуравновешенные нагрузки и магазины, но они ужасно медленны по сравнению с согласованными нагрузками и стоит избегать, если это вообще возможно.
Ответ 2
Вам нужно убедиться, что ваши загрузки и хранилища всегда обращаются к адресам с выравниванием по 16 байт. В качестве альтернативы, если вы не можете гарантировать это по какой-либо причине, используйте _mm_loadu_ps
/_mm_storeu_ps
вместо _mm_load_ps
/_mm_store_ps
- это будет менее эффективно, но не будет сбой по неверным адресам.