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

Is using size() for the 2nd expression in a for construct always bad?

本文关键字:size for 表达式 第二个      更新时间:2023-10-16

在下面的示例中,我应该期望每次循环都会调用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((。

这里有一种方法可以明确 - size(( 只调用一次。

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

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

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

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

附言我在仔细阅读Benoit的回答之前写了这篇文章,我相信我们完全同意。

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

我会更加防御并引入临时方法,因为您不能保证编译器的效率。

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

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

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

我很难考虑什么是重构作为优化。 但最终,这是按照"你说番茄,我说番茄"的路线......

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

如果它太慢,请寻找使其更快的方法,例如使用临时变量来保存大小的结果。

无论优化设置如何,将 .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);

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

无论如何,这实际上应该使用迭代器编写 - 因为访问特定示例可能会有更多的开销。

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

如果迭代顺序不重要,那么您可以随时将其编写为效率稍高。

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

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

at的意思是确保不会对无效索引执行任何操作。但是,在您的循环中永远不会出现这种情况,因为您从代码中知道索引将是什么(始终假设单线程(。

即使您的代码包含逻辑错误,导致您访问无效元素,在这个地方at也是无用的,因为您不希望产生异常,因此您不处理它(或者您是否用try块将所有循环括起来?

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

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

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

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

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

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

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

如果

循环变得足够大以至于可能会影响性能,就不用担心了。

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

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

来自 std::vector 的 size 方法应该由编译器内联,这意味着对 size(( 的每个调用都会被其实际主体替换(有关内联的更多信息,请参阅问题为什么我应该使用内联代码(。由于在大多数实现中 size(( 基本上计算 end(( 和 begin(( 之间的差异(也应该内联(,因此您不必太担心性能损失。

此外,如果我没记错的话,一些编译器足够"聪明",可以检测 for 构造第二部分中表达式的恒定性,并生成仅计算一次表达式的代码。

编译器不会知道 .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(( 位置并将其用作边界。

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

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

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

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

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

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

然后当然for可以成为一个while,并且根本不需要临时变量 - 你甚至可以将迭代器传递给sumVector函数而不是常量引用/值。

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

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

但是,说真的,使用临时会略微降低可读性,几乎可以肯定没有任何收获。仅当分析表明它是富有成效的时,才优化到临时。如果你在任何地方都进行这些微优化,你的代码可能会变得不可读,甚至可能是你自己。

顺便说一句

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