Java代码使用原始类型的效率问题

3

我想问在Java中哪一段代码更有效率? 代码1:

void f()
{
 for(int i = 0 ; i < 99999;i++)
 {
  for(int j = 0 ; j < 99999;j++)
  {
   //Some operations
  }
 }

}

代码 2:

void f()
{
 int i,j;
 for(i = 0 ; i < 99999;i++)
 {
  for(j = 0 ; j < 99999;j++)
  {
   //Some operations
  }
 }

}

我的老师说第二种方法更好,但我不同意这个观点。


1
你可以使用两种代码自行进行测试(通常大于99999),并计算运行代码所花费的时间。 对于我来说,如果我不会在for循环之外使用i、j,我会尝试使用第一种方法。理想情况下,第一种方法也会在完成循环后销毁int变量。 - Random
15
javac会将它们编译成完全相同的代码。请不要让你的老师说出这种废话,否则他将来还会教给其他学生。 - Oak
2
这是你的拉丁语老师吗?历史、化学,还是什么? - Kevin Bourrillion
顺便提一下,通常认为在高质量的代码中,变量声明应尽可能靠近执行它们的区域。我真的希望你的老师说过这样做更有效(可能也是错误的,但更少错误)而不是更好。无论效率如何,特别是在Java中,这种做法几乎被普遍认为是更糟糕的实践。 - kingfrito_5005
9个回答

32

IT.并不会有什么区别。

停止微观优化。这些小技巧并不能让程序运行更快。

集中精力进行大局优化和编写可读性更强的代码。

在有意义的地方声明变量,并在大的上下文语境中帮助理解整个代码语义,而不是因为你认为在某个地方它会更快。


1
同意!你应该关注为什么要使用嵌套循环以及嵌套循环内部进行了哪些操作。而不是外部的两个弱小整数! - basszero
然而,问题仍然非常有趣。为什么Java编译器没有优化掉这两个循环,因为它们没有副作用?即使“// some operations”不仅仅是注释,对于javac来说,这也是一种容易的微观优化,因此两种变体应该同样快。 - Alexandru
1
@Alexandru:.java 到字节码的转换是未经优化的。最有效的优化发生在 JIT 级别,而不是 javac 级别。 - polygenelubricants
1
我同意不进行微观优化的观点,但有一件事很重要:变量作用域。这可能会导致真正的问题。 - cletus
1
@Alexandru 调试、快速编译、平台相关的优化、基于使用情况的优化是一些原因。 - josefx
显示剩余5条评论

17
我更喜欢第一种写法,因为它将循环变量与方法中的其他代码分开。它们在循环之外不可见,这样您就不会无意中在以后引用它们了。
其他答案也是正确的:不要为了性能而担心这种事情。但是出于代码可读性和向下传达程序员意图的原因,请考虑这个问题。这比微观优化问题更重要。
现在,这是在Java语言(如Java语言规范)级别上的。在Java虚拟机级别上,使用这两种方式没有任何区别。局部变量的分配完全相同。
如果您不确定,可以始终编译它并查看发生了什么。让我们为这两个版本创建两个类f1和f2:
$ cat f1.java
public class f1 {
  void f() {
    for(int i = 0 ; i < 99999;i++) {
      for(int j = 0 ; j < 99999;j++) {
      }
    }
  }
}

$ cat f2.java
public class f2 {
  void f() {
    int i, j;
    for(i = 0 ; i < 99999;i++) {
      for(j = 0 ; j < 99999;j++) {
      }
    }
  }
}

编译它们:

$ javac f1.java
$ javac f2.java

并反编译它们:

$ javap -c f1 > f1decomp
$ javap -c f2 > f2decomp

然后进行比较:

