11

私はGo言語の並行性で遊んでいて、私にはちょっと不透明なものを見つけました。

並列行列の乗算を記述しました。つまり、各タスクは、ソース行列の対応する行と列を乗算して、積行列の1行を計算します。

これが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;
}

これはGoプログラムです

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)
    }
}

nthreads=1およびnthreads=2でJavaプログラムを使用すると、デュアルコアN450Atomネットブックの速度がほぼ2倍になります。GOMAXPROCS=1およびGOMAXPROCS=2でGoプログラムを使用すると、スピードアップはまったくありません。

Javaコードはsに追加のストレージを使用Futureし、ワーカーコードで直接配列を更新する代わりに結果マトリックスに値を収集しますが(これはGoバージョンが行うことです)、いくつかのコアではGoバージョンよりもはるかに高速に実行されます。

特に面白いのは、GOMAXPROCS = 2のGoバージョンは両方のコアをロードします(htopはプログラムの動作中に両方のプロセッサに100%の負荷を表示します)が、計算時間はGOMAXPROCS = 1の場合と同じです(htopは1つのコアにのみ100%の負荷を表示します)この場合)。

もう1つの懸念は、単純なシングルスレッド乗算でもJavaプログラムがGo oneよりも高速であるということですが、これはまったく予期しないことではなく(ここからのベンチマークを考慮に入れると)、マルチコアパフォーマンス乗算器に影響を与えることはありません。

私がここで間違っていることは何ですか?Goプログラムを高速化する方法はありますか?

UPD:私が間違っていることを見つけたようです。シェルコマンドを使ってJavaプログラムSystem.currentTimeMillis()とGoプログラムの時間をチェックしていました。timezsh出力からの「ユーザー」時間を「合計」ではなくプログラム作業時間として誤って取得しました。今、私は計算速度を再確認しました、そしてそれは私にもほぼ2倍の速度を与えます(それはJavaのものよりわずかに遅いですが):

% 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プログラムは、同じケースで5分の1の時間を提供します。しかし、それは私が思う別の質問の問題です。

4

3 に答える 3

11

あなたはおそらく偽共有の影響を経験しています。一言で言えば、2つのデータがたまたま同じCPUキャッシュラインにある場合、異なるCPUコアで実行されるスレッドからのこれら2つのデータを変更すると、高価なキャッシュコヒーレンシプロトコルがトリガーされます。

この種のキャッシュ「ピンポン」は診断が非常に難しく、メモリ内に十分近くに配置されているという理由だけで、論理的に完全に無関係なデータで発生する可能性があります。100%のCPU負荷は、偽共有の典型的なものです。コアは実際には100%動作しており、プログラムでは動作していません。キャッシュの同期に取り組んでいます。

Javaプログラムでは、最終結果に「統合」するときまでスレッドプライベートデータがあるという事実が、誤った共有からあなたを救うものです。私はGoに精通していませんが、あなた自身の言葉で判断すると、スレッドは共通配列に直接書き込んでいます。これはまさに、偽共有を引き起こす可能性のある種類のものです。これは、完全に有効なシングルスレッドの推論がマルチスレッド環境で正反対のことを行う方法の例です。

このトピックに関するより詳細な議論については、ハーブサッターの記事:偽共有の排除、または講義:マシンアーキテクチャ:プログラミング言語が決して教えてくれなかったこと(および関連するPDFスライド)を強くお勧めします。

于 2012-04-04T18:53:14.190 に答える
1

Linux環境でこれらのコードを実行できる場合は、perfを使用して偽共有の影響を測定できます。

于 2012-04-04T19:20:11.363 に答える
0

Linux、Windows 32、および同上64の場合、AMDのCodeXLおよびCodeAnalystもあります。適用可能なパフォーマンスレジスタが異なるため、AMDプロセッサで実行されているアプリケーションをIntelのアプリケーションよりもはるかに詳細にプロファイルします。

于 2014-01-16T13:54:47.243 に答える