Я пытаюсь использовать OpenMP для распараллеливания уже векторизованного кода со встроенными функциями, но проблема в том, что я использую один регистр XMM в качестве внешней «переменной», которую я увеличиваю в каждом цикле. Сейчас я использую shared
пункт
__m128d xmm0 = _mm_setzero_pd();
__declspec(align(16)) double res[2];
#pragma omp parallel for shared(xmm0)
for (int i = 0; i < len; i++)
{
__m128d xmm7 = ... result of some operations
xmm0 = _mm_add_pd(xmm0, xmm7);
}
_mm_store_pd(res, xmm0);
double final_result = res[0] + res[1];
поскольку atomic
операция не поддерживается (в VS2010)
__m128d xmm0 = _mm_setzero_pd();
__declspec(align(16)) double res[2];
#pragma omp parallel for
for (int i = 0; i < len; i++)
{
__m128d xmm7 = ... result of some operations
#pragma omp atomic
xmm0 = _mm_add_pd(xmm0, xmm7);
}
_mm_store_pd(res, xmm0);
double final_result = res[0] + res[1];
Кто-нибудь знает умный обходной путь?
РЕДАКТИРОВАТЬ: я также попробовал это с помощью библиотеки параллельных шаблонов только сейчас:
__declspec(align(16)) double res[2];
combinable<__m128d> xmm0_comb([](){return _mm_setzero_pd();});
parallel_for(0, len, 1, [&xmm0_comb, ...](int i)
{
__m128d xmm7 = ... result of some operations
__m128d& xmm0 = xmm0_comb.local();
xmm0 = _mm_add_pd(xmm0, xmm7);
});
__m128d xmm0 = xmm0_comb.combine([](__m128d a, __m128d b){return _mm_add_pd(a, b);});
_mm_store_pd(res, xmm0);
double final_result = res[0] + res[1];
но результат был разочаровывающим.
Вы решаете проблему неправильно. Вы должны использовать сокращение вместо атомарных операций:
Это лучший подход:
double sum = 0;
#pragma omp parallel for reduction(+:sum)
for (int i = 0; i < len; i++)
{
__m128d xmm7;// = ... result of some operations
// Collapse to a "double".
_declspec(align(16)) double res[2];
_mm_store_pd(res, xmm7);
// Add to reduction variable.
sum += res[0] + res[1];
}
double final_result = sum;
Сокращение — это, по сути, операция, которая сворачивает «сводит» все к одной переменной, используя ассоциативную операцию, такую как +
,
Если вы делаете сокращение, всегда старайтесь использовать реальный подход сокращения. Не пытайтесь обмануть это атомарными операциями или критическими секциями.
Причиной этого является то, что подходы атомарного / критического секций по своей природе не масштабируемы, поскольку они поддерживают длинную зависимость данных критического пути. Правильный подход к сокращению уменьшает этот критический путь к log(# of threads)
,
Единственным недостатком, конечно, является то, что он нарушает ассоциативность с плавающей точкой. Если это имеет значение, то вы в основном застряли с последовательным суммированием каждой итерации.
То, что вы ищете, это сокращение. Вы можете сделать это как сокращение omp, если ваш компилятор поддерживает это (gcc делает), или вы можете свернуть один самостоятельно, суммируя в частный xmm для каждого потока. Ниже приведена простая версия, которая делает оба:
#include <emmintrin.h>
#include <omp.h>
#include <stdio.h>int main(int argc, char **argv) {
const int NTHREADS=8;
const int len=100;
__m128d xmm0[NTHREADS];
__m128d xmmreduction = _mm_setzero_pd();
#pragma omp parallel for num_threads(NTHREADS)
for (int i=0; i<NTHREADS; i++)
xmm0[i]= _mm_setzero_pd();
__attribute((aligned(16))) double res[2];
#pragma omp parallel num_threads(NTHREADS) reduction(+:xmmreduction)
{
int tid = omp_get_thread_num();
#pragma omp for
for (int i = 0; i < len; i++)
{
double d = (double)i;
__m128d xmm7 = _mm_set_pd( d, 2.*d );
xmm0[tid] = _mm_add_pd(xmm0[tid], xmm7);
xmmreduction = _mm_add_pd(xmmreduction, xmm7);
}
}
for (int i=1; i<NTHREADS; i++)
xmm0[0] = _mm_add_pd(xmm0[0], xmm0[i]);
_mm_store_pd(res, xmm0[0]);
double final_result = res[0] + res[1];
printf("Expected result = %f\n", 3.0*(len-1)*(len)/2);
printf("Calculated result = %lf\n", final_result);
_mm_store_pd(res, xmmreduction);
final_result = res[0] + res[1];
printf("Calculated result (reduction) = %lf\n", final_result);
return 0;
}
С большой помощью людей, которые ответили на мой вопрос, я придумал это:
double final_result = 0.0;
#pragma omp parallel reduction(+:final_result)
{
__declspec(align(16)) double r[2];
__m128d xmm0 = _mm_setzero_pd();
#pragma omp for
for (int i = 0; i < len; i++)
{
__m128d xmm7 = ... result of some operations
xmm0 = _mm_add_pd(xmm0, xmm7);
}
_mm_store_pd(r, xmm0);
final_result += r[0] + r[1];
}
Он в основном разделяет развал и сокращение, очень хорошо работает.
Большое спасибо всем, кто мне помог!
Я думаю, вы не можете добавить свои собственные встроенные функции в компилятор, и компиляторы MS решили пропустить встроенный ассемблер. Не уверен, что есть простое решение вообще.