为什么字节添加性能如此不可预测?

ski*_*iwi 18 java benchmarking

几个小时前我回答了另一个Stack Overflow问题,结果非常令人惊讶.答案可以在这里找到.答案是/部分错误,但我觉得专注于字节添加.

严格来说,它实际上是字节到长的添加.

这是我一直使用的基准代码:

public class ByteAdditionBenchmark {
    private void start() {
        int[] sizes = {
            700_000,
            1_000,
            10_000,
            25_000,
            50_000,
            100_000,
            200_000,
            300_000,
            400_000,
            500_000,
            600_000,
            700_000,
        };

        for (int size : sizes) {
            List<byte[]> arrays = createByteArrays(size);
            //Warmup
            arrays.forEach(this::byteArrayCheck);
            benchmark(arrays, this::byteArrayCheck, "byteArrayCheck");
        }
    }

    private void benchmark(final List<byte[]> arrays, final Consumer<byte[]> method, final String name) {
        long start = System.nanoTime();
        arrays.forEach(method);
        long end = System.nanoTime();
        double nanosecondsPerIteration = (end - start) * 1d / arrays.size();
        System.out.println("Benchmark: " + name + " / iterations: " + arrays.size() + " / time per iteration: " + nanosecondsPerIteration + " ns");
    }

    private List<byte[]> createByteArrays(final int amount) {
        Random random = new Random();
        List<byte[]> resultList = new ArrayList<>();
        for (int i = 0; i < amount; i++) {
            byte[] byteArray = new byte[4096];
            byteArray[random.nextInt(4096)] = 1;
            resultList.add(byteArray);
        }
        return resultList;
    }

    private boolean byteArrayCheck(final byte[] array) {
        long sum = 0L;
        for (byte b : array) {
            sum += b;
        }
        return (sum == 0);
    }

