Java中哪一段代码更快?

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

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

答案就在这个网站上 (问题3)。 我只是想不通为什么? 来自网站:

3. a

当你到达最低级别(机器代码,但我将使用程序集,因为它主要是一对一映射),空循环递减到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或其他)都没用,因为它们都没有做任何有用的事情。 这两个循环的最快版本根本没有循环。 我挑战任何人提出比这更快的版本:-)

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

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

  ld b,100000 dsw a sub b,a dsw b 

(当然, dsw是臭名昭着的汇编程序助记符)。

因为每次迭代你只会在一次递增循环中获取一次,所以每次迭代你都会在减法中至少进行一次减法(假设你将使用i ,否则几乎不需要循环所有),你应该选择更自然的版本。

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

在许多编译器中,为循环向后发射的机器指令更有效,因为测试零(因此将寄存器置零)比立即加载的常量值更快。

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

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

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

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

让编译器做它的目的并让你明确意图 (编译器和阅读器)。 另一个常见的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)怎么样? 我知道我宁愿读第二个,所以我并不在乎第一个不会产生函数调用开销。

在这个列表中有一些有用的东西,比如知道在System.exit()上没有调用finally可能是有用的。 知道将float除以0.0不会抛出exception是有用的。

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

一个更好的问题是;

哪个更容易理解/合作?

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

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

 Java(TM)SE运行时环境1.6.0_05-b13
 Java HotSpot(TM)Server VM 10.0-b19
 up 1000000000:1817ms 1.817ns / iteration(总和499999999500000000)
 up 1000000000:1786ms 1.786ns / iteration(总和499999999500000000)
 up 1000000000:1778ms 1.778ns / iteration(sum 499999999500000000)
 up 1000000000:1769ms 1.769ns / iteration(总和499999999500000000)
 up 1000000000:1769ms 1.769ns / iteration(总和499999999500000000)
 up 1000000000:1766ms 1.766ns / iteration(sum 499999999500000000)
 up 1000000000:1776ms 1.776ns / iteration(sum 499999999500000000)
 up 1000000000:1768ms 1.768ns / iteration(总和499999999500000000)
 up 1000000000:1771ms 1.771ns / iteration(sum 499999999500000000)
 up 1000000000:1768ms 1.768ns / iteration(总和499999999500000000)
下降1000000000:1847ms 1.847ns /迭代(总和499999999500000000)
下降1000000000:1842ms 1.842ns /迭代(总和499999999500000000)
下降1000000000:1838ms 1.838ns /迭代(总和499999999500000000)
下降1000000000:1832ms 1.832ns /迭代(总和499999999500000000)
下降1000000000:1842ms 1.842ns /迭代(总和499999999500000000)
下降1000000000:1838ms 1.838ns /迭代(总和499999999500000000)
下降1000000000:1838ms 1.838ns /迭代(总和499999999500000000)
下降1000000000:1847ms 1.847ns /迭代(总和499999999500000000)
下降1000000000:1839ms 1.839ns /次迭代(总和499999999500000000)
下降1000000000:1838ms 1.838ns /迭代(总和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) 

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

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

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

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

  1. 告诉你它真的,真的没关系,你浪费你的时间甚至想知道。

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

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

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 

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

您确定询问此类问题的面试官是否期望得到直接答案(“第一个更快”或“第二个更快”),或者如果要求此问题引发讨论,那么人们在答案中会发生这样的问题。在这里?

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

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

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

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

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

如问题所示,可以使用ifgt进行> 0测试,而< 100001测试则需要if_icmplt 。

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

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

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

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

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

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

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

我> 0; 我<100001;

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

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

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

我不是100%肯定这是它背后的原因,但这似乎是一个可能的原因……

答案是(你可能在网站上发现)

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

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

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

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

当我尝试计算java花了多长时间做“没事”时,花了大约500纳秒才有了想法。

然后我测试了运行for语句增加所需的时间:

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

然后五分钟后我尝试了“向后”的一个:

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

而且我在声明的第一个和第二个之间有一个巨大的差异(在一个微小的水平)16%,后者快16%。

在2000次测试期间运行“增加”语句的平均时间: 1838 n / s

在2000次测试期间运行“减少”语句的平均时间: 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); } 

结论:差别基本上没有,与for语句测试相比,它已经花费了大量“无”做“无”,使得它们之间的差异可以忽略不计,只需要导入像java这样的库所花费的时间。 util.Scanner比运行for语句更需要加载,它不会显着提高应用程序的性能,但它仍然很酷。