more effective C++ 效率

282 阅读9分钟

效率

C++可以书写高效率的程序,但是对于原以存在的效率问题无能为力,若要实现高效率的C++程序,首先要求写出一个高效的程序。
产生和销毁过多的对象是会降低程序的性能的。

谨记80-20原则

  1. 软件的整体性能是有构成要素的很小的一部分决定的。(二八原则)
  2. 为了找到限制瓶颈的20%,必须要借助程序分析器,用该分析器去定性的测量你所关心的资源,如内存、运行时间。 在分析时可以加入二分法的思维步步逼近问题点所在的区间
  3. 使用分析器时,一定要注意使用的测试数据(用来分析你所关注资源的数据)产生的结果一定要是可以重现的,并且一定要尽可能多的数据

我的一个真实实例
问题:程序在删除配置文件后,首次启动会非常慢,
首先我想到的是在多台电脑上复现,确定是否是电脑慢原因导致(否)
后来就将启动过程中步骤耗费的时间打印出来,最后确认是复制大文件时导致。

考虑lazy-evaluation(缓式评估)

  1. 最好的运算就是从未被执行的运算。所谓lazy-evaluation指的是:知道某些运算结果刻不容缓的被需要时才运算;如果一直未达到刻不容缓的时候,则不计算。

  2. lazy-evaluation(缓式评估)四个用途

    引用计数 std::string的拷贝构造函数,在仅是读内存时,并未给拷贝构造出来的对象分配空间,通过引用计数,读内存,当修改时才会分配内存。
    区分读或写
    缓式取出
    当需要的时候再读取
    表达式缓评估
    知道需要读取结果时才计算所需要的那项。

  3. 如果计算非必要,那么lazy-evaluation可以节省工作或事件,但如果计算为必要,则不仅会减慢程序,同时增加内存用量,毕竟要处理为lazy-evaluation而设计的数据结构。

分期摊还预期的时间成本

  1. 超前进度的做“要求以外”的更多工作,此背后的哲学时超急评估(over-eager evaluation)。
  2. 预期程序常常会用到某项计算,可以将数据提前计算出来,随用随取,则会在用时降低每次计算的成本,此即为超前预估。

iterator本身是对象,所以并不保证->操作符可用,但是stl明确表明“.”和“*”对于iterator必须有效,则可以通过(*iter).操作iter指代的对象
3. 缓存可以分期摊还预期计算的成本,预先取出则是另一种方法。 > 预先取出的案例:取用内存时一般取一大块内存、动态数组扩容已2倍原有内存的方式扩充内存。 4. 无论缓存还是预先取出的方法来实现分期摊还预期计算的时间成本,都需要更大的缓存,即空间换时间。 5. 重要:

如果必须支持某项运算且其结果并不总是需要的时候,lazy-evaluation可以改善程序效率。
如果必须支持某项运算且结果总是被需要,或其结果总是多次被需要的时候,over-eager evaluation可以改善效率。

了解临时对象的来源

临时对象为不可见——不会在代码中可见,只要产生non-heap object而没有命名,便产生一个临时对象

  1. 产生此等匿名对象的通常有两种情况:

    隐式类型转换以求函数可以调用成功//编译器生成临时对象,该对象传递给被调用的函数
    函数返回对象

  2. C++禁止为non-const reference参数生成临时变量,但是reference to cosnt则不会。
  3. 针对返回对象的情况可以通过+=消除这份成本,还有返回值优化。
  4. 重要:

    任何时候看到reference-to-const参数,就极有可能会一个临时对象产生出来绑定到该参数
    任何时候只要你看到函数返回一个对象,就会产生临时对象。

协助完成返回值优化

  1. 如果函数一定得以by-value方式返回对象,你绝对无法消除它,以传址、传指针均不能有效避免。
  2. 返回值优化的背景知识为:C++允许编译器将临时变量优化,使他们不存在。所以通过调用构造函数来产生匿名的临时变量,给予编译器优化的机会。
const Rational operator*(const Rational& lhs,const Rational& rhs)
{
    return Rational(lhs.num*rhs.num, lhs.den*rhs.den);
}

利用重载(overload)技术避免隐式类型转换(implicit type conversions)

  1. 每个重载操作符必须获得至少一个用户定制类型的自变量
  2. 运算时为匹配操作符,会发生隐式类型转换,此时会产生临时变量,为消灭临时变量的创建和销毁的成本,可以通过重载操作符,消除类型转换。
  3. 避免产生临时对象不仅局限于操作符,char*和string也类似于此。
  4. 重载解决的是由于隐式转换带来的生成临时变量的成本问题。
  5. 总结:

    由于隐式类型转换过程中会产生临时变量,而生成临时变量的过程会有构造、析构的成本;为了降低成本,需要取消隐式转换,而取消隐式转换的技术方案为重载