$ diff f1decomp f2decomp
1,3c1,3
< Compiled from "f1.java"
< public class f1 extends java.lang.Object{
< public f1();
---
> Compiled from "f2.java"
> public class f2 extends java.lang.Object{
> public f2();

字节码完全没有区别。


那是一个“Booyaah,在你的脸上,老师!!”的回答。 :P - st0le

13

小心微基准测试的危险!!!

我将代码放入一个方法中并在循环中运行了10次。结果如下:

50, 3, 
3, 0, 
0, 0, 
0, 0, 
....

如果循环中没有实际的代码,编译器可以判断出这些循环没有任何用处,并将它们完全优化掉。根据所测得的性能,我怀疑这种优化可能是由javac完成的。

教训一:编译器通常会优化掉无用的代码。编译器越智能,就越有可能发生这种情况。如果你在编写代码时没有考虑到这一点,那么基准测试可能就毫无意义。

因此,我在两个循环中都添加了以下简单的计算if (i < 2 * j) longK++;并让测试方法返回longK的最终值。结果如下:

32267, 33382,
34542, 30136,
12893, 12900,
12897, 12889,
12904, 12891,
12880, 12891,
....
我们明显已经停止编译器将循环优化掉的操作。但现在我们可以在(这种情况下)前两对循环迭代中看到 JVM 预热的效果。第一和第二个循环迭代(一个方法调用)可能完全在解释模式下运行。看起来第三次迭代实际上可能是与 JIT 并行运行。到第三对迭代时,我们很可能正在运行纯本地代码。从那时起,两个版本循环的时间差异只是噪音。

教训2:始终考虑 JVM 预热的影响。这可能会严重扭曲基准测试结果,无论是微观还是宏观。

结论-一旦 JVM 预热完成,两个版本的循环之间没有可测量的差异。


3
+1 对于良好的解释和 JVM/JIT 过程优化效果的示例。我个人认为,人们经常忘记编译后的代码不必与原始代码完全相同,只要它在计算上等效即可。例如,递归斐波那契数列可以被聪明的编译器转换为简单的循环(在进入 JVM/JIT 之前)。 - M. Jessup
我非常确定编译器(javac)不负责优化第一组结果(零次)。这是由JVM完成的:只有在使用-server选项运行时才会得到零,否则通常需要约10秒。检查字节码不显示任何优化(尽管为两个循环重用本地变量的相同地址,无论它们在哪里声明)。 - user85421

6
第二个更糟糕。为什么?因为循环变量在循环外部被作用域化。循环结束后,i和j将具有值。通常这不是你想要的。第一个对循环变量进行了作用域限定,所以它只在循环内可见。

同意 - 任何效率提高都不值得因i和j的重用可能引起的问题。然而,有趣的是,在C++中,并非所有的编译器都强制执行在循环中声明的i和j的范围。 - Robben_Ford_Fan_boy
@Marcelo - @cletus 从没提过编译错误。 - Stephen C
Stephen在第一个版本中做了这个。我不知道为什么它没有出现在编辑中,但是我看到了。备注是第二个版本甚至不能编译。 - Marcelo Cantos
已经编辑过了吗?这两个代码在我的Eclipse中都可以正常工作... 编辑:好的,好的。 - Random

2
我猜对于任何半好的JVM实现,效率上没有任何区别。

2
不,这并不会对速度产生任何影响。它们都编译成相同的代码。而且像MasterGaurav所说的那样,没有分配和释放。

当方法开始时,JVM为所有本地变量分配足够的内存插槽,直到方法结束时才会进行更多的分配。

唯一微小的区别(除了范围之外)是,在第一个示例中,为i和j分配的内存可以被重用于其他变量。因此,JVM将为此方法分配较少的内存插槽(你节省了一些位数)。


0
首先,是的,你的老师是错的,第二个代码并不更好。到底什么是更好呢?这是因为在任何普通循环中,循环体内的操作都是耗时的部分。因此,代码2只是一种微小的优化,它并没有增加足够的速度(如果有的话)来证明代码的可读性差。

-4

第二种方法速度更快。

原因是在第一种情况下,变量j的作用域仅限于内部的for循环。

因此,一旦内部循环完成,变量j的内存将被释放,并再次分配给外部循环的下一次迭代。

由于内存分配和释放需要一些时间,即使它在堆栈上,第一种方法的性能也较慢。


2
不存在堆栈“分配”这样的东西。代码只是选择使用可用堆栈的某个部分来存储int。 - Marcelo Cantos
是的,答案是错误的。在JVM中没有分配和释放操作。 - H-H
变量j在第一个情况下超出了其作用域...那么,这是什么意思呢?我同意没有什么堆栈分配...但是,使用堆栈上的值并使其超出作用域... 这只是编译时的事情吗? - gvaish
MasterGaurav,我在我的回答中已经解释了这一点。JVM为所有本地变量分配足够的内存。当一个本地变量超出其作用域时,同一内存槽位将被用于另一个本地变量。 - H-H
谢谢 HH!我的理解有误 :) - gvaish

-4

还有一个非常重要的方面与这两个不同版本有关:

在变体2中,只创建了两个临时对象(i和j),而变体1将创建100000个对象(1个i和999999个j)。可能您的编译器会对此进行优化,但您不能确定。如果它没有进行优化,垃圾回收将表现得明显更差。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接