【问题标题】:Which is optimal?哪个是最优的?
【发布时间】:2009-12-11 05:13:54
【问题描述】:

在循环内声明一个变量是好的还是在 Java 中动态声明最佳。在循环内声明时是否涉及任何性能成本?

例如。

选项 1:在循环之外

List list = new ArrayList();
int value;

//populate list
for(int i = 0 ; i < list.size(); i++) {
  value = list.get(i);
  System.out.println(“value is ”+ value);
}

选项 2:循环内部

List list = new ArrayList();

//populate list
for(int i = 0; i < list.size(); i++) {
  int value = list.get(i);
  System.out.println(“value is ”+ value);
}

【问题讨论】:

  • 我的理解是选项 1 只会创建和使用单个内存地址,而选项 2 将为列表中的许多项目创建内存地址。
  • 我认为像这样的原始数据类型在本地声明时会出现在 VM 堆栈上。如果是这样,则性能没有差异——int value 是通过在堆栈帧中保留一个字来“分配”的。
  • 你的理解是错误的。从语义上讲,您可以将其视为在每次循环迭代开始时“分配”一个新的局部变量,然后在结束时“释放”它 - 因此,有理由认为,您永远不会有多个本地“分配” ”。在实践中,它将一直是堆栈上的单个内存位置(甚至可能是单个寄存器)。
  • 所有数据类型在本地声明时进入堆栈,无论是否原始。非原始类型是引用类型,reference 进入堆栈。对于堆上的任何东西,某个地方的某个人必须 new 它(或使用数组初始化器或字符串文字)。
  • 注意:编写的示例会产生编译错误,因为原始ArrayList 上的get 方法将返回Object 而不是int

标签: java performance loops


【解决方案1】:

Clean Code 中,Robert C. Martin 建议 Java 编码人员将变量声明为尽可能接近它们的使用位置。变量的范围不应超出必要范围。将变量声明靠近它的使用位置有助于为阅读器提供类型和初始化信息。不要太在意性能,因为 JVM 非常擅长优化这些事情。而是专注于可读性。

顺便说一句:如果您使用的是 Java 5 或更高版本,则可以使用以下 Java-5 的新功能显着优化您的代码示例:

  • foreach 构造
  • 泛型
  • 自动装箱

我已重构您的示例以使用上述新功能。

List<Integer> list = new ArrayList<Integer>();

// populate list

for (int value : list) {
    System.out.println("value is " + value);
}

【讨论】:

  • 另外,两个选项之间的行为不同。在第一个选项中,“值”变量的状态有可能在循环迭代之间“泄漏”。在第二个中,这是不可能的。如果两者之间的性能差异是您最担心的,那么您的位置比我见过的任何代码都好!
  • 我同意,始终选择易读性而不是性能 - 直到您进入优化阶段,此时您将计时一切,您会发现这种明显的优化对底部没有任何影响线。我经常看到“圈外”的方法,真的很讨厌它。
【解决方案2】:

从性能角度来看,您实施哪种方式应该没有区别。

但更重要的是,您不应该像这样浪费时间对代码进行微优化……除非您已经分析了整个应用程序并确定此片段是性能瓶颈。 (如果你这样做了,你就可以很好地查看两个版本的代码之间是否真的有任何区别。但如果有的话,我会非常惊讶......)

【讨论】:

  • 这可能会有所不同,因为寄存器分配可能不同;取决于正在使用的寄存器分配算法,具有较长“生存期”的变量通常会溢出到内存中,而具有较短生存期的变量将存储在寄存器中。
  • 绝对确定的唯一方法是分析应用程序。这才是我真正的意思。
【解决方案3】:

虽然您的示例是一个假设的、很可能不是真实世界的应用程序,但简单的答案是在这种情况下您根本不需要变量。没有理由分配内存。简单地说,浪费的内存成为 JVM 中的炮灰。您已经分配了内存来将值存储在 List 中,为什么要将它复制到另一个变量中?

变量的使用范围通常会决定它应该在哪里声明。例如:

Connection conn = null;
try {
    conn.open();
} catch (Exception e) {
    e.printStackTrace();
} finally {
    conn.close();
}

