【有书共读】《More Effective C++》笔记4
Rule 16 牢记80-20 准则
1. 如果想写出一个高效的C++程序,你必须首先能写出一个高效的算法。
2. 软件整体的性能取决于代码组成中的一小部分。
3. profiler 告诉你每条语句执行了多少次或各函数被调用了多少次,这是一个作用有限的工具。
4. profiler 仅能够告诉你在某一次运行(或某几次运行)时一个程序运行情况,所以如果你用不具有代表性的输入数据profile 一个程序,那你所进行的profile 也没有代表性。
Rule 17 考虑使用惰性计算
1. 一种惰性方法:避免不需要的对象拷贝,除非你确实需要,不去为任何东西制作拷贝。我们应该是懒惰的,只要可能就共享使用其它值。
2. 通过使用operator[]区分出读操作:我们如果判断调用operator[]的context 是读取操作还是写入操作呢?残酷的事实是我们不可能判断出来。通过使用lazy evaluation和条款M30 中讲述的proxy class,我们可以推迟做出是读操作还是写操作的决定,直到我们能判断出正确的答案。
3. 避免不需要的数据库读取操作:当LargeObject对象被建立时,不从磁盘上读取所有的数据,当需要某个数据时,这个数据才被从数据库中取回。
4. 避免不需要的数字操作:一般APL的用户加、乘或除以矩阵不是因为他们需要整个矩阵的值,而是仅仅需要其一小部分的值。APL 使用lazy evaluation 来拖延它们的计算直到确切地知道需要矩阵哪一部分的结果,然后仅仅计算这一部分。
5. 如果你的计算都是重要的,lazy evaluation 可能会减慢速度并增加内存的使用,因为除了进行所有的计算以外,你还必须维护数据结构让lazy evaluation 尽可能地在第一时间运行。在某些情况下要求软件进行原来可以避免的计算,这时lazy evaluation 才是有用的。
6. 主流程序设计语言采用的是eager evaluation,C++是主流语言。不过C++特别适合用户实现lazy evaluation,因为它对封装的支持使得能在类里加入lazy evaluation,而根本不用让类的使用者知道。
Rule 18 分期摊还期望的计算
1. over-eager evaluation(过度热情计算法):在要求你做某些事情以前就完成它们。
[lazy evaluation, eager evaluation, over-evaluation]
2. 隐藏在over-eager evaluation 后面的思想是如果你认为一个计算需要频繁进行,你就可以设计一个数据结构高效地处理这些计算需求,这样可以降低每次计算需求时的开销。
3. 采用over-eager 最简单的方法就是caching(缓存)那些已经被计算出来而以后还有可能需要的值。
4. Cache 运算结果需要更多的内存,但是一旦需要被缓存的结果时就能减少需要重新生成的时间。Prefetch 需要空间放置被prefetch 的东西,但是它减少了访问它们所需的时间。
5. 如果需要一个地方的数据,则很可能也需要它旁边的数据。这是位置相关现象。
Rule 19 理解临时对象的来源
1. 在C++中真正的临时对象是看不见的,它们不出现在你的源代码中。建立一个没有命名的非堆(non-heap)对象会产生临时对象。这种未命名的对象通常在两种条件下产生:为了使函数成功调用而进行隐式类型转换和函数返回对象时。理解如何和为什么建立这些临时对象是很重要的,因为构造和释放它们的开销对于程序的性能来说有着不可忽视的影响。
2. 仅当通过传值(by value)方式传递对象或传递常量引用(reference-to-const)参数时,才会发生这些类型转换。
Rule 20 协助完成返回值优化
1. 一些函数(operator*也在其中)必须要返回对象。这就是它们的运行方法。不要与其对抗,你不会赢的。
2. 通过使用函数的return 位置(或者在函数被调用位置用一个对象来替代)来消除局部临时对象――是众所周知的和被普遍实现的。它甚至还有一个名字:返回值优化(这种优化对普通的赋值运算无效,编译器不能够用拷贝构造函数取代赋值运算动作,最终结论是:在确保语意正确的前题下没有更好的优化可能了)。实际上这种优化有自己的名字本身就可以解释为什么它被广泛地使用。
[这一小节看的还是有点含糊,从头至尾都没看到怎么优化返回对象]
Rule 21 通过重载避免隐式类型转换
1. 通过重载避免隐式类型转换产生的临时对象的开销。
2. 在C++中有一条规则是每一个重载的operator 必须带有一个用户定义类型(user-defined type)的参数。
3. 没有必要实现大量的重载函数,除非你有理由确信程序使用重载函数以后其整体效率会有显著的提高。
Rule 22 考虑用运算符的赋值形式(op=)取代其单独形式(op)
1. 确保operator 的赋值形式(例如operator+=)与一个operator的单独形式(例如 operator+ )之间关系: operator+ 根据operator+=来实现。
2. operator 的赋值形式比其单独形式效率更高,因为单独形式要返回一个新对象,从而在临时对象的构造和释放上有一些开销。operator 的赋值形式把结果写到左边的参数里,因此不需要生成临时对象来容纳operator 的返回值。
[operator的赋值形式效率更高,但是operator的单独形式看起来更直观]
Rule 23 考虑变更程序库
1. 理想的程序库应该是短小的、快速的、强大的、灵活的、可扩展的、直观的、普遍适用的、具有良好的支持、没有使用约束、没有错误的。
2. iostream 程序库与C 中的stdio 相比有几个优点。例如它是类型安全的(type-safe),它是可扩展的。然而在效率方面,iostream 程序库总是不如stdio,因为stdio 产生的执行文件与iostream 产生的执行文件相比尺寸小而且执行速度快。应该注意到stdio 的高效性主要是由其代码实现决定的,所以我已经测试过的系统其将来的实现或者我没有测试过的系统的当前实现都可能表现出iostream 和stdio 并没有显著的差异。事实上,有理由相信会发现一种iostream 的代码实现比stdio 要快,因为iostream在编译时确定它们操作数的类型,而stdio 的函数则是在运行时去解析格式字符串(format string)
3. 不同的程序库在效率、可扩展性、移植性、类型安全和其他一些领域上蕴含着不同的设计理念,通过变换使用给予性能更多考虑的程序库,你有时可以大幅度地提高软件的效率。
[程序设计很难十全十美,需要一定的折衷/平衡]
Rule 24 理解虚拟函数、多继承、虚基类和RTTI 所需的代价
1. 虚函数所需的第一个代价:必须为每个包含虚函数的类的virtual talbe 留出空间。类的vtbl 的大小与类中声明的虚函数的数量成正比(包括从基类继承的虚函数)。每个类应该只有一个virtual table,所以virtual table 所需的空间不会太大,但是如果你有大量的类或者在每个类中有大量的虚函数,你会发现vtbl 会占用大量的地址空间。
2. 启发式算法来决定哪一个object 文件应该包含类的vtbl。通常启发式算法是这样的:要在一个object 文件中生成一个类的vtbl,要求该object 文件包含该类的第一个非内联、非纯虚拟函数(non-inline non-pure virual function)定义(也就是类的实现体)。
3. 每个声明了虚函数的对象都带有vptr,它是一个看不见的数据成员,指向对应类的vtbl;vptr被编译器加在对象里,位置只有才编译器知道。
4. “内联”是指在编译期间用被调用的函数体本身来代替函数调用的指令,但是虚函数的“虚”是指直到运行时才能知道要调用的是哪一个函数。如果编译器在某个函数的调用点不知道具体是哪个函数被调用,你就能知道为什么它不会内联该函数的调用。这是虚函数所需的第三个代价:你实际上放弃了使用内联函数。
5. 虚函数能使对象变得更大,而且不能使用内联,多继承和虚基类也会增加对象的大小。
6. RTTI被设计为在类的vtbl 基础上实现。,RTTI 耗费的空间是在每个类的vtbl 中的占用的额外单元再加上存储type_info 对象的空间。
7. 理解虚函数、多继承、虚基类、RTTI 所需的代价是重要的,但是如果你需要这些功能,你得为此付出代价。有时你确实有一些合理的原因要绕过编译器生成的服务,所以你可能希望用某种方法模拟这些特性,能更加容易地完成这些任务;不过从效率的观点来看,你自己编写代码不可能做得比编译器生成的代码更好。
#笔记#
1. 如果想写出一个高效的C++程序,你必须首先能写出一个高效的算法。
2. 软件整体的性能取决于代码组成中的一小部分。
3. profiler 告诉你每条语句执行了多少次或各函数被调用了多少次,这是一个作用有限的工具。
4. profiler 仅能够告诉你在某一次运行(或某几次运行)时一个程序运行情况,所以如果你用不具有代表性的输入数据profile 一个程序,那你所进行的profile 也没有代表性。
Rule 17 考虑使用惰性计算
1. 一种惰性方法:避免不需要的对象拷贝,除非你确实需要,不去为任何东西制作拷贝。我们应该是懒惰的,只要可能就共享使用其它值。
2. 通过使用operator[]区分出读操作:我们如果判断调用operator[]的context 是读取操作还是写入操作呢?残酷的事实是我们不可能判断出来。通过使用lazy evaluation和条款M30 中讲述的proxy class,我们可以推迟做出是读操作还是写操作的决定,直到我们能判断出正确的答案。
3. 避免不需要的数据库读取操作:当LargeObject对象被建立时,不从磁盘上读取所有的数据,当需要某个数据时,这个数据才被从数据库中取回。
4. 避免不需要的数字操作:一般APL的用户加、乘或除以矩阵不是因为他们需要整个矩阵的值,而是仅仅需要其一小部分的值。APL 使用lazy evaluation 来拖延它们的计算直到确切地知道需要矩阵哪一部分的结果,然后仅仅计算这一部分。
5. 如果你的计算都是重要的,lazy evaluation 可能会减慢速度并增加内存的使用,因为除了进行所有的计算以外,你还必须维护数据结构让lazy evaluation 尽可能地在第一时间运行。在某些情况下要求软件进行原来可以避免的计算,这时lazy evaluation 才是有用的。
6. 主流程序设计语言采用的是eager evaluation,C++是主流语言。不过C++特别适合用户实现lazy evaluation,因为它对封装的支持使得能在类里加入lazy evaluation,而根本不用让类的使用者知道。
Rule 18 分期摊还期望的计算
1. over-eager evaluation(过度热情计算法):在要求你做某些事情以前就完成它们。
[lazy evaluation, eager evaluation, over-evaluation]
2. 隐藏在over-eager evaluation 后面的思想是如果你认为一个计算需要频繁进行,你就可以设计一个数据结构高效地处理这些计算需求,这样可以降低每次计算需求时的开销。
3. 采用over-eager 最简单的方法就是caching(缓存)那些已经被计算出来而以后还有可能需要的值。
4. Cache 运算结果需要更多的内存,但是一旦需要被缓存的结果时就能减少需要重新生成的时间。Prefetch 需要空间放置被prefetch 的东西,但是它减少了访问它们所需的时间。
5. 如果需要一个地方的数据,则很可能也需要它旁边的数据。这是位置相关现象。
Rule 19 理解临时对象的来源
1. 在C++中真正的临时对象是看不见的,它们不出现在你的源代码中。建立一个没有命名的非堆(non-heap)对象会产生临时对象。这种未命名的对象通常在两种条件下产生:为了使函数成功调用而进行隐式类型转换和函数返回对象时。理解如何和为什么建立这些临时对象是很重要的,因为构造和释放它们的开销对于程序的性能来说有着不可忽视的影响。
2. 仅当通过传值(by value)方式传递对象或传递常量引用(reference-to-const)参数时,才会发生这些类型转换。
Rule 20 协助完成返回值优化
1. 一些函数(operator*也在其中)必须要返回对象。这就是它们的运行方法。不要与其对抗,你不会赢的。
2. 通过使用函数的return 位置(或者在函数被调用位置用一个对象来替代)来消除局部临时对象――是众所周知的和被普遍实现的。它甚至还有一个名字:返回值优化(这种优化对普通的赋值运算无效,编译器不能够用拷贝构造函数取代赋值运算动作,最终结论是:在确保语意正确的前题下没有更好的优化可能了)。实际上这种优化有自己的名字本身就可以解释为什么它被广泛地使用。
[这一小节看的还是有点含糊,从头至尾都没看到怎么优化返回对象]
Rule 21 通过重载避免隐式类型转换
1. 通过重载避免隐式类型转换产生的临时对象的开销。
2. 在C++中有一条规则是每一个重载的operator 必须带有一个用户定义类型(user-defined type)的参数。
3. 没有必要实现大量的重载函数,除非你有理由确信程序使用重载函数以后其整体效率会有显著的提高。
Rule 22 考虑用运算符的赋值形式(op=)取代其单独形式(op)
1. 确保operator 的赋值形式(例如operator+=)与一个operator的单独形式(例如 operator+ )之间关系: operator+ 根据operator+=来实现。
2. operator 的赋值形式比其单独形式效率更高,因为单独形式要返回一个新对象,从而在临时对象的构造和释放上有一些开销。operator 的赋值形式把结果写到左边的参数里,因此不需要生成临时对象来容纳operator 的返回值。
[operator的赋值形式效率更高,但是operator的单独形式看起来更直观]
Rule 23 考虑变更程序库
1. 理想的程序库应该是短小的、快速的、强大的、灵活的、可扩展的、直观的、普遍适用的、具有良好的支持、没有使用约束、没有错误的。
2. iostream 程序库与C 中的stdio 相比有几个优点。例如它是类型安全的(type-safe),它是可扩展的。然而在效率方面,iostream 程序库总是不如stdio,因为stdio 产生的执行文件与iostream 产生的执行文件相比尺寸小而且执行速度快。应该注意到stdio 的高效性主要是由其代码实现决定的,所以我已经测试过的系统其将来的实现或者我没有测试过的系统的当前实现都可能表现出iostream 和stdio 并没有显著的差异。事实上,有理由相信会发现一种iostream 的代码实现比stdio 要快,因为iostream在编译时确定它们操作数的类型,而stdio 的函数则是在运行时去解析格式字符串(format string)
3. 不同的程序库在效率、可扩展性、移植性、类型安全和其他一些领域上蕴含着不同的设计理念,通过变换使用给予性能更多考虑的程序库,你有时可以大幅度地提高软件的效率。
[程序设计很难十全十美,需要一定的折衷/平衡]
Rule 24 理解虚拟函数、多继承、虚基类和RTTI 所需的代价
1. 虚函数所需的第一个代价:必须为每个包含虚函数的类的virtual talbe 留出空间。类的vtbl 的大小与类中声明的虚函数的数量成正比(包括从基类继承的虚函数)。每个类应该只有一个virtual table,所以virtual table 所需的空间不会太大,但是如果你有大量的类或者在每个类中有大量的虚函数,你会发现vtbl 会占用大量的地址空间。
2. 启发式算法来决定哪一个object 文件应该包含类的vtbl。通常启发式算法是这样的:要在一个object 文件中生成一个类的vtbl,要求该object 文件包含该类的第一个非内联、非纯虚拟函数(non-inline non-pure virual function)定义(也就是类的实现体)。
3. 每个声明了虚函数的对象都带有vptr,它是一个看不见的数据成员,指向对应类的vtbl;vptr被编译器加在对象里,位置只有才编译器知道。
4. “内联”是指在编译期间用被调用的函数体本身来代替函数调用的指令,但是虚函数的“虚”是指直到运行时才能知道要调用的是哪一个函数。如果编译器在某个函数的调用点不知道具体是哪个函数被调用,你就能知道为什么它不会内联该函数的调用。这是虚函数所需的第三个代价:你实际上放弃了使用内联函数。
5. 虚函数能使对象变得更大,而且不能使用内联,多继承和虚基类也会增加对象的大小。
6. RTTI被设计为在类的vtbl 基础上实现。,RTTI 耗费的空间是在每个类的vtbl 中的占用的额外单元再加上存储type_info 对象的空间。
7. 理解虚函数、多继承、虚基类、RTTI 所需的代价是重要的,但是如果你需要这些功能,你得为此付出代价。有时你确实有一些合理的原因要绕过编译器生成的服务,所以你可能希望用某种方法模拟这些特性,能更加容易地完成这些任务;不过从效率的观点来看,你自己编写代码不可能做得比编译器生成的代码更好。
#笔记#
查看14道真题和解析