    public static void main(String[] args) {
        new ByteAdditionBenchmark().start();
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我得到的结果:

基准:byteArrayCheck/iterations:700000 /每次迭代的时间:50.26538857142857 ns
基准:byteArrayCheck/iterations:1000 /每次迭代的时间:20.12 ns
基准:byteArrayCheck/iterations:10000 /每次迭代的时间:9.1289 ns
基准:byteArrayCheck/iterations:25000 /每次迭代的时间:10.02972 ns
基准:byteArrayCheck/iterations:50000 /每次迭代的时间:9.04478 ns
基准:byteArrayCheck/iterations:100000 /每次迭代的时间:18.44992 ns
基准:byteArrayCheck/iterations:200000 /每次迭代的时间:15.48304 ns
基准: byteArrayCheck/iterations:300000 /每次迭代的时间:15.806353333333334 ns
基准:byteArrayCheck/iterations:400000 /每次迭代的时间:16.923685 ns
基准:byteArrayCheck/iterations:500000 /每次迭代的时间:16.131066 ns
基准:byteArrayCheck/iterations:600000 /次迭代:16.435461666666665 ns
基准:byteArrayCheck/iterations:700000 /每次迭代的时间:17.107615714285714 n 小号

据我所知,在开始吐出基准测试数据之前,JVM在首次700000次迭代后已经完全预热.

那怎么可能,尽管热身,性能仍然是不可预测的?几乎直接在预热后,字节加法变得非常快,但在此之后它似乎再次收敛到每次加法的标称16 ns.

这些测试是在配备Intel i7 3770库存时钟和16 GB RAM的PC上运行的,因此我不能超过700000次迭代.如果重要的话,它在Windows 8.1 64位上运行.

根据raphw的建议,事实证明JIT正在优化所有东西.

因此,我用以下内容替换了基准测试方法:

private void benchmark(final List<byte[]> arrays, final Predicate<byte[]> method, final String name) {
    long start = System.nanoTime();
    boolean someUnrelatedResult = false;
    for (byte[] array : arrays) {
        someUnrelatedResult |= method.test(array);
    }
    long end = System.nanoTime();
    double nanosecondsPerIteration = (end - start) * 1d / arrays.size();
    System.out.println("Result: " + someUnrelatedResult);
    System.out.println("Benchmark: " + name + " / iterations: " + arrays.size() + " / time per iteration: " + nanosecondsPerIteration + "ns");
}
Run Code Online (Sandbox Code Playgroud)

这将确保它无法优化,测试结果也显示出来(为清晰起见省略了结果打印):

基准:byteArrayCheck/iterations:700000 /每次迭代的时间:1658.2627914285715 ns
基准:byteArrayCheck/iterations:1000 /每次迭代的时间:1241.706 ns
基准:byteArrayCheck/iterations:10000 /每次迭代的时间:1215.941 ns
基准:byteArrayCheck/iterations:25000 /每次迭代的时间:1332.94656 ns
基准:byteArrayCheck/iterations:50000 /每次迭代的时间:1456.0361 ns
基准:byteArrayCheck/iterations:100000 /每次迭代的时间:1753.26777 ns
基准:byteArrayCheck/iterations:200000 /每次迭代的时间:1756.93283 ns
基准: byteArrayCheck/iterations:300000 /每次迭代的时间:1762.9992266666666 ns
基准:byteArrayCheck/iterations:400000 /每次迭代的时间:1806.854815 ns
基准:byteArrayCheck/iterations:500000 /每次迭代的时间:1784.09091 ns
基准:byteArrayCheck/iterations:600000 /次迭代:1804.6096366666666 ns
基准:byteArrayCheck/iterations:700000 /每次迭代的时间:181 1.0597585714286 ns

我会说这些结果在计算时间方面看起来更有说服力.但是,我的问题仍然存在.随机时间重复测试,相同的模式仍然是具有较少迭代次数的基准测试比具有更多迭代次数的基准测试更快,尽管它们似乎稳定在100,000次迭代或更低的某个地方.

解释是什么?

Raf*_*ter 19

结果的原因是你实际上并不知道你在测量什么.Java的即时编译器肯定会查看您的代码,而您可能只是在测量任何东西.

编译器非常聪明,可以确定您List<byte[]>实际上并未使用任何东西.因此,它最终将从正在运行的应用程序中删除所有相关代码.因此,您的基准测试最有可能测量越来越空的应用程序.

所有这些问题的答案总是如此:在我们实际查看有效基准之前,不值得进行讨论.诸如JMH(我可以推荐)之类的基准线束知道一个叫做黑洞的概念.黑洞是为了混淆即时编译器,以便认为计算值实际上用于某些东西,即使它不是.使用这样的黑洞,否则将保留作为无操作删除的代码.

本土基准测试的另一个典型问题是优化的循环.同样,即时编译器会注意到循环导致任何迭代的计算相同,因此将完全删除循环.使用(质量)基准测试工具,您只会建议运行多个循环而不是硬编码.这样,线束可以处理欺骗编译器.

用JMH写一个基准,你会发现你的测量时间会有很大不同.

关于你的更新:我只能重复一遍.永远不要相信未经证实的基准!查看JVM对代码执行的操作的一种简单方法是运行JITwatch.您的基准测试的主要问题是它忽略了JVM的分析.配置文件是JVM尝试记住代码属性的一种尝试,然后基于其优化.对于您的基准测试,您将不同运行的配置文件混合在一起.然后,JVM必须更新其当前的配置文件,并在运行时重新编译字节代码,这需要花费多少时间.

为了避免这个问题,像JMH这样的安全工具允许您为每个基准测试分配JVM新进程.这是我用一个利用的基准测量的东西:

Benchmark                    Mode   Samples         Mean   Mean error    Units
o.s.MyBenchmark.test100k     avgt        20     1922.671       29.155    ns/op
o.s.MyBenchmark.test10k      avgt        20     1911.152       13.217    ns/op
o.s.MyBenchmark.test1k       avgt        20     1857.205        3.086    ns/op
o.s.MyBenchmark.test200k     avgt        20     1905.360       18.102    ns/op
o.s.MyBenchmark.test25k      avgt        20     1832.663      102.562    ns/op
o.s.MyBenchmark.test50k      avgt        20     1907.488       18.043    ns/op
Run Code Online (Sandbox Code Playgroud)

以下是基于提到的JMH的基准测试的源代码:

@State(Scope.Benchmark)
public class MyBenchmark {

    private List<byte[]> input1k, input10k, input25k, input50k, input100k, input200k;

    @Setup
    public void setUp() {
        input1k = createByteArray(1_000);
        input10k = createByteArray(10_000);
        input25k = createByteArray(25_000);
        input50k = createByteArray(50_000);
        input100k = createByteArray(100_000);
        input200k = createByteArray(200_000);
    }

    private static List<byte[]> createByteArray(int length) {
        Random random = new Random();
        List<byte[]> resultList = new ArrayList<>();
        for (int i = 0; i < length; i++) {
            byte[] byteArray = new byte[4096];
            byteArray[random.nextInt(4096)] = 1;
            resultList.add(byteArray);
        }
        return resultList;
    }

    @GenerateMicroBenchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @OperationsPerInvocation(1_000)
    public boolean test1k() {
        return runBenchmark(input1k, this::byteArrayCheck);
    }

    @GenerateMicroBenchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @OperationsPerInvocation(10_000)
    public boolean test10k() {
        return runBenchmark(input10k, this::byteArrayCheck);
    }

    @GenerateMicroBenchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @OperationsPerInvocation(25_000)
    public boolean test25k() {
        return runBenchmark(input25k, this::byteArrayCheck);
    }

    @GenerateMicroBenchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @OperationsPerInvocation(50_000)
    public boolean test50k() {
        return runBenchmark(input50k, this::byteArrayCheck);
    }

    @GenerateMicroBenchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @OperationsPerInvocation(100_000)
    public boolean test100k() {
        return runBenchmark(input100k, this::byteArrayCheck);
    }

    @GenerateMicroBenchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @OperationsPerInvocation(200_000)
    public boolean test200k() {
        return runBenchmark(input200k, this::byteArrayCheck);
    }

    private static boolean runBenchmark(List<byte[]> arrays, Predicate<byte[]> method) {
        boolean someUnrelatedResult = false;
        for (byte[] array : arrays) {
            someUnrelatedResult |= method.test(array);
        }
        return someUnrelatedResult;
    }

    private boolean byteArrayCheck(final byte[] array) {
        long sum = 0L;
        for (byte b : array) {
            sum += b;
        }
        return (sum == 0);
    }

    public static void main(String[] args) throws RunnerException {
        new Runner(new OptionsBuilder()
                .include(".*" + MyBenchmark.class.getSimpleName() + ".*")
                .forks(1)
                .build()).run();
    }
}
Run Code Online (Sandbox Code Playgroud)