以下代码对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优化过程,导致执行代码比“完全优化的代码”更快。
我仍然不清楚为什么会这样。欢迎任何答案和评论。