Java中哪一段代码更快?

时间:2009-11-01 05:53:32

标签: java performance micro-optimization

a) for(int i = 100000; i > 0; i--) {}

b) for(int i = 1; i < 100001; i++) {}

答案在this website(问题3)。我只是想不出 为什么? 来自网站:

  

3. a

16 个答案:

答案 0 :(得分:67)

当你达到最低级别(机器代码,但我将使用程序集,因为它主要是一对一映射),空循环递减到0和一个递增到50(例如)之间的差异是经常沿着:

      ld  a,50                ld  a,0
loop: dec a             loop: inc a
      jnz loop                cmp a,50
                              jnz loop

这是因为当你达到零时,大多数理智CPU中的零标志由递减指令设置。当增量指令达到50时,通常不能说同样的增长指令(因为这个值没有什么特别的,不像零)。因此,您需要将寄存器与50进行比较以设置零标志。


然而,问两个循环中的哪一个:

for(int i = 100000; i > 0; i--) {}
for(int i = 1; i < 100001; i++) {}

更快(在几乎任何环境中,Java或其他)是没用的,因为它们都没有做任何有用的事情。这两个循环的最快版本根本没有循环。我挑战任何人提出比这更快的版本: - )

当你开始在大括号内做一些有用的工作时,它们才会变得有用,此时, work 将决定你应该使用哪个顺序。

例如,如果需要从1到100,000计数,则应使用第二个循环。这是因为每次需要使用它时,你必须在循环内部评估100000-i这一事实可能会淹没倒计时(如果有的话)的优势。在汇编方面,这将是:

之间的区别
     ld  b,100000             dsw a
     sub b,a
     dsw b

dsw当然是臭名昭着的do something with汇编程序助记符。

因为每次迭代你只会对一次递增循环进行一次加注,并且每次迭代你将获得至少的减法次数<假设你将使用i,否则根本不需要循环,你应该选择更自然的版本。

如果你需要数数,请计数。如果你需要倒计时,倒计时。

答案 1 :(得分:23)

在许多编译器中,为循环向后发送的机器指令更有效,因为测试零(因此对寄存器进行归零)比立即加载的速度更快。

另一方面,一个好的优化编译器应该能够检查循环内部并确定向后反转不会导致任何副作用......

顺便说一句,在我看来,这是一个糟糕的面试问题。除非你在谈论一个运行了数百万次的循环,并且你已经确定了重新创建正向循环值(n-i)的许多实例并没有超过轻微的增益,否则任何性能增益都将是最小的。

一如既往,如果没有性能基准测试并且以更难理解代码为代价,请不要进行微量优化。

答案 2 :(得分:17)

这些问题在很大程度上是一种无关紧要的分心,有些人会对此痴迷。称之为微优化崇拜或任何你喜欢的东西,但循环上升或下降更快?真的吗?您可以使用适合您所做的任何事情。您不需要编写代码来保存两个时钟周期或其他任何时钟周期。

让编译器完成它的工作,让你 intent 清除(编译器和阅读器)。另一个常见的Java悲观化是:

public final static String BLAH = new StringBuilder().append("This is ").append(3).append(' text").toString();

因为过多的连接会导致内存碎片,但是对于常量,编译器可以(并且会)优化它:

public final static String BLAH = "This is a " + 3 + " test";

它不会优化第一个,第二个更容易阅读。

(a>b)?a:b vs Math.max(a,b)怎么样?我知道我宁愿阅读第二篇,所以我并不在意第一篇文章不会产生函数调用开销。

此列表中有一些有用的内容,比如知道在finally上未调用System.exit()可能有用。知道将float除以0.0不会抛出异常是有用的。

但是,除非 真的 很重要,否则不要再费力猜测编译器(而且我敢打赌你99.99%的时间没有)。

答案 3 :(得分:13)

更好的问题是;

哪个更易于理解/使用?

这比性能上的名义差异重要得多。就个人而言,我想指出,绩效不应成为确定差异的标准。如果他们不喜欢我挑战他们的假设,我不会因为没有得到这份工作而感到不快。 ;)

答案 4 :(得分:10)

在现代Java实现中,这不是真的。 总结数字高达10亿作为基准:

