for 循环是不是在每次迭代中重新评估其主体中的函数?

Posted

技术标签:

【中文标题】for 循环是不是在每次迭代中重新评估其主体中的函数?【英文标题】:Does a for loop re-evaluate the functions in its body in each iteration?for 循环是否在每次迭代中重新评估其主体中的函数? 【发布时间】:2017-04-14 19:55:29 【问题描述】:

当我像这样写一个简单的for循环时

for (int i = 0; i < myList.size(); i++)

在 Java、C 和 C++ 中,它是在每次迭代中重新评估 myList.size() 还是仅在循环开始时重新评估一次?如果它重新评估,是否预先评估大小,即,

int n = myList.size();
for (int i = 0; i < n; i++)

在性能方面给了我们一些重要的东西?

【问题讨论】:

哪种语言? c++、c 还是 java? 选择一种语言。你提到的每一个都有一个不同的规范文档来管理它的行为方式 没有 C/C++ 这样的东西。不要散布这种废话。 @StoryTeller:它现在是一个更大的超集,但仍然是一个超集。 @DaveDoknjas - 不是,也从来不是。 int *p = malloc(sizeof *p); 在 C 中始终有效,而在 C++ 中从不有效。自 2011 年以来,两种语言之间的差距更大,两者都不是另一种语言的子集。停止传播错误信息。 【参考方案1】:

对于 Java:

for (int i=0; i<myList.size(); i++)

条件在每个循环迭代期间进行评估;因此,将调用移至循环前面的 size() 会略微提高性能。

但在 Java 中你应该更喜欢 for-each

for (Whatever thingy : list)

无论如何都记法(在可能的情况下)。

您可以阅读 Java 语言规范,章节 14.14.1。

【讨论】:

也许值得指出的是,它在JLS Sec 14.14.1中有所描述。 在后台有这么棒的校对员真是太好了。谢谢! 大多数编译器会通过确定结束条件是否有可能在循环内改变来优化这一点——如果不能改变,则没有性能问题。 java 编译器不做这种优化。在我们的世界中,JIT 对几乎未优化的字节码进行了繁重的工作。 javac 做了一些简单的事情,比如常量折叠,但仅此而已。【参考方案2】:

在 C 和 C++ 中,控制表达式在循环的每次次迭代中计算。

来自C standard 的第 6.8.5.3 节:

声明

for (clause-1; expression-2; expression-3) statement

表现如下:表达式expression-2是 在每次执行之前评估的控制表达式 表达式 expression-3 被计算为 每次执行循环体后的 void 表达式。如果 clause-1 是一个声明,它声明的任何标识符的范围 是声明的其余部分和整个循环,包括 其他两个表达式;它是按照之前的执行顺序到达的 控制表达式的第一次评估。如果clause-1 是 表达式,它在第一个表达式之前被评估为 void 表达式 控制表达式的评估。

C++ standard 的第 6.5.3 节包含类似的措辞。

因此,如果您的控制表达式涉及函数调用或其他一些潜在的昂贵计算不会在循环体中改变,您应该事先评估它,将它存储在一个变量中,然后使用它控制表达式中的变量。

【讨论】:

我不知道我是否会说您“应该”调用该函数一次并存储它。这不一定能节省您的时间。现代编译器和 STL 实现可能已经对 size() 函数的调用进行了高度优化。列表的大小也有可能在循环中被修改。 (不建议这样做,但并非不可能。)过早的优化是万恶之源。 另外,正如其他答案所指出的,编译器可以优化控制表达式中的调用,因此控制表达式总是在每次迭代时都被评估并不是绝对正确的。跨度> 【参考方案3】:

逻辑上在每次迭代时都会评估条件。

您可以通过查看您的示例来判断这一点,因为表达式中 i 的值每次都会发生变化。

在实践中,它可能取决于语言或实现。编译器或运行时可能能够缓存或优化部分表达式。孤立地看代码不一定能分辨出来。

【讨论】:

【参考方案4】:

取决于 myList 是否在 for 循环内被修改。此外,编译器(尤其是在 C 中)试图通过将评估移到循环外来优化此类代码。优化也是基于对象是否在循环内被修改。

【讨论】:

【参考方案5】:

在 C 和 C++ 中,标准中没有要求优化函数调用(例如只调用一次)。

编译器的优化引擎需要知道size 的返回类型以及该值是否会改变。一项艰巨的任务。

如果你希望函数被调用一次,在循环之前将结果赋值给一个常量变量:

const size_t size = myList.size();
for (size_t i = 0; i < size; ++i)

  // ...

如果myList 在循环内更改,则所有保证关闭。每次迭代都需要调用函数size

【讨论】:

【参考方案6】:

我只熟悉 C/C++,正确答案是:有时会被评估,有时不会。编译器保证它将在每次迭代中评估它认为与您的原始代码等效的汇编代码(但不是您真正想要的)。如果您在调试模式下编译代码(没有编译器优化),或者您的变量在其定义中具有关键字 volatile,或者您更改了列表的大小,那么每次迭代都会评估条件(列表的大小)。但是,如果您使用速度优化进行编译,不要在循环中更改列表,那么现代编译器足够聪明,可以将列表的大小存储在寄存器中,而不是每次迭代都对其进行评估。此外,函数 list.size() 将被内联,实际上不会调用代价高昂的函数调用。

请注意,在多线程编程中,这是灾难性的。一个线程可以将元素附加到列表中,而另一个遍历列表的线程将永远不会看到新元素。要强制评估,列表的大小必须定义为 volatile(或者如果您熟悉汇编,则可以使用内存屏障)。无论如何,当您为生产编译应用程序时,请确保启用编译器优化,并且每次迭代中所谓的函数调用的运行时间损失将被取消。

另一个问题:一些非常激进的编译器优化甚至可能展开你的循环。例如,如果您的列表的大小为 403 个元素,则编译器可能会执行 100 次迭代,每次迭代使您的循环内代码重复 4 次 + 使用原始代码对另一个循环进行 3 次迭代。所以总共会有 103 次迭代而不是 403 次(因此无法争论代码是否在每次迭代中执行,因为很难定义什么是迭代)。 这种激进优化的示例:复制长字符串或内存缓冲区。如果复制 X 字节,在 64 位机器上执行 X/8 次迭代(一次复制 64 位)+ 在单独的循环中剩余 0..7 字节实际上更快。有些处理器甚至在单个操作中支持 128 和高达 512 字节的单位

最后建议:编写易于理解的代码非常重要,因此在这种情况下,我会在上面的答案中使用好的建议(关于 java 列表迭代器)或故意将 list.size() 保存到临时变量中

const in list_size = list.size()

只是为了便于阅读/调试

【讨论】:

以上是关于for 循环是不是在每次迭代中重新评估其主体中的函数?的主要内容,如果未能解决你的问题,请参考以下文章

在 C# 中是不是每次都评估循环声明?

for 循环如何评估其参数

我需要一种类似于for循环重新评估测试条件的行为的算法

如何编写用于迭代 DataFrame 的 for 循环并将其子集化以仅包含在每次迭代中检索到的那些列?

如何在Django视图中使用for循环返回每次迭代[关闭]

for 循环的“计数限制”表达式是不是只计算一次,还是在每次迭代时计算?