仿函数上的Java8 HotSpot

时间:2015-08-26 12:10:32

标签: java performance java-8

以下代码对Java8的性能比较是违反直觉的。

import java.util.Arrays;

class Main {
    interface Dgemv {
        void dgemv(int n, double[] a, double[] x, double[] y);
    }

    static final class Dgemv1 implements Dgemv {
        public void dgemv(int n, double[] a, double[] x, double[] y) {
            Arrays.fill(y, 0.0);
            for (int j = 0; j < n; ++j)
                dgemvImpl(x[j], j * n, n, a, y);
        }

        private void dgemvImpl(final double xj, final int aoff,
                final int n, double[] a, double[] y) {
            for (int i = 0; i < n; ++i)
                y[i] += xj * a[i + aoff];
        }
    }

    static final class Dgemv2 implements Dgemv {
        public void dgemv(int n, double[] a, double[] x, double[] y) {
            Arrays.fill(y, 0.0);
            for (int j = 0; j < n; ++j)
                new DgemvImpl(x[j], j * n).dgemvImpl(n, a, y);
        }

        private static final class DgemvImpl {
            private final double xj;
            private final int aoff;

            DgemvImpl(double xj, int aoff) {
                this.xj = xj;
                this.aoff = aoff;
            }

            void dgemvImpl(final int n, double[] a, double[] y) {
                for (int i = 0; i < n; ++i)
                    y[i] += xj * a[i + aoff];
            }
        }
    }

    static long runDgemv(long niter, int n, Dgemv op) {
        double[] a = new double[n * n];
        double[] x = new double[n];
        double[] y = new double[n];
        long start = System.currentTimeMillis();
        for (long i = 0; i < niter; ++i) {
            op.dgemv(n, a, x, y);
        }
        return System.currentTimeMillis() - start;
    }

    static void testDgemv(long niter, int n, int mode) {
        Dgemv op = null;
        switch (mode) {
        case 1: op = new Dgemv1(); break;
        case 2: op = new Dgemv2(); break;
        }
        runDgemv(niter, n, op);
        double sec = runDgemv(niter, n, op) * 1e-3;
        double gflps = (2.0 * n * n) / sec * niter  * 1e-9;
        System.out.format("mode=%d,N=%d,%f sec,%f GFLPS\n", mode, n, sec, gflps);
    }

    public static void main(String[] args) {
        int n = Integer.parseInt(args[0]);
        long niter = ((long) 1L << 32) / (long) (2 * n * n);
        testDgemv(niter, n, 1);
        testDgemv(niter, n, 2);
    }
}

Java8(1.8.0_60)和Core i5 4570(3.2GHz)的结果是:

$ java -server Main
mode=1,N=500,1.239000 sec,3.466102 GFLPS
mode=2,N=500,1.100000 sec,3.904091 GFLPS

在Java7(1.7.0_80)上进行相同计算的结果是:

mode=1,N=500,1.291000 sec,3.326491 GFLPS
mode=2,N=500,1.491000 sec,2.880282 GFLPS

似乎HotSpot比静态方法更热切地优化仿函数,无论额外的复杂性如何。

有人能解释为什么Dgemv2运行得更快吗?

编辑:

来自openjdk/jmh的更准确的基准统计数据。 (谢谢Kayaman的评论)

N = 500/1秒x 20次预热/ 1秒x 20次迭代(10套)

Java 8(1.8.0_60)

Benchmark               Mode  Cnt     Score   Error  Units
MyBenchmark.runDgemv1  thrpt  200  6965.459 ? 2.186  ops/s
MyBenchmark.runDgemv2  thrpt  200  7329.138 ? 1.598  ops/s

Java 7(1.7.0_80)

Benchmark               Mode  Cnt     Score   Error  Units
MyBenchmark.runDgemv1  thrpt  200  7344.570 ? 1.994  ops/s
MyBenchmark.runDgemv2  thrpt  200  7358.988 ? 2.189  ops/s

从这些统计数据来看,Java 8 HotSpot似乎没有优化静态方法。但我注意到的另一件事是,在一些预热部分,性能提高了10%。挑选极端情况:

N = 500/1秒x 8次预热/ 1秒x 8次迭代(10套)

Java 8(1.8.0_60)

Benchmark               Mode  Cnt     Score    Error  Units
MyBenchmark.runDgemv1  thrpt   80  6952.315 ? 11.483  ops/s
MyBenchmark.runDgemv2  thrpt   80  7719.843 ? 66.773  ops/s

Dgemv2的9秒到15秒之间的迭代率始终优于长期平均值约5%。随着优化程序的继续,HotSpot似乎并不总能产生更快的代码。

我目前的猜测是Dgemv2中的Functor对象实际上扰乱了HotSpot优化过程,导致执行代码比“完全优化的代码”更快。

我仍然不清楚为什么会这样。欢迎任何答案和评论。

0 个答案:

没有答案