• 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 西雅图

  • 相关阅读:
    第九章 ZYNQ-MIZ701 片上ADC的使用
    第八章 ZYNQ-MIZ701 软硬调试高级技巧
    第七章 ZYNQ-MIZ701 GPIO使用之EMIO
    第六章 ZYNQ-MIZ701 GPIO使用之MIO
    第十章 MIZ702 ZYNQ制作UBOOT固化程序
    bzoj1070 [SCOI2007]修车
    bzoj1449 [JSOI2009]球队收益
    bzoj2007 [Noi2010]海拔
    bzoj1001 [BeiJing2006]狼抓兔子
    网络流的一类经典问题--二元费用问题
  • 原文地址:https://www.cnblogs.com/ly8838/p/3926236.html
Copyright © 2020-2023  润新知