我有一个关于paralel编程的学校任务,我遇到了很多问题。 我的任务是创建给定矩阵乘法代码的并行版本并测试其性能(是的,它必须以KIJ顺序):
void multiply_matrices_KIJ()
{
for (int k = 0; k < SIZE; k++)
for (int i = 0; i < SIZE; i++)
for (int j = 0; j < SIZE; j++)
matrix_r[i][j] += matrix_a[i][k] * matrix_b[k][j];
}
这是我到目前为止所提出的:
void multiply_matrices_KIJ()
{
for (int k = 0; k < SIZE; k++)
#pragma omp parallel
{
#pragma omp for schedule(static, 16)
for (int i = 0; i < SIZE; i++)
for (int j = 0; j < SIZE; j++)
matrix_r[i][j] += matrix_a[i][k] * matrix_b[k][j];
}
}
那是我发现让我感到困惑的地方。这个并行版本的代码运行速度比非并行版慢50%。根据矩阵大小,速度的差异只有一点点的变化(测试的SIZE = 128,256,512,1024,2048和各种计划版本 - 动态,静态,到目前为止还没有它等。) / p>
有人可以帮我理解我做错了什么吗?可能是因为我使用了KIJ订单而且使用openMP它不会更快?
我正在使用Visual Studio 2015社区版在Windows 7 PC上工作,在Release x86模式下进行编译(x64也没有帮助)。我的CPU是:英特尔酷睿i5-2520M CPU @ 2,50GHZ(是的,是的,它是一台笔记本电脑,但我在我的家用I7 PC上获得了相同的结果)
我正在使用全局数组:
float matrix_a[SIZE][SIZE];
float matrix_b[SIZE][SIZE];
float matrix_r[SIZE][SIZE];
我将随机(浮点)值分配给矩阵a和b,矩阵r用0填充。
到目前为止,我已经测试了各种矩阵大小的代码(128,256,512,1024,2048等)。对于其中一些,它不适合缓存。 我当前的代码版本如下所示:
void multiply_matrices_KIJ()
{
#pragma omp parallel
{
for (int k = 0; k < SIZE; k++) {
#pragma omp for schedule(dynamic, 16) nowait
for (int i = 0; i < SIZE; i++) {
for (int j = 0; j < SIZE; j++) {
matrix_r[i][j] += matrix_a[i][k] * matrix_b[k][j];
}
}
}
}
}
为了清楚起见,我知道循环的顺序不同,我可以得到更好的结果,但事情就是这样 - 我必须使用KIJ命令。我的任务是并行执行KIJ for循环并检查性能的提高。我的问题是,我希望(编辑)至少快一点的执行速度(比我现在获得的速度快多了5-10%),即使它是并行的I循环(可以&# 39;用K循环来做,因为我会得到不正确的结果,因为它是矩阵_r [i] [j])。
这些是我在使用上面显示的代码时得到的结果(我做了数百次计算并得到了平均时间):
答案 0 :(得分:4)
注意:这个答案不是关于如何从循环次序中获得最佳性能或如何并行化它,因为我认为由于几个原因它是次优的。我会尝试就如何改进订单(并将其并行化)提出一些建议。
循环顺序
OpenMP通常用于在多个CPU上分配工作。因此,您希望最大化每个线程的工作负载,同时最大限度地减少所需的数据和信息传输量。
您希望并行执行最外层循环而不是第二个循环。因此,您希望将r_matrix
索引之一作为外循环索引,以便在写入结果矩阵时避免竞争条件。
接下来就是你想要以内存存储顺序遍历矩阵(具有更快的变化索引作为第二个而不是第一个下标索引)。
您可以使用以下循环/索引顺序来实现两者:
for i = 0 to a_rows
for k = 0 to a_cols
for j = 0 to b_cols
r[i][j] = a[i][k]*b[k][j]
哪里
j
的变化速度超过i
或k
,k
的变化速度超过i
。i
是结果矩阵下标,i
循环可以并行运行以这种方式重新排列multiply_matrices_KIJ
可以提高性能。
我做了一些简短的测试,我用来比较时间的代码是:
template<class T>
void mm_kij(T const * const matrix_a, std::size_t const a_rows,
std::size_t const a_cols, T const * const matrix_b, std::size_t const b_rows,
std::size_t const b_cols, T * const matrix_r)
{
for (std::size_t k = 0; k < a_cols; k++)
{
for (std::size_t i = 0; i < a_rows; i++)
{
for (std::size_t j = 0; j < b_cols; j++)
{
matrix_r[i*b_cols + j] +=
matrix_a[i*a_cols + k] * matrix_b[k*b_cols + j];
}
}
}
}
模仿您的multiply_matrices_KIJ()
功能与
template<class T>
void mm_opt(T const * const a_matrix, std::size_t const a_rows,
std::size_t const a_cols, T const * const b_matrix, std::size_t const b_rows,
std::size_t const b_cols, T * const r_matrix)
{
for (std::size_t i = 0; i < a_rows; ++i)
{
T * const r_row_p = r_matrix + i*b_cols;
for (std::size_t k = 0; k < a_cols; ++k)
{
auto const a_val = a_matrix[i*a_cols + k];
T const * const b_row_p = b_matrix + k * b_cols;
for (std::size_t j = 0; j < b_cols; ++j)
{
r_row_p[j] += a_val * b_row_p[j];
}
}
}
}
实施上述订单。
英特尔i5-2500k上两个 2048x2048 矩阵相乘的时间消耗
mm_kij()
:6.16706s。
mm_opt()
:2.6567s。
给定的顺序还允许外部循环并行化,而不会在写入结果矩阵时引入任何竞争条件:
template<class T>
void mm_opt_par(T const * const a_matrix, std::size_t const a_rows,
std::size_t const a_cols, T const * const b_matrix, std::size_t const b_rows,
std::size_t const b_cols, T * const r_matrix)
{
#if defined(_OPENMP)
#pragma omp parallel
{
auto ar = static_cast<std::ptrdiff_t>(a_rows);
#pragma omp for schedule(static) nowait
for (std::ptrdiff_t i = 0; i < ar; ++i)
#else
for (std::size_t i = 0; i < a_rows; ++i)
#endif
{
T * const r_row_p = r_matrix + i*b_cols;
for (std::size_t k = 0; k < b_rows; ++k)
{
auto const a_val = a_matrix[i*a_cols + k];
T const * const b_row_p = b_matrix + k * b_cols;
for (std::size_t j = 0; j < b_cols; ++j)
{
r_row_p[j] += a_val * b_row_p[j];
}
}
}
#if defined(_OPENMP)
}
#endif
}
每个线程写入单个结果行的位置
英特尔i5-2500k(4个OMP线程)上两个 2048x2048 矩阵相乘的时间消耗
mm_kij()
:6.16706s。
mm_opt()
:2.6567s。
mm_opt_par()
:0.968325s。
不完美缩放,但比串行代码更快。
答案 1 :(得分:1)
OpenMP实现创建了一个线程池(尽管OpenMP标准没有规定线程池,我已经看到过OpenMP的每个实现都这样做),因此每次并行区域都不必创建和销毁线程进入。然而,每个并行区域之间存在障碍,因此所有线程都必须同步。并行区域之间的fork连接模型可能存在一些额外的开销。因此,即使不必重新创建线程,它们仍然必须在并行区域之间进行初始化。可以找到更多详细信息here。
为了避免进入并行区域之间的开销,我建议在最外层循环上创建并行区域,但在i
上的内循环上进行工作共享,如下所示:
void multiply_matrices_KIJ() {
#pragma omp parallel
for (int k = 0; k < SIZE; k++)
#pragma omp for schedule(static) nowait
for (int i = 0; i < SIZE; i++)
for (int j = 0; j < SIZE; j++)
matrix_r[i][j] += matrix_a[i][k] * matrix_b[k][j];
}
使用#pragma omp for
时存在隐含障碍。 nowait
子句删除了障碍。
还要确保使用优化进行编译。在未启用优化的情况下比较性能几乎没有意义。我会使用-O3
。
答案 2 :(得分:0)
请始终牢记,对于缓存目的,循环的最佳排序将是最慢的 - &gt;最快的。在你的情况下,这意味着我,K,L顺序。如果您的序列代码没有被编译器从KIJ-&gt; IKL排序中自动重新排序(假设您有“-O3
”),我会感到非常惊讶。但是,编译器无法使用并行循环执行此操作,因为这会破坏您在并行区域中声明的逻辑。
如果你真的无法对你的循环重新排序,那么你最好的选择可能是重写并行区域以包含最大可能的循环。如果您有OpenMP 4.0,您也可以考虑在最快的维度上使用SIMD矢量化。但是,由于KIJ订购中固有的上述缓存问题,我仍然怀疑你能够打败你的串行代码......
void multiply_matrices_KIJ()
{
#pragma omp parallel for
for (int k = 0; k < SIZE; k++)
{
for (int i = 0; i < SIZE; i++)
#pragma omp simd
for (int j = 0; j < SIZE; j++)
matrix_r[i][j] += matrix_a[i][k] * matrix_b[k][j];
}
}