2012-04-04 104 views
11

我在玩Go語言併發,發現了一些對我來說有點不透明的東西。多線程程序沒有加速

我寫並行矩陣乘法,即,每個任務計算乘積矩陣的單個行,相應行和源矩陣的列相乘。

這裏是Java程序

public static double[][] parallelMultiply(int nthreads, final double[][] m1, final double[][] m2) { 
    final int n = m1.length, m = m1[0].length, l = m2[0].length; 
    assert m1[0].length == m2.length; 

    double[][] r = new double[n][]; 

    ExecutorService e = Executors.newFixedThreadPool(nthreads); 
    List<Future<double[]>> results = new LinkedList<Future<double[]>>(); 
    for (int ii = 0; ii < n; ++ii) { 
     final int i = ii; 
     Future<double[]> result = e.submit(new Callable<double[]>() { 
      public double[] call() throws Exception { 
       double[] row = new double[l]; 
       for (int j = 0; j < l; ++j) { 
        for (int k = 0; k < m; ++k) { 
         row[j] += m1[i][k]*m2[k][j]; 
        } 
       } 
       return row; 
      } 
     }); 
     results.add(result); 
    } 
    try { 
     e.shutdown(); 
     e.awaitTermination(1, TimeUnit.HOURS); 
     int i = 0; 
     for (Future<double[]> result : results) { 
      r[i] = result.get(); 
      ++i; 
     } 
    } catch (Exception ex) { 
     ex.printStackTrace(); 
     return null; 
    } 

    return r; 
} 

,這是圍棋程序

type Matrix struct { 
    n, m int 
    data [][]float64 
} 

func New(n, m int) *Matrix { 
    data := make([][]float64, n) 
    for i, _ := range data { 
     data[i] = make([]float64, m) 
    } 
    return &Matrix{n, m, data} 
} 

func (m *Matrix) Get(i, j int) float64 { 
    return m.data[i][j] 
} 

func (m *Matrix) Set(i, j int, v float64) { 
    m.data[i][j] = v 
} 

func MultiplyParallel(m1, m2 *Matrix) *Matrix { 
    r := New(m1.n, m2.m) 

    c := make(chan interface{}, m1.n) 
    for i := 0; i < m1.n; i++ { 
     go func(i int) { 
      innerLoop(r, m1, m2, i) 
      c <- nil 
     }(i) 
    } 

    for i := 0; i < m1.n; i++ { 
     <-c 
    } 

    return r 
} 

func innerLoop(r, m1, m2 *Matrix, i int) { 
    for j := 0; j < m2.m; j++ { 
     s := 0.0 
     for k := 0; k < m1.m; k++ { 
      s = s + m1.Get(i, k) * m2.Get(k, j) 
     } 
     r.Set(i, j, s) 
    } 
} 

當我用Java程序來確定nthreads = 1,來確定nthreads = 2有我的雙核近一倍加速N450凌動上網本。 當我使用GOMAXPROCS = 1和GOMAXPROCS = 2的Go程序時,根本沒有加速!

儘管Java代碼使用額外的存儲空間Future秒,然後collectes它們的值的結果矩陣,而不是在工人的代碼(這就是圍棋的版本一樣)直接序列更新,它執行快多了幾個核心比Go版本。

特別有趣的是Go版本與GOMAXPROCS = 2加載兩個核心(htop在程序工作時在兩個處理器上顯示100%負載),但計算時間與GOMAXPROCS = 1相同(htop顯示100%只有在這種情況下的一個核心)。另一個值得關注的問題是即使在簡單的單線程乘法中,Java程序的速度也要快於Go程序,但這並不意外(考慮到here的基準)並且不應該影響多核性能乘數。

我在這裏做錯了什麼?有沒有辦法加快Go程序?

UPD: 看來,我發現我在做什麼錯誤。我正在使用System.currentTimeMillis()檢查java程序的時間,並使用time shell命令執行Go程序。我錯誤地將來自zsh輸出的「用戶」時間視爲程序工作時間,而不是「全部」時間。現在,我仔細檢查所有的運算速度,這讓我幾乎翻倍加速太(雖然它比Java的slighlty較小):

% time env GOMAXPROCS=2 ./4-2-go -n 500 -q 
env GOMAXPROCS=2 ./4-2-go -n 500 -q 22,34s user 0,04s system 99% cpu 22,483 total 
% time env GOMAXPROCS=2 ./4-2-go -n 500 -q -p 
env GOMAXPROCS=2 ./4-2-go -n 500 -q -p 24,09s user 0,10s system 184% cpu 13,080 total 

看來我得更加周到。

仍然Java程序給同一案件五次更少的時間。但是我認爲這是另一個問題。

+0

我編譯和運行你的代碼,並沒有注意到這一點。相反,對於一個1000x1000矩陣的乘法,我看到了預期的加速。你應該發佈一個完整的程序,並提供你如何計時的細節。 – Sonia 2012-04-04 18:56:06

+0

謝謝你的回答。請參閱我的問題更新。 – 2012-04-05 07:18:22

回答

10

您可能遇到的假共享的影響。簡而言之,如果兩條數據碰巧落入同一CPU高速緩存線,修改這些在不同CPU內核上執行的線程中的兩條數據將觸發昂貴的高速緩存一致性協議。

這種緩存「乒乓」的是非常難以診斷,並能發生在邏輯上完全不相關的數據,僅僅是因爲他們碰巧被放置在內存足夠接近。 100%的CPU負載是虛假共享的典型代表 - 你的核心確實是正在工作100%,他們只是沒有在你的程序上工作 - 他們正在努力同步他們的緩存。

,在Java程序中你有一個線程專用數據,直到時機成熟時「整合」成最後的結果事實上就是從錯誤共享節省了你。我並不熟悉Go,但根據你自己的話來看,線程直接寫入通用數組,這正是可能引發虛假共享的事情。這是一個例子,說明在多線程環境中完全有效的單線程推理完全相反!

欲瞭解更多關於該主題的深入討論,我熱烈推薦Herb Sutter的文章:Eliminate False Sharing,或者演講:Machine Architecture: Things Your Programming Language Never Told You(及相關的PDF slides)。

+0

非常感謝您的詳細解答。從我的問題更新中可以看出,這可能不是我的情況(除了go程序仍然是java的5倍,並行和非並行),但我仍然會將您的答案標記爲已接受。 – 2012-04-05 07:21:37

+0

@Branko見「錨定」在http://en.wikipedia.org/wiki/List_of_cognitive_biases – 2012-04-06 07:15:55

+0

@Atom這不只是一個有點熟悉情況的本能反應。有跡象表明虛假共享:**零**縮放,100%CPU利用率,單獨與「近」數據。這一切都是在**之前完成的**我意識到這個問題已經被編輯,而且OP承認犯了一個測量錯誤。但即使出現這種錯誤,仍然不夠完美,所以我可能仍然是對的! – 2012-04-06 08:13:59

1

如果你能夠運行Linux的環境中,這些代碼可以使用perf測量錯誤共享效果。

+0

謝謝,那會很有用。不知道這個工具。 – 2012-04-05 07:19:14

0

支持Linux,Windows 32和64同上也有AMD的CodeXLCodeAnalyst。由於適用的性能寄存器不同,它們將比在英特爾處理器上詳細描述在AMD處理器上運行的應用程序的詳細程度。