Почему время матрицы VC++ ускоряется при использовании openMP по сравнению с асинхронностью?
Я кодировал умножение вектора на матрицу двумя способами, один с openMP, другой с std::async. Я ожидал, что производительность будет практически одинаковой. OpenMP работает медленно при первом вызове, возможно потому, что откладывает создание пула потоков. После этого асинхронная версия работает на 40% медленнее. (У меня есть Intel Core i5, который 4 ядра.)
В чем дело? Разве VC++ не использует пул потоков для асинхронности? Я делаю что-то глупое? (Скорее всего.) Я думаю, что доступ к выходному вектору достаточно разнесен, чтобы избежать ложного обмена.
#include "stdafx.h"
# include <iostream>
# include <vector>
# include <omp.h>
# include <ctime>
#include <numeric>
#include <thread>
#include <chrono>
#include <future>
// Matrix multiplication of vector using omp
template<class Mat, class Vec>
void mult_mat_vec_omp
(const Mat &mat, const Vec &inp, Vec &out) {
const int steps = static_cast<int>(std::size(mat));
using std::begin; using std::end;
auto N = std::thread::hardware_concurrency();
omp_set_num_threads(N);
#pragma omp parallel for
for (int i=0; i < steps; ++i) {
out[i] = std::inner_product(begin(mat[i]), end(mat[i]), begin(inp), 0.0);
}
}
// Matrix multiplication of vector using async
template<class Mat, class Vec>
void mult_mat_vec_async
(const Mat &mat, const Vec &inp, Vec &out) {
using std::begin; using std::end;
auto N = std::thread::hardware_concurrency();
typedef decltype(N) usigned;
const unsigned steps = static_cast<unsigned>(std::size(mat));
auto f = [&](unsigned id) {
for (unsigned i=id; i < steps; i+= N) {
out[i] = std::inner_product(begin(mat[i]), end(mat[i]), begin(inp), 0.0);
}
};
std::vector<std::future<void>> threads;
for (unsigned i = 1; i<N; ++i) {
threads.push_back(std::async(std::launch::async, f, i));
}
f(0);
for (auto &x: threads) {
x.get();
}
}
double test() {
using std::vector;
using clock=std::chrono::high_resolution_clock;
vector<double> a;
vector<double> b;
vector<double> c;
vector<vector<double>> mat;
vector<double> v;
int rows = 350;
int cols = 350;
for (int i = 0; i< cols; ++i) {
a.push_back(i/10.0);
b.push_back(-999);
c.push_back(8888);
}
for (int i=0; i<rows; ++i) {
v.clear();
for (int j=0; j<cols; ++j) {
v.push_back (((i+.5)*j)/100.0);
}
mat.push_back(v);
}
clock::time_point start = clock::now();
int N = 10000;
for (int i=0; i< N/10; ++i) {
mult_mat_vec_omp(mat, a, b) ;
mult_mat_vec_omp(mat, a, b);
mult_mat_vec_omp(mat, a, b);
mult_mat_vec_omp(mat, a, b);
mult_mat_vec_omp(mat, a, b);
mult_mat_vec_omp(mat, a, b);
mult_mat_vec_omp(mat, a, b);
mult_mat_vec_omp(mat, a, b);
mult_mat_vec_omp(mat, a, b);
mult_mat_vec_omp(mat, a, b);
};
long long duration =
std::chrono::duration_cast<std::chrono::milliseconds>(clock::now()-start).count();
start = clock::now();
size_t cutoff = 0; // 2*rows*cols;
for (int i=0; i< N/10; ++i) {
mult_mat_vec_async(mat, a, c);
mult_mat_vec_async(mat, a, c);
mult_mat_vec_async(mat, a, c);
mult_mat_vec_async(mat, a, c);
mult_mat_vec_async(mat, a, c);
mult_mat_vec_async(mat, a, c);
mult_mat_vec_async(mat, a, c);
mult_mat_vec_async(mat, a, c);
mult_mat_vec_async(mat, a, c);
mult_mat_vec_async(mat, a, c);
};
long long duration2 = std::chrono::duration_cast<std::chrono::milliseconds>(clock::now()-start).count();
//cout << mat[0][5] << " " << b[0] << " " << c[0] << endl;
bool good = (b==c);
std::cout << duration*(1.0/N) << ' ' << duration2*(1.0/N) << " " << good << std::endl;
return 0;
}
int main ()
{
for(int i=0; i<15; ++i) test();
return 0;
}
1 ответ
На Intel Core i7-2600, отключенном HT, с gcc 7.2 / Linux цифры несколько отличаются, асинхронная версия примерно на 10% медленнее.
Теперь дискуссия идет по правильному пути относительно эффективности кэша и ложного обмена. Вы должны попытаться получить доступ к последовательным элементам одним и тем же потоком, по крайней мере, до размера строки кэша (например, 64 байта). Для чтения вы просто экономите на доступе к памяти, используя кэш / локальность данных более эффективно - для записи это еще хуже, потому что ложное совместное использование отскочит вокруг строки кэша между ядрами. Однако важно признать, что речь идет не о реальном доступе к данным - это происходит в std::inner_product
и то же самое для обеих версий. Если бы фактический доступ к данным был в таком шаблоне с чередованием потоков, производительность была бы намного ниже, чем на 40%.
Теперь его легко избежать и проверить, помогает ли это:
const unsigned steps = static_cast<unsigned>(std::size(mat));
auto f = [&](unsigned id) {
const auto chunk_size = 1 + ((steps - 1) / N);
const auto max = std::min(chunk_size * (id + 1), steps);
for (unsigned i = chunk_size * id; i < max; i++)
{
out[i] = std::inner_product(begin(mat[i]), end(mat[i]), begin(inp), 0.0);
}
};
В моей конфигурации это устраняет все различия в производительности между версиями.
Если вы по-прежнему видите разницу в производительности вашей системы, я бы порекомендовал использовать подходящий инструмент для анализа производительности. Я не знаком с вашей экосистемой, поэтому не могу давать никаких рекомендаций, но важно не догадываться о производительности.
Обратите внимание, что std::vector<std::vector<>>
не является хорошей структурой данных для высокопроизводительного доступа к данным / умножения матриц. Вы не приблизитесь к производительности высоко оптимизированной библиотеки, использующей непрерывную память для матрицы.