C++ 性能剖析

来源:互联网 发布:汽车资源软件下载 编辑:程序博客网 时间:2024/06/15 19:13

  • 两个性能隐患
  • 值语义 value semantics
  • Heap Object对比 Stack Object

两个性能隐患

性能问题不是仅仅用“技术”可以解决的,它往往是架构,测试,假设等综合难题。不过,对于一个工程师来说,必须从小做起,把一些“明显”的小问题解决。否则的话积小成多,千里堤坝,溃于蚁穴。
下面用一例子,来做一下对比,看看一些微妙的细节是如何影响程序性能的。

struct intPair{    int ip1;    int ip2;                          intPair(int i1, int i2) : ip1(i1), ip2(i2) {}    intPair(int i1) : ip1(i1), ip2(i1) {}}; // Calc sum (usinh value semantic)Int Sum1(intPair p){    return p.ip1 + p.ip2;} // Calc sum (usinh ref semantic)int Sum2(intPair &p){    return p.ip1 + p.ip2;}// Calc sum (usinh const ref semantic)Int Sum3(const intPair& p){    return p.ip1 + p.ip2;}

上面这个简单的struct,有三个Sum函数,作的事情完全一样,但是性能是否一样呢?我们用下面的程序来测试:

double Sum(int t, int loop){    using namespace std;    if (t == 1)    {         clock_t begin = clock();         int x =0;         for(int i = 0; i < loop; ++i)         {             x += Sum1(intPair(1,2));         }          clock_t end = clock();         return double(end - begin) / CLOCKS_PER_SEC;   }   else if (t == 2)   {        clock_t begin = clock();        int x =0;        intPair p(1,2);        for(int i = 0; i < loop; ++i)        {            x += Sum1(p);        }        clock_t end = clock();        return double(end - begin) / CLOCKS_PER_SEC;   }   else if (t == 3)   {       clock_t begin = clock();       int x =0;       intPair p(1,2);       for(int i = 0; i < loop; ++i)       {           x += Sum2(p);       }       clock_t end = clock();       return double(end - begin) / CLOCKS_PER_SEC;                       }   else if (t == 4)   {       clock_t begin = clock();       int x =0;       intPair p(1,2);       for(int i = 0; i < loop; ++i)       {           x += Sum3(p);       }       clock_t end = clock();       return double(end - begin) / CLOCKS_PER_SEC;                                              }  else if (t == 5)  {      clock_t begin = clock();      int x =0;      for(int i = 0; i < loop; ++i)      {          x += Sum3(10);      }      clock_t end = clock();      return double(end - begin) / CLOCKS_PER_SEC;                                               }   return 0;}

我们用了5个案列,对Sum1和Sum3 风别用了两种调用方式,对Sum2用了一种调用方式。我们测试了10万次调用:

double sec = Sum(1, 100000);printf("Sum1 (use  ctor) time: %f \n", sec);sec = Sum(2, 100000);printf("Sum1 (use no c'tor) time: %f \n", sec);sec = Sum(3, 100000);printf("Sum2 time: %f \n", sec);sec = Sum(4, 100000);printf("Sum3 without conversion time: %f \n", sec);sec = Sum(5, 100000);printf("Sum3 with conversion time: %f \n", sec);

我们在VisualStidio 2010 中测试,结果是:

用例1 18ms

用例2 9ms

用例3 6ms

用例4 7ms

用例5 12ms

也就是说:用例1和5最慢,其他基本没有差别。

细心的读者不难看出,

1)用例5的性能问题,是因为Sum3用了C++的implicit conversion ,将整数自动转化成intPair 的临时变量。这是一个应用层面的问题,如果我们不得不将整数作这个转换,也就不得不付出这个性能上的代价。

2)用例1的问题和5类似,都是因为不得不每次创建临时变量。当然,可以强迫constructor inline 来使得临时变量的生成成本降低。

3)用例2用了在函数调用前了编译自生的copy constructor,不过因为 intPair object 很小,影响可以忽略不计了。

4)用例3性能是稳定的,但是它用了“间接”方式(详情请看我关于reference的博克),所以产生的指令比用例2多两条。但对性能的影响不大,估计和Intel的L1,L2 缓存有关。

*注意到OOP函数如果仅仅对 this 的成员存取数据,一般可以充分利用缓存,除非 object 过大。 

5)用例4 和用例3生成代码完全一样,应该没有差别。const 只是编译时有用,生成的代码与const 与否无关。

性能问题的话题太多,本文只是蜻蜓点水,但是已经触及了C++的两个最大的性能隐患:

  • 临时变量
  • Implicit conversion (沉默转换)

值语义 (value semantics)

Value Semantics (值语义) 是C++的一个有趣的话题。

什么是值语义? 简单的说,所有的原始变量(primitive variables)都具有value semantics. 也可以说,它们可以对应传统数学中的变量。有人也称它为POD (plain old data), 也就是旧时的老数据(有和 OOP 的新型抽象数据对比之意)。

对一个具有值语义的原始变量变量赋值可以转换成内存的bit-wise-copy。

