调整数据成员和成员函数以提高性能

Alignment of data members and member functions for performance

本文关键字:高性能 函数 成员 数据成员 调整      更新时间:2023-10-16

对齐结构/类的数据成员是否真的不再像以前那样带来好处,尤其是在nehalem上,因为硬件的改进?如果是这样的话,与过去的CPU相比,对齐总是会带来更好的性能,只是非常小的显著改进吗?

成员变量的对齐是否扩展到成员函数?我相信我曾经读过(可能在维基百科"C++性能"上),有将成员函数"打包"到各种"单元"(即源文件)的规则,以优化加载到指令缓存中?(如果我这里的术语有误,请纠正我)。

处理器仍然比RAM提供的速度快得多,因此它们仍然需要缓存。缓存仍然由固定大小的缓存行组成。此外,主存储器是以页为单位提供的,并且使用翻译后备缓冲区来访问页。这个缓冲区同样有一个固定大小的缓存。

这意味着空间和时间位置都很重要(即你如何打包东西,以及你如何访问它)。良好地包装结构(根据填充/对齐要求进行分类),而不是按照一些随意的顺序包装,通常会导致结构尺寸更小。

较小的结构尺寸意味着,如果你有大量的数据:

  • 更多的结构适合一个缓存行(缓存未命中=50-200个周期)
  • 需要更少的页面(页面故障=10-20万个CPU周期)
  • 需要的TLB条目越少,TLB未命中就越少(TLB未中=50-500个周期)

在几个千兆字节的紧密封装的SoA数据上线性处理可能比在布局/封装不好的情况下以天真的方式做同样的事情快3个数量级(或者8-10个数量级,如果涉及页面错误)。

无论您是否手动将单个4字节或2字节值(例如,典型的intshort)与2或4字节对齐,在最近的英特尔CPU上都会产生非常小的差异(几乎不明显)。到目前为止,在这方面进行"优化"似乎很诱人,但我强烈建议不要这样做。
这通常是最好不要担心的事情,而是留给编译器来解决。如果没有其他原因,那么因为收益充其量是微不足道的,但如果你弄错了,其他一些处理器架构会引发异常。因此,如果你过于聪明,一旦在其他架构上编译,就会突然出现无法解释的崩溃。当这种情况发生时,你会感到抱歉。

当然,如果你没有至少几十兆字节的数据要处理,你根本不需要关心。

调整数据以适应处理器永远不会有什么坏处,但有些处理器会比其他处理器有更明显的缺点,我认为这是回答这个问题的最佳方法。

将函数与缓存行单元对齐对我来说有点转移注意力。对于小函数,如果可能的话,你真正想要的是内联。如果代码不能内联,那么它可能比缓存行还大。[当然,除非是虚拟功能]。我认为这从来都不是一个巨大的因素——要么代码通常被经常调用,因此通常在缓存中,要么它不经常被调用,也不经常在缓存中。我相信有可能会想出一些代码,其中调用一个函数,func1()也会将func2()拖到缓存中,所以如果你总是在短时间内连续调用func1(。但这并不是一个很大的好处,除非你有很多函数对或函数组被紧密地调用。[顺便说一句,我不认为编译器可以保证按照任何特定的顺序放置函数代码,无论你在源文件中的顺序如何]。

缓存对齐是一个稍微不同的问题,因为如果你做对了与做错了,缓存线仍然会产生巨大的影响。这对于多线程来说比一般的"加载数据"更重要。这里的关键是避免在处理器之间共享同一缓存行中的数据。大约10年前,在我参与的一个项目中,一个基准测试有一个函数,它使用两个整数的数组来计算每个线程的迭代次数。当它被拆分为两个独立的缓存行时,基准测试从在单个处理器上运行的0.6倍提高到了在一个处理器上运行1.98倍。同样的效果也会发生在现代CPU上,即使它们快得多——效果可能不完全一样,但会大幅放缓(共享数据的处理器越多,效果就越大,所以四核系统会比双核系统更糟糕,等等)。这是因为每当处理器更新缓存行中的某个内容时,读取该缓存行的所有其他处理器都必须从更新它的处理器(或以前的内存)重新加载它。