我不熟悉C++,也不了解它是如何编译的,但我想知道:"将类的成员变量按最常用和不常用排序。"
- 这个说法准确吗?
- 怎么/为什么?
- 它适用于其他(编译/脚本)语言吗?
我不熟悉C++,也不了解它是如何编译的,但我想知道:"将类的成员变量按最常用和不常用排序。"
这里有两个问题:
之所以可能有帮助,是因为内存加载到 CPU 缓存中的块称为“缓存行”。这需要时间,一般来说,加载到您对象的缓存行越多,耗费的时间就越长。同时,更多其他内容会被清除以腾出空间,这会以不可预测的方式减慢其他代码的速度。
缓存行的大小取决于处理器。如果与对象大小相比较大,则很少有对象跨越缓存行边界,因此整个优化基本上无关紧要。否则,你可以有时只在缓存中保留对象的一部分,其余部分在主内存(或L2缓存,也许)中。如果最常见的操作(访问经常使用的字段的操作)尽可能少地使用对象的缓存,那么将这些字段分组在一起会使你更有机会实现这一点。
总的原则称为“局部性”。程序访问的不同存储器地址越接近,则获取良好的缓存行为的机会越大。通常很难事先预测性能:相同架构的不同处理器型号可以表现不同,多线程意味着你经常不知道会有什么在缓存中,等等。但通常可以谈论发生的可能性大多数时间。如果您想要了解任何事情,通常必须测量它。
请注意,这里有一些陷阱。如果您使用基于 CPU 的原子操作(C++0x中的原子类型通常会),则可能会发现 CPU 锁定整个缓存行以锁定字段。然后,如果您有几个相邻的原子字段,不同的线程在不同的核心上运行,并且同时在不同的字段上操作,则会发现所有这些原子操作被串行化,因为它们都锁定相同的内存位置,即使它们在不同的字段上操作。如果它们在不同的缓存行上操作,那么它们将并行工作并更快地执行。实际上,正如 Glen(通过 Herb Sutter)在他的回答中指出的那样,在一致性高速缓存体系结构上,即使没有原子操作,这也会发生,并可能完全破坏您的一天。因此,在涉及多个核心的情况下,局部性引用不一定是一个好东西,即使它们共享缓存。你可以期望它是,因为缓存未命中通常是速度损失的来源,但在你特定的情况下可能会非常错误。
现在,除了区分常用和不常用的字段之外,对象越小,它占用的内存(因此缓存)就越少。这基本上是好消息,至少在没有大量争用的情况下是这样。对象的大小取决于其中的字段以及必须插入字段之间的任何填充以确保它们对于体系结构正确对齐。C ++(有时)根据它们声明的顺序放置字段的约束条件在一个对象中。这是为了使低级编程更容易。所以,如果您的对象包含:那么很有可能它将占用16字节的内存。顺便说一下,int的大小和对齐方式在每个平台上都不同,但4非常常见,这只是一个例子。
在这种情况下,编译器将在第二个int之前插入3个字节的填充,以正确对齐它,并在末尾插入3个字节的填充。对象的大小必须是其对齐方式的倍数,以便可以将相同类型的对象相邻地放置在内存中。这就是C / C ++中的数组,相邻的内存对象。如果结构体是int,int,char,char,则相同的对象可能只有12个字节,因为char没有对齐要求。
我说过int是否4对齐取决于平台:在ARM上,它绝对必须是,因为未对齐访问会引发硬件异常。在x86上,您可以访问不对齐的整数,但通常速度较慢,并且我IRC非原子。因此,编译器通常(总是?)在x86上将int 4对齐。
编写代码时的经验法则是,如果您关心打包,请查看结构的每个成员的对齐要求。然后按最大对齐类型的字段首先排序,然后按下一个最小的字段排序,以此类推,直到没有对齐要求的成员。例如,如果我正在尝试编写可移植代码,则可能会得出以下结果:
struct some_stuff {
double d; // I expect double is 64bit IEEE, it might not be
uint64_t l; // 8 bytes, could be 8-aligned or 4-aligned, I don't know
uint32_t i; // 4 bytes, usually 4-aligned
int32_t j; // same
short s; // usually 2 bytes, could be 2-aligned or unaligned, I don't know
char c[4]; // array 4 chars, 4 bytes big but "never" needs 4-alignment
char d; // 1 byte, any alignment
};
#pragma pack
及其对成员对齐的影响。 - Remus Rusanu根据你运行的程序类型,这些建议可能会增加性能,也可能会极大地减缓速度。
在多线程程序中这样做意味着增加“虚假共享”的机会。
请查阅Herb Sutter在这个主题上的文章此处
我之前已经说过,我将继续说。唯一真正获得性能提升的方式是测量您的代码,并使用工具识别真正的瓶颈,而不是随意更改代码库中的东西。
- 结构体中字段的内存对齐
许多程序员都很清楚地了解到内存对齐的注意事项,所以我不会在这里详细介绍。
在大多数CPU架构中,结构体中的字段必须以本地对齐方式进行访问以提高效率。这意味着如果您混合使用各种大小的字段,则编译器必须在字段之间添加填充以保持对齐要求正确。因此,为了优化结构使用的内存,重要的是要牢记这一点,并布置字段,使最大的字段后跟较小的字段,以将所需的填充量最小化。如果一个结构体要被“打包”以防止填充,则访问未对齐的字段的运行时成本很高,因为编译器必须使用一系列访问来访问未对齐的字段,以及移位和掩码来组装寄存器中的字段值。
- 结构体中经常使用的字段的偏移量
在许多嵌入式系统上,另一个需要考虑的问题是将经常访问的字段放在结构的开头。
一些架构在指令中有限数量的位可用于编码对指针访问的偏移量,因此如果您访问的字段的偏移量超过该位数,则编译器将不得不使用多个指令来形成对该字段的指针。例如,ARM的Thumb架构有5位可用于编码偏移量,因此只有在距离起始位置124字节以内的情况下,才能在单个指令中访问一个字大小的字段。因此,如果您有一个大的结构体,嵌入式工程师可能要考虑的一种优化是将经常使用的字段放在结构布局的开头。
首个成员不需要添加偏移量到指针上来进行访问。
我专注于性能和执行速度,而不是内存使用。 编译器在没有任何优化开关的情况下,将按照代码中声明的顺序映射变量存储区域。 想象一下
unsigned char a;
unsigned char b;
long c;
出现了大问题?没有对齐开关,低内存操作等等,我们将会在您的DDR3 DIMM上使用一个64位字来使用无符号字符,另一个64位字用于另一个变量,而长整型则是不可避免的。
因此,每个变量都需要一次提取。
然而,打包或重新排序将导致一次提取和一次AND掩码以便使用无符号字符。
因此,在当前64位字内存机器上,对齐、重新排序等都是不可行的。我做微控制器的东西,在那里,打包/非打包的差异真的非常明显(谈论小于10MIPS处理器,8位字内存)
此外,众所周知,为了性能而调整代码所需的工程努力,除了好的算法指导您要做什么和编译器能够优化的内容之外,通常会导致毫无实际效果的烧胶。还有一些语法上存在问题的只写代码。
我看到的最后一步优化(在微处理器上,不认为PC应用程序可以做到)是将您的程序编译为单个模块,让编译器进行优化(更通用的速度/指针分辨率/内存打包等),并让链接器删除未被调用的库函数、方法等。理论上,如果你有大对象,它可以减少缓存未命中。但通常最好将相同大小的成员分组在一起,这样可以更紧密地打包内存。
我非常怀疑这与CPU的改进有任何关系 - 或许只是可读性。如果在同一组页面中执行在给定帧内执行的常用基本块,则可以优化可执行代码。 这是相同的想法,但不知道如何在代码中创建基本块。我猜编译器按其看到它们的顺序放置函数,没有进行任何优化,因此您可以尝试将常见功能放在一起。
尝试运行分析器/优化器。首先,您需要使用某些分析选项进行编译,然后运行程序。一旦分析了exe文件,它就会转储一些分析信息。使用此转储作为输入运行优化器。
我已经离开这个领域多年了,但工作方式并没有太大改变。