《提高C++性能的编程技术》 读书笔记

第一章 跟踪范例

跟踪功能对于调试、维护和理解大中型软件的执行流程是非常重要的,要警惕由于跟踪实现不当,导致对软件性能的影响。
在添加性能跟踪函数时,要注意,在关闭跟踪时,要消除没有价值的对象和计算,换言之,关闭跟踪等同于没有添加一行跟踪代码。

第二章 构造函数和析构函数

对象的创建或清除引发对父对象和成员对象的递归创建或清除。

对象的生命周期不是无偿的,除非打算要使用它,否则就不要创建它。通常情况下,要把对象的创建推迟到要使用它的块中。

构造函数和析构函数被编译器内联。

尽可能使用最简单的解决方案。

第三章 虚函数

在编译期解析函数调用关系,例如函数重载、内联和模板。
在运行期,通过虚函数机制动态解析函数调用,核心数据结构为虚表,它包含为该类所有虚函数指针。每个有虚函数的类都有一个虚表,该类的每个对象有一个隐藏的指向该表的指针。

由于虚函数的类型判断发生在运行时,所以编译器不能内联虚函数。

比起继承,模板可提供更好的性能,它们把类型的确定提前到编译期。

返回值优化

返回值优化(RVO,Return Value Optimization)。当函数要返回内部对象时,外部会有一个临时对象来负责接收。这会有内部局部对象到临时对象的转换,RVO通过复用这两个对象地址,直接将内部局部对象构造在外部临时对象位置,减少一次copy ctor的调用。具体实现取决于编译器优化能力以及函数实现。

临时对象

涵盖C++性能的主要问题是不必要对象的创建和清除。

临时对象会以构造函数和析构函数的形式损失性能。
把构造函数声明为explicit,可阻止编译器隐式使用类型转换。
在尽可能的情况下,应尽量避免对象复制,按引用来传递和返回对象。

内联

使用内联的优点是不需要为执行被调用方法而进行跳转。由于需要的指令不在预取缓存中,频繁跳转涉及执行管道的延迟,对指令流进行重定向,这会发生在方法调用时和方法返回时。

声明内联的方法:

  • 在类头部声明中定义的短小精悍的方法,编译器会将其优化为内联函数。
  • 在函数声明前加上 inline 关键字,并在类外部实现时也加上。

内联操作给编译器提供有助于优化的信息,允许编译器对方法执行源代码和机器代码级别的优化,这基于对方法调用上下文关系认识的基础上。
优化范围不仅包括方法调用,还有方法间的优化。

引用计数

如果很多对象有相同的值,将这个值存储多次是冗余的,更好的办法是让所有的对象共享这个值的实现。
引用计数的基本思想是把对象清除的责任从应用代码转移给对象本身,对象自身跟踪当前对它的引用数量,并在引用数量达到0时删除自己。

以下代码是 《More Effective C++》中的Item M29的一个引用计数的实现。

class String{
public:
    String(const char* initValue="")
    :value(new StringValue(initValue)){}
   
   String(const String& rhs)
   :value(rhs.value)
   {    // 拷贝一个指针并增加一次引用计数
        ++value->refCount;
   }
   
   ~String()
   {    // 只有当唯一的使用者被析构时,才真正销毁对象
        if (--value->refCount == 0) delete value;
   }
  
    String& operator=(const String& rhs)
    {   //  s1 = s2 。该操作会使得 s1 和 s2指向相同的对           
        //  象。原有的s1对象会指向s2对象,s1对象的引用计数
        //  会减少,s2对象的引用计数会增加。
        
        if (this = &rhs) return *this;
        
        if (--value->refCount == 0) delete []value;
        
        value = rhs.value;  // s1指向s2的数据。
        value->refCount++;
        return *this;
    }
    
private:
   
    // 保存引用计数以及跟踪的数据
    struct StringValue{
        int refCount;      // 引用计数
        char* data;        // 跟踪的数据
        StringValue(const char* initValue);
        ~StringValue();
    };
    StringValue *value;
}
String::StringValue::StringValue(const char* initValue)
:relCount(1)
{
    data = new char[strlen(initValue) + 1];
    strcpy(data, initValue);
}

String::StringValue::~StringValue
{
    delete []data;
}

设计优化

  • 缓式计算,把计算延迟到真正需要的时候再进行。该原则也适用于对象的创建,不仅仅要延迟对象的创建,更应该将其创建延迟到具备了有效创建所需的各种条件之后。
  • 避免无用计算。
  • 设计对缓存友好的代码
class X{
public:
    X():a(1).c(2){}
    ...
private:
    int a;
    char b[4096];
    int c;
}

X的构造函数初始化成员a和c,实际内存排布按照声明的顺序来排列。成员a和成员c被成员b所分离,不会位于相同的缓存行中,构造函数在访问c时可能会遭到缓存失败。
这就要求,在成员变量的位置设计上,相同大小的尽量放在一起。

可伸缩性

SMP是对称多处理器,所有的处理器有相同的能力,它们对任何内存位置和任何I/O设备都进行相同的访问,通过唯一的总线与唯一的内存系统相连接。除非应用程序代码非要那么做,否则线程不会与任何特定的CPU产生任何密切关系。S
MP体系结构提供缓存一致性保证,既两个CPU中的缓存都包含变量x的一份拷贝,CPU1更新了它的私有拷贝,缓存一致性保证其他缓存拷贝和主存拷贝都将得到更新。这对应用程序是不可见的。

实现可伸缩性的技巧是减少或消除串行化代码:

  • 任务分解:把单一的任务分解成多个子任务,这有益于并发线程并发执行。
  • 代码移出:临界区应该包括重要的、操作共享资源的代码,其他无关代码应该放在外面。
posted @ 2020-03-07 16:06  浩天之家  阅读(377)  评论(0编辑  收藏  举报