更短的循环,相同的覆盖范围,为什么我在 Visual Studio 2013 的 c++ 中获得更多的“最后一级缓存未命
Shorter loop, same coverage, why do I get more Last Level Cache Misses in c++ with Visual Studio 2013?
我试图了解是什么导致了缓存未命中,以及最终它们在我们的应用程序中的性能方面花费了多少。但是对于我现在正在做的测试,我很困惑。
假设我的 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在其他架构上运行,不是吗?无论如何,我不记得该架构的所有细节。
因为您通常不知道运行软件的硬件上的缓存系统究竟是什么样子,并且它会随着时间的推移而变化,所以通常最好只是阅读一般的缓存原则,并尝试编写可能产生较少未命中的常规代码。试图在你正在开发的特定机器上使代码完美通常是浪费时间。例外情况是某些嵌入式控制系统和其他类型的低级系统,它们不太可能对您进行更改;除非这描述了你的工作,否则我建议你读一些关于计算机缓存的好文章或书籍。
- 读取文件的最后一行并输入到链接列表时出错
- lower_bound()返回最后一个元素
- 从控制台中删除最后打印的元素
- 获取向量C++中第一个值和最后一个值的和
- 获取用C/C++打印的最后一个字符串
- 读取最后一行代码算法 - c++ 时出现问题
- 使用运算符 [] 引用 std::vector 上最后一个元素时出现问题<>
- 为什么这个程序返回最后一个单词而不是最长的单词?
- 为什么我的最后一个 ELSE 条件无法正确执行
- 如何在最后一步使用CryptDecrypt解决NTE_BAD_DATA
- Lower_bound不适用于具有 3 个元素的向量的最后一个元素
- 查找数组中第一个最小值和最后一个最大值元素之间的算术平均值
- 仅显示链表的最后一个元素
- 这段代码的最后一行在做什么?
- 删除列表 c++ 中的最后 3 个元素
- 如何检测是否在缓冲绘画动画中绘制最后一帧?
- 测试迭代器是否位于列表中的最后一个
- push_back通过自行创建的对象获取最后一个元素的向量
- 斐波那契数列部分和的最后一位数字
- 使用 map.end() 访问 map 的最后一个元素