对 for 构造中的第二个表达式使用 size() 总是不好的吗?

Posted

技术标签:

【中文标题】对 for 构造中的第二个表达式使用 size() 总是不好的吗?【英文标题】:Is using size() for the 2nd expression in a for construct always bad? 【发布时间】:2010-09-15 01:31:17 【问题描述】:

在下面的示例中,我是否应该期望每次循环都会调用values.size()?在这种情况下,引入一个临时的vectorSize 变量可能是有意义的。或者现代编译器是否应该能够通过识别向量大小不能改变来优化调用。

double sumVector(const std::vector<double>& values) 
    double sum = 0.0;
    for (size_t ii = 0; ii < values.size(); ++ii) 
        sum += values.at(ii);
    

请注意,我不在乎是否有更有效的方法来对向量的内容求和,这个问题只是关于在 for 构造中使用 size()。

【问题讨论】:

这并不重要,但请更正您的示例代码并将“h”重命名为“sum”... 通过 const 值传递参数很少有用。您最好通过 const 引用传递它。有关详细信息,请参阅 GoTW#81 (gotw.ca/gotw/081.htm)。 【参考方案1】:

这是一种明确的方法 - size() 只被调用一次。

for (size_t ii = 0, count = values.size();  ii < count;  ++ii)

编辑:我被要求实际回答这个问题,所以这是我最好的选择。

编译器通常不会优化函数调用,因为它不知道从一次调用到下一次调用是否会获得不同的返回值。如果循环内有无法预测副作用的操作,它也不会优化。内联函数可能会有所作为,但没有任何保证。局部变量更容易被编译器优化。

有些人会称之为过早优化,我同意在少数情况下您会注意到速度差异。但是,如果它不会使代码更难理解,为什么不将其视为最佳实践并采用它呢?肯定不会痛的。

附:我在仔细阅读Benoit's 答案之前写了这篇文章,我相信我们完全同意。

【讨论】:

没有冒犯,但是 - 我很惊讶这得到了 9 票。它实际上并没有回答这个问题......【参考方案2】:

这完全取决于向量的实现大小、编译器的激进程度以及它是否侦听/使用内联指令。

我会更具防御性并引入临时性,因为您无法保证编译器的效率。

当然,如果这个例程被调用一两次,并且向量很小,那真的没关系。

如果它会被调用数千次,那么我会使用临时的。

有些人可能称之为过早优化,但我倾向于不同意这种评估。 当您尝试优化代码时,您并没有投入时间或以性能的名义混淆代码。

我很难将重构视为优化。但说到底,这就是“你说番茄,我说番茄”……

【讨论】:

size() 是否被声明为内联并不真正相关,因为编译器可能决定内联未声明为内联的方法,也可以决定 明确限定的内联方法。 相关的是 size() 在#includes 的编译单元中可供编译器使用。为此,大小必须内联在标题中。 编译器无法知道函数 size() 是否为纯函数。因此,它无法优化呼叫。在 C++ 中每次都会调用 size()。【参考方案3】:

从“for”结构中的 size() 开始,直到您需要优化速度。

如果太慢,想办法让它更快,比如使用临时变量来保存大小的结果。

【讨论】:

过早的优化不好,所以我同意这个帖子。 不仅不好,而且有时会导致二进制代码的优化程度低于编译器最初生成的代码。 我很难考虑这种重构过早优化的情况。 当您发现自己重复调用始终返回相同值的函数时,让临时变量存储值被认为是一种很好的做法。一般来说,这些多次调用不会在一个循环中,所以这是代码清晰性的问题,而不是速度问题,但原则也适用于此。【参考方案4】:

无论优化设置如何,将 .size() 调用放在第二个表达式中最多与在 for 循环之前概述 .size() 调用一样高效。那就是:

size_t size = values.size();
for (size_t ii = 0; ii < size; ++ii) 
    sum += values.at(ii)

将始终表现得至少和以下一样好,如果不是更好的话:

