C++应用程序性能优化读书笔记

粗浅的看了下,有点虎头蛇尾。暂且记下。

第一篇 C++程序优化基础

第1章 C++对象模型

1.1 基本概念

1.1.1 程序使用内存区

    一个程序占用的内存区一般分为5种:全局/静态数据区、常量区、代码区、栈、堆。

    例子代码:
    #include <stdio.h>
    #include <stdlib.h>

    int nGlobal = 100;

    int main(void)
    {
        char *pLocalString1 = "LocalString1";  // 书本有误
        const char *pLocalString2 = "LocalString2";
   
        // 如下
        char *pLocalString3 = "LocalString3";
        char **pLocalString4 = &pLocalString3;

        static int nLocalStatic = 100;

        int nLocal1 = 1;
        const int nLocalConst = 20;

        int *pNew = new int[5];
        char *pMalloc = (char*)malloc(1);

        printf("global variable:                0x%x/n", &nGlobal);
        printf("static variable:                0x%x/n", &nLocalStatic);
        printf("local expression 1:                0x%x/n", pLocalString1);
        printf("local expression (const):        0x%x/n", pLocalString2);
        printf("/n");

        printf("new:                            0x%x/n", pNew);
        printf("malloc:                            0x%x/n", pMalloc);
        printf("/n");

        printf("local poniter(pNew):            0x%x/n", &pNew);
        printf("local poniter(pLocalString2):    0x%x/n", &pLocalString2);
        printf("local poniter(pLocalString1):    0x%x/n", &pLocalString1);
        printf("local variable(nLocal):            0x%x/n", &pMalloc);
        printf("local poniter(pmalloc):            0x%x/n", &pMalloc);
        printf("local const varible:            0x%x/n", &nLocalConst);

        return 0;
    }
    常量存储区是按4个字节对齐的,堆上分配的内存是16个字节对齐。
   
    C/C++程序中的struct、union、class在编译时也会对变量进行对齐处理。可以用#pragma pack()或者编译

选项来控制struct、union、class的成员变量俺多少字节对齐,或者关闭对齐。

1.1.2 全局/静态存储区及常量数据区
   
    下面这段代码是错误的:
    char *pLocalString = "LocalString";    // 指向一个字符串常量
    pLocalString[1] = a;    // 试图修改不可修改的内存区
   
    静态变量在第一次进入作用域时被初始化。在同一个类的多个对象之间共享数据,(可以使用全局变量,但

会破坏类的封装性),我们使用静态变量。如下:
    #include <stdio.h>
    #include <stdlib.h>

    class A
    {
    public:
        int val;
        static int nCount;
        A() {nCount++;};
        A~() {nCount--;};
    }   

    int A::nCount = 0;

    int main()
    {
        A a;
        B b;
        printf("number of A:        %d/n", A::nCount);
        printf("non-static variable: 0x%x/n", &a.val);
        printf("non-static variable: 0x%x/n", &b.val);
        printf("static class member: 0x%x/n", &a.nCount);
        printf("static class member: 0x%x/n", &b.nCount);
    }

1.1.3 堆和栈
   
    栈自动释放;堆由开发人员释放。
    栈的大小是固定的,有编译器决定,vs2003的默认值为1MB;堆只受限于系统有效的虚拟内存。
    栈的效率较高不产生碎片;堆效率低,容易产生碎片。

1.1.4 C++中的对象

    对象的三种创建方式:
    1) 通过定义变量创建
    2) 通过new操作符创建
    3) 通过实现创建对象:这种情况一般是指一些隐藏的中间临时变量的创建和销毁。
                 它们的生命周期很短,也不易被开发人员发觉,但常常是造成程序性能下降的瓶颈


       
    #include <stdio.h>
    class A
    {
    public:
        A() { printf("A create./n");}
        A(A& a) { printf("A create with copy/n");}
        ~A() { printf("A destroyed. /n");}
    };

    A foo(A a)
    {
        A b;
        return b;
    }

    int main(void)
    {
        A a;
        a = foo( a);
        return 0;
    }
   
    结果如下:
    A create.
    A create with copy
    A create.
    A create with copy
    A destroyed.
    A destroyed.
    A destroyed.
    A destroyed.
    Press any key to continue   

    原因:foo()的参数和返回值都是通过值传递的。在调用foo()的时候,需要把实参复制一份,压入foo()的

栈中,返回值也要复制一份放在栈中。
    解决办法:可以通过传递引用的方式来解决,即A foo(A& a)。返回值则可以根据情况返回指针或引用,也

