我是Java的新手,并且昨晚正在运行一些代码,这确实让我感到困扰。我正在构建一个简单的程序,以在for循环中显示每个X输出,当我将模数用作variable % variablevariable % 5000或诸如此类时,我注意到性能大大下降。有人可以向我解释这是什么原因,是什么造成的?这样我会更好...

这是“有效的”代码(很抱歉,如果我语法有点错误,我现在不在使用该代码的计算机上)

long startNum = 0;
long stopNum = 1000000000L;

for (long i = startNum; i <= stopNum; i++){
    if (i % 50000 == 0) {
        System.out.println(i);
    }
}

这是“无效代码”
long startNum = 0;
long stopNum = 1000000000L;
long progressCheck = 50000;

for (long i = startNum; i <= stopNum; i++){
    if (i % progressCheck == 0) {
        System.out.println(i);
    }
}

请注意,我有一个日期变量来衡量差异,一旦它变长了,第一个变量要花费50毫秒,而另一个变量要花费12秒或类似的时间。如果您的PC比我的PC效率高或其他效率不高,则可能需要增加stopNum或降低progressCheck

我在网上寻找了这个问题,但找不到答案,也许我只是问的不对。

编辑:
我没想到我的问题会如此受欢迎,我感谢所有答案。我确实在所花费的每个时间上执行了基准测试,效率低下的代码花费了更长的时间,即1/4秒vs. 10秒的付出或付出。尽管他们使用的是println,但是它们的用量相同,所以我不会想象这会造成很大的偏差,特别是因为差异是可重复的。至于答案,由于我是Java新手,所以我现在让投票决定哪个答案是最好的。我会尽量在星期三之前选一个。

编辑2:
今晚我将进行另一个测试,其中不是模数,而是增加一个变量,并在到达progressCheck时执行一个测试,然后将该变量重置为0。

EDIT3.5:

我使用了这段代码,下面将显示结果。.谢谢大家的出色帮助!我还尝试将long的short值与0进行比较,因此我的所有新检查都发生过“65536”次,从而使其重复次数相等。
public class Main {


    public static void main(String[] args) {

        long startNum = 0;
        long stopNum = 1000000000L;
        long progressCheck = 65536;
        final long finalProgressCheck = 50000;
        long date;

        // using a fixed value
        date = System.currentTimeMillis();
        for (long i = startNum; i <= stopNum; i++) {
            if (i % 65536 == 0) {
                System.out.println(i);
            }
        }
        long final1 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();
        //using a variable
        for (long i = startNum; i <= stopNum; i++) {
            if (i % progressCheck == 0) {
                System.out.println(i);
            }
        }
        long final2 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();

        // using a final declared variable
        for (long i = startNum; i <= stopNum; i++) {
            if (i % finalProgressCheck == 0) {
                System.out.println(i);
            }
        }
        long final3 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();
        // using increments to determine progressCheck
        int increment = 0;
        for (long i = startNum; i <= stopNum; i++) {
            if (increment == 65536) {
                System.out.println(i);
                increment = 0;
            }
            increment++;

        }

        //using a short conversion
        long final4 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();
        for (long i = startNum; i <= stopNum; i++) {
            if ((short)i == 0) {
                System.out.println(i);
            }
        }
        long final5 = System.currentTimeMillis() - date;

                System.out.println(
                "\nfixed = " + final1 + " ms " + "\nvariable = " + final2 + " ms " + "\nfinal variable = " + final3 + " ms " + "\nincrement = " + final4 + " ms" + "\nShort Conversion = " + final5 + " ms");
    }
}

结果:
  • fixed = 874 ms(通常为1000ms,但由于为2的幂,所以更快)
  • 变量= 8590毫秒
  • 最终变量= 1944 ms(使用50000时约为1000ms)
  • 增量= 1904毫秒
  • 简短转换= 679毫秒

  • 不足为奇的是,由于缺乏划分,短转换比“快速”方式快23%。这很有趣。如果您需要每隔256次(或大约256次)显示或比较某项内容,可以执行此操作并使用
    if ((byte)integer == 0) {'Perform progress check code here'}
    

    一个最终的注意事项,在“最终声明的变量”上使用模数为65536(不是一个漂亮的数字)的速度是固定值的一半(慢)。它之前以相同的速度进行基准测试的地方。

    最佳答案

    您正在测量OSR (on-stack replacement)存根。

    OSR存根是已编译方法的特殊版本,专门用于在方法运行时将执行从解释模式转换为已编译代码。

    OSR存根不如常规方法那样优化,因为它们需要与解释的帧兼容的帧布局。我已经在以下答案中表明了这一点:123

    类似的事情也在这里发生。当“低效率代码”运行较长的循环时,该方法专门为循环内的堆栈替换而编译。状态从解释的帧转移到OSR编译的方法,并且此状态包括progressCheck局部变量。此时,JIT无法用常量替换变量,因此无法应用某些优化(例如strength reduction)。

    特别是,这意味着JIT不会将整数除法替换为乘法。 (如果启用了这些优化,则该值是内联/常量传播后的编译时常量(如果启用了这些优化,则从预先编译器获取的asm技巧请参见Why does GCC use multiplication by a strange number in implementing integer division?)。%表达式中的整数立即数也会通过以下方式进行优化: gcc -O0,类似于此处,即使在OSR存根中,JITer也会对其进行优化。)

    但是,如果您多次运行同一方法,则第二次及以后的运行将执行常规(非OSR)代码,该代码已完全优化。这是证明该理论的基准(benchmarked using JMH):

    @State(Scope.Benchmark)
    public class Div {
    
        @Benchmark
        public void divConst(Blackhole blackhole) {
            long startNum = 0;
            long stopNum = 100000000L;
    
            for (long i = startNum; i <= stopNum; i++) {
                if (i % 50000 == 0) {
                    blackhole.consume(i);
                }
            }
        }
    
        @Benchmark
        public void divVar(Blackhole blackhole) {
            long startNum = 0;
            long stopNum = 100000000L;
            long progressCheck = 50000;
    
            for (long i = startNum; i <= stopNum; i++) {
                if (i % progressCheck == 0) {
                    blackhole.consume(i);
                }
            }
        }
    }
    

    结果:
    # Benchmark: bench.Div.divConst
    
    # Run progress: 0,00% complete, ETA 00:00:16
    # Fork: 1 of 1
    # Warmup Iteration   1: 126,967 ms/op
    # Warmup Iteration   2: 105,660 ms/op
    # Warmup Iteration   3: 106,205 ms/op
    Iteration   1: 105,620 ms/op
    Iteration   2: 105,789 ms/op
    Iteration   3: 105,915 ms/op
    Iteration   4: 105,629 ms/op
    Iteration   5: 105,632 ms/op
    
    
    # Benchmark: bench.Div.divVar
    
    # Run progress: 50,00% complete, ETA 00:00:09
    # Fork: 1 of 1
    # Warmup Iteration   1: 844,708 ms/op          <-- much slower!
    # Warmup Iteration   2: 105,893 ms/op          <-- as fast as divConst
    # Warmup Iteration   3: 105,601 ms/op
    Iteration   1: 105,570 ms/op
    Iteration   2: 105,475 ms/op
    Iteration   3: 105,702 ms/op
    Iteration   4: 105,535 ms/op
    Iteration   5: 105,766 ms/op
    

    实际上,由于编译效率低下的OSR存根,divVar的第一次迭代确实要慢得多。但是,一旦从头开始重新运行该方法,就会执行新的不受限制的版本,该版本会利用所有可用的编译器优化。

    10-02 08:09