C++ 性能剖析

两个性能隐患

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

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 test
double 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中生成

C++性能优化 指南(强列推荐) chm版 Part I: Everything But the Code Chapter 1. Optimizing: What Is It All About? Performance Footprint Summary Chapter 2. Creating a New System System Requirements System Design Issues The Development Process Data Processing Methods Summary Chapter 3. Modifying an Existing System Identifying What to Modify Beginning Your Optimization Analyzing Target Areas Performing the Optimizations Summary Part II: Getting Our Hands Dirty Chapter 4. Tools and Languages Tools You Cannot Do Without Optimizing with Help from the Compiler The Language for the Job Summary Chapter 5. Measuring Time and Complexity The Marriage of Theory and Practice System Influences Summary Chapter 6. The Standard C/C++ Variables Variable Base Types Grouping Base Types Summary Chapter 7. Basic Programming Statements Selectors Loops Summary Chapter 8. Functions Invoking Functions Passing Data to Functions Early Returns Functions as Class Methods Summary Chapter 9. Efficient Memory Management Memory Fragmentation Memory Management Resizable Data Structures Summary Chapter 10. Blocks of Data Comparing Blocks of Data The Theory of Sorting Data Sorting Techniques Summary Chapter 11. Storage Structures Arrays Linked Lists Hash Tables Binary Trees Red/Black Trees Summary Chapter 12. Optimizing IO Efficient Screen Output Efficient Binary File IO Efficient Text File IO Summary Chapter 13. Optimizing Your Code Further Arithmetic Operations Operating System–Based Optimizations Summary Part III: Tips and Pitfalls Chapter 14. Tips Tricks Preparing for the Future Chapter 15. Pitfalls Algorithmic Pitfalls Typos that Compile Other Pitfalls
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值