我读过一篇2006年的文章,关于CPU如何在整个l1缓存行上执行操作,即使在你只需要执行l1行所包含的一小部分内容的情况下(例如,加载整个l1行)写一个布尔变量显然是矫枉过正的)。本文通过以l1缓存友好的方式管理内存来鼓励优化。
让我们说我有两个int
个变量恰好在内存中是连续的,在我的代码中我会连续写入两个变量。
硬件是否将我的两个代码操作整合到单个l1行上的一个物理操作中(授予CPU具有足以容纳两个变量的l1高速缓存行),或者不是?
有没有办法在C ++或C中向CPU提出这样的建议?
如果硬件没有以任何方式进行整合,那么您认为如果在代码中实现这样的事情,它可以产生更好的性能吗?分配一个大小与l1行相同的内存块并用尽可能多的热数据变量填充它?
答案 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位操作来加载或存储一对寄存器。