更短的循环,相同的覆盖范围,为什么我在 Visual Studio 2013 的 c++ 中获得更多的“最后一级缓存未命

Shorter loop, same coverage, why do I get more Last Level Cache Misses in c++ with Visual Studio 2013?

本文关键字:c++ 最后 一级缓存 2013 Visual 循环 覆盖 范围 为什么 Studio      更新时间:2023-10-16

我试图了解是什么导致了缓存未命中,以及最终它们在我们的应用程序中的性能方面花费了多少。但是对于我现在正在做的测试,我很困惑。

假设我的 L3 缓存是 4MB,我的 LineSize 是 64 字节,我希望这个循环(循环 1):

int8_t aArr[SIZE_L3];
int i;
for ( i = 0; i < (SIZE_L3); ++i )
{
  ++aArr[i];
}

。这个循环(循环 2):

int8_t aArr[SIZE_L3];
int i;
for ( i = 0; i < (SIZE_L3 / 64u); ++i )
{
  ++aArr[i * 64];
}
提供

大致相同数量的"最后一级缓存未命中",但提供不同数量的"非独占上一级缓存引用"。

然而,Visual Studio 2013的探查器给我的数字令人不安。

对于循环 1:

  • 包含最后一级缓存引用:53,000
  • 最后一级缓存未命中数:17,000

使用循环 2:

  • 包含最后一级缓存引用:69,000
  • 最后一级缓存未命中数:35,000

我已经使用动态分配的阵列在具有较大 L3 缓存 (8MB) 的 CPU 上对此进行了测试,并且在结果中得到了类似的模式。

为什么

我没有获得相同数量的缓存未命中,为什么我在更短的循环中有更多的引用

单独递增int8_t aArr[SIZE_L3];的每个字节已经足够慢了,硬件预取器可能能够在很多时候很好地跟上。 乱序执行可以使大量读取-修改-写入同时传输到不同的地址,但最好的情况仍然是每个存储时钟一个字节。 (存储端口 uops 上的瓶颈,假设这是对内存带宽没有许多其他需求的系统上的单线程测试)。

英特尔 CPU 的主要预取逻辑在二级高速缓存中(如英特尔优化指南中所述;请参阅 x86 标签 wiki)。 因此,在内核发出负载之前,成功的硬件预取到二级缓存意味着三级缓存永远不会错过。

John McCalpin 在此英特尔论坛帖子中的回答证实,L2 硬件预取不被正常的 perf 事件(如 MEM_LOAD_UOPS_RETIRED.LLC_MISS)计为 LLC 引用或未命中。 显然,您可以查看OFFCORE_RESPONSE事件。

IvyBridge引入了下一页硬件预取。 在此之前的英特尔微架构在预取时不会跨越页面边界,因此您仍然每 4k 就会错过一次。 如果操作系统没有机会性地将您的内存放在 2MiB 的巨大页面中,也许 TLB 会错过。 (但是,当您接近页面边界时,推测性页面漫游可能会避免太多延迟,并且硬件肯定会执行推测页面漫游)。


以 64 字节的步幅,执行可以比缓存/内存层次结构跟上的速度快得多。 您在 L3/主内存上遇到瓶颈。 乱序执行可以同时保持大约相同数量的读取/修改/写入操作,但相同的无序窗口覆盖的内存增加了 64 倍。


更详细地解释确切的数字

对于L3左右的阵列大小,IvyBridge的自适应替换策略可能会产生重大影响。

在我们知道确切的uarch以及测试的更多细节之前,我不能说。 目前尚不清楚您是否只运行了一次该循环,或者您是否有一个外部重复循环,并且这些未命中/参考编号是每次迭代的平均值。

如果它仅来自单次运行,那就是一个微小的嘈杂样本。 我认为它在某种程度上是可重复的,但我很惊讶每个字节版本的 L3 引用计数如此之高。 4 * 1024^2 / 64 = 65536 ,因此您接触的大多数缓存行仍然有一个 L3 引用。

当然,如果你没有重复循环,并且这些计数包括代码除了循环之外所做的一切,也许这些计数中的大部分来自程序中的启动/清理开销。 (即,注释掉循环的程序可能有 48k L3 引用,IDK。


我已经用动态分配的数组对此进行了测试

完全不足为奇,因为它仍然是连续的。

在具有较大 L3 缓存 (8MB) 的 CPU 上,我在结果中得到了类似的模式。

此测试是否使用了更大的阵列? 或者您是否在具有 8MiB L3 的 CPU 上使用 4MiB 阵列?

假设"如果我跳过数组中的更多元素,从而减少循环迭代次数和数组访问次数,那么我应该有更少的缓存未命中"似乎忽略了数据提取到缓存中的方式。

当您访问内存时,缓存中保留的数据不仅仅是您访问的特定数据。如果我访问 intArray[0],那么 intArray[1] 和 intArray[2] 也可能同时被获取。这是允许缓存帮助我们更快地工作的优化之一。因此,如果我连续访问这三个内存位置,这有点像您只需要等待 1 个内存读取。

如果增加步幅,而不是访问 intArray[0],然后访问 intArray[100] 和 intArray[200],则数据可能需要 3 次单独的读取,因为第二次和第三次内存访问可能不在缓存中,从而导致缓存未命中。

特定问题的所有确切细节都取决于您的计算机体系结构。我假设您运行的是基于英特尔x86的架构,但是当我们谈论如此低级别的硬件时,我不应该假设(我认为您可以让Visual Studio在其他架构上运行,不是吗?无论如何,我不记得该架构的所有细节。

因为您通常不知道运行软件的硬件上的缓存系统究竟是什么样子,并且它会随着时间的推移而变化,所以通常最好只是阅读一般的缓存原则,并尝试编写可能产生较少未命中的常规代码。试图在你正在开发的特定机器上使代码完美通常是浪费时间。例外情况是某些嵌入式控制系统和其他类型的低级系统,它们不太可能对您进行更改;除非这描述了你的工作,否则我建议你读一些关于计算机缓存的好文章或书籍。