可以增加一个指针类型的参数作为函数的输出。
   
    在C++软件开发中:还会有大量其他类型的隐性临时对象存在,如重载+以及++等操作符。
    当一个派生类实例化一个对象时,会先构造一个父类对象。同样在销毁一个派生类的对象时,要首先销毁其

父类对象。如果构造父类对象的开销很大,会造成所有子类的构造都有很大的开销。

1.3 C++对象的内存布局

1.3.1 简单对象

    简单C++对象内存布局:
    1) 非静态数据成员是影响对象占据内存大小的主要因素(4字节对齐)
    2) 静态数据成员不占对象内存
    3) 静态成员函数和非静态成员函数不会影响内存的大小
    4) 如果对象中包含虚函数,会增加4个字节的空间,不管有多少个虚函数
       虚函数表vtale中不必完全是指向虚函数实现的指针。当指定编译器打开RTTI开关时,vtable中的第一个

指针指向的是一个typeinfo的结构,每个类只产生一个typeinfo结构的实例。当程序调用typeid()来获取类的信息时

,实际上就是通过vtable中的第一个指针获得了typeinfo。

1.3.2 单继承

    // 此处应该是被修改 参考这篇文章http://student.csdn.net/space.php?uid=112600&do=blog&id=6424

    派生类在构造时,不会在创建一个新的虚函数表,而应该在基类的虚函数表中增加或修改。

1.3.3 多继承
   
    多继承中派生类的内存包含了多个完整的基类内存(即每个基类都有自己的虚函数表)。
    多继承要注意避免二义性(编译器会报二义性错误)。

    菱形继承:
    在派生类中有两个中间类,而每个中间类都会有一个基类。不仅浪费了内存,而且会产生二义性。
    所以派生类在调用时必需指明调用的是哪一个基类的方法:
    aSimple.midClass2::SetValue();
    aSimple.baseClass::GetValue();    //此处出现问题,编译器默认调用的是

aSimple.midClass1::baseClass::GetValue()
   
    aSimple.midClass1::SetValue();
    aSimple.baseClass::GetValue();
   
    解决办法:
    虚拟继承:当使用虚拟继承时,公共的基类只存在一个实例。
    class midClass1 : virtual public baseClass
    ...
    class midClass2 : virtual public baseClass
    ...
    baseClass的实例放在derivedClass实例的内存空间的最后一部分
    为了支持虚拟继承,不同的编译器的做法会有所不同
    vc++中:
    不使用虚拟继承时    使用虚拟继承时
    baseClass 12字节    baseClass 12字节
    midClass1 16字节    midClass1 24字节    (+8 增加了虚基类表、虚函数表?)   
    midClass2 16字节    midClass2 24字节
    derivedClass  36    derivedClass  40    ( 8 + 8 - 12?)

1.4 构造与析构
   
    编译器会提够默认的构造函数,此函数不带任何参数,也不会对成员数据进行任何初始化。
    必需自己定义,建议使用构造函数初始化列表(C++Primer)。
    编译器会提供默认的拷贝构造函数,这个函数执行的是位拷贝,在有new存在的时候会出现问题。
    必需自己定义拷贝构造函数。
   


第2章 C++语言特性的性能分析

    当一个程序的性能需要提高时,首先要做的就是用性能检测工具对其运行时间的分布进行一个准确的测量,

找出关键路径和瓶颈所在,然后针对瓶颈进行分析和优化。   
    C++中的一个语言特性比其他因素更容易成为程序的瓶颈:
    1) 缺页(第4章) 缺页意味着需要访问外部内存。应尽量想办法减少缺页。
    2) 从堆中动态申请和释放内存 优先考虑栈而减少从动态堆中申请内存。因为在堆中开辟内存比在栈中要慢

很多,而且还会引起缺页
    3) 复杂对象的创建和销毁  非常注意
    4)  函数调用 C语言的宏和C++的内联函数都是为了保持函数调用的模块化特征基础上消除函数调用的固定

额外开销而引入的。宏提供性能优势的同事也给开发和调试带来了不便,在C++中更多提倡的是使用内联函数。

2.1 构造函数与析构函数
   
    创建一个对象分成两个步骤:首先取得对象所需的内存,然后在该块内存上执行构造函数。构造函数也分两

个步骤:先执行初始化(通过初始化列表),再执行构造函数的函数体。
    构造函数是一个递归操作,在每层递归内部的操作遵循严格的次序。
    构造该类自己的成员变量时, 1)严格按照成员变量在类中的声明顺序进行,而与其在初始化列表中出现的

