是否会在循环的每次迭代中重新分配变量会影响性能?

时间:2008-10-08 20:05:41

标签: java optimization

考虑以下两种在Java中编写循环的方法,以查看列表是否包含给定值:

样式1

boolean found = false;
for(int i = 0; i < list.length && !found; i++)
{
   if(list[i] == testVal)
     found = true;
}

样式2

boolean found = false;
for(int i = 0; i < list.length && !found; i++)
{
   found = (list[i] == testVal);
}

这两个是等价的,但我总是使用样式1,因为1)我觉得它更具可读性; 2)我假设将found重新分配给false数百次感觉就像需要更多时间。我想知道:第二个假设是真的吗?

Nitpicker的角落

  • 我很清楚这是一个过早优化的案例。这并不意味着它不是有用的东西。
  • 我不在乎你认为哪种风格更具可读性。我只对与其他人相比是否有性能损失感兴趣。
  • 我知道样式1的优点是允许你在break;块中添加if语句,但我不在乎。同样,这个问题是关于表现,而不是风格。

13 个答案:

答案 0 :(得分:8)

好吧,只需写一个微观基准:

import java.util.*;

public class Test {
    private static int[] list = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9} ;
    private static int testVal = 6;


    public static boolean version1() {
        boolean found = false;
        for(int i = 0; i < list.length && !found; i++)
        {
        if(list[i] == testVal)
            found = true;
        }
        return found;

    }

    public static boolean version2() {
    boolean found = false;
    for(int i = 0; i < list.length && !found; i++)
        {
        found = (list[i] == testVal);
        }

    return found;
    }


    public static void main(String[] args) {

        // warm up
    for (int i=0; i<100000000; i++) {
        version1();
        version2();
    }


    long time = System.currentTimeMillis();
    for (int i=0; i<100000000; i++) {
        version1();
    }

    System.out.println("Version1:" + (System.currentTimeMillis() - time));

    time = System.currentTimeMillis();
    for (int i=0; i@lt;100000000; i++) {
        version2();
    }

        System.out.println("Version2:" + (System.currentTimeMillis() - time));
    }
}

在我的机器上,版本1似乎要快一点:

版本1:5236

版本2:5477

(但是在1亿次迭代中这是0.2秒。我不关心这个。)

如果查看生成的字节码,版本2中还有两条指令可能导致执行时间更长:

public static boolean version1();
  Code:
   0:   iconst_0
   1:   istore_0
   2:   iconst_0
   3:   istore_1
   4:   iload_1
   5:   getstatic   #2; //Field list:[I
   8:   arraylength
   9:   if_icmpge   35
   12:  iload_0
   13:  ifne    35
   16:  getstatic   #2; //Field list:[I
   19:  iload_1
   20:  iaload
   21:  getstatic   #3; //Field testVal:I
   24:  if_icmpne   29
   27:  iconst_1
   28:  istore_0
   29:  iinc    1, 1
   32:  goto    4
   35:  iload_0
   36:  ireturn

public static boolean version2();
  Code:
   0:   iconst_0
   1:   istore_0
   2:   iconst_0
   3:   istore_1
   4:   iload_1
   5:   getstatic   #2; //Field list:[I
   8:   arraylength
   9:   if_icmpge   39
   12:  iload_0
   13:  ifne    39
   16:  getstatic   #2; //Field list:[I
   19:  iload_1
   20:  iaload
   21:  getstatic   #3; //Field testVal:I
   24:  if_icmpne   31
   27:  iconst_1
   28:  goto    32
   31:  iconst_0
   32:  istore_0
   33:  iinc    1, 1
   36:  goto    4
   39:  iload_0
   40:  ireturn

答案 1 :(得分:3)

关于nitpicks角落的评论:

如果您真的关注绝对性能,那么暂停和删除“&amp;&amp;!found”将在理论上为#1提供更好的性能。每次迭代都要担心两个二进制运算。

如果你想在不使用休息的情况下获得优化,那么

boolean notFound = true;
    for(int i = 0; notFound && i < list.length; i++)
    {
       if(list[i] == testVal)
         notFound = false;
    }

在平均情况下比现有选项#1运行得更快。

当然这是个人偏好,但我宁愿永远不要在for循环的头部进行任何额外的评估。我发现它在阅读代码时会引起混淆,因为它很容易被遗漏。如果我无法使用break / continue获得所需的行为,我将使用while或do / while循环。

答案 2 :(得分:2)

实际上,由于pipeline,“if”会使您的程序速度下降超过作业。

答案 3 :(得分:1)

这取决于您使用的编译器,因为不同的编译器可能会进行不同的优化。

答案 4 :(得分:1)

我相信风格2的速度要快得多 - 比如说大约1个时钟周期。

但是,如果我正在处理它,我会将其重写为以下内容:

for(i=0; i<list.length && list[i]!=testval; i++);
boolean found = (i!=list.length);

答案 5 :(得分:1)

在我看来,如果你希望在列表结尾之前找到你的价值,那么你最好用#2 - 因为它会在循环条件中找到!假设你放弃了第一个选项(唯一明智的东西,IMO),那么伪装配将看起来像:

选项1:

start:
  CMP i, list.length
  JE end
  CMP list[i], testval
  JE equal
  JMP start
equal:
  MOV true, found
end:

选项2:

start:
  CMP i, list.length
  JE end
  CMP true, found
  JE end
  CMP list[i], testval
  JE equal
  JNE notequal
equal:
  MOV true, found
  JMP start
notequal:
  MOV false, found
  JMP start
end:

我认为选项1在这里更优越,因为它的指令少了1/3。当然,这是没有优化的 - 但这是编译器和特定情况(在此之后发现了什么?我们可以一起优化它吗?)。

答案 6 :(得分:1)

这是另一种风格

for(int i = 0; i < list.length; i++)
{
   if(list[i] == testVal)
     return true;
}

return false;

答案 7 :(得分:1)

从性能的角度来看,我认为这两种选择都有待改进。

考虑每次迭代进行多少次测试(几乎总是跳转),并尽量减少数量。

Matt的解决方案是,当找到答案时返回,将测试次数从三次(循环迭代器,在循环中找到测试,实际比较)减少到两次。做“发现”测试基本上两次是浪费。

我不确定经典但有些模糊不清的向后循环技巧是否是Java中的一个胜利,而且在阅读JVM代码时也不够热,无论如何都要解决它。

答案 8 :(得分:0)

我想说在98%的系统中,没关系。差异(如果有的话)几乎不可察觉,除非该循环是代码的主要部分并且运行了多次思考。

编辑:这是假设它尚未被编译器优化。

答案 9 :(得分:0)

任何体面的编译器都会在循环的持续时间内保留在寄存器中,因此成本绝对可以忽略不计。

如果第二种样式没有分支,那么它会更好,因为CPU的管道不会中断那么多......但这取决于编译器如何使用指令集。

答案 10 :(得分:0)

这只能在对性能非常敏感的代码(模拟器,模拟器,视频编码软件等)中进行测量,在这种情况下,您可能希望手动检查生成的代码,以确保编译器实际生成合理的代码。

答案 11 :(得分:0)

可以肯定的是,您应该编译两个版本(比如Sun的最新编译器)并使用适当的工具检查生成的字节码......这是唯一可靠的方法,确切地知道,其他一切都是疯狂猜测。

答案 12 :(得分:0)

boolean found = false;
for(int i = 0; i < list.length && !found; i++)
{
   if(list[i] == testVal)
     found = true;
}

我在块中没有看到中断语句。

除此之外,我更喜欢这种风格。它提高了可读性,从而提高了维护者误读和错误修复的可能性。