for (size_t ii = 0; ii < values.size(); ++ii) 
    sum += values.at(ii);

实际上,这可能无关紧要,因为概述 .size() 调用是一种常见的编译器优化。不过,我确实发现第二个版本更易于阅读。

不过,我发现这更容易:

double sum = std::accumulate(values.begin(), values.end(), 0);

【讨论】:

【参考方案5】:

值得注意的是,即使您要处理数百万个项目,开销也可以忽略不计。

在任何情况下,这都应该使用迭代器来编写 - 因为访问特定示例可能会有更多开销。

编译器真的不可能假设 size() 不会改变 - 因为它可以做到..

如果迭代的顺序不重要,那么您总是可以将其写成稍微更有效的方式。

for (int i=v.size()-1; i>=0 ;i--)

   ...

【讨论】:

尺寸如何变化?向量被声明为常量。 在这种情况下,大小不能改变。在更一般的情况下(非常量参数,更复杂的循环体,函数调用将非常量参数传递给可能修改其参数的函数),那么理查德的观点可能是有效的。 而且即使是 const 也不代表不能改变。这意味着您不能使用 const 引用来更改它。但是,在更复杂的循环体中,它可能会被别名。例如,您从循环中调用的任何函数都可能具有对其的非常量静态引用。 对于评论者,在这种情况下,由于其他原因,减量可能会更快:***.com/questions/24886/… 编译器可以强制限制不修改 const 对象 - 但不能确定该对象是不可变的。它不知道对象的内部工作原理,因此它必须调用该方法。程序员可以通过不同的 for 循环措辞来决定,从而进行优化。【参考方案6】:

这不是问题的一部分,但您为什么在代码中使用 at 代替下标运算符 []

at 的含义是确保不会发生对无效索引的操作。但是,在您的循环中永远不会出现这种情况,因为您从代码中知道索引将是什么(总是假设单线程)。

即使如果您的代码包含逻辑错误,导致您访问无效元素,at 在这个地方将毫无用处,因为您不希望出现异常,因此您不会t 对待它(或者你是否用try 块将你的循环所有括起来?)。

这里使用at 具有误导性,因为它告诉读者您(作为程序员)不知道索引将具有什么值——这显然是错误的。

我同意 Curro 的观点,这是使用迭代器的典型案例。虽然这更冗长(至少如果你不使用像 Boost.Foreach 这样的结构),但它也更具表现力和安全性。

Boost.Foreach 允许您编写如下代码:

double sum = 0.0;
foreach (double d, values)
    sum += d;

此操作安全、高效、简短、易读。

【讨论】:

实际上 .at() 是我在将代码扔给狼之前试图完全正确。原来只是用了[]。【参考方案7】:

这根本不重要。 .at() 的性能开销非常大(它包含一个条件抛出语句),以至于非优化版本将大部分时间都花在那里。一个足够聪明以消除条件抛出的优化编译器必然会发现 size() 不会改变。

【讨论】:

【参考方案8】:

我同意 Benoit 的观点。引入一个新变量,尤其是一个 int 甚至是一个 short 将比每次调用它有更大的好处。

如果循环变得足够大以致可能影响性能,那就不用担心了。

【讨论】:

【参考方案9】:

如果你将向量的大小保存在一个临时变量中,你将独立于编译器。

我的猜测是,大多数编译器都会以某种方式优化代码,即 size() 只会被调用一次。但是使用临时变量会给你一个保证,size() 只会被调用一次!

【讨论】:

【参考方案10】:

std::vector 的 size 方法应该由编译器内联,这意味着每次对 size() 的调用都被它的实际主体替换(有关内联的更多信息,请参阅问题 Why should I ever use inline code)。由于在大多数实现中 size() 基本上计算 end() 和 begin() 之间的差异(也应该内联),因此您不必太担心性能损失。

此外,如果我没记错的话,一些编译器足够“聪明”,可以在 for 构造的第二部分检测表达式的常量性,并生成只对表达式求值一次的代码。