顺序无关
                   2)当有些成员变量或父类对象没有在初始化列表中出现时,它们仍然在初始

化操作这一步骤中被初始化
    常量( const)型和引用(refence)型必须在初始化列表中正确初始化,而不能将其初始化放在构造函数内。
   
    析构函数与构造函数逆序。

    开发人员要尽量避免编译器为其程序生成的临时对象。

    减少对象创建/销毁的一个很简单且常见的方法是在函数声明中降所有的值传递改为常量引用传递。

2.2 继承与虚函数

    除非通过性能检测确定性能的瓶颈是由于虚拟函数没有利用到内联函数的优势这一缺陷引起,否则可以不必

考虑虚拟函数的影响。

2.3 临时对象

    产生临时对象一般有如下两种场合:
    1) 当实际调用函数时传入的参数与函数定义中声明的变量类型不匹配
    2) 当函数返回一个对象时(有例外)
   
    class Rational
    {
    piblic:   
        /*explicit*/ Rational( int a=0, int b=1):m(a),n(b){}
    pravite:
        int m, n;
    }
    ...
   
    void foo()
    {
        Rational r;
        r = 100;    // 看起来无法编译,但是编译器会将右边的100通过调用Rational::Rational

(100,1)生成一个临时对象
        ...
    }
   
    通过对类的构造函数添加"explicit"来阻止编译器进行自动类型转换,即阻止因此引起的临时对象的产生。
    explicit的含义是开发人员只能根据这个构造函数的定义调用,而不允许编译器利用其来进行隐式的类型转

换。
   
    非内建类型的对象,尽量将对象延迟到已经确切知道其有效状态时。
    Rational r;    // 应写为 Rational r = a+b, 此时是一个初始化,而不是赋值,不会在重载+的函数返

回值的时候创建临时对象
    r = a+b;

    const Rational operate+( const Rational& a, const Rational& b)
    {
        Rational temp;
        temp.m = a.m + b.m;
        temp.n = a.n + b.n;
        return temp;
    }
    可优化为
    const Ratioanl operate+( const Rational& a, const Rational& b)
    {
        return Rational( a.m + b.m, a.n + b.n);         // 与上一个优化配合才有效
    }

    在设计类时,如果要重载operator+,最好也重载operator+=,并且考虑到维护性,operator+用operator+=来

实现,只须更改一处。
    Rational& operator+=( const Rational& rhs)            // 特别注意此处的返回值类型为

Rational&
    {
        m += rhs.m;
        n += rhs.n;
        return (*this)
    }
    const Ratioanl operate+( const Rational& a, const Rational& b)
    {
        return Rational(a) += b;        
    }   
    同理 -=, *=, /=。

    尽量使用++i, --i; 因为前置是先将值返回,然后其值增1;后置是先讲值增1,再返回其值,实现中必须

要保留其原来的值。

    string a, b;
    const char* str;
    if( strlen( str = (a+b).c_str() ) > 5)        //1   
    {
        printf("%s/n",str);            //2
        ...
    }
    存放a+b临时的临时对象的生命在包含其创建的最长语句结束后也相应的结束了,1处。当执行到2处,该临

时对象已经不存在。指向它内部字符串内容的str指向的是一段已经被回收的内存。会出现问题。
    但这条规范有一个特例,当用一个临时对象来初始化一个常量引用时,该临时对象的生命会持续到与其绑定

到其上的常量引用销毁时
    string a,b;
    if(...)
    {
        const string& c = a + b;    // 临时对象被绑定
        cout << c << endl;
        ...;
    }

2.4 内联函数

    使用内联函数,编译器可能会因为获得的信息更多,从而对调用函数的优化做得更加深入和彻底,致使最终

的代码量变得更小。
   
    函数调用时执行的操作:参数压栈(逆序进行);保存返回值;保存维护调用函数栈帧信息的寄存器内容;

保存一些通用寄存器的内容。
    执行被调用函数结束:恢复通用寄存器的值。恢复保存调用函数栈帧信息的寄存器的值。移动栈指针,销毁

被调用函数栈帧,将保存的返回地址出栈,并赋给IP寄存器;通过移动栈指针,回收传给被调用函数的参数所占用的

空间。
   
    内联后可以降低“缺页”的几率。

    内联应该在开发后期引入,因为内联函数的修改,会引起所有用到它的编译单元重新编译。

    如果第三方程序修改了内联函数,而开发小组的程序已经发布,就会产生相当高的重新编译的成本。

    递归函数中,如果递归次数不能知道实际值,编译器会拒绝内联。即使知道递归次数,编译器会视次数大小

