跳转到内容

优化 C++/编写高效代码/性能降低功能

来自 Wikibooks,开放书籍,为开放世界

与 C 相比,C++ 有一些功能,如果使用不当会降低效率。

其中一些功能仍然非常高效,可以在需要时随意使用。但是,当不需要该功能时,应该避免其增加的成本。

其他功能则效率低下,因此应谨慎使用。

在本节中,将介绍一些避免降低性能的 C++ 功能的指导原则。

throw 运算符

[编辑 | 编辑源代码]

仅当您想通知用户当前命令失败时才调用throw 运算符。

与函数调用相比,抛出异常的成本非常高。它需要数千个处理器周期。如果仅在用户界面上显示消息或写入日志文件时抛出异常,则可以保证它不会在没有通知的情况下执行得太频繁。

相反,如果异常成为算法的一部分,即使最初认为很少见,它们最终也可能执行得太频繁。

virtual 成员函数

[编辑 | 编辑源代码]

仅当类至少包含另一个virtual 成员函数或类可能从(即该类旨在用作基类)派生时,才将析构函数定义为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”关键字。

每次实例化类模板时,都会复制所有使用的成员函数。如果这些类包含虚函数,即使它们的vtableRTTI 数据也会被复制。这些数据会使代码膨胀。

自动释放器的使用

[编辑 | 编辑源代码]

仅当您可以证明其对特定情况的便利性时,才使用基于垃圾收集 或某种引用计数智能指针(例如Boost 库中的shared_ptr)。

垃圾收集或未引用内存的自动回收,允许程序员省略内存释放调用的写法,并防止内存泄漏。但是,您必须通过非标准库实现垃圾收集,因为这些功能不包含在 C++ 中。此外,使用垃圾收集的代码通常比使用显式释放(当显式调用delete 运算符时)的代码执行速度慢。此外,当垃圾收集器运行时,程序的其余部分不会运行。这使得程序的执行不确定性。

C++98 标准库只包含一种智能指针auto_ptr,这种智能指针非常高效。非标准库提供的其他智能指针(例如 Boost)将由 C++11 标准库提供。智能指针依赖于引用计数,但效率低于简单指针。例如,shared_ptr 是标准的 Boost 智能指针。但是,如果使用 Boost 的线程安全版本,则该库在创建、销毁和复制这些指针时的性能非常差,因为它必须保证这些操作的互斥访问。

通常,您应该尝试在设计时将每个动态分配的对象分配给单个所有者。当这种分配很困难时,例如如果多个对象倾向于将销毁对象的责任传递给彼此,那么使用引用计数智能指针来处理该对象会很方便。

volatile 修饰符

[编辑 | 编辑源代码]

仅将由硬件设备异步更改的变量定义为volatile

使用volatile 修饰符可以阻止编译器将变量定位到寄存器中,即使是短暂的时间。这保证了所有设备都看到相同的变量,但会减慢访问该变量的操作速度。

volatile 不能保证其他线程会看到相同的值,因为它不会强制编译器生成必要的内存屏障和锁指令。因此,即使在同一个 CPU 上(在中断和信号的情况下很重要),尤其是跨内存缓存和总线访问其他 CPU,对 volatile 值的读写访问也可能无序进行。您必须使用适当的线程或原子内存 API 或编写机器代码来保证操作的正确顺序,以确保线程安全。

华夏公益教科书