【讨论】:

但是vector的size()方法不会是一个const。如果你修改循环中的向量怎么办?示例中没有这样做,但可以这样做,所以我不确定编译器是否能够判断 size() 调用在这种情况下是否保持不变。 你不能在循环中修改向量,它是一个常量引用......或者至少在我更正它时会这样;-)【参考方案11】:

编译器不会知道 .size() 的值是否在调用之间发生变化,因此它不会进行任何优化。我知道您刚刚询问了 .size() 的使用,但无论如何您都应该使用迭代器。

std::vector<double>::const_iterator iter = values.begin();
for(; iter != values.end(); ++iter)

    // use the iterator here to access the value.

在这种情况下,对 .end() 的调用类似于您使用 .size() 公开的问题。如果您知道循环不会在向量中执行任何使迭代器无效的操作,则可以在进入循环之前将迭代器初始化到 .end() 位置并将其用作边界。

【讨论】:

【参考方案12】:

始终按照您的意思在第一次编写代码。如果您正在从零迭代向量到 size(),请这样写。不要将 size() 调用优化为临时变量,除非您已将调用分析为程序中需要优化的瓶颈。

一个好的编译器很可能会优化掉对 size() 的调用,特别是考虑到向量被声明为 const。

【讨论】:

【参考方案13】:

如果您使用的容器中 size() 为 O(n)(如 std::list)而不是 O(1)(如 std::vector),则不会使用索引遍历该容器。您将改用迭代器。

无论如何,如果循环的主体是如此微不足道,以至于重新计算 std::vector::size() 很重要,那么无论它是什么,都可能有一种更有效(但可能特定于平台)的方法来进行计算。如果循环体很重要,那么每次重新计算 std::vector::size() 不太重要。

【讨论】:

【参考方案14】: 如果您在 for 循环中修改向量(添加或删除元素),则不应使用临时变量,因为这可能会导致错误。 如果您没有在 for 循环中修改向量大小,那么我会一直使用临时变量来存储大小(这将使您的代码独立于 vector::size 的实现细节。

【讨论】:

【参考方案15】:

在这种情况下,使用迭代器更简洁——在某些情况下甚至更快。对容器只有一次调用 - 如果有任何剩余,则让迭代器持有指向向量成员的指针,否则为 null。

那么for 当然可以变成while 并且根本不需要临时变量 - 您甚至可以将迭代器传递给 sumVector 函数而不是 const 引用/值。

【讨论】:

【参考方案16】:

size() 的大多数,甚至可能是所有的标准实现将被编译器内联到相当于临时或最多指针取消引用的内容。

但是,您永远无法确定。内联就像这些东西一样隐藏,并且第 3 方容器可能有虚函数表 - 这意味着您可能不会被内联。

但是,说真的,使用临时变量会稍微降低可读性,几乎可以肯定没有任何好处。如果分析表明它是富有成效的,那么只优化到一个临时的。如果您在各处进行这些微优化,您的代码可能会变得不可读,甚至对您自己来说也是如此。

顺便说一句,没有编译器会将 size() 优化为分配给临时的调用之一。 C++ 中几乎没有 const 的保证。编译器不能冒险假设 size() 将为整个循环返回相同的值。例如。另一个线程可以在循环迭代之间更改向量。

【讨论】:

向量不是易失的,没有使用互斥锁。编译器很可能决定优化 size() 调用。我们不要假设作者在没有任何证据的情况下制造了错误。

以上是关于对 for 构造中的第二个表达式使用 size() 总是不好的吗?的主要内容,如果未能解决你的问题,请参考以下文章

根据子列表中的第二个元素按字母顺序对列表进行排序,但不区分大小写[重复]

for循环执行原理

将第二个表中的第二个(条件)结果添加到 SQL 查询

如何根据对的第二个元素对对的向量进行排序?

正则表达式删除第一个单词并使用 c# 将第二个单词的第一个字符大写

ES6学习总结之 正则表达式