对于用户定义的类呢?我们说,如果一个type X 具有值语义, 则:

  1. X 的size在编译时可以确定。
    这一点看似自然,其实在C++里有许多变量的size编译时无法确定。比如我在reference 三位一体里提到的polymorphic 变量,因为是“多身份”的,其(内容)的size是动态的。

  2. 将X的变量x,赋值与另一个变量y,无须专门的 = operator,简单的bit-wise-copy 即可。

  3. 当上述赋值发生后,x和y脱离关系:x 和 y 可以独立销毁, 其内存也可以独立释放。

    了解第三点很重要,比如下面的class A就不具备值语义:

class A{      char * p;      public:        A() { p = new char[10]; }         ~A() { delete [] p; }};

A 满足1和2,但不满足3。因为下面的程序会出错误:

Foo(){    A a;    A b = a;} // crash here

改进的方法是定义一个A::operator=(constA &),并且用reference counting 的技术来保护指针,实现起来并不简单。所以我们说一旦一个class 失去了value semantics, 它也就失去了简单明了的 = 语义。

从上面的分析可以得出结论,value semantics 有个简单的 = , 也正是数学意义上的 = 。

学过Java, C#, 和JavaScript的程序员都知道,这些语言里的object都不具有值语义,因为它们都是指针,= 并不copy内容。也不满足条件3。

那么value semantics 对C++性能有什么影响呢?有以下几方面:

1)std 库是基于值语义 的。std container 包含的元素,都具有值语义. 不理解这一点,就不能正确使用std,也不会对std的性能,做出合理预期。

2)简单的bit-wise-copy 赋值语句一般会提高赋值性能,因为它不需要特殊的 = operator 了。在使用std container 时,会有大量的copy 或assignment。 bit-wise-copy对于小的变量通常比函数划算得多。

3)具有值语义的,size 不大的变量,在stack里,作为auto变量,传递,拷贝,释放全部和原始变量的用法完全一致,既好用,一般也具有优良的性能。动态语言缺乏这个(值语义)的语言构造和能力,(C# 有有限地支持:c# struct),所以速度上很难优化。

4)注意,在设计具有值语义的类时,不要保留无用的destructor. destructor 的存在,使得你的类的语义和原始类有了本质的区别,C++ 编译会为此处心积虑地添加管理代码,使得一个简单的函数复杂化, 并且严重影响性能。这些当然是有附加值的,但是必须是设计需求的,而不是简单照搬的。

那么,什么样的类没有值语义呢?我们不妨称这种型为 none-value-semantics type (NVST).

1)有virtual function 的类

2)包含NVST成员的类

3)NVST 的衍生类(derived classed)

4)定义了自己的 = operator 的类

5)继承 virtual 基类的衍生类

这里需要解释的是,对于有virtual function 的类,之所以不具备值语义,是因为它不具有“bit-wise-copy”语义。比如:
A:public B {
virtual void foo() {}
};
上面的A,B在内存中有一个隐形成员:vtbl. 他不能随便bit-wise-copy。比如:

B *pB = new A();
B b = *pB;

你本来想将pB的内用拷贝到b中,但是你实际上将一个A的vtbl(虚拟函数表)拷过来了。b实际上不是B了,而是A,因为它的虚拟函数指向了A。
所以有了虚拟函数的类不具备“值语义”了。

Heap Object对比 Stack Object

Java, C#,和JavaScript的程序员一般都不用管自己创建的object是在heap里还是在stack里,因为对于这些语言,object 只能“生活在”heap里。这无疑对于程序员来说简单了许多。但是对于C++程序员来说,你可以选择三处来创建object:

  • 程序的data section
  • stack区
  • heap

Object 因该生活在哪里?这个问题必须由应用的属性来决定,有时是没有选择的,比如对于动态产生的全程变量,只有活在heap里,别无它途。

然而,一旦我们有选择,比如临时的,作为复杂数据的载体的object,答案是清楚的:应该首选stack. 比如下面简单的例子:

// heap vs stack testdouble HeapVsStack(bool heap, int loop, int & result){    if (heap)    {        clock_t begin = clock();        for (int i = 0; i < loop; ++i)        {            intPair * p = new intPair(1, 2);            result += p - >ip1 + p - >ip2;            delete p;        }        clock_t end = clock();        return double(end - begin) / CLOCKS_PER_SEC;    }    else    {        clock_t begin = clock();        for (int i = 0; i < loop; ++i)        {            intPair p = intPair(1, 2);            result += p.ip1 + p.ip2;        }        clock_t end = clock();        return double(end - begin) / CLOCKS_PER_SEC;    }}

上方在heap中创建了一个intPair,用完后delete掉。下方在stack里定义一个同样的auto变量,用完后无须care.

对这个程序作下列简单测试调用:

int result = 0;

printf(“Heap time: %f \n”, HeapVsStack(true, 100000, result));

printf(“Stack time: %f \n”, HeapVsStack(false, 100000, result));

我不得不调用100000次,原因是它们的耗时差别实在太大了:stack 的用例不到10000次以上都显示0ms.

测试结果,heap用了300ms, stack用了5ms, 相差60倍。

结论:

1) 如果应用逻辑容许,用 stack-based auto 变量,千万不用 heap 变量.

2) 如果需要大量用heap,建议用std::vector来当作自己的 heap 简单管理器用。避免直接地,大量地用heap来创建 ad-hoc object.

3) 有些临时计算用的class可以考虑禁止在heap中生成

原创粉丝点击