Java顺序实现比并行实现快4倍

时间:2015-05-05 16:23:07

标签: java multithreading parallel-processing threadpool executorservice

我创造了一个非常简单的场景,我认识到一种我无法理解的非常奇怪的行为。

在以下链接中,我创建了一个顺序实现: http://ideone.com/B8JYeA 基本上有几个固定大小的大阵列。算法迭代它们并改变值。

for(int i = 0; i < numberOfCells; i++) {
    h0[i] =  h0[i] + 1;
    h1[i] =  h1[i] + 1;
    h2[i] =  h2[i] + 1;
    h3[i] =  h3[i] + 1;
    h4[i] =  h4[i] + 1;
}

如果我在工作站上运行它需要大约5秒钟。

我在并行版本中实现了相同的功能。 8个线程同时运行它。代码应该是线程安全的,并且线程之间没有依赖关系。

但是我的工作站上的代码运行速度仍然慢了4倍: http://ideone.com/yfwVmr

final int numberOfThreads = Runtime.getRuntime().availableProcessors();

ExecutorService exec = Executors.newFixedThreadPool(numberOfThreads);

for(int thread = 0; thread < numberOfThreads; thread++) {
    final int threadId = thread;
    exec.submit(new Runnable() {
        @Override
        public void run() {
            for(int i = threadId; i < numberOfCells; i += numberOfThreads) {
                h0[i] =  h0[i] + 1;
                h1[i] =  h1[i] + 1;
                h2[i] =  h2[i] + 1;
                h3[i] =  h3[i] + 1;
                h4[i] =  h4[i] + 1;
            }
        }
    });
}

exec.shutdown();

有谁知道为什么会这样?

编辑:此问题与其他问题不同,因为其原因可能是缓存问题。我该如何解决这个缓存问题?

2 个答案:

答案 0 :(得分:4)

最大的开销是启动和停止线程所花费的时间。如果我将数组的大小从10000减少到10,则需要大约相同的时间。

如果保留线程池,并将每个线程的工作分成写入本地数据集,则在具有6个内核的计算机上快4倍。

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;


public class ParallelImplementationOptimised {
    static final int numberOfThreads = Runtime.getRuntime().availableProcessors();
    final ExecutorService exec = Executors.newFixedThreadPool(numberOfThreads);

    private int numberOfCells;

    public ParallelImplementationOptimised(int numberOfCells) {
        this.numberOfCells = numberOfCells;
    }

    public void update() throws ExecutionException, InterruptedException {

        List<Future<?>> futures = new ArrayList<>();
        for(int thread = 0; thread < numberOfThreads; thread++) {
            final int threadId = thread;
            futures.add(exec.submit(new Runnable() {
                @Override
                public void run() {
                    int num = numberOfCells / numberOfThreads;
                    double[] h0 = new double[num],
                            h1 = new double[num],
                            h2 = new double[num],
                            h3 = new double[num],
                            h4 = new double[num],
                            h5 = new double[num],
                            h6 = new double[num],
                            h7 = new double[num],
                            h8 = new double[num],
                            h9 = new double[num];
                    for (int i = 0; i < num; i++) {
                        h0[i] = h0[i] + 1;
                        h1[i] = h1[i] + 1;
                        h2[i] = h2[i] + 1;
                        h3[i] = h3[i] + 1;
                        h4[i] = h4[i] + 1;
                        h5[i] = h5[i] + 1;
                        h6[i] = h6[i] + 1;
                        h7[i] = h7[i] + 1;
                        h8[i] = h8[i] + 1;
                        h9[i] = h9[i] + 1;
                    }
                }
            }));
        }
        for (Future<?> future : futures) {
            future.get();
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        ParallelImplementationOptimised si = new ParallelImplementationOptimised(10);

        long start = System.currentTimeMillis();

        for (int i = 0; i < 10000; i++) {
            if(i % 1000 == 0) {
                System.out.println(i);
            }
            si.update();
        }

        long stop = System.currentTimeMillis();
        System.out.println("Time: " + (stop - start));
        si.exec.shutdown();
    }

}

SequentialInmplementation 3.3秒 ParallelImplementationOptimised 0.8秒。

您似乎正在写入同一缓存行上的相同数据。这意味着数据必须通过L3缓存未命中传递,这比访问L1缓存花费的时间长20倍。我建议您尝试完全独立的数据结构,这些数据结构至少相隔128个字节,以确保您没有触及相同的缓存行。

注意:即使您打算完成覆盖整个缓存行,x64 CPU也会首先提取缓存行的先前值。

另一个问题可能是

  

为什么这20倍慢?

抓取高速缓存行的CPU核心可能有两个运行超线程的线程(即两个线程可以在本地访问数据),并且CPU在丢失高速缓存行之前可能会绕过循环几次CPU核心要求它。这意味着每次访问或每次循环都不会产生20倍的惩罚,但通常会导致结果慢得多。

答案 1 :(得分:0)

不是一个真正的答案,但是:首先,我会尝试尽可能保持数据访问的位置:

final int numberOfCellsPerThread = numberOfCells / numberOfThreads;

public void run() {
    final int start = threadId * numberOfCellsPerThread;
    final int end = start + numberOfCellsPerThread;
    for(int i = start; i < end; i++) {
        h0[i] =  h0[i] + 1;
        h1[i] =  h1[i] + 1;
        h2[i] =  h2[i] + 1;
        h3[i] =  h3[i] + 1;
        h4[i] =  h4[i] + 1;
    }
}

有关地点事项重要性的更多解释,请参阅Why does cache locality matter for array performance?http://en.wikipedia.org/wiki/Locality_of_reference

基本上它只是在可能的情况下使用缓存中已有的数据。由于缓存的大小有限,如果a[i]已经在缓存中,例如由于先前的读取操作,a[i+1]在缓存中的可能性也相当高。例如,至少高于a[i+100]的机会。

此外,内存中的顺序读取可能会被硬件优化为突发,并且通过预取逻辑最容易预测。