考虑以操作符复合形式(op=)取代其独身形式(op)

例如,以+=代替+

  1. 要确保操作符的符合形式和独身形式之间的自然关系能够存在,一个很好的方式是以复合形式为基础实现独身形式。
class Rational
{
public:
    Rational& operator+=(cosnt Rational& rhs);

    const Rational operator+(cosnt Rational& lhs,cosnt Rational& rhs)
    {
        return Rational(lhs)+=rhs;//以复合形式实现独身形式,该形式可以应用编译器的返回值优化
    }
}
  1. 效率相关问题

    操作符复合形式比独身形式效率更高,因为独身形式通常必须返回一个新对象,所以独身形式必须负担临时对象的构造、析构成本;而复合形式直接将结果写入左端自变量,无需临时变量。
    同时提供复合形式和独身形式,便允许代码的用户在便利性和效率之间做出取舍。(复合形式效率高,独身形式便利性高)。同时,通过复合形式实现独身形式,可以保证客户从某种选择切换为另一种选择时,操作语法仍然保持不变。
    匿名对象总比命名对象更容易消除。当面临命名对象或临时对象的抉择时,最好选择临时对象。

考虑使用其他程序库

  1. 两个程序库提供类似机能,却有着不同的性能表现。例如iostream和stdio。
  2. 不同程序库即使提供类似机能,也往往表现出不同的性能取舍策略,所以一旦找出程序的瓶颈,应该思考是否可能因为改用另一个程序库而移除那些瓶颈。

了解virtual function、multiple inheritance、virtual base classes、runtime type identification的成本

  1. 当一个虚函数被调用,执行代码必须对应于“调用者对象的动态类型”。编译器通过虚函数表和虚函数指针来提供这样的行为。
  2. 虚函数表是由“虚函数指针”架构而成的数组。程序中的每个class凡是声明或继承虚函数均有自己的虚函数表。而虚函数表中的条目就是class的各个虚函数实现体的指针。而非虚函数——包括必定是非虚函数的构造函数——会像一般的C函数一样实现,所以他们的使用无特殊性能考虑。
  3. 凡是拥有虚函数的class均需有虚函数表空间,其大小视虚函数的个数而定。每个class应该只有一个虚函数表,当这样的类很多时或者类中有很多虚函数时,虚函数表们占用的空间也不少。
  4. 虚函数不要被声明为内联(inline)。
  5. 虚函数表指针用于指定每个对象对应于那个虚函数表。凡是有虚函数的类,其对象含有一个隐藏的data member,用于指向该类的虚函数表,这个隐藏的data member ——所谓的虚函数指针——被编译器加入到对象内的某个编译器才知道的位置。
  6. 拥有虚函数的对象内付出“一个额外指针”的代价(空间代价)。较大的对象意味着很难塞入一个缓存分页或虚拟内存页中,意味着需要更多的翻页活动
  7. 虚函数的调用流程:依据对象的虚函数表指针找到虚函数表,在虚函数表中找到函数地址;调用之。所以虚函数的调用成本和“通过函数指针调用函数”一样。
  8. 虚函数的真正成本发生于运行期和inline互动的时候。虚函数放弃了inline。背后原因:inline是在编译期将调用端的调用动作被调用函数的函数本地取代,而虚函数需要在运行时期才知道那个函数被信用。所以使用virtual意味着放弃了inline。当然,如果虚函数通过对象调用是可以inlined的。
  9. 多重继承往往导致“虚拟基类”的需求。在非虚拟基类中,派生类在若在基类中有多条继承路径,则基类的data members会在每个派生类对象体内复制滋生。每个副本对应的派生类和基类之间有一条继承路线。让基类成为virtual,则避免复制,而是通过指针指向“virtual base class成分”,消除复制行为,对象体内可能出现多个这样的指针,即虚基类会导致对象内隐藏指针增加。
  10. 运行期类型辨识(RTTI)使我们可以在运行期获得对象和类的相关信息,这些信息被保存在type_info的对象内,通过typeid操作符可以获得相应的type_info对象。C++规范说明,只有当某种类型至少有一个虚函数,才保证我们能够检验该类型对象的动态类型。
  11. RTTI的空间成本就是只需要在虚函数表内增加一个条目,再加上每个类所需的一份type_info对象的空间。