矩阵乘法序列与并行性能测试

时间:2015-02-15 15:35:42

标签: java performance matrix parallel-processing performance-testing

我正在为我在Java中为并行矩阵乘法编写的算法进行性能测试。 我从运行时获取cpu核心数,然后使用线程池划分可用核心之间的计算循环。我测量顺序和并行版本的运行时间,然后在excel图表中显示结果。 我注意到一个奇怪的行为: enter image description here

这是从大小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;
        }
    }
}

1 个答案:

答案 0 :(得分:2)

几句话:

  1. 要消除并行计算中的大量固定开销,您必须将ExecutorService作为单例并重用它。这本身可以解释图表中并行计算线的行为;

  2. 从多个线程写入相同的数组容易受到 false sharing 的影响,其中CPU缓存因写入冲突而不堪重负。然后,这将在图表中显示为变形;

  3. 而不是ExecutorService你应该考虑一种基于Fork / Join框架的方法,它将更有效地分割工作,并且通过正确的方法,可以消除错误共享(尽管通过做一些数组复制,但这可以得到回报)。