【问题标题】:Alignment of data members and member functions for performance数据成员和成员函数的对齐以提高性能
【发布时间】:2013-02-03 11:20:41
【问题描述】:

对齐结构/类的数据成员是否真的不再产生以前的好处,尤其是由于硬件改进而在 nehalem 上?如果是这样,对齐总是会带来更好的性能,与过去的 CPU 相比,是否仍然会有非常小的显着改进?

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

【问题讨论】:

  • 对齐仍然更快(标量和向量的机器人)。但是,滥用palignr 来替换未对齐的向量负载不再值得。

标签: c++ performance optimization alignment latency


【解决方案1】:

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

这意味着空间和时间位置都非常重要(即你如何打包东西,以及你如何访问它)。良好地包装结构(按填充/对齐要求排序)而不是以某种随意的顺序包装它们通常会导致更小的结构尺寸。

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

  • 一个缓存行可容纳更多结构(缓存未命中 = 50-200 个周期)
  • 需要更少的页面(页面错误 = 10-20 百万个 CPU 周期)
  • 需要更少的 TLB 条目,更少的 TLB 未命中(TLB 未命中 = 50-500 个周期)

以线性方式处理几 GB 紧密打包的 SoA 数据可能比以糟糕的布局/打包方式以幼稚的方式执行相同的事情快 3 个数量级(或 8-10 个数量级,如果涉及页面错误) .

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

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

【讨论】:

  • 这是否适用于将指令以及数据加载到缓存中?每当我读到缓存优化时,他们从来没有提到指令,只有数据?两者在优化原理上有什么区别?
  • 它确实适用,但监督、控制和预测有点困难。在某些情况下,较大的代码(认为循环展开)更快,但在许多情况下,由于缓存,较小的代码更快。出于这个原因,有时建议使用Os 而不是O3 进行编译,但这样的经验法则并不普遍适用。再加上指令解码(也被缓存)、分支预测和流水线,这一切都变得相当复杂。大多数编译器允许您将函数标记为“热”和“冷”,将它们放在单独的部分中,或者让您选择部分名称。这...
  • ... 改善了时间相关(好吧,希望如此)函数的空间局部性,例如,更多“相似”函数最终出现在同一页面中。也就是说,如果你能正确地判断出哪些函数应该是“热的”,这又不是完全无关紧要的(通过猜测来做这样的事情,即没有分析器几乎可以保证只会让它变得更糟)。
【解决方案2】:

对齐数据以适应处理器永远不会受到伤害,但某些处理器会比其他处理器具有更明显的缺点,我认为这是回答这个问题的最佳方法。

对我来说,将函数对齐到缓存行单元似乎有点牵强。对于小型函数,如果可能的话,您真正想要的是内联。如果代码不能被内联,那么它可能比缓存行大。 [当然,除非它是一个虚函数]。我不认为这曾经是一个巨大的因素 - 要么代码通常被经常调用,因此通常在缓存中,或者它不经常被调用,并且在缓存中不经常被调用。我敢肯定,可能会想出一些代码来调用一个函数,func1() 也会将 func2() 拖到缓存中,所以如果你总是连续调用 func1() 和 func2(),它会有一些好处。但这真的不是什么大好处,除非你有很多函数,它们具有成对或成组的函数,它们被紧密地调用在一起。 [顺便说一句,我不认为编译器可以保证以任何特定的顺序放置你的函数代码,无论你把它放在源文件中的哪个顺序]。

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

【讨论】:

  • 为什么“除非它是一个虚函数”,你的意思是没有任何实现?
  • 不,“除非它是一个虚函数”是指一个虚函数不能被内联[除非编译器完全理解发生了什么],所以无论如何,它将是对函数的调用异常。所以,如果有一些函数可以做一些简单的事情,比如“return (x * x) + (y * y);”要计算它与其他东西的距离,将它们放在一起可能会有一点好处 - 尽管不能保证编译器无论如何都不会填充和/或移动函数。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-05-20
  • 2011-08-27
  • 2010-12-13
  • 1970-01-01
  • 2021-11-13
  • 1970-01-01
  • 2011-06-04
相关资源
最近更新 更多