更短的循环,相同的覆盖率,为什么我在使用Visual Studio 2013的c ++中获得更多的Last Level Cache Misses?

时间:2016-07-13 01:51:11

标签: c++ performance visual-studio-2013 x86 profiling

我试图了解是什么造成了缓存未命中,并最终在应用程序中的性能方面花了多少钱。但是通过我现在正在进行的测试,我很困惑。

假设我的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上进行了测试,并且在结果中得到了类似的模式。

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

2 个答案:

答案 0 :(得分:2)

分别增加int8_t aArr[SIZE_L3];的每个字节的速度足够慢,硬件预取程序可能会在很多时候保持良好状态。乱序执行可以将大量读取 - 修改 - 写入一次保存到不同的地址,但最好的情况仍然是每个时钟的存储一个字节。 (存储端口uop上的瓶颈,假设这是一个系统上的单线程测试,没有很多其他内存带宽需求)。

Intel CPU have their main prefetch logic in L2 cache(如英特尔优化指南中所述;请参阅标记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。)

  

我用动态分配的数组

测试了这个

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

  

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

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

答案 1 :(得分:1)

你的假设是“如果我跳过数组中的更多元素,减少循环次数和减少数组访问,我应该减少缓存未命中数”似乎忽略了数据被提取到缓存中的方式

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

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

具体问题的所有具体细节取决于您的计算机体系结构。我假设您正在运行基于intel x86的架构,但是当我们讨论这个低级别的硬件时,我不应该假设(我认为您可以让Visual Studio在其他架构上运行,不是吗?) ;而且我不记得该架构的所有细节。

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