Java(TM) SE Runtime Environment 1.6.0_05-b13
Java HotSpot(TM) Server VM 10.0-b19
up 1000000000: 1817ms 1.817ns/iteration (sum 499999999500000000)
up 1000000000: 1786ms 1.786ns/iteration (sum 499999999500000000)
up 1000000000: 1778ms 1.778ns/iteration (sum 499999999500000000)
up 1000000000: 1769ms 1.769ns/iteration (sum 499999999500000000)
up 1000000000: 1769ms 1.769ns/iteration (sum 499999999500000000)
up 1000000000: 1766ms 1.766ns/iteration (sum 499999999500000000)
up 1000000000: 1776ms 1.776ns/iteration (sum 499999999500000000)
up 1000000000: 1768ms 1.768ns/iteration (sum 499999999500000000)
up 1000000000: 1771ms 1.771ns/iteration (sum 499999999500000000)
up 1000000000: 1768ms 1.768ns/iteration (sum 499999999500000000)
down 1000000000: 1847ms 1.847ns/iteration (sum 499999999500000000)
down 1000000000: 1842ms 1.842ns/iteration (sum 499999999500000000)
down 1000000000: 1838ms 1.838ns/iteration (sum 499999999500000000)
down 1000000000: 1832ms 1.832ns/iteration (sum 499999999500000000)
down 1000000000: 1842ms 1.842ns/iteration (sum 499999999500000000)
down 1000000000: 1838ms 1.838ns/iteration (sum 499999999500000000)
down 1000000000: 1838ms 1.838ns/iteration (sum 499999999500000000)
down 1000000000: 1847ms 1.847ns/iteration (sum 499999999500000000)
down 1000000000: 1839ms 1.839ns/iteration (sum 499999999500000000)
down 1000000000: 1838ms 1.838ns/iteration (sum 499999999500000000)

请注意,时间差异很脆弱,环路附近的小变化可能会使它们转过来。

修改 基准循环

        long sum = 0;
        for (int i = 0; i < limit; i++)
        {
            sum += i;
        }

        long sum = 0;
        for (int i = limit - 1; i >= 0; i--)
        {
            sum += i;
        }

使用int类型的和大约快三倍,但总和溢出。 使用BigInteger它的速度要慢50倍:

BigInteger up 1000000000: 105943ms 105.943ns/iteration (sum 499999999500000000)

答案 5 :(得分:6)

通常,实际代码将向上运行更快。这有几个原因:

  • 处理器针对读取内存进行了优化。
  • HotSpot(可能是其他字节码 - >本机编译器)大大优化了正向循环,但不会因为它们很少发生而烦恼。
  • 向上通常更明显,更清晰的代码通常更快。

很高兴做正确的事情通常会更快。不必要的微优化是邪恶的。自编程6502汇编程序以来,我没有故意编写反向循环。

答案 6 :(得分:6)

实际上只有两种方法可以回答这个问题。

  1. 告诉你它真的,无所谓,你在浪费你的时间甚至想知道。

  2. 告诉您,唯一可以知道的方法是在您关心的实际生产硬件,操作系统和JRE安装上运行可靠的基准测试。

  3. 所以,我为你制作了一个可运行的基准测试,你可以用来试试这个:

    http://code.google.com/p/caliper/source/browse/trunk/test/examples/LoopingBackwardsBenchmark.java

    这个Caliper框架尚未准备好迎接黄金时段,因此可能并不完全明白该如何处理,但如果你真的非常关心,你可以搞清楚。以下是我在linux盒子上给出的结果:

         max benchmark        ns
           2  Forwards         4
           2 Backwards         3
          20  Forwards         9
          20 Backwards        20
        2000  Forwards      1007
        2000 Backwards      1011
    20000000  Forwards   9757363
    20000000 Backwards  10303707
    

    向后循环看起来像是对任何人的胜利吗?

答案 7 :(得分:3)

你确定询问这样一个问题的面试官是否期望得到一个直接的回答(“第一个更快”或“第二个更快”),或者如果要求这个问题引发讨论,就像在人们在这里给出的答案?

一般来说,不可能说哪一个更快,因为它在很大程度上取决于Java编译器,JRE,CPU和其他因素。只是因为你认为两个中的一个更快而不了解最低级别的细节是superstitious programming,所以在你的程序中使用一个或另一个。即使一个版本在您的特定环境中比另一个版本更快,但差异很可能很小,以至于无关紧要。

写清楚代码,而不是试图变得聪明。

答案 8 :(得分:3)

此类问题的基础是旧的最佳实践建议。 这都是关于比较:已知比较为0更快。多年前,这可能被视为非常重要。如今,尤其是Java,我宁愿让编译器和VM完成他们的工作,而是专注于编写易于维护和理解的代码。

