优化 C++/代码优化/内存访问
当应用程序访问主内存时,它会隐式使用各种处理器缓存和操作系统虚拟内存管理器进行的磁盘交换机制。
处理器缓存和虚拟内存管理器都以块为单位处理数据,因此如果少数内存块包含单个命令使用的代码和数据,则软件运行速度更快。一个命令处理的数据和代码应该驻留在内存中的相邻区域的原则称为 引用局部性。
在多核系统上的多线程应用程序中,此原则对于性能变得更加重要,因为如果在不同内核上运行的多个线程访问同一个缓存块,则争用会导致性能下降。
在本节中,提出了一些技术来优化处理器缓存和虚拟内存的使用,方法是增加代码和数据的引用局部性。
在同一个编译单元中将属于同一个瓶颈的所有函数定义放在附近。
这样,编译这些函数生成的机器代码将具有相邻的地址,从而提高代码的引用局部性。
另一个积极的结果是,这些函数声明和使用的局部静态数据将具有相邻的地址,从而提高数据的引用局部性。
但是,不同线程使用的数据应该位于不同的缓存行,以避免错误共享(即缓存行反弹)。Gerber/Bik/Smith/Tian 在《软件优化手册》(第 262 页)中提出的建议是将此类数据间隔至少 128 字节(这大于任何 L1/2/3 缓存行)。
在中型或大型数组或集合中,使用联合体
。
联合体
允许在可变类型结构中节省内存空间,从而使它们更加紧凑。
但是,不要将它们用于小型或微型对象,因为没有明显的空间增益,并且对于某些编译器,放入联合体
中的对象不会保留在处理器寄存器中。
如果中型或大型对象包含几个范围很小的整数,则将它们转换为位域。
位域减小了对象大小。
例如,代替以下结构
struct {
bool b;
unsigned short ui1, ui2, ui3; // range: [0, 1000]
};
占用 8 个字节,您可以定义以下结构
struct {
unsigned b: 1;
unsigned ui1: 10, ui2: 10, ui3: 10; // range: [0, 1000]
};
仅占用(1 + 10 + 10 + 10 = 31 位,31 <= 32)4 个字节。
另一个例子,代替以下数组
unsigned char a[5]; // range: [-20, +20]
占用 5 个字节,您可以定义以下结构
struct {
signed a1: 6, a2: 6, a3: 6, a4: 6, a5: 6; // range: [-20, +20]
};
仅占用(6 + 6 + 6 + 6 + 6 = 30 位,30 <= 32)4 个字节。
但是,打包和解包字段会产生性能损失。此外,在最后一个示例中,字段不再可以通过索引访问。
需要注意的是,位域的行为没有很好地定义,因此不同的编译器会给出不同的结果。位域结构成员不是线程安全的,因为内存粒度问题,因此在多处理器平台上并发访问时具有未定义的行为,这会导致难以检测的错误。因此,许多编译器会尝试完全忽略它们。
这种优化最适合微控制器。由于这些设备通常是单处理器设备,因此并发访问没有问题。它们还拥有非常有限的内存,因此无法承受浪费的位。此外,许多微控制器具有专门的指令,旨在提高位操作的性能,例如测试字节中某个偏移量处单个位是否设置的指令。
如果在类模板中,一个非平凡的成员函数不依赖于任何模板参数,则定义一个具有相同主体但不是成员的函数,并将原始函数主体替换为对新函数的调用。
假设您编写了以下代码
template <typename T>
class C {
public:
C(): x_(0) { }
int f(int i) { body(); return i; }
private:
T x_;
};
尝试将上面的代码替换为以下代码
template <typename T>
class C {
public:
C(): x_(0) { }
int f(int i) { return f_(i); }
private:
T x_;
};
int f_(int i) { body(); return i; }
对于使用该类模板的函数的每个类模板实例化,函数的整个代码都被实例化。如果该类模板中的函数不依赖于任何模板参数,则在每次实例化时都会复制函数机器代码。这种代码复制会使程序膨胀。
在一个类模板或函数模板中,一个大型函数可能有一大部分不依赖于任何模板参数。在这种情况下,首先将该代码部分作为单独的函数分离出来,然后应用此指南。