在C ++中优化成员变量顺序

时间:2009-05-21 12:48:14

标签: c++ performance optimization embedded

我正在为blog post的游戏编码器阅读Introversion,他正在忙着试图从代码中挤出每个CPU蜱。他提到的一个伎俩是

  

“重新排序a的成员变量   最常用和最少使用的类。“

我不熟悉C ++,也不熟悉它的编译方式,但我想知道是否

  1. 这句话准确吗?
  2. 如何/为什么?
  3. 它是否适用于其他(编译/脚本)语言?
  4. 我知道这个技巧节省的(CPU)时间量是最小的,这不是一个交易破坏者。但另一方面,在大多数函数中,很容易识别哪些变量将是最常用的,并且默认情况下只是以这种方式开始编码。

10 个答案:

答案 0 :(得分:57)

答案 1 :(得分:10)

根据您运行的程序类型,此建议可能会导致性能提升,或者可能会大幅减慢速度。

在多线程程序中执行此操作意味着您将增加“虚假共享”的可能性。

查看Herb Sutters关于该主题的文章here

我以前说过,我会继续说。获得真正性能提升的唯一真正方法是测量代码,并使用工具识别真正的瓶颈而不是随意更改代码库中的内容。

答案 2 :(得分:6)

这是优化working set size的方法之一。 John Robbins对如何通过优化工作集大小来加快应用程序性能有一个很好的article。当然,它涉及仔细选择最终用户可能使用该应用程序执行的最常见的用例。

答案 3 :(得分:3)

虽然改进数据访问的缓存行为的引用位置通常是一个相关的考虑因素,但在需要优化时控制布局还有其他几个原因 - 特别是在嵌入式系统中,即使在许多嵌入式系统上使用的CPU也是如此甚至没有缓存。

- 结构中字段的内存对齐

很多程序员都很清楚对齐注意事项,所以我不会在这里详细介绍。

在大多数CPU架构中,必须以原生对齐方式访问结构中的字段以提高效率。这意味着如果混合使用各种大小的字段,编译器必须在字段之间添加填充以保持对齐要求的正确性。因此,为了优化结构使用的内存,重要的是要牢记这一点并布置字段,使得最大的字段后跟较小的字段以使所需的填充保持最小。如果要“打包”结构以防止填充,则访问未对齐字段会带来高运行时成本,因为编译器必须使用对字段较小部分的一系列访问来访问未对齐字段以及移位和掩码以组装字段寄存器中的值。

- 结构中常用字段的偏移量

在许多嵌入式系统中,另一个重要的考虑因素是在结构的开头有频繁访问的字段。

某些体系结构在指令中可用的位数有限,无法将偏移量编码为指针访问,因此,如果访问偏移量超过该位数的字段,编译器将不得不使用多个指令来形成指向场。例如,ARM的Thumb架构有5位来编码偏移量,因此只有当字段从一开始就在124字节以内时,它才能访问单个指令中的字大小字段。因此,如果您有一个大型结构,嵌入式工程师可能想要记住的优化是将常用字段放在结构布局的开头。

答案 4 :(得分:3)

我们对这里的成员有一些略微不同的指导原则(ARM架构目标,出于各种原因主要是THUMB 16位codegen):

  • 按对齐要求分组(或者,对于新手来说,“按大小分组”通常可以解决问题)
  • 最小的

“按比例分组”有点明显,超出了这个问题的范围;它避免了填充,使用更少的内存等。

然而,第二个项目源于THUMB LDRB(加载寄存器字节),LDRH(加载寄存器半字)和LDR(加载寄存器)指令上的小的5位“立即”字段大小。

5位表示可以编码0-31的偏移。实际上,假设“this”在寄存器中很方便(通常是这样):

  • 如果8位字节存在于此+ 0到此+ 31
  • ,则可以加载8位字节
  • 16位半字,如果它们存在于此+ 0到此+ 62;
  • 32位机器字,如果它们存在于此+ 0到此+ 124。

如果它们超出此范围,则必须生成多个指令:一系列ADD具有立即在寄存器中累积适当的地址,或者更糟糕的是,在函数末尾从文字池加载

如果我们确实点击了文字池,那就会受到伤害:文字池通过d-cache,而不是i-cache;这意味着对于第一个文字池访问,至少有一个来自主内存的高速缓存行负载,如果文本池没有在其自己的高速缓存上启动,则d-cache和i-cache之间会出现大量潜在的驱逐和失效问题line(即如果实际代码没有在缓存行的末尾结束)。

(如果我对我们正在使用的编译器有一些意愿,强制文字池在高速缓存行边界上启动的方法将是其中之一。)

(不相关地,我们为避免文字池使用而采取的措施之一就是将所有“全局”保留在一个表中。这意味着对“GlobalTable”进行一次文字池查找,而不是为每个全局查找多次查找。如果你真的很聪明,你可以将你的GlobalTable保存在某种内存中,无需加载文字池条目即可访问 - 是.sbss吗?)

答案 5 :(得分:2)

在C#中,成员的顺序由编译器决定,除非你把属性[LayoutKind.Sequential / Explicit]强制编译器以你告诉它的方式布局结构/类。

据我所知,编译器似乎最小化打包,同时按照自然顺序对齐数据类型(即4字节地址的4字节int开始)。

答案 6 :(得分:2)

第一个成员不需要在指针上添加一个偏移量来访问它。

答案 7 :(得分:1)

我专注于性能,执行速度,而不是内存使用。 没有任何优化开关的编译器将使用代码中相同的声明顺序映射变量存储区域。 想象

 unsigned char a;
 unsigned char b;
 long c;

大乱?没有对齐开关,低内存操作。等等,我们将在你的DDR3 dimm上使用64位字,而另一个使用另外64位字,而在长时间内使用不可避免的字。

所以,这是每个变量的抓取。

但是,打包或重新排序会导致一次获取和一次AND屏蔽,以便能够使用未签名的字符。

所以速度方面,在目前的64位字存储器机器上,对齐,重新排序等等都是不可能的。我做微控制器的东西,有打包/非打包的差异真的很明显(谈论< 10MIPS处理器,8位字存储器)

另一方面,人们早就知道,除了优秀的算法指示你做的事情之外,调整代码以获得性能所需的工程工作,以及编译器能够优化的内容,通常会导致橡胶燃烧而没有真正的影响。那是一个只写的dubius代码。

我看到的最后一步优化(在uPs中,不认为它对PC应用程序是可行的)是将程序编译为单个模块,让编译器对其进行优化(更一般的速度/指针视图)解析/内存打包等),并让链接器废弃非调用库函数,方法等。

答案 8 :(得分:0)

理论上,如果你有大对象,它可以减少缓存未命中。但通常最好将相同大小的成员组合在一起,这样你就可以收紧更紧密的内存。

答案 9 :(得分:0)

我非常怀疑这会对CPU改进产生任何影响 - 可能是可读性。如果在给定帧内执行的常用基本块位于同一组页面中,则可以优化可执行代码。这是相同的想法,但不知道如何在代码中创建基本块。我的猜测是编译器按照它看到的顺序放置函数而没有优化,所以你可以尝试将常用功能放在一起。

尝试并运行探查器/优化器。首先使用一些分析选项进行编译然后运行程序。一旦profiled exe完成,它将转储一些配置文件信息。获取此转储并将其作为输入通过优化器运行。

我已经远离这项工作多年,但没有太多改变他们的工作方式。