更短的循环,相同的覆盖率,为啥我会在 Visual Studio 2013 的 c++ 中获得更多的 Last Level Cache Misses?
Posted
技术标签:
【中文标题】更短的循环,相同的覆盖率,为啥我会在 Visual Studio 2013 的 c++ 中获得更多的 Last Level Cache Misses?【英文标题】:Shorter loop, same coverage, why do I get more Last Level Cache Misses in c++ with Visual Studio 2013?更短的循环,相同的覆盖率,为什么我会在 Visual Studio 2013 的 c++ 中获得更多的 Last Level Cache Misses? 【发布时间】:2016-07-13 01:51:11 【问题描述】:我试图了解造成缓存未命中的原因以及最终它们在我们的应用程序性能方面的成本。但是对于我现在正在做的测试,我很困惑。
假设我的 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];
给出大致相同数量的 Last Level Cache Misses,但不同数量的 Inclusive Last Level Cache References。
然而,Visual Studio 2013 的分析器给我的数字令人不安。
使用循环 1:
包含最后一级缓存引用:53,000 最后一级缓存未命中数:17,000使用循环 2:
包含最后一级缓存引用:69,000 最后一级缓存未命中数:35,000我已经使用动态分配的数组在具有更大 L3 缓存 (8MB) 的 CPU 上对此进行了测试,我在结果中得到了类似的模式。
为什么我没有得到相同数量的缓存未命中,为什么我在较短的循环中有更多个引用?
【问题讨论】:
您要测试的具体硬件是什么?我认为这是最近的某种英特尔,因为您提到了 L3 缓存和视觉工作室。但是一些 AMD Piledriver 芯片具有 L3 缓存,并且可能具有不同的预取算法。甚至英特尔 uarch 的版本也很重要,因为您正在使用 L3 大小进行测试,所以在这里可能很重要。英特尔 IvyBridge(引入了an adaptive replacement policy for L3 cache)以减轻代码中热工作集的污染,该代码还循环在一个重用率低的巨型数据集上。 【参考方案1】:分别增加int8_t aArr[SIZE_L3];
的每个字节的速度足够慢,以至于硬件预取器 可能在很多时候都能很好地跟上。乱序执行可以使大量的读-修改-写同时运行到不同的地址,但最好的情况仍然是每个存储时钟一个字节。 (存储端口 uops 的瓶颈,假设这是在系统上进行的单线程测试,对内存带宽没有很多其他要求)。
Intel CPU have their main prefetch logic in L2 cache(如 Intel 的优化指南中所述;请参阅 x86 标签 wiki)。因此,在内核发出负载之前成功地将硬件预取到 L2 缓存意味着 L3 缓存永远不会出现未命中。
John McCalpin's answer on this Intel forum thread 确认 L2 硬件预取不会被MEM_LOAD_UOPS_RETIRED.LLC_MISS
等正常性能事件计为 LLC 引用或未命中。显然您可以查看OFFCORE_RESPONSE
事件。
IvyBridge introduced next-page HW prefetch。之前的英特尔微架构在预取时不会跨越页面边界,因此每 4k 仍然会丢失一次。如果操作系统没有机会性地将您的内存放入 2MiB 的巨页中,那么 TLB 可能会丢失。 (但是,当您接近页面边界时,推测性的页面遍历可能会避免很多延迟,并且硬件肯定会进行推测性的页面遍历)。
步长为 64 字节,执行访问内存的速度比缓存/内存层次结构所能跟上的快得多。您在 L3 / 主内存上遇到瓶颈。乱序执行可以同时保持大约相同数量的读取/修改/写入操作,但相同的乱序窗口会占用 64 倍以上的内存。
更详细地解释确切的数字
对于 L3 左右的数组大小,IvyBridge's adaptive replacement policy 可能会产生显着差异。
在我们知道确切的 uarch 以及测试的更多细节之前,我不能说。目前尚不清楚您是否只运行了一次该循环,或者您是否有一个外部重复循环并且这些未命中/参考数字是每次迭代的平均值。
如果它仅来自一次运行,那是一个很小的嘈杂样本。我认为它有点可重复,但我很惊讶 L3 引用计数对于每字节版本来说如此之高。 4 * 1024^2 / 64 = 65536
,所以你接触的大部分缓存行仍然有一个 L3 引用。
当然,如果您没有重复循环,并且这些计数包括代码除了循环之外所做的所有事情,那么这些计数中的大部分可能来自您程序中的启动/清理开销。 (即您的循环注释掉的程序可能有 48k L3 引用,IDK。)
我已经用动态分配的数组对此进行了测试
完全不足为奇,因为它仍然是连续的。
在具有更大三级缓存 (8MB) 的 CPU 上,我在结果中得到了类似的模式。
此测试是否使用了更大的数组?或者您是否在具有 8MiB L3 的 CPU 上使用了 4MiB 阵列?
【讨论】:
【参考方案2】:您的假设“如果我跳过数组中的更多元素,从而减少循环迭代和数组访问次数,我应该有更少的缓存未命中”似乎忽略了将数据提取到缓存中的方式.
当您访问内存时,缓存中保存的数据不仅仅是您访问的特定数据。如果我访问 intArray[0],那么 intArray[1] 和 intArray[2] 很可能会同时被获取。这是允许缓存帮助我们更快工作的优化之一。因此,如果我连续访问这三个内存位置,就好像只需要等待 1 个内存读取。
如果增加步幅,而不是访问 intArray[0],然后是 intArray[100] 和 intArray[200],则数据可能需要 3 次单独读取,因为第二次和第三次内存访问可能不在缓存中,从而导致缓存未命中。
您的具体问题的所有确切细节取决于您的计算机架构。我会假设您正在运行基于英特尔 x86 的架构,但是当我们谈论硬件时,我不应该假设(我认为您可以让 Visual Studio 在其他架构上运行,不是吗?) ;而且我不记得该架构的所有细节。
因为您通常不知道缓存系统在运行您的软件的硬件上到底是什么样的,而且它会随着时间的推移而改变,所以通常最好只是阅读缓存原则并尝试编写一般可能产生更少未命中的代码。试图在您正在开发的特定机器上使代码完美无缺通常是浪费时间。例外情况是某些嵌入式控制系统和其他类型的低级系统,它们不太可能在您身上改变;除非这描述了你的工作,否则我建议你阅读一些关于计算机缓存的好文章或书籍。
【讨论】:
正如问题中所写,我的假设是我将有相同数量的缓存未命中,但缓存引用更少。我已经阅读了很多关于缓存未命中的内容,他们似乎都同意,当您访问内存中的某个位置时,它将获取并缓存整行(在我的情况下为 64 个字节)。这就是为什么我假设在较短的循环中我将有更少的缓存访问(因为显然我访问数组的次数更少),但如果我跳过缓存行的大小,则未命中的数量相同。 哦。你可能想改变你的问题标题。在我阅读您的问题的整个过程中,我一直从标题中所述的缓存 misses 的角度来考虑它,并且您在结束语中也确实提出了这一问题。此外,由于您的缓存线与循环中的步幅大小相同,我也认为您的结果很奇怪。我可能无法在这里提供更多帮助,所以希望其他人有更多的见解。 第一个版本总体上可能有更多的缓存引用,但您正在查看的分析器指标是 LLC 引用。即,在较高缓存级别中丢失的引用。在您的基准测试中,我希望字节 2 到 64 都在 L1 中命中,因此它们不会出现在 LLC 统计数据中。以上是关于更短的循环,相同的覆盖率,为啥我会在 Visual Studio 2013 的 c++ 中获得更多的 Last Level Cache Misses?的主要内容,如果未能解决你的问题,请参考以下文章
Oracle UTL_HTTP 请求返回比在 Python 或 curl 中完成相同请求时更短的响应
如何在更短的时间内向初始 DataFrame 添加大量列(5000~1000 列)?