C++ 性能剖析 (一)

C++ 性能剖析 (一)

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

C++ 的性能为什么总是排在C之后 (见http://benchmarksgame.alioth.debian.org/u32/performance.php?test=binarytrees 等网站的最新测试结果)?我认为这是3个方面的原因:

1)用于测试的C++ 编译器没有使用最新的优化技术

2)C++ 附加的价值没有考虑到测试之中

3)C++ 应用层面的“微妙性”(可参考我的关于C++的其他博客)使得一般程序员往往望而却步,选择“教科书用例”,使得一些副作用没有在应用层面被剔出。 

记得10多年前,我在微软做开发时,曾向C++最早编译器的作者李伯曼(Stan Lippman)(时任微软VC++架构师)咨询过一系列我们小组的C++性能难题,在他的帮助下,我们在关键地方用了诸如inline,RVO等技术,完全解决了性能问题,还找出了VC++ 的几个不小的错误。我认识到,C++的性能问题多数在于我们对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++的两个最大的性能隐患:

  a) 临时变量

  b) Implicit conversion (沉默转换)

 

2014-6-20 西雅图

你可能感兴趣的:(C++)