我们有一个问题:在C语言中,i++
和++i
有性能差异吗?
C++的答案是什么?
[简要概述:如果没有特殊原因,请使用++i
而不是i++
]
C++方面的答案稍微有些复杂。
如果i
是一个简单类型(不是C ++类的实例),则与C语言相同,答案是一样的(没有性能差异),因为编译器会生成代码。
但是,如果i
是C ++类的一个实例,则i ++
和++ i
都会调用其中一个operator ++
函数。下面是这些函数的标准对:
Foo& Foo::operator++() // called for ++i
{
this->data += 1;
return *this;
}
Foo Foo::operator++(int ignored_dummy_value) // called for i++
{
Foo tmp(*this); // variable "tmp" cannot be optimized away by the compiler
++(*this);
return tmp;
}
由于编译器不生成代码,而只是调用operator++
函数,因此无法优化tmp
变量及其关联的复制构造函数。 如果复制构造函数很昂贵,则这可能会对性能产生重大影响。
是的,有这样一种情况。
++运算符可能被定义为函数,也可能不是。对于原始类型(int、double等),这些运算符是内置的,因此编译器可能能够优化您的代码。但是对于定义了++运算符的对象,情况就不同了。
operator++(int)函数必须创建一个副本。这是因为后缀++预期返回与其持有的值不同的值:它必须将其值保存在临时变量中,将其值增加并返回该临时变量。对于operator++(),前缀++,则没有必要创建一个副本:对象可以自己递增,然后简单地返回自身。
以下是说明:
struct C
{
C& operator++(); // prefix
C operator++(int); // postfix
private:
int i_;
};
C& C::operator++()
{
++i_;
return *this; // self, no copy created
}
C C::operator++(int ignored_dummy_value)
{
C t(*this);
++(*this);
return t; // return a copy
}
每次调用operator++(int)都必须创建一个副本,并且编译器无法干涉。在选择时,请使用operator++(); 这样您就不需要保存一个副本。这在多个增量(大循环?)和/或大型对象的情况下可能是重要的。
C t(*this); ++(*this); return t;
在第二行,您正在递增此指针,因此如果您正在递增此指针,那么t
如何更新。难道不是将此的值已经复制到t
中了吗? - rasen58The operator++(int) function must create a copy.
no, it is not. No more copies than operator++()
- Severin Pappadeux// a.cc
#include <ctime>
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> data;
};
int main () {
Something s;
for (int i=0; i<1024*1024*30; ++i) ++s; // warm up
std::clock_t a = clock();
for (int i=0; i<1024*1024*30; ++i) ++s;
a = clock() - a;
for (int i=0; i<1024*1024*30; ++i) s++; // warm up
std::clock_t b = clock();
for (int i=0; i<1024*1024*30; ++i) s++;
b = clock() - b;
std::cout << "a=" << (a/double(CLOCKS_PER_SEC))
<< ", b=" << (b/double(CLOCKS_PER_SEC)) << '\n';
return 0;
}
// b.cc
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> data;
};
Something& Something::operator++()
{
for (auto it=data.begin(), end=data.end(); it!=end; ++it)
++*it;
return *this;
}
Something Something::operator++(int)
{
Something ret = *this;
++*this;
return ret;
}
使用g++ 4.5在虚拟机上的结果(计时以秒为单位):
Flags (--std=c++0x) ++i i++
-DPACKET_SIZE=50 -O1 1.70 2.39
-DPACKET_SIZE=50 -O3 0.59 1.00
-DPACKET_SIZE=500 -O1 10.51 13.28
-DPACKET_SIZE=500 -O3 4.28 6.82
现在让我们看下面的文件:
// c.cc
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> data;
};
Something& Something::operator++()
{
return *this;
}
Something Something::operator++(int)
{
Something ret = *this;
++*this;
return ret;
}
在递增时它不做任何事情。这模拟了递增具有恒定复杂度的情况。
现在结果变化极大:
Flags (--std=c++0x) ++i i++
-DPACKET_SIZE=50 -O1 0.05 0.74
-DPACKET_SIZE=50 -O3 0.08 0.97
-DPACKET_SIZE=500 -O1 0.05 2.79
-DPACKET_SIZE=500 -O3 0.08 2.18
-DPACKET_SIZE=5000 -O3 0.07 21.90
如果您不需要先前的值,请养成使用前缀递增的习惯。即使是内置类型,也要保持一致,这样您就会习惯它,并且在将来用自定义类型替换内置类型时不会遭受不必要的性能损失。
i++
表示 增加 i,我对先前的值感兴趣
。++i
表示 增加 i,我对当前值感兴趣
或者 增加 i,我不关心先前的值
。无论如何,您都会习惯它,即使现在还没有。过早优化是万恶之源。同样,过早悲观也是如此。
for (it=nearest(ray.origin); it!=end(); ++it) { if (auto i = intersect(ray, *it)) return i; }
,不要在意实际的树结构(BSP、kd、Quadtree、Octree Grid等)。这样的迭代器需要维护一些状态,例如“父节点”、“子节点”、“索引”等等。总的来说,我的立场是,即使只有很少的例子存在,... - Sebastian Mach前置自增和前置自减
在处理迭代器和其他模板对象时,请使用前缀形式(++i)的增量和减量运算符。
定义:当一个变量被自增(++i或i++)或自减(--i或i--)且表达式的值未被使用时,需要决定是使用前置自增(减)还是后置自增(减)。
优点:当返回值被忽略时,“前置”形式(++i)永远不会比“后置”形式(i++)效率低,并且通常更高效。这是因为后置自增(或减)需要复制i的值作为表达式的值。如果i是迭代器或其他非标量类型,则复制i可能很昂贵。由于这两种类型的自增操作在值被忽略时的行为相同,为什么不总是使用前置自增呢?
缺点:在C语言中,使用后置自增在表达式的值未被使用时已经成为了一种传统,特别是在for循环中。有些人认为后置自增更容易阅读,因为“主语”(i)在“动词”(++)之前,正如英语中的一样。
决策:对于简单标量(非对象)值,没有理由偏好其中一种形式,我们允许使用任何一种。对于迭代器和其他模板类型,请使用前置自增。
不能完全地说编译器无法在后缀情况下优化掉临时变量的复制。使用 VC 做一个快速测试可以看到,在某些情况下它至少可以这样做。
比如下面的例子,产生的代码对于前缀和后缀来说是相同的:
#include <stdio.h>
class Foo
{
public:
Foo() { myData=0; }
Foo(const Foo &rhs) { myData=rhs.myData; }
const Foo& operator++()
{
this->myData++;
return *this;
}
const Foo operator++(int)
{
Foo tmp(*this);
this->myData++;
return tmp;
}
int GetData() { return myData; }
private:
int myData;
};
int main(int argc, char* argv[])
{
Foo testFoo;
int count;
printf("Enter loop count: ");
scanf("%d", &count);
for(int i=0; i<count; i++)
{
testFoo++;
}
printf("Value: %d\n", testFoo.GetData());
}
无论你使用 ++testFoo 还是 testFoo++,最终的代码还是一样的。实际上,在不从用户读取计数的情况下,优化器将整个过程缩减为了一个常量。因此,下面的代码:
for(int i=0; i<10; i++)
{
testFoo++;
}
printf("Value: %d\n", testFoo.GetData());
导致以下结果:
00401000 push 0Ah
00401002 push offset string "Value: %d\n" (402104h)
00401007 call dword ptr [__imp__printf (4020A0h)]
因此,尽管后缀版本可能会更慢,但如果您不使用它,优化器很可能足够好以消除临时副本。
如果不使用返回值,则编译器保证在++i的情况下不使用临时变量。不能保证更快,但保证不会更慢。
如果使用返回值,则i++允许处理器将增量和左侧推入流水线,因为它们彼此不依赖。++i 可能会导致流水线停顿,因为处理器在预增操作完全完成之前无法启动左侧。同样,流水线停顿并不是保证发生,因为处理器可能会找到其他有用的东西可供使用。
http://dobbscodetalk.com/index.php?option=com_myblog&show=Efficiency-versus-intent.html&Itemid=29
在我们公司,为了保持一致性和性能,我们也使用++iter的惯例,但是Andrew提出了关于意图与性能之间被忽视的细节。有时候我们想要使用iter++而不是++iter。
因此,首先确定您的意图,如果前置或后置不重要,则选择前置,因为它将通过避免创建额外的对象并抛出它来获得一些性能优势。
@Ketan
...提出了关于意图与性能的被忽视的细节。有时候我们想要使用iter++而不是++iter。
显然,后缀和前缀递增具有不同的语义,我相信每个人都同意当结果被使用时应该使用适当的运算符。我认为问题在于当结果被丢弃时应该怎么做(例如在for
循环中)。对于这个问题的答案(在我看来)是,由于性能考虑最多可以忽略不计,因此您应该做更自然的事情。对于我来说,++i
更自然,但我的经验告诉我,我是少数派,使用i++
将导致大多数人阅读您的代码时产生较少的心理负担。
毕竟这就是语言没有被称为"++C
"的原因。[*]
[*] 插入关于++C
是更合乎逻辑的名称的义务讨论。
++i
和 i++
之间的性能差异将更加明显。为了更好地理解发生了什么,以下代码示例将使用 int
来表示struct
。
++i
会先增加变量,然后返回结果。在许多情况下,这可以在原地完成,并且需要极少的 CPU 时间,只需要一行代码即可。int& int::operator++() {
return *this += 1;
}
但对于i++
来说就不同了。
后增量运算符i++
通常被视为在增加之前返回原始值。然而,函数只有在完成时才能返回结果。因此,需要创建一个包含原始值的变量副本,增加该变量,然后返回保存原始值的副本:
int int::operator++(int& _Val) {
int _Original = _Val;
_Val += 1;
return _Original;
}
当使用前缀自增和后缀自增没有功能差异时,编译器可以进行优化,使得两者之间没有性能差异。但是,如果涉及到复合数据类型,例如struct
或class
,则在后缀自增时会调用复制构造函数,如果需要进行深度复制,则无法执行此优化。因此,前缀自增通常比后缀自增更快,并且需要更少的内存。
Mark: 只是想指出operator++是很好的内联候选函数,如果编译器选择这样做,在大多数情况下冗余复制将被消除。(例如POD类型,通常是迭代器。)
话虽如此,在大多数情况下使用++iter仍然是更好的风格。 :-)
i++
和--i
比++i
和i--
更有效率。 - user207421