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

Rob*_*man 177 java performance

我是java的新手,并且昨晚运行了一些代码,这真让我烦恼.我正在构建一个简单的程序来显示for循环中的每个X输出,当我使用模数作为variable % variablevs variable % 5000或诸如此类时,我注意到性能的大幅下降.有人可以向我解释为什么会这样,是什么导致它?所以我可以更好......

这是"高效"代码(对不起,如果我得到一些语法错误我现在不在计算机上的代码)

long startNum = 0;
long stopNum = 1000000000L;

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

这是"效率低下的代码"

long startNum = 0;
long stopNum = 1000000000L;
long progressCheck = 50000;

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

请注意,我有一个日期变量来衡量差异,一旦它变得足够长,第一个花了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");
    }
}
Run Code Online (Sandbox Code Playgroud)

结果:

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

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

if ((byte)integer == 0) {'Perform progress check code here'}
Run Code Online (Sandbox Code Playgroud)

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

apa*_*gin 139

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

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

OSR存根不像常规方法那样优化,因为它们需要与解释帧兼容的帧布局.我在下面的回答表明这已经:1,2,3.

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

特别是这意味着JIT不会用乘法替换整数除法.(请参阅为什么在执行整数除法由一个陌生的号码确实GCC使用乘法?对ASM伎俩从名列前茅的时间编译器,当值是内联/恒定传播之后编译时间常数,如果这些优化被启用表达式中的整数文字右边也得到了优化,类似于这里它甚至在OSR存根中由JITer优化.)%gcc -O0

但是,如果多次运行相同的方法,则第二次和后续运行将执行完全优化的常规(非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);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

结果如下:

# 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
Run Code Online (Sandbox Code Playgroud)

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

  • 我对这个投票犹豫不决.一方面,这听起来像一个复杂的说法"你弄乱了你的基准,读了一些关于JIT的东西".另一方面,我想知道为什么你似乎确信OSR是这里的主要相关点.我的意思是,做一个涉及`System.out.println`的(微)基准将几乎*必然*产生垃圾结果,而且这两个版本同样快的事实不需要对*特定*中的OSR做任何事情,如据我所知.. (5认同)
  • @ Marco13有一个简单的启发式:没有JIT的活动,每个`%`操作都会有相同的权重,因为优化执行只有在优化器完成实际工作时才有可能.因此,一个循环变体明显快于另一个循环变量的事实证明了优化器的存在,并进一步证明它未能将其中一个循环优化到与另一个相同的程度(在同一方法中!).由于这个答案证明了将两个循环优化到相同程度的能力,必然会有一些阻碍优化的东西.在99.9%的情况下,这是OSR (4认同)
  • @ Marco13这是一个基于HotSpot Runtime知识和以前分析类似问题的经验的"有根据的猜测".如此长的循环很难以OSR以外的方式编译,尤其是在简单的手工制作基准测试中.现在,当OP发布完整代码时,我只能通过运行带有`-XX:+ PrintCompilation -XX:+ TraceNMethodInstalls`的代码再次确认推理. (4认同)
  • (我很好奇,并且喜欢理解这一点.我希望评论不会令人不安,可能会在以后删除它们,但是:)链接"1"有点可疑 - 空循环也可以完全优化掉.第二个更类似于那个.但同样,不清楚为什么要将差异归因于OSR**.我只想说:在某些时候,该方法是JITed并且变得更快.根据我的理解,OSR只会导致最终优化代码的使用(大致)被"推迟到下一个优化过程".(继续...) (2认同)

Ole*_*hov 42

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

for 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
Run Code Online (Sandbox Code Playgroud)

用于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
Run Code Online (Sandbox Code Playgroud)

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

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)
Run Code Online (Sandbox Code Playgroud)

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

  • 为了给"慢"提供一个数量级,对于x86_64:`imul`是3个循环,`idiv`在30到90个循环之间.因此整数除法比整数乘法慢10到30倍. (14认同)
  • @NicoHaase两条评论线是唯一重要的线条.在第一部分中,代码执行整数乘法,而第二部分,代码执行整数除法.如果你考虑手工进行乘法和除法,当你乘以时,你通常做一堆小的乘法,然后是一大组加法,但除法是一个小的除法,一个小的乘法,一个减法和重复.分裂很慢,因为你基本上做了一堆乘法. (7认同)
  • @MBraedley:更重要的是,现代CPU中的乘法很快,因为部分乘积是独立的,因此可以单独计算,而除法的每个阶段都取决于前面的阶段. (6认同)
  • @MBraedley感谢您的输入,但这样的解释应该添加到答案本身,而不是隐藏在评论部分 (4认同)
  • 你能解释所有这些对于那些感兴趣但不会说汇编的读者意味着什么吗? (2认同)

Jim*_*myB 26

正如其他人所指出的那样,一般模数运算需要进行除法.在某些情况下,可以通过乘法(由编译器)替换除法.但与加/减相比,两者都可能很慢.因此,可以通过以下方式预期最佳性能:

long progressCheck = 50000;

long counter = progressCheck;

for (long i = startNum; i <= stopNum; i++){
    if (--counter == 0) {
        System.out.println(i);
        counter = progressCheck;
    }
}
Run Code Online (Sandbox Code Playgroud)

(作为一个次要的optmiziation尝试,我们使用一个预先递减递减计数器在这里,因为在很多平台上,以比较0的运算处理后,因为ALU的标志由前述操作已经设定适当的成本究竟0指令/ CPU周期.一个体面的优化但是,即使您编写,编译器也会自动执行该优化if (counter++ == 50000) { ... counter = 0; }.)

请注意,通常你并不真正想要/需要模数,因为你知道你的循环计数器(i)或其他任何东西只增加1,而你真的不关心模数会给你的实际余数,只看到如果递增计数器达到某个值.

另一个"技巧"是使用两个幂的值/限制,例如progressCheck = 1024;.模数可以通过按位快速计算2的幂and,即if ( (i & (1024-1)) == 0 ) {...}.这应该也非常快,并且可能在一些架构上优于counter上面的显式.

  • 智能编译器会在这里反转循环.或者你可以在源代码中做到这一点.`if()`主体成为外循环体,`if()`之外的东西变成了一个内循环体,运行'min(progressCheck,stopNum-i)`迭代.所以在开始时,每次`counter`达到0时,你会做`long next_stop = i + min(progressCheck,stopNum -i);`设置`for(; i <next_stop; i ++){}环.在这种情况下,内部循环是空的,并且应该完全消失,你可以在源代码中做到这一点并使JITer变得容易,将循环减少到i + = 50k. (3认同)
  • 但是,一般情况下,对于fizzbuzz/progresscheck类型的东西,向下计数器是一种很好的有效技术. (2认同)
  • @RobertCotterman是的,`--counter`是一个人.`counter - `会给你准确的'progressCheck`迭代次数(或者你可以设置`progressCheck = 50001;`当然). (2认同)

归档时间:

查看次数:

17726 次

最近记录:

6 年,8 月 前