其他答案很好地揭示了您提供的示例的其他缺陷。有更好的方法来遍历列表,无论是使用实际的迭代器还是 foreach 循环,泛型将有助于消除创建原始副本的需要。

【讨论】:

【解决方案4】:

好吧,如果您担心优化该代码 - 我不确定 Java 如何评估循环,但是在循环声明内部调用 list.size() 可能不如在循环外部有效(并将其设置为一个变量listLength 也许)。我很确定该方法在 JavaScript 中更快。它可能更有效的原因是在 for 循环中调用 size 函数意味着它必须在每次运行测试时调用该函数以查看循环是否完成,而不是针对静态值进行测试。

【讨论】:

    【解决方案5】:

    遍历列表的最佳方式是使用迭代器

    for (Iterator iter=list.iterator(); iter.hasNext();) {
      System.out.println(iter.next());
    }
    

    【讨论】:

    • 您需要将其转换为字符串 - iter.next() 将返回一个对象
    • @OMG Ponies:System.out.println(Object o) 有一个定义,所以不需要转换为字符串 - 此外,实现将调用 String.valueOf(o),根据1.5 api文档。
    • 如果你从一个集合的开始迭代到结束,使用每个循环的内置是最确定的。
    【解决方案6】:

    在这样的简单情况下,很可能没有区别,编译器生成完全相同的代码(假设您在声明变量时没有设置初始值)。

    在更复杂的情况和更长的函数中,在循环或其他块中声明局部变量可能会更有效。这缩短了变量的生命周期,从而使编译器更容易优化代码。当块外不存在变量时,用于存储变量的寄存器可用于其他用途。

    这当然取决于编译器的实现。我不了解 Java,但至少有一些 C 编译器制造商在他们的文档中给出了这个建议。

    至于可读性,我的观点是,在简短的函数中,最好在开头声明所有变量,这样可以很容易地找到它们。在很长的函数中(无论如何都应该避免),在块内声明变量可能会更好(这恰好也更有效)。

    【讨论】:

      【解决方案7】:

      除了别人给出的有用建议,请记住一件事:

      从不尽早优化。如果你真的认为你的代码很慢并且可能需要改进,那么对你的代码使用一个分析器来发现瓶颈所在,然后才对它们进行重构。了解错误在哪里。下次不要重蹈覆辙。

      在您的情况下,我会说,根据 java VM 版本,您的性能(猜猜看)可能会有所不同。出于经验,我不会在循环中声明变量;一个 int 肯定会被编译器优化出来并重用相同的内存地址,额外的计算成本可能可以忽略不计。

      但是。

      如果您在循环中声明一个对象,那么情况会有所不同。如果您的对象在创建时进行 I/O 写入怎么办?网络 DNS 查找?你可能不知道/不在乎。因此,最佳做法是将其声明在外面。

      另外,不要将性能与最佳实践混为一谈。他们可能会将您带入危险的领域。

      【讨论】:

        【解决方案8】:

        Java 编译器根据您使用局部变量的方式确定堆栈帧中需要多少“槽”。这个数字是根据方法中任何给定点的最大活动本地数确定的。

        如果你的代码只是

        int value;
        for (...) {...}
        

        for (...) {
           int value;
           ...
        }
        

        运行时堆栈上只需要一个插槽;无论循环运行多少次,同一个槽都可以在循环内重复使用。

        但是,如果您在循环之后执行需要另一个插槽的操作:

        int value;
        for (...) {...}
        int anotherValue;
        

        for (...) {
           int value;
           ...
        }
        int anotherValue;
        

        我们会看到不同之处 - 第一个版本需要两个插槽,因为两个变量在循环后都处于活动状态;在第二个示例中,只需要一个槽,因为我们可以在循环后将 "value" 中的槽用于 "anotherValue"。

        注意:编译器可以根据实际使用数据的方式进行更智能的优化,但这个简单的示例旨在证明堆栈帧分配可能存在差异。

        【讨论】:

          猜你喜欢
          • 2021-02-18
          • 2021-11-02
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-12-23
          • 1970-01-01
          • 2022-01-20
          • 2021-06-19
          相关资源
          最近更新 更多