当前位置:  开发笔记 > 编程语言 > 正文

在C++中i ++和++ i之间是否存在性能差异?

如何解决《在C++中i++和++i之间是否存在性能差异?》经验,为你挑选了5个好方法。

我们有一个问题是Ci++++i C之间有性能差异吗?

C++的答案是什么?



1> Mark Harriso..:

[执行摘要:++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变量及其关联的复制构造函数.如果复制构造函数很昂贵,那么这会对性能产生重大影响.


是的,如果operator ++是内联的并且tmp从未使用过,则除非tmp对象的构造函数或析构函数具有副作用,否则可以删除它.
如果运算符++内联,编译器不能完全避免这种情况吗?
@kriss:C和C++之间的区别在于,你可以保证运算符将被内联,并且在那时一个不错的优化器将能够消除差异; 相反,在C++中你不能假设内联 - 并非总是如此.
编译器可以避免的是通过NRVO在调用者中分配tmp来返回tmp的第二个副本,如另一个注释所述.
如果答案提到了一些关于将指针(无论是自动的,智能的还是原始的)保存到动态分配(堆)内存的类,那么复制构造函数必须执行深层复制.在这种情况下,没有争论,++ i可能比i ++更有效率.它们的关键是习惯于在算法实际上不需要后增量语义的情况下使用预增量,然后你就会养成编写代码的习惯,这些代码本质上可以提高效率,无论如何你的编译器可以优化.

2> wilhelmtell..:

是.有.

++运算符可以定义为函数,也可以不定义.对于原始类型(int,double,...),内置运算符,因此编译器可能能够优化您的代码.但是在定义++运算符的对象的情况下,事物是不同的.

operator ++(int)函数必须创建一个副本.这是因为postfix ++应该返回一个与它所拥有的值不同的值:它必须在temp变量中保存其值,增加其值并返回temp.对于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 ++(); 这样您就不会保存副本.在许多增量(大循环?)和/或大对象的情况下,它可能很重要.


"预增量运算符在代码中引入了数据依赖:CPU必须等待增量操作完成才能在表达式中使用它.在深度流水线的CPU上,这会引入一个停顿.没有数据依赖性对于后增量算子." (*游戏引擎架构(第2版)*)因此,如果后增量的副本不是计算密集型的,它仍然可以超过预增量.

3> Sebastian Ma..:

这是增量运算符在不同翻译单元中的基准.用g ++ 4.5编译.

暂时忽略样式问题

// a.cc
#include 
#include 
class Something {
public:
    Something& operator++();
    Something operator++(int);
private:
    std::array 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;
}

O(n)增量

测试

// b.cc
#include 
class Something {
public:
    Something& operator++();
    Something operator++(int);
private:
    std::array 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

O(1)增量

测试

现在让我们采取以下文件:

// c.cc
#include 
class Something {
public:
    Something& operator++();
    Something operator++(int);
private:
    std::array 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.

++iincrement i, I am interested in the current valueincrement i, no interest in the previous value.再一次,你会习惯它,即使你现在不是.

克努特.

过早优化是万恶之源.因为过早的悲观化.



4> James Suther..:

说后缀情况下编译器无法优化临时变量副本并不完全正确.使用VC进行的快速测试表明,在某些情况下,它至少可以做到这一点.

在以下示例中,生成的代码对于前缀和后缀是相同的,例如:

#include 

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

无论你是使用++ 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)] 

因此,虽然后缀版本的速度肯定会慢一些,但如果您不使用它,优化器可能会很好地摆脱临时副本.


你忘了注意重要一点,这里所有内容都是内联的.如果操作员的定义不可用,则无法避免在外线代码中完成的复制; 内联优化是非常明显的,所以任何编译器都会这样做.

5> martjno..:

在谷歌C++风格指南说:

预增量和预先减少量

使用带有迭代器和其他模板对象的递增和递减运算符的前缀形式(++ i).

定义:当变量递增(++ i或i ++)或递减(--i或i--)并且未使用表达式的值时,必须决定是否预先递增(递减)或后递增(递减).

优点:当忽略返回值时,"pre"形式(++ i)的效率从不低于"post"形式(i ++),并且通常更有效.这是因为后递增(或递减)需要制作i的副本,这是表达式的值.如果我是迭代器或其他非标量类型,复制我可能会很昂贵.由于当忽略该值时,两种类型的增量表现相同,为什么不总是预先递增?

缺点:在C中开发的传统是在不使用表达式值时使用后递增,特别是在for循环中.有些人认为后增量更容易阅读,因为"主题"(i)在"动词"(++)之前,就像在英语中一样.

决策:对于简单的标量(非对象)值,没有理由选择一种形式,我们也允许.对于迭代器和其他模板类型,请使用预增量.


呃,......,那是什么东西?
推荐阅读
135369一生真爱_890
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有