在for构造中为第二个表达式使用size()总是不好吗?
在下面的示例中,我是否希望每次循环都调用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的实现细节。