【问题标题】:No speedup in multithread program多线程程序没有加速
【发布时间】:2012-04-18 12:14:00
【问题描述】:

我在玩 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;
}

这是 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 程序时,我的双核 N450 Atom 上网本的速度几乎提高了一倍。 当我使用 GOMAXPROCS=1 和 GOMAXPROCS=2 的 Go 程序时,根本没有加速!

尽管 Java 代码为 Futures 使用了额外的存储空间,然后将它们的值收集到结果矩阵中,而不是在工作代码中直接更新数组(Go 版本就是这样做的),它执行很多 在几个核心上比 Go 版本更快。

特别有趣的是,GOMAXPROCS=2 的 Go 版本会加载两个内核(htop 在程序运行时显示两个处理器上 100% 的负载),但计算时间与 GOMAXPROCS=1 相同(htop 仅显示 100% 的负载在这种情况下在一个核心上)。

另一个问题是,即使在简单的单线程乘法中,Java 程序也比 Go 程序更快,但这并不完全出乎意料(考虑到来自 here 的基准测试)并且不应该影响多核性能倍增器。

我在这里做错了什么?有没有办法加速 Go 程序?

更新: 看来我发现我做错了什么。我使用System.currentTimeMillis() 检查java 程序的时间,使用time shell 命令检查Go 程序的时间。我错误地将 zsh 输出中的“用户”时间作为程序工作时间而不是“总”时间。现在我再次检查了计算速度,它也给了我几乎两倍的加速(虽然它比 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 程序在相同的情况下给出的次数要少五次。但我认为这是另一个问题。

【问题讨论】:

  • 我编译并运行了您的代码,但没有观察到这一点。相反,对于 1000x1000 矩阵的单次乘法,我看到了预期的加速。你应该发布一个完整的程序并详细说明你是如何计时的。
  • 感谢您的回答。请在问题中查看我的更新。

标签: java multithreading performance parallel-processing go


【解决方案1】:

您可能正在经历虚假分享的影响。简而言之,如果两条数据恰好落在同一个 CPU 缓存行上,那么从在不同 CPU 内核上执行的线程修改这两条数据将触发昂贵的缓存一致性协议。

这种缓存“乒乓”非常难以诊断,并且可能发生在逻辑上完全不相关的数据上,只是因为它们恰好在内存中放置得足够近。 100% 的 CPU 负载是错误共享的典型表现 - 您的核心确实在 100% 工作,它们只是没有在您的程序上工作 - 他们正在同步缓存。

在 Java 程序中,您拥有线程私有数据,直到“集成”到最终结果中,这一事实使您免于虚假共享。我对 Go 不熟悉,但是根据您自己的话判断,线程是直接写入公共数组的,这正是可能触发错误共享的那种事情。这是一个完美有效的单线程推理如何在多线程环境中完全相反的示例!

如需对该主题进行更深入的讨论,我强烈推荐 Herb Sutter 的文章:Eliminate False Sharing,或讲座:Machine Architecture: Things Your Programming Language Never Told You(以及相关联的PDF slides)。

【讨论】:

  • 非常感谢您的详细解答。从我对问题的更新来看,这很可能不是我的情况(除了 go 程序仍然比 java 慢 5 倍,并行和非并行),但我仍然会将您的答案标记为已接受。跨度>
  • @Atom 这不仅仅是对模糊熟悉的情况的本能反应。存在虚假共享的明显迹象: 扩展、100% CPU 利用率、单独与“接近”数据。这一切都是在 我意识到问题已被编辑并且 OP 承认犯了测量错误之前完成的。但即使有这个错误,缩放比例仍然不够完美,所以我可能仍然是对的!
  • @googolplex 您的缩放比例仍然不够完美。毕竟,虚假分享可能起了作用。
  • @Branko 在 Go 代码中,最内层循环的每个 m1.m 迭代只有 1 个 r.Set(i, j, s)。当m1.m 大约为 500(如问题中所述)时,错误共享应该不是主要问题。在错误共享的情况下,r.data[i][j] 在 CPU 的缓存中,而不是在内存中 - 因此与处理最内层循环所需的时间相比,可以预期处理冲突会相当快。如果r.data[i][j] 在内存中,那么这是程序使用缓存的有效性问题(因此这不是错误共享问题)。
【解决方案2】:

如果你能够在 Linux 环境下运行这些代码,你可以使用perf 来衡量虚假共享效果。

【讨论】:

  • 谢谢,这很有用。不知道这个工具。
【解决方案3】:

对于 Linux、Windows 32 和同上 64,还有 AMD 的 CodeXLCodeAnalyst。由于适用的性能寄存器不同,他们将比英特尔更详细地分析在 AMD 处理器上运行的应用程序。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-07-02
    • 1970-01-01
    • 1970-01-01
    • 2012-05-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多