面试题答案
一键面试编译器对成员变量的布局方式
- 基本规则
- 编译器按照成员变量声明的顺序依次分配内存空间。但是,为了满足数据对齐的要求,可能会在成员变量之间插入填充字节。数据对齐是为了提高内存访问效率,现代计算机系统通常要求特定类型的数据在内存中按照特定的边界对齐。例如,在32位系统中,4字节的
int
类型变量通常要求其内存地址是4的倍数;在64位系统中,8字节的double
类型变量通常要求其内存地址是8的倍数。 - 对于类中的基类子对象,其布局在派生类对象的开头,遵循基类自身的布局规则。
- 虚函数表指针(如果类有虚函数)通常位于对象的开头部分。
- 编译器按照成员变量声明的顺序依次分配内存空间。但是,为了满足数据对齐的要求,可能会在成员变量之间插入填充字节。数据对齐是为了提高内存访问效率,现代计算机系统通常要求特定类型的数据在内存中按照特定的边界对齐。例如,在32位系统中,4字节的
- 示例说明
- 假设有如下类定义:
class Example {
char a;
int b;
short c;
};
- 假设
char
占1字节,int
占4字节,short
占2字节。由于int
类型需要4字节对齐,编译器会在char
类型的a
后面插入3个填充字节,使得b
的地址是4的倍数。short
类型的c
不需要额外填充,因为它可以从b
之后自然对齐。这样,Example
类对象的总大小为1 + 3 + 4 + 2 = 10字节。
不同布局策略对缓存命中率的影响
- 缓存原理
- 计算机的缓存是为了缓解CPU和内存之间的速度差异。缓存以缓存行(cache line)为单位进行数据传输,通常缓存行大小在32字节到128字节之间。当CPU访问内存中的数据时,会将包含该数据的整个缓存行加载到缓存中。如果后续访问的数据也在这个缓存行中,就可以直接从缓存中获取,这就是缓存命中。
- 布局影响
- 紧凑布局:如果成员变量布局紧凑,使得相关的成员变量尽可能地靠近,它们更有可能被加载到同一个缓存行中。例如,在一个表示向量的类中,如果
x
、y
、z
坐标成员变量紧密排列,当访问其中一个坐标时,其他坐标也更可能已经在缓存中,从而提高缓存命中率。 - 非紧凑布局:如果成员变量布局不合理,例如频繁访问的成员变量之间间隔较大,可能会导致每次访问都需要从内存中加载新的缓存行,降低缓存命中率。比如,一个游戏对象类中,经常访问的位置信息和很少访问的纹理数据放在一起,当访问位置信息时,纹理数据也会被加载到缓存中,占用缓存空间,而纹理数据又很少被用到,降低了缓存空间的有效利用率。
- 紧凑布局:如果成员变量布局紧凑,使得相关的成员变量尽可能地靠近,它们更有可能被加载到同一个缓存行中。例如,在一个表示向量的类中,如果
包含多个不同类型成员变量的类设计成员变量顺序以提高性能的方法
- 按访问频率分组
- 将经常一起访问的成员变量放在相邻位置。例如,在一个图形渲染类中,如果经常同时访问顶点坐标和颜色信息,应将表示顶点坐标和颜色的成员变量紧挨着声明。
- 按数据类型大小排序
- 一般来说,先声明较大的数据类型成员变量,再声明较小的数据类型成员变量。这样可以减少填充字节的数量,使得对象布局更加紧凑。例如,先声明
double
类型变量,再声明int
类型变量,最后声明char
类型变量。
- 一般来说,先声明较大的数据类型成员变量,再声明较小的数据类型成员变量。这样可以减少填充字节的数量,使得对象布局更加紧凑。例如,先声明
- 避免跨越缓存行
- 了解缓存行的大小(例如64字节),尽量将频繁访问的成员变量安排在不超过一个缓存行的范围内。如果一个类的成员变量较多,可以根据访问频率和相关性,将其分成多个组,每个组的大小尽量控制在缓存行大小以内。
- 考虑虚函数表指针
- 如果类有虚函数,虚函数表指针通常在对象开头。在安排成员变量顺序时,要考虑虚函数表指针对数据对齐和缓存布局的影响。例如,尽量将经常访问的成员变量放在离虚函数表指针较远的位置,避免虚函数表指针的更新频繁干扰到成员变量的缓存命中。