[英]Matrix multiplication, KIJ order, Parallel version slower than non-parallel
我有關於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和各種計划版本 - 動態,靜態,到目前為止還沒有它等)。
有人能幫助我理解我做錯了什么嗎? 是因為我使用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循環並檢查性能的提高。 我的問題是我期望(ed)至少快一點的執行速度(比我現在最快的速度快5到10%),即使它是並行的I循環(也不能用K循環,因為我將得到不正確的結果,因為它是matrix_r [i] [j])。
這些是我在使用上面顯示的代碼時獲得的結果(我正在進行數百次計算並得到平均時間):
注意:這個答案不是關於如何從循環次序中獲得最佳性能或如何並行化它,因為我認為由於幾個原因它是次優的。 我將嘗試提供一些關於如何改進訂單(並將其並行化)的建議。
循環順序
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。
不完美的縮放,但作為一個比串行代碼更快的開始。
OpenMP實現創建了一個線程池(盡管OpenMP標准沒有規定線程池,我已經看到過OpenMP的每個實現),因此每次輸入並行區域時都不必創建和銷毀線程。 然而,每個並行區域之間存在障礙,因此所有線程都必須同步。 並行區域之間的fork連接模型可能存在一些額外的開銷。 因此,即使不必重新創建線程,它們仍然必須在並行區域之間進行初始化。 更多細節可以在這里找到。
為了避免進入並行區域,我建議建立在最外層循環的並行區域,但這樣做的內循環工作共享過的開銷, 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
。
請記住,出於緩存目的,循環的最佳排序將是最慢的 - >最快。 在你的情況下,這意味着我,K,L順序。 如果您的序列代碼沒有被編譯器從KIJ-> 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];
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.