Результаты моих тестов очень странные. С одной стороны, у меня есть последовательная функция для вычисления квадратичной формы. С другой стороны, я написал две параллельные версии. Для одного потока все функции должны иметь примерно одинаковое время работы. Но на одну параллельную функцию нужно половину времени. Есть ли «скрытая» оптимизация?
Серийная версия:
double quadratic_form_serial(const std::vector<double> & A,const std::vector<double> & v, const std::vector<double> & w){
int N= v.size();
volatile double q=0.0;
for(int i=0; i<N; ++i)
for(int j=0; j<N; ++j)
q +=v[i]*A[i*N+j]*w[j];
return q;
}
Параллельная версия 1:
double quadratic_form_parallel(const std::vector<double> & A,const std::vector<double> & v, const std::vector<double> & w, const int threadnum){
int N= v.size();
omp_set_num_threads(threadnum);
volatile double q[threadnum];
volatile double val = 0.0;
#pragma omp parallel
{
int me = omp_get_thread_num();
q[me] = 0.0;
#pragma omp for collapse(2)
for(int i=0; i<N; ++i)
for(int j=0; j<N; ++j)
q[me]+=v[i]*A[i*N+j]*w[j];
#pragma omp atomic
val+=q[me];
}
return val;
}
Параллельная версия 2:
double quadratic_form_parallel2(const std::vector<double> & A,const std::vector<double> & v, const std::vector<double> & w, const int threadnum){
int N= v.size();
volatile double result =0.0;
omp_set_num_threads(threadnum);
#pragma omp parallel for reduction(+: result)
for (int i=0; i<N; ++i)
for (int j=0; j<N; ++j)
result += v[i] * A[i*N + j] * w[j];
return result;
}
Я запускаю код для N = 10000 и очищаю кеш перед вызовом функции. Функция quadratic_form_parallel2 требует с потоком один меньше половины времени, необходимого для двух других функций:
threads serial Parallel1 Parallel2
1 0.0882503 0.0875649 0.0313441
Если бы вы писали на Фортране, у вас могло бы не возникнуть соблазна воспользоваться опрометчивым коллапсом внутреннего цикла. В зависимости от выбранных вами оптимизаций для вашего компилятора вы можете получить или не получить те же результаты, что и при умножении на v [i], явно перемещенном из внутреннего цикла. С gcc для оптимизации вам потребуется -ffast-math.
Конечно, если бы вы писали даже грязный C++, внутренний цикл был бы заменен результатом + = v [i] * inner_product (& A [i * N], & A [i * N + N], w), поэтому вам снова следует обратите внимание на более чем одну причину, чтобы избежать коллапса. Как здесь часто упоминается, нет смысла делать выводы о производительности на неоптимизированном коде, хотя ваш вопрос больше о том, как вы предотвратили оптимизацию.
Спасибо вам обоим! Я использовал флаги компилятора: CXXFLAGS = -Wall -std = C++ 11 -O3 -fopenmp и компилятор: g ++. Извините, я не совсем понял ту часть, которую вы упомянули с помощью collapse-clausel. Я считаю, что эта часть очень важна для меня при подготовке к экзамену. Не могли бы вы еще раз объяснить, почему коллапс-клаузель - плохая идея (здесь).





Скорее всего, это результат того, что result является переменной reduction во второй версии OpenMP. Это означает, что каждый поток получает частную копию result, которая объединяется после параллельной области. Эта частная копия, вероятно, не соблюдает временные ограничения и, следовательно, может быть оптимизирована в большей степени. Я предполагаю, что подробное взаимодействие между volatile и private не указано.
Это показывает, что помечать переменную как volatile - предположительно, чтобы избежать оптимизации всего кода - это плохая идея. Вместо этого просто выведите результат.
Или назначьте окончательный результат для volatile, но да, используя переменную volatileвнутри, цикл будет разрушать производительность, вводя сохранение / перезагрузку и предотвращая автовекторизацию.
Имейте в виду, что намного проще дать хороший ответ на ваш вопрос, если вы предоставите минимальный воспроизводимый пример и подробное описание вашего подхода к измерению (параметры компилятора, характеристики ЦП, инструкции по выполнению, ...)