【问题标题】:How to write instruction cache friendly program in c++?如何在 C++ 中编写指令缓存友好的程序?
【发布时间】:2014-05-20 05:23:30
【问题描述】:

最近 Herb Sutter 在"Modern C++: What You Need to Know" 上做了一场精彩的演讲。本次演讲的主题是效率以及数据局部性和访问内存的重要性。 他还解释了 CPU 将如何喜欢内存(数组/向量)的线性访问。他从另一个经典参考文献"Game performance by Bob Nystrom" 中就该主题举了一个例子。

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

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

Cachegrind 工具还测量我们程序的缓存类型检测信息。许多文章/博客已经解释了第一点以及如何实现良好的数据缓存效率(数据局部性)。

但是我没有得到太多关于主题指令缓存的信息以及我们应该在我们的程序中注意什么样的事情来获得更好的性能?。根据我的理解,我们(程序员)对执行哪条指令或执行什么顺序没有太多控制。

如果小型 c++ 程序能够解释这个计数器(即指令缓存)如何随着我们编写程序的风格而变化,那就太好了。在这一点上,程序员应该遵循哪些最佳实践来获得更好的性能?

我的意思是我们可以理解数据缓存主题,如果我们的程序以类似的方式(向量与列表)可以解释第二点。这个问题的主要目的是尽可能地理解这个话题。

【问题讨论】:

  • 避免使用虚函数,将复杂代码分解成更小的循环。
  • @Leeor:那是什么重复?
  • 对不起,我提到了错误的问题,一旦我在电脑旁边,我就会撤回投票,但我很肯定它之前已经通过代码缓存上下文得到了回答,包括跟踪缓存,解码的 uop 缓存等。

标签: c++ caching c++11 c++14 cachegrind


【解决方案1】:

任何改变执行流程的代码都会影响指令缓存。这包括函数调用和循环以及取消引用函数指针。

当执行分支或跳转指令时,处理器必须花费额外的时间来决定代码是否已经在指令缓存中,或者是否需要重新加载指令缓存(从分支的目的地)。

例如,某些处理器可能有足够大的指令缓存来保存小循环的执行代码。一些处理器没有大的指令缓存并简单地重新加载它。指令高速缓存的重新加载需要花费执行指令的时间。

搜索这些主题:

  • 循环展开
  • 条件指令执行(适用于 ARM 处理器)
  • 内联函数
  • 指令管道

编辑 1:提高性能的编程技术
要提高性能并减少指令缓存重新加载,请执行以下操作:

减少“if”语句 设计您的代码以最小化“if”语句。这可能包括布尔代数,使用更多的数学或简化比较(它们真的需要吗?)。最好减少“then”和“else”子句的内容,以便编译器可以使用条件汇编语言指令。

将小函数定义为内联或宏
调用函数会产生开销,例如存储返回位置和重新加载指令缓存。对于具有少量语句的函数,请尝试向编译器建议将它们内联。内联意味着将代码的内容粘贴到执行所在的位置,而不是进行函数调用。由于避免了函数调用,因此需要重新加载指令缓存。

展开循环
对于小迭代,不要循环,而是重复循环的内容(一些编译器可能会在更高的优化级别设置下这样做)。重复的内容越多,循环顶部的分支数量就越少,重新加载指令缓存的需要就越少。

使用表查找,而不是“if”语句
一些程序使用“if-else-if”梯形图将数据映射到值。每个“if”语句都是指令缓存中执行的中断。有时,通过一些数学运算,可以将值放在一个像数组一样的表格中,并以数学方式计算索引。一旦知道索引,处理器就可以在不中断指令缓存的情况下检索数据。

更改数据或数据结构
如果数据的类型是恒定的,则可以围绕数据优化程序。例如,处理消息数据包的程序可以基于数据包 ID(想想函数指针数组)进行操作。功能将针对数据包处理进行优化。

将链表更改为数组或其他随机访问容器。可以使用数学而不是中断执行来访问数组的元素。必须遍历(循环)链表才能找到项目。

【讨论】:

  • "Unroll loops" -> 我相信这是完全错误的。完整的循环很可能会保存在缓存中,并且分支到顶部不会导致任何问题。保持循环是代码存储在缓存中而不仅仅是预取的主要原因之一。实际上可能正好相反,建议不要展开循环以避免生成大量重复指令,这会使您的代码更大并且不太可能适合缓存。
  • 循环是否适合指令缓存取决于处理器。那里有许多不同的处理器。此外,编译器可以识别循环展开,并且编译器可以发出用于并行处理的指令。循环展开的主要目的是避免分支指令,在最坏的情况下,会导致指令缓存的重新加载。因此,即使循环不适合指令缓存,您也可以通过展开来消除分支到循环顶部的成本。
  • 此外,还有时间与空间的权衡。您会发现快速算法往往比空间优化占用更多空间。节省空间通常会减慢执行速度。所以,让代码变大是为了让代码变快而做出的牺牲。
  • 虽然您的解释很有意义,但这是我第一次听说使用循环展开来改进预取。现在大多数处理器似乎都有 32 kB 的指令缓存,这对我来说看起来足够大,可以处理大多数循环,即使循环不适合缓存,处理器也不会预测和预取循环分支吗?
  • 我不是在明确谈论预取,而是实际并行执行指令。一些处理器具有并行处理指令的能力。在高级优化下,编译器可以向这些处理器发出指令以并行执行指令。
猜你喜欢
  • 2014-01-17
  • 1970-01-01
  • 1970-01-01
  • 2013-04-20
  • 2023-03-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-24
相关资源
最近更新 更多