决定是否进行内联。

    内联函数是编译期行为,虚函数是执行期行为,因此编译器一般会拒绝对虚函数进行内联。但是下面两种情

况除外:
    1) 通过对象,而不是指向对象的指针或者对象的引用调用虚函数,这时编译器在编译器就已经知道对象的

确切类型。
    2) 虽然是指向对象的指针或者对象的引用调用虚函数,但是编译器能知道指针或者对象对应到的对象的确

切类型。(可深入)

    与register关键字性质类似,inline仅仅是给编译器一个“建议”,编译器完全可以视实际情况而忽略之。

    内联是编译期行为,宏是预处理期行为。编译器会对内联参数进行类型检查,宏则不会,宏的参数在宏体内

出现两次或者两次以上经常会产生副作用,尤其在宏体内对参数进行++或者--操作。宏肯定会被展开,而内联函数不

会。

    一个程序的唯一入口main()肯定不会被内联化。另外编译器合成的默认构造函数、拷贝构造函数、析构函数

,以及赋值运算符一般都会被内联化。

 

第三章 常用数据结构性能分析

3.1 常用数据结构性能分析
    1.数组:优点:查找方便(下标);添加删除元素时不会产生内存碎片;不需要考虑数据节点指针的存储
        确定:内存使用率低,可扩展性差
    2.链表:数组的所有数据项被存放在一段连续的存储空间,链表的数据项则可能随机的被分配到内存中的某

个位置
    3.哈希表:最常用的 除法映射 F(k) = k % D, k是数据节点的关键字, D是一个事先设定的常量,F(k)是

桶号
3.1.1 遍历 数组:下标  链表:指针 哈希表:结合 二叉树:3种
    // 前序遍历
template<class E>
void preTraverse( TreeNode<E> *node)
{
    if( node != NULL)
    {
        Dosomething( pNode);
        PreTraverse( pNode->Left);
        PreTraverse( pNode->Right);
    }
}

// 中序遍历
template<class E>
void InTraverse( TreeNode<E> *node)
{
    if( node != NULL)
    {
        InTraverse( pNode);
        DoSomething( pNode);
        InTraverse( pNode);
    }   
}

// 后序遍历
template<class E>
void PostTraverse( TreeNode<E> *node)
{
    if( node != NULL)
    {
        PostTravese( pNode);
        PostTravese( pNode);
        DoSomething( pNode);
    }
   
}

3.1.2 插入
    数组 O(1)-0(n) 链表O(1) 哈希表O(1)-O(M) 平衡二叉树O(Log 以2为底 n),完全非平衡二叉树O(n)
3.1.3 删除
    同上, 红黑二叉树可解决二叉树删除后的平衡性问题
3.1.5 查找
    数组  O(1)-0(n) 有序数组 二分查找法/折半查找法 O(Log 以2为底 n)
// 折半查找
template<class E>
E BinSearch( E array[], const E&value, int start, int end)
{
    // 判断起点和终点是否合法
    if( end - start < 0)
        return INVALID_INPUT;
    // 如果起点或者终点有符合要求的,直接将其返回
    if( value == array[start])
        return array[start];
    if( value == array[end])
        return array[end];
    while( end > (start+1) )
    {
        int temp = (start+end)/2;
        if( value == array[temp])
            return array[temp];
        if( array[temp] < value)
            start = temp;
        else
            end = temp
    }
    throw CANNOT_FIND;
}
    链表 O(n) 双向有序链表O(n/2) 跳转链表
    哈希表 O(m) m为桶的长度
    二叉树 平衡二叉树O(Log 以2为底 n),完全非平衡二叉树O(n)
// 哈希表查找算法
template<class E, class key >
ptrLinkNode HashTable::SearchNode( const key& k) const
{
    int idx = hashFun(k);    // 定位桶
    if( NULL == hash_array[idx]);
        return NULL;
    prtLinkNode p = hash_array[idx];
    while( p)
    {
        if( k == p->GetKey() )
            return p;
        p = p->Next();   
    }
    retun NULL;
}


3.2.1 动态数组
    优点:可分配的空间较大;使用灵活
    缺点: 空间分配效率比静态数组低,栈由机器系统提供,堆由C++函数库提供;容易造成内存泄露


第二部分 内存使用优化

第4章 操作系统的内存管理

4.1  window内存管理
    win32虚拟内存管理器为每一个win32进程提供了进程私有且基于页的4GB(32)位大小的线性虚拟地址空间


    1.“进程私有”意味着每个进程有只能访问属于自己的地址空间(父子进程除外)。进程运行的dll没有属

