Java 为什么封装在函数中的简单检查的效率是直接检查的两倍?

Java 为什么封装在函数中的简单检查的效率是直接检查的两倍?,java,performance,function-call,Java,Performance,Function Call,下面是两个版本的代码,它们计算数组中三元组的数量加起来等于零。一个使用函数调用进行实际测试,另一个在函数体中执行测试 它在性能时间方面表现出有趣的行为。使用函数调用的变量执行速度快两倍。为什么? /** * Find triples of integers, which add up to zero */ public class SumCheck { public static void main(String[] args) { int a = 1000000

下面是两个版本的代码,它们计算数组中三元组的数量加起来等于零。一个使用函数调用进行实际测试,另一个在函数体中执行测试

它在性能时间方面表现出有趣的行为。使用函数调用的变量执行速度快两倍。为什么?

/**
 * Find triples of integers, which add up to zero
 */
public class SumCheck {

    public static void main(String[] args) {

        int a = 1000000;
        int b = 3000;
        int[] input = new int[b];
        for (int i = 0; i < b; i++) {
            input[i] = StdRandom.uniform(-a, a);
        }

        double startTime2 = System.currentTimeMillis() / 1000.0;
        int counter2 = count2(input);
        double endTime2 = System.currentTimeMillis() / 1000.0;
        System.out.printf("%d +(%.0f seconds)\n", counter2, endTime2 - startTime2);


        double startTime = System.currentTimeMillis() / 1000.0;
        int counter = count(input);
        double endTime = System.currentTimeMillis() / 1000.0;
        System.out.printf("%d +(%.0f seconds)\n", counter, endTime - startTime);

    }

    private static int count(int[] a) {
        int counter = 0;
        for (int i = 0; i < a.length; i++) {
            for (int j = i + 1; j < a.length; j++) {
                for (int k = j + 1; k < a.length; k++) {
                    if (a[i] + a[j] + a[k] == 0)
                        counter++;
                }
            }
        }
        return counter;
    }

    // same as count function but comparison is being done through a function call
    private static int count2(int[] a) {
        int counter = 0;
        for (int i = 0; i < a.length; i++) {
            for (int j = i + 1; j < a.length; j++) {
                for (int k = j + 1; k < a.length; k++) {
                    counter = counter + check(a, i, j, k);
                }
            }
        }
        return counter;
    }

    private static int check(int[] a, int i, int j, int k) {
        if (a[i] + a[j] + a[k] == 0) {
            return 1;
        }
        return 0;
    }
}
/**
*求整数的三元组,它们相加为零
*/
公共类SumCheck{
公共静态void main(字符串[]args){
INTA=1000000;
int b=3000;
int[]输入=新的int[b];
对于(int i=0;i
特别是,其中一次运行产生以下时间: 12秒,
33秒。

不是。

如果您修改

if(a[i]+a[j]+a[k]==0){
柜台++
}

计数器=计数器+(a[i]+b[i]+c[i]==0?1:0)
i、 e.手动将
检查
插入
count2
以创建
count
,两种变体所用的时间完全相同

那么,为什么添加1或0会比if和increment快呢

有一个类似的案例和一个非常详细的答案。 它声称java正在将两个整数的三元表达式转换为cpu上的一个特殊操作CMOV。与正常的if-else结构相比,如果跳转不能成功,CMOV将获胜。我们可以通过使分支预测器的工作更容易来检查这篇论文:而不是使用
a=1000000
, 我们使用
a=0
。现在,if将一直采用,而且两种方法都同样快速


然而,如果我们使用一个更大的a,
a=800_000_000
,则存在一个位数的情况,
a[i]+a[j]+a[k]==0
,但如果增量仍然大约是系数2,则会更慢。这让我非常惊讶,因为如果3000**3个分支中只有10个被执行,那么分支预测器应该能够很好地预测这些未执行的分支,并且10个异常值不会让过程慢那么多。然而,我对java和java基准的了解还不够深入。

我试着按照一个已经链接的问题的相同步骤进行研究,这使我得出结论,我们正在处理一个不同的情况。因为我是JMH的新手,这个案例似乎需要更仔细的处理,所以我将尝试发布足够的代码,以便更容易发现最终的缺陷。我在这个过程中遇到的最重要和最相关的一点信息是,除非测量循环本身,否则应该避免在基准测试中使用循环

这里是一个更好的基准测试的尝试,我添加了一个内联版本和
base
方法来粗略估计随机数生成开销:

    package org.sample;                                   
    import java.util.*;
    import java.util.concurrent.*;
    import org.openjdk.jmh.annotations.*;
     
    @State(Scope.Thread)
    public class MyBenchmark {
        public static int counter1=0;
        public static int counter2=0;
        public static int counter3=0;
          
        @Benchmark
        public static int[] base() {
            int[] input=new int[3];
            for(int i=0;i<3;i++){
                input[i]=ThreadLocalRandom.current().nextInt(1000000*2) - 1000000;
            }
            return input;
        }
    
    
        @Benchmark
        public static int ifInc() {
            int[] input=new int[3];
            for(int i=0;i<3;i++){
                input[i]=ThreadLocalRandom.current().nextInt(1000000*2) - 1000000;
            }
            if (input[0] + input[1] + input[2] == 0){
                counter1++;
            }
            return counter1;
        }
    
        @Benchmark
        public static int method() {
            int[] input=new int[3];
            for(int i=0;i<3;i++){
                input[i]=ThreadLocalRandom.current().nextInt(1000000*2) - 1000000;
            }
            counter2 = counter2 + check(input, 0, 1, 2);
            return counter2;
        }
    
        @Benchmark
        public static int inline() {
            int[] input=new int[3];
            for(int i=0;i<3;i++){
                input[i]=ThreadLocalRandom.current().nextInt(1000000*2) - 1000000;
            }
            counter3 = counter3 + (input[0]+input[1]+input[2] == 0 ? 1 : 0);
            return counter3;
        }
    
    
        public static int check(int[] a, int i, int j, int k) {
            if (a[i] + a[j] + a[k] == 0) {
                return 1;
            }
            return 0;
        }
    }
这是一种摆脱开销的尝试,同时仍然避免循环优化,结果有些一致:

    package org.sample;
    import java.util.*;
    import java.util.concurrent.*;
    import org.openjdk.jmh.annotations.*;
    import org.openjdk.jmh.infra.Blackhole;
    
    @State(Scope.Thread)
    public class MyLoopBenchmark {
        Random random=new Random();
        static int a = 1000000;
        static int b = 300;
        static int counter1 = 0;
        static int counter2 = 0;
        static int counter3 = 0;
        static int[] input = new int[b];
        @Setup(Level.Iteration)
        public void prepare() {
            for (int q = 0; q < b; q++) {
                input[q] = a-random.nextInt(a*2);
            }
        }
        public static int ifInc(int i,int j,int k) {
            if (i+j+k == 0){
                counter1++;
            }
            return counter1;
        }
    
        public static int method(int i,int j,int k) {
            counter2 = counter2 + check(i, j, k);
            return counter2;
        }
    
        public static int inline(int i,int j,int k) {
            counter3 = counter3 + (i+j+k == 0 ? 1 : 0);
            return counter3;
        }
    
    
        public static int check(int i, int j, int k) {
           if (i + j + k == 0) {
                return 1;
            }
            return 0;
        }
        @Benchmark
        public void measureIf(Blackhole bh) {
            for (int i = 0; i < input.length; i++) {
                for (int j = 0; j < input.length; j++) {
                    for (int k = 0; k < input.length; k++) {
                        bh.consume(ifInc(input[i],input[j],input[k]));
                    }
                }
            }
        }
        @Benchmark
        public void measureMethod(Blackhole bh) {
            for (int i = 0; i < input.length; i++) {
                for (int j = 0; j < input.length; j++) {
                    for (int k = 0; k < input.length; k++) {
                        bh.consume(method(input[i],input[j],input[k]));
                    }
                }
            }
        }
        @Benchmark
        public void measureInline(Blackhole bh) {
            for (int i = 0; i < input.length; i++) {
                for (int j = 0; j < input.length; j++) {
                    for (int k = 0; k < input.length; k++) {
                        bh.consume(inline(input[i],input[j],input[k]));
                    }
                }
            }
        }
    }

一些测量整个循环的尝试表明,包含循环的
ifInc
方法在第一次迭代中速度较慢,直到出现令人印象深刻且不可预测的优化,而其他两个循环的时间更为恒定。如果没有一些明确定义的条件,对看似不重要的更改甚至是这种完美设计的基准测试都非常敏感的结果会产生误导。

JIT(准时制)编译器正在做的事情有点不可预测。也许它已经编译了这个方法,因为它经常被使用阅读这篇关于如何编写微基准测试的文章。如果你调用
check
方法的次数足够多,它将被内联,因此你不太可能发现2
count
方法与正确的基准测试之间存在差异。它还将有助于监控编译哪个方法以及何时编译。谢谢!我知道,这不是一个完美的基准测试,但仅仅是简单地交换这两个执行就表明,双重差异仍然存在。因此,问题可能在于Java优化代码的方式,这似乎很奇怪。在C语言中,必须使用
inline
对函数进行优化(通过将函数体嵌入调用函数体中),这恰恰相反。
    package org.sample;
    import java.util.*;
    import java.util.concurrent.*;
    import org.openjdk.jmh.annotations.*;
    import org.openjdk.jmh.infra.Blackhole;
    
    @State(Scope.Thread)
    public class MyLoopBenchmark {
        Random random=new Random();
        static int a = 1000000;
        static int b = 300;
        static int counter1 = 0;
        static int counter2 = 0;
        static int counter3 = 0;
        static int[] input = new int[b];
        @Setup(Level.Iteration)
        public void prepare() {
            for (int q = 0; q < b; q++) {
                input[q] = a-random.nextInt(a*2);
            }
        }
        public static int ifInc(int i,int j,int k) {
            if (i+j+k == 0){
                counter1++;
            }
            return counter1;
        }
    
        public static int method(int i,int j,int k) {
            counter2 = counter2 + check(i, j, k);
            return counter2;
        }
    
        public static int inline(int i,int j,int k) {
            counter3 = counter3 + (i+j+k == 0 ? 1 : 0);
            return counter3;
        }
    
    
        public static int check(int i, int j, int k) {
           if (i + j + k == 0) {
                return 1;
            }
            return 0;
        }
        @Benchmark
        public void measureIf(Blackhole bh) {
            for (int i = 0; i < input.length; i++) {
                for (int j = 0; j < input.length; j++) {
                    for (int k = 0; k < input.length; k++) {
                        bh.consume(ifInc(input[i],input[j],input[k]));
                    }
                }
            }
        }
        @Benchmark
        public void measureMethod(Blackhole bh) {
            for (int i = 0; i < input.length; i++) {
                for (int j = 0; j < input.length; j++) {
                    for (int k = 0; k < input.length; k++) {
                        bh.consume(method(input[i],input[j],input[k]));
                    }
                }
            }
        }
        @Benchmark
        public void measureInline(Blackhole bh) {
            for (int i = 0; i < input.length; i++) {
                for (int j = 0; j < input.length; j++) {
                    for (int k = 0; k < input.length; k++) {
                        bh.consume(inline(input[i],input[j],input[k]));
                    }
                }
            }
        }
    }
    Benchmark                      Mode  Cnt    Score   Error  Units
    MyLoopBenchmark.measureIf      avgt   25  123,262 ? 0,660  ms/op
    MyLoopBenchmark.measureInline  avgt   25  139,877 ? 0,447  ms/op
    MyLoopBenchmark.measureMethod  avgt   25  140,355 ? 0,482  ms/op