【问题标题】:Matrix multiplication sequential vs parallel performance test矩阵乘法顺序与并行性能测试
【发布时间】:2015-04-16 03:46:14
【问题描述】:

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

这是对从 50 到 1500 的方阵进行矩阵乘法的性能测试。结果是 10 次运行的平均值。测量值由线条上的点表示,线条本身是平滑的。 如您所见,顺序函数和并行函数的行交叉了两次。实际上,是 3 次,但第一次大约是 60-80 的矩阵大小,因此在此图表中不可见。这很正常,因为线程会引入开销,所以快速函数按顺序运行会更好。

另外两个路口正常吗?我在 500-700 区域进行了多次测量,这种行为似乎很普遍。

我认为可以参与其中的事情:操作系统线程管理、JVM 线程管理、一些线程池特定的行为、英特尔超线程(因为我的机器中有一个英特尔 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);
    }
}

数据数组中的值稍后除以“测量值”以获得平均值。

乘数单位:

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;
        }
    }
}

【问题讨论】:

  • 我想知道矩阵大小为 650 时还会发生什么;也许矩阵大小达到某种缓存限制?类似的轮廓变化似乎发生在并行矩阵大小为 500 处。
  • @MarkoTopolnik 我已经包含了相关代码,但几乎所有这些都是相关的
  • 我可能会创建一个jmh 基准。编写自定义基准测试代码是出了名的不可靠。

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


【解决方案1】:

两句话:

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-08-21
    • 1970-01-01
    • 2021-03-08
    • 1970-01-01
    • 2015-02-03
    相关资源
    最近更新 更多