我在学校被告知修改for loop
的索引变量是不好的做法:
示例:
for(int i = 0 ; i < limit ; i++){
if(something){
i+=2; //bad
}
if(something){
limit+=2; //bad
}
}
论证是某些编译器优化可以优化循环,而不是重新计算索引并在每个循环中绑定。
我在java
进行了一些测试,看来默认索引和绑定每次都会重新计算。
我想知道是否可以在JVM HotSpot
中激活此类功能?
例如,优化这种循环:
for(int i = 0 ; i < foo.getLength() ; i++){ }
无需写:
int length = foo.getLength()
for(int i = 0 ; i < length ; i++){ }
这只是一个例子,我很想尝试看看改进。
修改
根据Peter Lawrey的回答为什么在这个简单的例子中JVM没有内联getLenght()
方法?:
public static void main(String[] args) {
Too t = new Too();
for(int j=0; j<t.getLength();j++){
}
}
class Too {
int l = 10;
public Too() {
}
public int getLength(){
//System.out.println("test");
return l;
}
}
在输出“test”中打印10次。
我认为优化这种执行可能会很好。
编辑2: 似乎我误解了......
我删除了println
,实际上,探查者告诉我,在这种情况下,方法getLength()
甚至不会调用一次。
答案 0 :(得分:13)
这取决于foo.getLength()的作用。如果它可以内联,它可以是有效的相同的东西。如果无法内联,则JVM无法确定结果是否相同。
顺便说一句,你可以写一个班轮。
for(int i = 0, length = foo.getLength(); i < length; i++){ }
编辑:没有任何价值;
答案 1 :(得分:12)
我在java中做了一些测试,看起来默认索引和绑定每次重新计算。
根据Java语言规范,这个:
for(int i = 0 ; i < foo.getLength() ; i++){ }
表示在每次循环迭代时调用getLength()
。 Java编译器只有允许才能将getLength()
调用移出循环,如果他们能够有效证明它不会改变可观察行为。 (例如,如果getLength()
每次只是从同一个变量返回相同的值,那么JIT编译器很可能可以内联调用,然后然后推断它可以执行但如果getLength()
涉及获取并发或同步集合的长度,则允许优化的可能性很小......因为其他线程可能会采取行动。)
这就是允许执行的编译器。
我想知道是否可以在JVM HotSpot中激活这种功能?
简单的答案是否。
您似乎建议使用编译器开关来告知/允许编译器忽略JLS规则。没有这样的开关。这样的转换将是 BAD IDEA 。这将导致正确/有效/工作程序中断。考虑一下:
class Test {
int count;
int test(String[] arg) {
for (int i = 0; i < getLength(arg); i++) {
// ...
}
return count;
}
int getLength(String[] arg) {
count++;
return arg.length;
}
}
如果允许编译器将getLength(arg)
调用移出循环,它将改变调用该方法的次数,从而更改test
方法返回的值。
改变正确编写的Java程序行为的Java优化不是有效的优化。 (请注意,多线程会使水变得混乱.JLS,特别是内存模型规则,允许编译器执行优化,这可能导致不同的线程看到应用程序状态的不一致版本...如果它们不同步从开发人员的角度来看,导致行为是不正确的。但真正的问题在于应用程序,而不是编译器。)
顺便说一句,更有说服力的原因是你不应该更改循环体中的循环变量,这会让你的代码更难理解。
答案 2 :(得分:4)
不这样做的主要原因是它使得理解和维护代码变得更加困难。
无论JVM如何优化,都不会损害程序的正确性。如果由于索引在循环内被修改而无法进行优化,那么它将不会对其进行优化。如果有这样的优化,我无法看到Java测试如何显示。
无论如何,Hotspot会为你优化很多东西。而你的第二个例子是Hotspot很乐意为你做的一种明确的优化。
答案 3 :(得分:2)
在我们进行更多推理为什么之前,不会内联字段访问。也许我们应该表明是的,如果你知道你在寻找什么(这在Java中真的是非常重要的),那么字段访问就好了。
首先,我们需要对JIT如何工作有一个基本的了解 - 我真的不能在一个答案中这样做。可以说JIT仅在经常调用函数后才起作用(通常> 10k)
因此我们使用以下代码进行实际测试:
public class Test {
private int length;
public Test() {
length = 10000;
}
public static void main(String[] args) {
for (int i = 0; i < 14000; i++) {
foo();
}
}
public static void foo() {
Test bar = new Test();
int sum = 0;
for (int i = 0; i < bar.getLength(); i++) {
sum += i;
}
System.out.println(sum);
}
public int getLength() {
System.out.print("_");
return length;
}
}
现在我们编译这段代码并用java.exe -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,*Test.foo Test >Test.txt
运行它会产生一个邪恶的长输出,但有趣的部分是:
0x023de0e7: mov %esi,0x24(%esp)
0x023de0eb: mov %edi,0x28(%esp)
0x023de0ef: mov $0x38fba220,%edx ; {oop(a 'java/lang/Class' = 'java/lang/System')}
0x023de0f4: mov 0x6c(%edx),%ecx ;*getstatic out
; - Test::getLength@0 (line 24)
; - Test::foo@14 (line 17)
0x023de0f7: cmp (%ecx),%eax ;*invokevirtual print
; - Test::getLength@5 (line 24)
; - Test::foo@14 (line 17)
; implicit exception: dispatches to 0x023de29b
0x023de0f9: mov $0x3900e9d0,%edx ;*invokespecial write
; - java.io.PrintStream::print@9
; - Test::getLength@5 (line 24)
; - Test::foo@14 (line 17)
; {oop("_")}
0x023de0fe: nop
0x023de0ff: call 0x0238d1c0 ; OopMap{[32]=Oop off=132}
;*invokespecial write
; - java.io.PrintStream::print@9
; - Test::getLength@5 (line 24)
; - Test::foo@14 (line 17)
; {optimized virtual_call}
0x023de104: mov 0x20(%esp),%eax
0x023de108: mov 0x8(%eax),%ecx ;*getfield length
; - Test::getLength@9 (line 25)
; - Test::foo@14 (line 17)
0x023de10b: mov 0x24(%esp),%esi
0x023de10f: cmp %ecx,%esi
0x023de111: jl 0x023de0d8 ;*if_icmpge
; - Test::foo@17 (line 17)
这是我们实际执行的内部循环。请注意,以下0x023de108: mov 0x8(%eax),%ecx
将长度值加载到寄存器中 - 上面的内容用于System.out调用(我已将其删除,因为它使其更复杂,但因为不止一个人认为这个会妨碍内联我把它留在那里)。即使你不适合x86程序集,你也可以清楚地看到:除了原生的写调用之外,没有任何调用指令。