the - java转go




为什么if(variable1%variable2== 0)效率低下? (3)

在后续的 @phuclv comment ,我检查了JIT 1 生成的代码,结果如下:

对于 variable % 5000 (除以常数):

mov     rax,29f16b11c6d1e109h
imul    rbx
mov     r10,rbx
sar     r10,3fh
sar     rdx,0dh
sub     rdx,r10
imul    r10,rdx,0c350h    ; <-- imul
mov     r11,rbx
sub     r11,r10
test    r11,r11
jne     1d707ad14a0h

对于 variable % variable

mov     rax,r14
mov     rdx,8000000000000000h
cmp     rax,rdx
jne     22ccce218edh
xor     edx,edx
cmp     rbx,0ffffffffffffffffh
je      22ccce218f2h
cqo
idiv    rax,rbx           ; <-- idiv
test    rdx,rdx
jne     22ccce218c0h

因为除法总是比乘法更长,所以最后一个代码片段的性能较差。

Java版本:

java version "11" 2018-09-25
Java(TM) SE Runtime Environment 18.9 (build 11+28)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11+28, mixed mode)

1 - 使用的VM选项: -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,src/java/Main.main

我是java的新手,并且昨晚运行了一些代码,这真让我烦恼。 我正在构建一个简单的程序来显示for循环中的每个X输出,当我使用模数作为 variable % variable vs variable % 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秒或类似的东西。 如果您的电脑比我的电脑效率更高或者不是,那么您可能需要增加 stopNum 或减少 progressCheck

我在网上找了这个问题,但我找不到答案,也许我只是没有问它。

编辑:我没想到我的问题如此受欢迎,我感谢所有的答案。 我确实在每一半的时间内执行了一个基准测试,效率低下的代码需要相当长的时间,1/4秒与10秒的时间相比。 当然他们正在使用println,但他们都做了相同的数量,所以我不认为这会扭曲很多,特别是因为差异是可重复的。 至于答案,因为我是Java的新手,我会让投票现在决定哪个答案最好。 我会在星期三之前选择一个。

EDIT2:我今晚要进行另一次测试,而不是模数,它只是递增一个变量,当它达到progressCheck时,它将执行一次,然后将该变量重置为0.对于第三个选项。

EDIT3.5:

我使用了这段代码,下面我将展示我的结果..谢谢大家的精彩帮助! 我也尝试将long的短值与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");
    }
}

结果:

  • 固定= 874毫秒(通常约1000毫秒,但由于它是2的幂,因此速度更快)
  • 变量= 8590毫秒
  • 最终变量= 1944 ms(使用50000时为~1000ms)
  • 增量= 1904毫秒
  • 短转换= 679 ms

不足为奇,由于缺乏划分,短转换比“快速”方式快23%。 这很有趣。 如果你需要每256次(或那里)显示或比较一些东西,你可以这样做,并使用

if ((byte)integer == 0) {'Perform progress check code here'}

一个最终有趣的注意事项,使用“最终声明的变量”上的模数与65536(不是一个漂亮的数字)是固定值(慢)的一半。 之前它的基准测试速度接近相同的速度。


您正在测量 OSR(堆栈替换) 存根。

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

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

类似的事情也发生在这里。 虽然“效率低下的代码”正在运行一个长循环,但该方法是专门为循环内的堆栈替换而编译的。 状态从解释的帧转移到OSR编译的方法,并且该状态包括 progressCheck 局部变量。 此时JIT不能用常量替换变量,因此不能应用某些优化,如 强度降低

特别是这意味着JIT不会用 乘法 替换 整数除法 。 ( 为什么GCC在实现整数除法时使用乘以一个奇数的? 对于来自提前编译器的asm技巧,当内联/常量传播后该值为编译时常量时,如果启用了那些优化。 % 表达式中的整数文字右边也被 gcc -O0 优化,类似于此处,即使在OSR存根中它也由JITer优化。)

但是,如果多次运行相同的方法,则第二次和后续运行将执行完全优化的常规(非OSR)代码。 以下是证明该理论的 基准 使用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

divVar 第一次迭代确实要慢得多,因为编译效率低的OSR存根。 但是一旦方法从头重新开始,就会执行新的无约束版本,该版本利用所有可用的编译器优化。


看到上述代码的表现,我也感到惊讶。 这完全取决于编译器按照声明的变量执行程序所花费的时间。 在第二个(低效)示例中:

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

您正在两个变量之间执行模数运算。 在这里,编译器必须每次迭代后检查 stopNumprogressCheck 的值以转到这些变量的特定内存块,因为它是一个变量,其值可能会发生变化。

这就是为什么在每次迭代后编译器进入内存位置以检查变量的最新值。 因此,在编译时,编译器无法创建有效的字节代码。

在第一个代码示例中,您在变量和常量数值之间执行模数运算符,该值在执行期间不会更改,编译器无需从内存位置检查该数值的值。 这就是编译器能够创建有效字节代码的原因。 如果将 progressCheck 声明为 finalfinal static 变量,那么在运行时/编译时编译器知道它是最终变量并且其值不会改变时,编译器会将 progressCheck 替换为代码中的 50000

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

现在您可以看到此代码看起来也像第一个(高效)代码示例。 第一个代码的性能和我们上面提到的两个代码都能有效地工作。 两个代码示例的执行时间没有太大差异。





performance