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

时间:2020-03-06 15:04:21  来源:igfitidea点击:

在下面的示例中,我是否希望每次循环都调用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()的使用。

解决方案

这完全取决于向量的大小实现方式,编译器的攻击性以及是否侦听/使用内联指令。

我会采取防御措施,并介绍临时选项,因为我们无法保证编译器的效率。

当然,如果此例程被调用一次或者两次并且向量很小,则实际上并不重要。

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

有人可能会将此称为过早的优化,但我倾向于不同意这种评估。
当我们尝试优化代码时,我们并没有在性能上投入时间或者混淆代码。

我很难考虑什么是重构,这是一种优化。但是最后,这是按照"你说西红柿,我说西红柿"的思路进行的。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

我同意贝诺瓦特的观点。引入新变量,尤其是int甚至是short会带来比每次调用更大的好处。

如果循环变得足够大以至于可能影响性能,则不必担心。

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

而且,如果我没记错的话,有些编译器"足够聪明",足以检测到for构造的第二部分中表达式的恒定性,并生成仅对表达式进行一次评估的代码。

从" for"构造中的size()开始,直到我们需要优化速度为止。

如果速度太慢,请寻找提高速度的方法,例如使用临时变量保存大小结果。

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

我的猜测是,大多数编译器都会以某种方式优化代码,而size()将仅被调用一次。但是使用临时变量将为我们提供保证,size()将仅被调用一次!

值得一提的是,即使我们要处理数百万个项目,开销也可以忽略不计。

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

编译器实际上不可能假设size()不会更改,因为它可以更改。

如果迭代顺序不重要,那么我们总是可以编写它,因为效率更高。

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

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

在这种情况下,使用迭代器在某些情况下更为干净,甚至更快。如果有剩余,则仅对容器进行一次调用,以使迭代器持有指向vector成员的指针,否则返回null。

当然," for"可以变成" while",根本不需要临时变量,我们甚至可以将迭代器传递给sumVector函数,而不是const引用/值。

始终完全按照意图编写代码。如果要将向量从零迭代到size(),则应这样编写。不要将对size()的调用优化为一个临时变量,除非我们已将该调用分析为程序中需要优化的瓶颈。

很有可能,一个好的编译器将能够优化对size()的调用,尤其是考虑到将向量声明为const的情况下。

这是一种使它的显式size()仅被调用一次的方法。

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

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

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

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

P.S.在认真阅读Benoit的答案之前,我写了这篇文章,我相信我们完全同意。

编译器将内联大多数(甚至全部)size()的标准实现,以内联方式等效于临时或者最多指针取消引用。

但是,我们永远无法确定。内联与这些事情一样隐蔽,并且第三方容器可能具有虚拟功能表,这意味着我们可能不会内联。

但是,严重的是,使用临时方法会稍微降低可读性,几乎可以肯定没有任何好处。仅在分析表明是富有成果的情况下,才优化为临时属性。如果我们在所有地方都进行了微优化,那么代码可能变得不可读,甚至对我们而言也是如此。

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

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

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

即使代码包含逻辑错误,导致我们访问无效的元素,在该位置的at也将是无用的,因为我们不希望所产生的异常,因此也不会对其进行对待(或者将所有你的循环是由try块来完成的?)。

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

我同意Curro,这是使用迭代器的典型案例。尽管这比较冗长(至少如果我们不使用Boost.Foreach之类的结构),但它也更具表现力和安全性。

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

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

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

没关系。 .at()的性能开销如此之大(它包含一个条件抛出语句),以至于未经优化的版本将在此花费大部分时间。一个足够聪明以消除条件抛出问题的优化编译器必然会发现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()是O(n)(例如std :: list)而不是O(1)(例如std :: vector),则不会遍历该容器使用索引。我们将改为使用迭代器。

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

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