我创造了一个非常简单的场景,我认识到一种我无法理解的非常奇怪的行为。
在以下链接中,我创建了一个顺序实现: 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();
有谁知道为什么会这样?
编辑:此问题与其他问题不同,因为其原因可能是缓存问题。我该如何解决这个缓存问题?
答案 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]
的机会。
此外,内存中的顺序读取可能会被硬件优化为突发,并且通过预取逻辑最容易预测。