2013-02-11 145 views
4

我寫了一些Naiive GEMM代碼,我想知道爲什麼它比等效的單線程GEMM代碼慢得多。多線程GEMM比單線程慢嗎?

使用200x200矩陣,單線程:7ms,多線程:108ms,CPU:3930k,線程池中有12個線程。

template <unsigned M, unsigned N, unsigned P, typename T> 
    static Matrix<M, P, T> multiply(const Matrix<M, N, T> &lhs, const Matrix<N, P, T> &rhs, ThreadPool & pool) 
    { 
     Matrix<M, P, T> result = {0}; 

     Task<void> task(pool); 
     for (auto i=0u; i<M; ++i) 
      for (auto j=0u; j<P; j++) 
       task.async([&result, &lhs, &rhs, i, j](){ 
        T sum = 0; 
        for (auto k=0u; k < N; ++k) 
         sum += lhs[i * N + k] * rhs[k * P + j]; 
        result[i * M + j] = sum; 
      }); 

     task.wait(); 

     return std::move(result); 
    } 
+0

我希望你總共測量了minimim 5+秒以上的長時間重複運行,平均運行時間是7ms和128ms?否則你的時間可能會不準確,不是很有意義。 – IvoTops 2013-02-11 12:37:14

回答

3

我沒有GEMM的經驗,但您的問題似乎與所有類型的多線程場景中出現的問題有關。

當使用多線程,你介紹了幾個潛在的額外開銷,其中最常見的,通常是

  1. 創建/開始/結束線程
  2. 上下文切換時(線程數)的清理>(CPU核數)資源
  3. 鎖定,等待以獲得鎖
  4. 緩存同步發出

項目2.和3.在您的示例中可能不起作用:您正在12(超線程)內核上使用12個線程,並且您的算法不涉及鎖定。

但是,1.可能與您的情況有關:您正在創建總計40000個線程,每個線程將相乘和相加200個值。我建議嘗試一個不太精細的線程,也許只能在第一個循環之後分割。不要把問題分成小於必要的小塊是個好主意。

另外4.很可能對你的情況很重要。在將結果寫入數組時(因爲每個線程正在寫入自己的索引位置),當您沒有遇到競爭條件時,您很可能會引發大量緩存同步。

「爲什麼?」你可能會想,因爲你正在寫內存中的不同地方。這是因爲典型的CPU高速緩存是按高速緩存行組織的,在目前的Intel和AMD CPU型號中,長度爲64個字節。這是可以用於從緩存轉移到緩存的最小尺寸,當發生改變時。既然所有的CPU核心都在讀取和寫入相鄰的內存字,那麼無論何時寫入4個字節(或8個,取決於所使用的數據類型的大小),都會導致所有內核之間的64字節同步。

如果內存不是問題,您可以簡單地用「虛擬」數據「填充」每個輸出數組元素,以便每個緩存行只有一個輸出元素。如果您使用4字節數據類型,則這意味着要爲每個1個實際數據元素跳過15個數組元素。緩存問題也會隨着線程的細化而提高,因爲每個線程都會在內存中訪問自己的連續區域,而不會干擾其他線程的內存。

編輯:由Herb薩特更詳細的說明(C的大師++的一個)可以在這裏找到:http://www.drdobbs.com/parallel/maximize-locality-minimize-contention/208200273

EDIT2:BTW,它的建議,以避免return語句std::move,因爲這可能會在這是標準現在要求自動發生的返回值優化和複製限制規則的方式。請參閱Is returning with `std::move` sensible in the case of multiple return statements?

+0

至於#1我保持我的線程在程序期間保持活動狀態,所以創建成本是一次性的。至於開始和結束線程,成本是對條件變量的調用,以喚醒線程和每個線程的互斥鎖以從任務容器中獲取任務。也就是說,通過在第一次循環之後將您的建議分開,時序會大大改善(500x500矩陣爲40ms,單線程版本爲114ms)。它不是我希望的12倍加速 – aCuria 2013-02-13 11:11:24

3

多線程意味着始終同步,上下文切換,函數調用。這一切都會增加CPU成本並增加成本,您可以花在主要任務本身上。

如果您只有第三個嵌套循環,您可以保存所有這些步驟,並且可以進行內聯計算而不是子例程,您必須在其中設置堆棧,調用,切換到另一個線程,返回結果並切換回到主線程。

多線程僅適用於與主任務相比這些成本較小的情況。我想,當矩陣大於200x200時,你會看到更好的多線程結果。

1

一般來說,多線程非常適用於花費大量時間的任務,最有利的是因爲複雜性而不是設備訪問。您向我們展示的循環需要簡短執行纔能有效地並行化。

你必須記住,線程創建有很多開銷。同步還有一些(但很少)的開銷。