优化 C++/编写高效代码/性能降低功能
与 C 相比,C++ 有一些功能,如果使用不当会降低效率。
其中一些功能仍然非常高效,可以在需要时随意使用。但是,当不需要该功能时,应该避免其增加的成本。
其他功能则效率低下,因此应谨慎使用。
在本节中,将介绍一些避免降低性能的 C++ 功能的指导原则。
仅当您想通知用户当前命令失败时才调用throw
运算符。
与函数调用相比,抛出异常的成本非常高。它需要数千个处理器周期。如果仅在用户界面上显示消息或写入日志文件时抛出异常,则可以保证它不会在没有通知的情况下执行得太频繁。
相反,如果异常成为算法的一部分,即使最初认为很少见,它们最终也可能执行得太频繁。
仅当类至少包含另一个virtual
成员函数或类可能从(即该类旨在用作基类)派生时,才将析构函数定义为virtual
。除析构函数外,除非您打算覆盖它们,否则不要将函数声明为virtual
。
具有virtual
成员函数的类所占用的存储空间比没有virtual
成员函数的类多。至少包含一个virtual
成员函数的类的实例占用更多空间(通常是 指针,可能还有一些填充),并且它们的构造需要更多时间(通常是 设置指针)比没有virtual
成员函数的类的实例。
此外,每个virtual
成员函数的调用时间都比相同的非virtual
成员函数慢。
仅当多个类必须共享一个共同基类的表示形式时才使用virtual
继承。
例如,考虑以下类定义
class A { ... };
class B1: public A { ... };
class B2: public A { ... };
class C: public B1, public B2 { ... };
对于此类定义,每个 C 类对象都包含两个不同的类 A 对象,一个从 B1 基类继承而来,另一个从 B2 类继承而来。
如果类 A 没有非static
成员变量,则没有问题。
如果相反,类 A 包含一些成员变量,并且您希望这些成员变量对每个 C 类实例都是唯一的,则必须使用virtual
继承,方法如下
class A { ... };
class B1: virtual public A { ... };
class B2: virtual public A { ... };
class C: public B1, public B2 { ... };
这种情况是唯一需要virtual
派生的情况。
如果使用virtual
继承,则类 A 的成员函数在 C 类对象上调用的速度会略微变慢。
不要定义多态类的模板。
换句话说,不要在同一个类定义中使用“template
”和“virtual
”关键字。
每次实例化类模板时,都会复制所有使用的成员函数。如果这些类包含虚函数,即使它们的vtable 和RTTI 数据也会被复制。这些数据会使代码膨胀。
仅当您可以证明其对特定情况的便利性时,才使用基于垃圾收集 或某种引用计数 的智能指针(例如Boost 库中的shared_ptr
)。
垃圾收集或未引用内存的自动回收,允许程序员省略内存释放调用的写法,并防止内存泄漏。但是,您必须通过非标准库实现垃圾收集,因为这些功能不包含在 C++ 中。此外,使用垃圾收集的代码通常比使用显式释放(当显式调用delete
运算符时)的代码执行速度慢。此外,当垃圾收集器运行时,程序的其余部分不会运行。这使得程序的执行不确定性。
C++98 标准库只包含一种智能指针auto_ptr
,这种智能指针非常高效。非标准库提供的其他智能指针(例如 Boost)将由 C++11 标准库提供。智能指针依赖于引用计数,但效率低于简单指针。例如,shared_ptr
是标准的 Boost 智能指针。但是,如果使用 Boost 的线程安全版本,则该库在创建、销毁和复制这些指针时的性能非常差,因为它必须保证这些操作的互斥访问。
通常,您应该尝试在设计时将每个动态分配的对象分配给单个所有者。当这种分配很困难时,例如如果多个对象倾向于将销毁对象的责任传递给彼此,那么使用引用计数智能指针来处理该对象会很方便。
仅将由硬件设备异步更改的变量定义为volatile
。
使用volatile
修饰符可以阻止编译器将变量定位到寄存器中,即使是短暂的时间。这保证了所有设备都看到相同的变量,但会减慢访问该变量的操作速度。
volatile
不能保证其他线程会看到相同的值,因为它不会强制编译器生成必要的内存屏障和锁指令。因此,即使在同一个 CPU 上(在中断和信号的情况下很重要),尤其是跨内存缓存和总线访问其他 CPU,对 volatile 值的读写访问也可能无序进行。您必须使用适当的线程或原子内存 API 或编写机器代码来保证操作的正确顺序,以确保线程安全。