于自己的虚拟地址空间
    2.“基于页”是指虚拟地址空间被划分为多个称为“页”的单元,页的大小由底层处理器决定,x86中的页

的大小为4KB。虚拟地址空间的申请和释放,以及内存和磁盘数据传输或置换都是以页为最小单位进行的。
    3.“4GB”大小以为着地址取值范围可以从0x00000000到0xFFFFFFFF.win32将低区的2GB留给进程使用,高区

的2GB则留给系统使用。
    调页文件和缺页错误

4.1.1 使用虚拟内存
    页的三种状态 自由free,预留reserved,提交committed
    防止栈溢出:减小函数的嵌套层数,减少递归函数的使用,尽量不要在函数中使用太大的局部变量(使用堆

创建)。

4.1.2 访问虚拟内存时的处理流程
4.1.3 虚拟内存要物理地址的映射
4.1.4 虚拟内存空间使用状态记录
4.1.5 进程工作集
    要想提高程序的运行效率    1.尽量编写紧凑的代码
                2.尽量将那些会一起访问的数据(比如链表)放在一起
4.1.6     Win32内存相关API
    1、传统的CRT函数(malloc/free系列)平台无关性
    2. global heap/local heap函数(GlobalAlloc/LocalAlloc系列) 建议不使用
    3. 虚拟内存函数(VirtualAlloc/VirtualFree系列)为开发人员提供了最大自由度,适合于为了大型连续

的数据结构开辟空间
    4.内存映射文件函数(CreateFileMapping/MapViewOfFile系列),windows系统利用它来有效使用exe和dll

文件,来发人员则可以方便地操作硬盘文件,而不用考虑那些繁琐的文件I/O操作;运行在同一台机器上的多个进程

可以通过内存映射文件函数来共享数据(这也是同一台机器上进程间进行数据共享和通信的最有效率和最方便的方法


    5.堆内存函数(HwapCreate/HeapAlloc系列) 当程序需要动态创建多个小数据结构师,最为合适。CRT函数

就是基于堆内存函数实现的

 

4.2 Linux内存管理机制

第5章 动态内存管理
    5.5 智能指针
    Boost中的智能指针有3类,即scoped_ptr/scoped_array, share_ptr/share_array和weak_ptr
    1.scoped_ptr/scoped_array是最基本的智能指针实现,只满足最基本的需求
        与std::auto_ptr相同scoped_ptr也不能用于数组的C++标准容器库,需要用于数组时,要用到

scoped_array
    2.share_ptr/share_array则是boost提供的适用于“可拷贝”或“可赋值”的智能指针。boost::share_ptr

采用引用计数来实现共享语义,因此不可避免地存在循环引用的问题,而打破循环引用的常规方法就是所谓的“弱引

用”,即在使用之前要检查是否可开。
    3.weak_ptr用来解决上述问题。
    Loki库是另一个智能指针库,采用基于策略的实现方式。

第6章 内存池


第三篇 应用程序启动性能优化

第7章 动态链接与动态库
    windows动态链接库(DLL)和linux系统的动态共享对象(DSO)

第8章 程序启动过程

8.1 win32程序启动过程
    1.操作系统负责把程序从磁盘读入内存并建立相应的运行环境
    2.应用程序自身的初始化过程

编译链接过程:
    1.预编译展开一些宏
    2.为每一个.cxx源文件编译一个目标文件(.obj, .o),目标文件中至少会包含二进制代码段和数据段。还

会包含一个符号表,用于记录自己引用的符号及自己提供的public符号。
    3.编译器合成这些目标文件成一个库文件(.lib),同事解析可以找到的符号引用。也包含符号表,记录所

引用其他库德符号
    4.编译器负责把目标文件和所有需要引用的静态/动态链接库,即需要首先把动态把其他静态库合成到可执

行文件中。转换相应的符号为引用地址,然后确保所引用的其他动态链接库的符号存在。
   
复杂的程序启动过程:
    1.操作系统首先创建相应的进程并分配私有的进程空间,然后操作系统的加载器负责把可执行文件的数据段

和代码段映射到进程的虚拟内存空间中。
    2.加载器读入可执行程序的导入符号表,根据这些符号表可以查找出该可执行程序所依赖的动态链接库。
    3.加载器针对该程序依赖的没一个动态链接库调用loadLibrary
    4.初始化应用程序的全局变量,对于全局变量自动调用构造函数
    5.进入程序入口点开始执行

8.2 linux�%

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值