如何在C++中编写指令缓存友好型程序?

17

最近Herb Sutter在“现代C++:你需要知道的”上做了一个很棒的演讲。这个演讲的主题是效率以及数据局部性和内存访问的重要性。

他还解释了线性访问内存(数组/向量)将会受到CPU的喜爱。他引用了另一个经典参考来源“Bob Nystrom的游戏性能”中的一个例子来说明这个问题。

阅读这些文章后,我发现有两种类型的缓存会影响程序性能:

  1. 数据缓存
  2. 指令缓存

Cachegrind工具也可以测量我们程序的这两种缓存类型的仪器信息。第一个点已经由许多文章/博客解释过了,以及如何实现良好的数据缓存效率(数据局部性)。

然而,我对指令缓存这个主题并没有得到太多信息,以及我们应该在程序中注意什么以实现更好的性能?据我理解,我们程序员没有太多控制权来决定执行哪些指令或以什么顺序执行。

如果有一些小的C++程序可以说明这个计数器(即指令缓存)如何随着我们编写程序的风格而变化那将非常好。程序员应该遵循哪些最佳实践才能在这个方面实现更好的性能呢?

我的意思是,如果我们的程序处理数据缓存相关话题,比如向量 vs 列表,是否有可能解释第二点?这个问题的主要目的是尽可能地理解这个主题。


2
避免使用虚函数,并将复杂的代码分解成较小的循环。 - László Papp
1
@Leeor:这个重复在哪里? - László Papp
抱歉,我参考了错误的问题,一旦我旁边有电脑,我会撤回投票,但我肯定它以前已经被回答过了,包括代码缓存上下文,包括跟踪缓存、解码uop缓存等。 - Leeor
1个回答

14
任何改变执行流程的代码都会影响指令缓存,包括函数调用、循环以及解引用函数指针。
当执行分支或跳转指令时,处理器必须花费额外的时间来决定代码是否已经在指令缓存中,或者是否需要重新加载指令缓存(从分支目的地处重新加载)。
例如,一些处理器的指令缓存足够大,可以容纳小循环的执行代码。而有些处理器则没有那么大的指令缓存,只能简单地重新加载它。重新加载指令缓存需要花费时间,这些时间本应用于执行指令。
搜索以下主题:
- 循环展开 - 条件指令执行(在ARM处理器上可用) - 内联函数 - 指令流水线
编辑1:提高性能的编程技巧
为了提高性能并减少指令缓存的重新加载,请按照以下步骤操作:
- 减少“if”语句 设计您的代码以最小化“if”语句。这可能包括布尔代数、使用更多的数学运算或简化比较(它们真正需要吗?)。尽量减少“then”和“else”子句的内容,以便编译器可以使用条件汇编语言指令。 - 将小函数定义为内联或宏 调用函数会带来开销,例如存储返回位置和重新加载指令缓存。对于只有少量语句的函数,请尝试建议编译器将其设置为内联。内联是指将代码内容粘贴到执行处,而不是进行函数调用。由于避免了函数调用,也就避免了重新加载指令缓存的需要。 - 展开循环 对于小迭代次数,不要使用循环,而是重复循环的内容(某些编译器可能会在更高的优化级别设置下执行此操作)。重复的内容越多,向循环顶部的分支就越少,也就越不需要重新加载指令缓存。 - 使用查找表而不是“if”语句有些程序使用"if-else-if"阶梯来将数据映射到值。每个“if”语句都会在指令缓存中中断执行。有时候,通过一些数学计算,这些值可以被放置在一个表格中,就像一个数组,然后利用数学计算来计算索引。一旦索引被知道,处理器就可以在不干扰指令缓存的情况下检索数据。
改变数据或数据结构。如果数据类型是固定的,程序就可以围绕数据进行优化。例如,一个处理消息包的程序可以基于数据包ID(类似于函数指针数组)进行操作,以便针对数据包处理进行优化的函数。
将链表更改为数组或其他随机访问容器。可以使用数学方法访问数组的元素,而不会中断执行。必须遍历(循环)链表才能找到项。

2
“展开循环” -> 我认为这个完全是错误的。整个循环很可能会被保留在缓存中,回到顶部不应该造成任何问题。保持循环是代码存储在缓存中而不仅仅是预取的主要原因之一。实际上,相反,建议不要展开循环,以避免生成大量重复指令,使您的代码变得更大且不太可能适合缓存。 - Saiph
1
此外,还存在时间与空间的权衡。你会发现快速算法往往占用比空间优化更多的空间。节省空间通常会减慢执行速度。因此,让代码变大是为了让代码更快而做出的牺牲。 - Thomas Matthews
1
虽然您的解释很有道理,但这是我第一次听说使用循环展开来改善预取。现在大多数处理器似乎都有32 kB指令缓存,对我来说看起来足够大以处理大多数循环,即使循环不适合缓存,处理器也会预测和预取循环分支,不是吗? - Saiph
@ThomasMatthews,你提到了“一个处理消息数据包的程序...函数将被优化以进行数据包处理”,能否详细说明一下“函数将被优化以进行数据包处理”是什么意思?你是指通过在数组中使用函数指针调用函数比使用switch块分派到这些函数更快(或更适合指令缓存)吗?谢谢! - HCSF
@HCSF:由于没有普遍标准化的数据包,我无法详细介绍数据包处理函数。所有细节都取决于数据或消息规范。例如,ATAPI消息与JSON数据包或TCP数据包具有不同的布局。在嵌入式系统世界中,设备到主机或主机到数据库可能会有自定义数据包。 - Thomas Matthews
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接