我正在为我在Java中为并行矩阵乘法编写的算法进行性能测试。 我从运行时获取cpu核心数,然后使用线程池划分可用核心之间的计算循环。我测量顺序和并行版本的运行时间,然后在excel图表中显示结果。 我注意到一个奇怪的行为:
这是从大小50到大小1500的平方矩阵的矩阵乘法的性能测试。结果是从10次运行计算的平均值。测量由线上的点表示,并且线本身被平滑。 如您所见,顺序和并行函数的行交叉两次。实际上,三次,但第一次是在矩阵大小60-80附近,因此在此图表中不可见。这是正常的,因为线程会引入开销,因此快速的函数会顺序运行。
其他两个十字路口是否正常?我在500-700区域进行了多次测量,这种行为似乎很普遍。
我认为可以参与其中的事情:操作系统线程管理,JVM线程管理,一些线程池特定的行为,英特尔超线程(因为我的机器中有一个intel i5-3210M cpu)。 然而,似乎不规则行为(至少对我而言)实际上是顺序算法。请注意,直到它达到650的大小,它几乎不会受到任何时间的惩罚。然后它突然变成650的大小。 相比之下,平行曲线似乎非常平滑。
我已经检查了几次算法,我很确定它们没有错误。计算结果是正确的,这是肯定的。 我的函数是在双循环中测量的:外部函数重复测量以便稍后求平均值,而内部函数则逐步增加矩阵大小。 在其中,源矩阵是随机的,运行和测量顺序函数,然后运行和测量并行函数。
图表上的行为是否正常?
主要:
// do n measurements
for (int n = 0; n < measurements; ++n) {
// display progress
System.out.println("Progress: " + (float) n / measurements * 100 + "%");
// single measurement
for (int i = 0, size_n = size; i < steps; ++i, size_n += increment) {
// allocate memory for matrices: source a, source b, result
float[][] src_a_seq = new float[size_n][size_n];
float[][] src_b_seq = new float[size_n][size_n];
float[][] src_a_par = new float[size_n][size_n];
float[][] src_b_par = new float[size_n][size_n];
float[][] res_seq = new float[size_n][size_n];
float[][] res_par = new float[size_n][size_n];
// fill source matrices with random values
miscManager.genRandMatrix(src_a_seq, size_n);
miscManager.genRandMatrix(src_b_seq, size_n);
miscManager.genRandMatrix(src_a_par, size_n);
miscManager.genRandMatrix(src_b_par, size_n);
// create time variables
long before, after, delta_t;
// time measurement, serial multiplication
before = System.nanoTime();
serialMultiplier.mul(src_a_seq, src_b_seq, res_seq, size_n);
after = System.nanoTime();
delta_t = after - before;
// add measurement to data
data[i][0] += delta_t;
// time measurement, parallel multiplication
before = System.nanoTime();
parallelMultiplier.mul(src_a_par, src_b_par, res_par, size_n);
after = System.nanoTime();
delta_t = after - before;
// add measurement to data
data[i][1] += delta_t;
}
}
System.out.println("Progress: 100.0%");
串行乘法:
public void mul(float[][] src_a, float[][] src_b, float[][] res, int size) {
for (int i = 0; i < size; ++i) {
for (int j = 0; j < size; ++j) {
res[i][j] = 0;
for (int k = 0; k < size; k++) {
res[i][j] += src_a[i][k] * src_b[k][j];
}
}
}
}
并行乘法:
public void mul(float[][] src_a, float[][] src_b, float[][] res, int size) {
// calculate data required for labor division
int n = size * size;
int load = n / cpuCoreCount + 1;
int remainder = n % cpuCoreCount;
// create thread pool
ExecutorService taskExecutor = Executors.newFixedThreadPool(cpuCoreCount);
// assign tasks
int m = 0;
int i = 0;
while (i < remainder) {
taskExecutor.execute(new MultiplierUnit(src_a, src_b, res, size, m, m + load));
m += load;
++i;
}
--load;
while (i < cpuCoreCount) {
taskExecutor.execute(new MultiplierUnit(src_a, src_b, res, size, m, m + load));
m += load;
++i;
}
// wait for tasks to finish
taskExecutor.shutdown();
try {
taskExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
System.out.println("error: thread pool interrupted exception");
System.exit(-1);
}
}
数据数组中的值后来除以&#34;测量值&#34;得到平均值。
MultiplierUnit:
public class MultiplierUnit implements Runnable {
// source a, source b, result
private final float[][] src_a, src_b, res;
// matrix dimensions, first entry to execute, last entry to execute
private final int size, first, last;
public MultiplierUnit(float[][] src_a, float[][] src_b, float[][] res,
int size, int first, int last) {
this.src_a = src_a;
this.src_b = src_b;
this.res = res;
this.size = size;
this.first = first;
this.last = last;
}
// parallel multiplication
@Override
public void run() {
// index setup
int i = first / size;
int j = first % size;
int n = first;
// computation
while (n < last) {
while (j < size && n < last) {
res[i][j] = 0;
for (int k = 0; k < size; k++) {
res[i][j] += src_a[i][k] * src_b[k][j];
}
++n;
++j;
}
j = 0;
++i;
}
}
}
答案 0 :(得分:2)
几句话:
要消除并行计算中的大量固定开销,您必须将ExecutorService
作为单例并重用它。这本身可以解释图表中并行计算线的行为;
从多个线程写入相同的数组容易受到 false sharing 的影响,其中CPU缓存因写入冲突而不堪重负。然后,这将在图表中显示为变形;
而不是ExecutorService
你应该考虑一种基于Fork / Join框架的方法,它将更有效地分割工作,并且通过正确的方法,可以消除错误共享(尽管通过做一些数组复制,但这可以得到回报)。