我正在使用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上网本几乎加倍。 当我使用Go程序与GOMAXPROCS = 1和GOMAXPROCS = 2时,根本没有加速!
即使Java代码为Future
使用了额外的存储空间,然后将它们的值收集到结果矩阵而不是工作程序代码中的直接数组更新(这就是Go版本所做的),它执行多< / em>在几个核心上比Go版本更快。
特别有趣的是带有GOMAXPROCS = 2的Go版本加载两个核心(htop在程序工作时显示100%负载在两个处理器上),但计算时间与GOMAXPROCS = 1相同(htop仅显示100%加载在这种情况下的一个核心)。
另一个问题是,即使在简单的单线程乘法中,Java程序也比Go程序快,但这并不是意料之外的(考虑here的基准)并且不应影响多核性能乘数。
我在这里做错了什么?有没有办法加速Go计划?
UPD:
好像我发现我做错了什么。我使用System.currentTimeMillis()
和Go程序使用time
shell命令检查java程序的时间。我错误地将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程序在同一个案例中给出了五次较少的时间。但这是我认为的另一个问题。
答案 0 :(得分:11)
您可能正在体验虚假分享的影响。简而言之,如果两个数据碰巧落在同一个CPU缓存行上,那么从在不同CPU内核上执行的线程修改这两个数据将触发昂贵的缓存一致性协议。
这种缓存“乒乓”非常难以诊断,并且可能发生在逻辑上完全不相关的数据上,只是因为它们碰巧放在内存中足够接近。 100%的CPU负载是典型的错误共享 - 您的核真的正在100%工作,他们只是不在您的程序上工作 - 他们正在努力同步他们的缓存。
事实上,在Java程序中,您拥有线程专用数据,直到将其“集成”到最终结果中,这样可以避免错误共享。我不熟悉Go,但是根据你自己的说法,线程直接写入公共数组,这正是可能触发错误共享的事情。这是一个完整有效的单线程推理如何在多线程环境中完全相反的例子!
有关该主题的更深入讨论,我热烈推荐Herb Sutter的文章:Eliminate False Sharing或讲座:Machine Architecture: Things Your Programming Language Never Told You(以及相关的PDF slides)。
答案 1 :(得分:1)
如果您能够在Linux环境中运行这些代码,则可以使用perf来衡量错误共享效果。
答案 2 :(得分:0)
对于Linux,Windows 32和ditto 64,还有AMD的CodeXL和CodeAnalyst。由于适用的性能寄存器不同,他们将比在英特尔处理器上运行在AMD处理器上运行的应用程序更详细。