硬件是否将多个代码操作合并为一个物理CPU操作?

时间:2014-10-09 06:59:39

标签: c++ c optimization cpu-cache consolidation

我读过一篇2006年的文章,关于CPU如何在整个l1缓存行上执行操作,即使在你只需要执行l1行所包含的一小部分内容的情况下(例如,加载整个l1行)写一个布尔变量显然是矫枉过正的)。本文通过以l1缓存友好的方式管理内存来鼓励优化。

让我们说我有两个int个变量恰好在内存中是连续的,在我的代码中我会连续写入两个变量。

硬件是否将我的两个代码操作整合到单个l1行上的一个物理操作中(授予CPU具有足以容纳两个变量的l1高速缓存行),或者不是?

有没有办法在C ++或C中向CPU提出这样的建议?

如果硬件没有以任何方式进行整合,那么您认为如果在代码中实现这样的事情,它可以产生更好的性能吗?分配一个大小与l1行相同的内存块并用尽可能多的热数据变量填充它?

4 个答案:

答案 0 :(得分:5)

缓存行的大小主要与并发性相关。它是可在多个处理器之间同步的最小数据块。

正如您所建议的那样,必须加载整个缓存行以仅在其几个字节上执行操作。如果你在同一个处理器上进行多重操作,虽然它不需要不断重新加载它。顾名思义,它实际上是缓存的。这包括缓存对数据的写入。只要只有一个处理器访问数据,您通常可以放心,它正在有效地执行此操作。

在多个处理器访问数据的情况下,对齐数据可能会有所帮助。使用C ++ alignas属性或编译器扩展可以帮助您获得以您希望的方式对齐的数据结构。

您可能对我的文章CPU Reordering – What is actually being reordered?感兴趣,该文章提供了一些关于低级别(至少在逻辑上)发生的事情的见解。

答案 1 :(得分:2)

这是一个相当广泛的问题,但我将尝试涵盖要点。

是的,将数据读入缓存仅查看单个bool有点浪费 - 但是,处理器通常不知道您计划在此之后做什么,例如,如果您需要下一个是否连续值。您可以依赖于位于相同类或结构中的数据位于彼此的下一个/接近位置,因此使用它来存储您经常一起操作的数据将为您带来好处。

对于“同时处理多个数据”的操作,大多数现代处理器具有各种形式的扩展,可以对多个数据项(SIMD - 相同指令,多个数据)执行相同的操作。这始于20世纪90年代后期的MMX,并已扩展到包括3DNow!,SSE和AVX for x86。在ARM中有“Neon”扩展,它也提供类似的功能。 PowerPC也有类似的东西,其名称目前让我无法逃脱。

C或C ++程序无法立即控制指令选择或缓存使用。但是现代编译器,如果有正确的选项,将产生代码,例如使用SIMD指令通过一次添加4个项来汇总更大数组中的所有int,然后,当完成整个批次时,水平添加4个值。或者如果你有一组X,Y,Z坐标,它可能会使用SIMD将两组这样的数据加在一起。执行此操作是编译器的选择,但它可以节省相当多的时间,因此编译器中的优化器正在被修改以查找有用的情况,并使用这些类型的指令。

最后,大多数较大的现代处理器(自1995年以来的x86,ARM A15,PowerPC)也执行超标量执行 - 一次执行多条指令,并且“乱序执行”(处理器理解的依赖性)指令并执行“准备好”执行的指令,而不是完全按照它们给予处理器的顺序执行。编译器会知道这一点并尝试“帮助”安排代码,以便处理器轻松完成任务。

答案 2 :(得分:2)

缓存的重点是允许很多高度本地化的内存操作快速发生。

当然,最快的操作涉及寄存器。使用它们的唯一延迟是在指令获取,解码和执行中。在一些寄存器丰富的体系结构(以及向量处理器)中,它们实际上像专用高速缓存一样使用。除了速度最慢的处理器外,其他所有处理器都有一个或多个级别的缓存,除了速度更快之外,它们看起来像普通指令的内存。

为简化相对于实际处理器的问题,请考虑一个运行频率为2 GHz(每时钟0.5 ns)的假设处理器,其内存需要5 ns才能加载任意64位(8字节)字的内存,但只需1 ns从内存加载每个连续的64位字。 (也假设写入类似。)在这样的机器上,在内存中翻转一点是非常慢的:1 ns加载指令(仅当它不在管道中时 - 但在远处分支后5 ns ),5 ns加载包含该位的字,0.5 ns执行指令,5 ns将更改后的字写回存储器。内存副本更好:加载指令大约为零(因为管道可能是正确的指令循环),5 ns加载前8个字节,0.5 ns执行指令,5 ns来存储前8个字节,每增加8个字节,增加1 + 0.5 + 1 ns。地方性使事情变得更容易。但是一些操作可能是病态的:递增数组的每个字节执行初始5 ns加载,0.5 ns指令,初始5 ns存储,然后是每字节1 + 0.5 + 1(而不是每个字)。 (不会出现在相同单词边界的内存副本也是坏消息。)

为了使这个处理器更快,我们可以添加一个缓存,在指令执行时间内,对于缓存中的数据,可以将加载和存储改善到0.5 ns。存储器副本在读取时不会改善,因为前8字节工作仍然需要5 ns,附加字仍然需要1 ns,但写入速度要快得多:每个字0.5 ns,直到缓存填满,并且填充后的正常5 + 1 + 1等速率,与其他使用内存较少的工作并行。对于初始加载,字节增量提高到5 ns,对于指令和写操作增加0.5 + 0.5 ns,然后每增加一个字节增加0.5 + 0.5 + 0.5 ns,除非在读取或写入时缓存停顿期间。重复相同的几个地址会增加缓存命中的比例。

真实处理器,多级缓存等会发生什么?简单的答案是事情变得更复杂。编写缓存感知代码包括尝试改进内存访问的局部性,分析以避免破坏缓存,以及大量的分析。

答案 3 :(得分:1)

是的,可以在某些CPU的存储缓冲区中合并对高速缓存行的相邻int32_t的背对背写入,因此它们可以作为单个8字节对齐更新提交给L1d。 (在许多非x86 CPU上,完整的32位存储在更新L1d高速缓存时避免了RMW周期,因此合并字节存储是不错的方法:Are there any modern CPUs where a cached byte store is actually slower than a word store?。在Alpha 21264上,甚至可以将32位存储合并为64位提交很重要)。

但是只有在分别执行多个存储指令之后,才会在存储缓冲区中合并。没有CPU将连续的负载或存储融合到执行单元的单个硬件操作中。


某些编译器(例如GCC8和更高版本的IIRC)可以将对相邻结构成员或局部变量的加载/存储合并为单个asm指令,例如使用一个32位存储一次存储4个char。 (或在64位计算机上2个int)。在某些x86之类的ISA上,即使不知道对齐方式,它也会这样做。

创建一个访问多个C对象的asm操作。在具有有效的未对齐加载/存储的ISA(例如x86)上,这通常是一个胜利。 (高速缓存行拆分并不常见,也不是太昂贵。在Skylake之前,跨越4k边界的拆分在Intel上要昂贵得多,例如约100个周期。)

在结构成员上使用alignas(8) int foo;可以使整个结构更加对齐,这可以在没有有效的未对齐加载/存储的情况下在ISA上实现此编译时优化。

我认为ARM ldp / stp(加载/存储对)在未完全对齐的情况下还不错,但是在对齐的情况下,它可以作为单个64位或128位操作来加载或存储一对寄存器。