我们的问题是 C和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) 函数必须创建一个副本。这是因为 postfix ++ 应该返回一个不同于它所持有的值:它必须将它的值保存在一个临时变量中,增加它的值并返回临时变量。在 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++(); 这样你就不会保存副本。在许多增量(大循环?)和/或大对象的情况下,它可能很重要。
这是增量运算符位于不同翻译单元中的情况的基准。使用 g++ 4.5 的编译器。
暂时忽略风格问题
// 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++
说increment i, I am interested in the previous value, though
。++i
说increment i, I am interested in the current value
或increment i, no interest in the previous value
。同样,即使您现在不习惯,您也会习惯它。过早的优化是万恶之源。正如过早的悲观情绪一样。
说编译器无法优化后缀情况下的临时变量副本并不完全正确。对 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++)或递减(--i 或 i--)且未使用表达式的值时,必须决定是预递增(递减)还是后递增(递减)。
优点:当返回值被忽略时,“pre”形式(++i)的效率永远不会低于“post”形式(i++),而且通常效率更高。这是因为后递增(或递减)需要制作 i 的副本,这是表达式的值。如果 i 是迭代器或其他非标量类型,复制 i 可能会很昂贵。既然当值被忽略时两种类型的增量行为相同,为什么不总是预增量呢?
缺点:在 C 中形成的传统是在不使用表达式值时使用后增量,尤其是在 for 循环中。有些人发现后增量更容易阅读,因为“主语”(i)在“动词”(++)之前,就像在英语中一样。
决定:对于简单的标量(非对象)值,没有理由偏爱一种形式,我们也允许。对于迭代器和其他模板类型,使用预增量。
我想指出 Andrew Koenig 最近在 Code Talk 上发表的一篇出色的文章。
http://dobbscodetalk.com/index.php?option=com_myblog&show=Efficiency-versus-intent.html&Itemid=29
在我们公司,我们还在适用的情况下使用 ++iter 的约定来保持一致性和性能。但安德鲁提出了关于意图与绩效的被忽视的细节。有时我们想使用 iter++ 而不是 ++iter。
因此,首先确定您的意图,如果 pre 或 post 无关紧要,然后使用 pre,因为它可以通过避免创建额外的对象并抛出它来获得一些性能优势。
@科坦
...提出了有关意图与性能的被忽视的细节。有时我们想使用 iter++ 而不是 ++iter。
显然 post 和 pre-increment 有不同的语义,我相信每个人都同意,当使用结果时,你应该使用适当的运算符。for
我认为问题是当结果被丢弃(如循环)时应该做什么。这个问题的答案(恕我直言)是,由于性能考虑充其量可以忽略不计,您应该做更自然的事情。对我自己++i
来说更自然,但我的经验告诉我,我是少数,对于大多数阅读你的代码的人来说,使用i++
会减少金属开销。
毕竟这就是语言不被称为“ ++C
”的原因。[*]
++C
[*] 插入关于成为更合乎逻辑的名称的强制性讨论。
当不使用返回值时,编译器保证不会在++i的情况下使用临时值。不保证更快,但保证不会变慢。
当使用返回值时, i++允许处理器将增量和左侧推入管道,因为它们不相互依赖。++i 可能会停止管道,因为处理器无法启动左侧,直到预增量操作一直蜿蜒通过。同样,不能保证流水线停顿,因为处理器可能会找到其他有用的东西来坚持。
Mark:只是想指出,operator++ 是内联的好候选,如果编译器选择这样做,在大多数情况下冗余副本将被消除。(例如,迭代器通常是 POD 类型。)
也就是说,在大多数情况下使用 ++iter 仍然是更好的风格。:-)
++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
or class
,则会在后增量时调用复制构造函数,如果需要深度复制,则无法执行此优化。因此,预增量通常比后增量更快并且需要更少的内存。
@Mark:我删除了我之前的答案,因为它有点翻转,仅此一项就值得一票。实际上,我认为这是一个很好的问题,因为它询问了很多人的想法。
通常的答案是 ++i 比 i++ 快,毫无疑问,但更大的问题是“你什么时候应该关心?”
如果用于递增迭代器的 CPU 时间比例小于 10%,那么您可能不会在意。
如果用于递增迭代器的 CPU 时间比例大于 10%,您可以查看哪些语句正在执行该迭代。看看你是否可以只增加整数而不是使用迭代器。你有可能,虽然在某种意义上它可能不太理想,但很有可能你会节省在这些迭代器上花费的所有时间。
我见过一个例子,其中迭代器递增消耗了超过 90% 的时间。在这种情况下,进行整数递增将执行时间减少了该数量。(即优于 10 倍加速)
@wilhelmtell
编译器可以省略临时。来自另一个线程的逐字记录:
即使这样做会改变程序行为,C++ 编译器也可以消除基于堆栈的临时变量。VC 8 的 MSDN 链接:
http://msdn.microsoft.com/en-us/library/ms364057(VS.80).aspx
即使在没有性能优势的内置类型上也应该使用 ++i 的原因是为自己养成一个好习惯。
预期的问题是关于何时未使用结果(从 C 的问题中可以清楚地看到)。由于问题是“社区维基”,有人可以解决这个问题吗?
关于过早优化,经常引用 Knuth。这是正确的。但是 Donald Knuth 永远不会用你现在看到的可怕的代码来辩护。在 Java 整数(不是 int)中见过 a = b + c 吗?这相当于 3 次装箱/拆箱转换。避免这样的事情很重要。并且无用地写 i++ 而不是 ++i 是同样的错误。编辑:正如 phresnel 在评论中所说的那样,这可以概括为“过早的优化是邪恶的,过早的悲观化也是如此”。
即使人们更习惯于 i++ 这一事实也是不幸的 C 遗产,这是由 K&R 的概念错误造成的(如果您遵循意图论证,这是一个合乎逻辑的结论;并且因为 K&R 是 K&R 而为 K&R 辩护是没有意义的,他们是很棒,但他们作为语言设计者并不出色;C 设计中存在无数错误,从 get() 到 strcpy(),再到 strncpy() API(从第一天起就应该有 strlcpy() API) )。
顺便说一句,我是那些对 C++ 还不够熟悉,以至于觉得 ++i 读起来很烦人的人之一。不过,我使用它,因为我承认它是正确的。
两者都一样快;)如果您希望处理器的计算相同,则只是执行顺序不同。
例如,以下代码:
#include <stdio.h>
int main()
{
int a = 0;
a++;
int b = 0;
++b;
return 0;
}
生成以下程序集:
0x0000000100000f24 <main+0>: push %rbp 0x0000000100000f25 <main+1>: mov %rsp,%rbp 0x0000000100000f28 <main+4>: movl $0x0,-0x4(%rbp) 0x0000000100000f2f <main+11>: incl -0x4(%rbp) 0x0000000100000f32 <main+14>: movl $0x0,-0x8(%rbp) 0x0000000100000f39 <main+21>: incl -0x8(%rbp) 0x0000000100000f3c <main+24>: mov $0x0,%eax 0x0000000100000f41 <main+29>: leaveq 0x0000000100000f42 <main+30>: retq
您会看到,对于 a++ 和 b++,它是一个 incl 助记符,所以它是相同的操作;)
由于您也要求使用 C++,因此这里是 java (made with jmh) 的基准:
private static final int LIMIT = 100000;
@Benchmark
public void postIncrement() {
long a = 0;
long b = 0;
for (int i = 0; i < LIMIT; i++) {
b = 3;
a += i * (b++);
}
doNothing(a, b);
}
@Benchmark
public void preIncrement() {
long a = 0;
long b = 0;
for (int i = 0; i < LIMIT; i++) {
b = 3;
a += i * (++b);
}
doNothing(a, b);
}
结果表明,即使在某些计算中实际使用了递增变量 (b) 的值,强制在后递增的情况下需要存储一个附加值,每次操作的时间也完全相同:
Benchmark Mode Cnt Score Error Units
IncrementBenchmark.postIncrement avgt 10 0,039 0,001 ms/op
IncrementBenchmark.preIncrement avgt 10 0,039 0,001 ms/op
++i
比i = i +1
因为在i = i + 1
两个操作中发生更快,第一次递增,第二次将其分配给变量。但i++
仅在增量操作中发生。
是时候为人们提供智慧的宝石了;) - 有一个简单的技巧可以使 C++ 后缀增量的行为与前缀增量几乎相同(这是为我自己发明的,但在其他人的代码中也看到了它,所以我不是独自的)。
基本上,技巧是使用辅助类在返回后推迟增量,RAII 来救援
#include <iostream>
class Data {
private: class DataIncrementer {
private: Data& _dref;
public: DataIncrementer(Data& d) : _dref(d) {}
public: ~DataIncrementer() {
++_dref;
}
};
private: int _data;
public: Data() : _data{0} {}
public: Data(int d) : _data{d} {}
public: Data(const Data& d) : _data{ d._data } {}
public: Data& operator=(const Data& d) {
_data = d._data;
return *this;
}
public: ~Data() {}
public: Data& operator++() { // prefix
++_data;
return *this;
}
public: Data operator++(int) { // postfix
DataIncrementer t(*this);
return *this;
}
public: operator int() {
return _data;
}
};
int
main() {
Data d(1);
std::cout << d << '\n';
std::cout << ++d << '\n';
std::cout << d++ << '\n';
std::cout << d << '\n';
return 0;
}
Invented 适用于一些繁重的自定义迭代器代码,它减少了运行时间。前缀与后缀的成本现在是一个参考,如果这是自定义运算符进行大量移动,前缀和后缀对我来说产生了相同的运行时间。
++i
比它更快,i++
因为它不返回值的旧副本。
它也更直观:
x = i++; // x contains the old value of i
y = ++i; // y contains the new value of i
这个 C 示例打印“02”而不是您可能期望的“12”:
#include <stdio.h>
int main(){
int a = 0;
printf("%d", a++);
printf("%d", ++a);
return 0;
}
#include <iostream>
using namespace std;
int main(){
int a = 0;
cout << a++;
cout << ++a;
return 0;
}