除非有理由这样做。请记住,Java应用程序并不总是在HotSpot和/或快速硬件上运行。

答案 9 :(得分:2)

关于在JVM中测试零:它显然可以用ifeq完成,而测试其他任何东西需要if_icmpeq,这也涉及在堆栈上添加一个额外的值。

问题中> 0的测试可以使用ifgt进行,而< 100001的测试则需要if_icmplt

答案 10 :(得分:2)

这是我见过的最愚蠢的问题。循环体是空的。如果编译器有任何好处,它将根本不发出任何代码。它不做任何事情,不能抛出异常,也不会修改其范围之外的任何东西。

假设您的编译器不那么聪明,或者您实际上没有空循环体: “向后循环计数器”参数对于某些汇编语言是有意义的(它也可能对java字节代码有意义,我不知道具体)。但是,编译器通常能够将循环转换为使用递减计数器。除非你有明确使用i值的循环体,否则编译器可以进行这种转换。所以你经常看到没有区别。

答案 11 :(得分:2)

我决定咬一口,然后回复该线程。

JVM将这两个循环都忽略为no-ops。所以基本上甚至其中一个循环直到10个,另一个循环直到10000000,没有区别。

循环回零是另一回事(对于jne指令,但同样,它不是那样编译的),链接的站点很简单(和错误)。

这种类型的问题不适合任何JVM(也没有任何其他可以优化的编译器)。

答案 12 :(得分:1)

除了一个关键部分外,循环是相同的:

我&gt; 0; 和 我&lt; 100001;

通过检查计算机的NZP(通常称为条件代码或负零或正位)位来完成大于零的检查。

只要加载,AND,加法等操作,就会设置NZP位。执行。

大于检查不能直接利用这个位(因此需要更长的时间......)一般的解决方案是使其中一个值为负(通过按位NOT然后加1)然后将其添加到比较值。如果结果为零,则它们是相等的。正,然后第二个值(不是负)更大。否定,则第一个值(neg)更大。此检查比直接nzp检查稍长。

我不是百分之百确定这是它背后的原因,但这似乎是一个可能的原因......

答案 13 :(得分:0)

答案是(正如您可能在网站上发现的那样)

我认为原因是终止循环的i > 0条件更快测试。

答案 14 :(得分:0)

最重要的是,对于任何非性能关键应用程序,差异可能是无关紧要的。正如其他人指出的那样,有时候使用++ i代替i ++可能会更快,但是,特别是在for循环中,任何现代编译器都应该优化这种区别。

尽管如此,差异可能与为比较生成的基础指令有关。测试值是否等于0只是 NAND NOR门。测试值是否等于任意常量需要将该常量加载到寄存器中,然后比较两个寄存器。 (这可能需要额外的门延迟或两个。)也就是说,使用流水线和现代ALU,如果区别是重要的,我会感到惊讶。

答案 15 :(得分:0)

我现在已经进行了大约15分钟的测试,除了eclipse以外什么都没有运行以防万一,我看到了真正的区别,你可以尝试一下。

当我尝试计算java需要多长时间时,&#34;没有什么&#34;有一个想法需要大约500纳秒。

然后我测试了运行for语句需要多长时间才能增加:

for(i=0;i<100;i++){}

然后五分钟后我尝试了#34;倒退&#34;之一:

for(i=100;i>0;i--)

我在第一个和第二个for陈述之间有16%的巨大差异(在很小的水平上),后者的速度提高了16%。

运行&#34;增加&#34;的平均时间2000年测试中的for陈述: 1838 n / s

运行&#34;减少&#34;的平均时间2000年测试期间的for声明: 1555 n / s

用于此类测试的代码:

public static void main(String[] args) {
    long time = 0;  
    for(int j=0; j<100; j++){
    long startTime = System.nanoTime();
    int i;
        /*for(i=0;i<100;i++){

        }*/
        for(i=100;i>0;i--){

        }
    long endTime = System.nanoTime();
    time += ((endTime-startTime));
    }
    time = time/100;
    System.out.print("Time: "+time);
}

<强>结论: 差别基本上没什么,它已经需要大量的&#34;没有&#34;做什么&#34;没什么&#34;与for语句测试相关,使它们之间的差异可以忽略不计,只需要导入库(例如 java.util.Scanner )所需的时间比运行{更多地加载{1}}声明,它不会显着提高您的应用程序的性能,但知道它仍然很酷。