刷新缓存以防止基准测试波动

问题描述 投票:4回答:2

我正在运行某人的c ++代码来对数据集进行基准测试。我遇到的问题是,我经常得到第一次运行的时间,如果我再次运行相同的代码,这些数字会大幅改变(即28秒到10秒)。我认为这是由于CPU的自动缓存。有没有办法刷新缓存,或以某种方式防止这些波动?

c++ caching benchmarking timing
2个回答
5
投票

没有一个“适用于所有事物,无处不在”。大多数处理器都有特殊指令来刷新缓存,但它们通常是特权指令,因此必须从OS内核内部完成,而不是用户模式代码。当然,对于每个处理器架构,它都是完全不同的指令。

所有当前的x86处理器都有一个clflush指令,它会刷新一个缓存行,但要做到这一点,你必须拥有要刷新的数据(或代码)的地址。这对于小而简单的数据结构来说很好,如果你有一个遍布整个地方的二叉树,那就太好了。当然,根本不是便携式的。

在大多数环境中,读取和写入大块替代数据,例如,就像是:

// Global variables.
const size_t bigger_than_cachesize = 10 * 1024 * 1024;
long *p = new long[bigger_than_cachesize];
...
// When you want to "flush" cache. 
for(int i = 0; i < bigger_than_cachesize; i++)
{
   p[i] = rand();
}

使用rand比填充常量/已知的东西慢得多。但编译器无法优化调用,这意味着它(几乎)保证代码将保留。

上面不会刷新指令缓存 - 这样做要困难得多,基本上,你必须运行一些(足够大的)其他代码才能可靠地执行。然而,指令缓存往往对整体基准性能的影响较小(指令缓存对于现代处理器的性能非常重要,这不是我所说的,但从某种意义上说,基准测试的代码通常足够小以至于它都适合在缓存中,基准测试在相同的代码上运行多次,所以它在第一次迭代时只会慢一些)

其他想法

模拟“非缓存”行为的另一种方法是为每个基准测试传递分配一个新区域 - 换句话说,在基准测试结束之前不释放内存或使用包含数据的数组,并输出结果,这样每次运行拥有自己的一组数据。

此外,实际测量基准测试的“热运行”的性能是常见的,而不是缓存为空的第一个“冷运行”。这当然取决于你实际想要实现的目标......


4
投票

这是我的基本方法:

  1. 如果您可以动态确定LLC大小(或者您是静态地确定),或者如果您没有,则分配LLC大小的2倍的内存区域,在感兴趣的平台1上的最大LLC大小的某个合理倍数。
  2. memset将内存区域设置为一些非零值:1会做得很好。
  3. 将指针“接收”到某处,以便编译器无法优化上面或下面的内容(写入volatile全局的工作几乎100%的时间)。
  4. 从区域中的随机索引读取,直到您平均触摸每个缓存行10次左右(将读取的值累积到以(3)类似的方式下沉的总和)。

下面是一些关于为什么它通常有效以及为什么少做可能不起作用的注意事项 - 细节以x86为中心,但类似的问题将适用于许多其他架构。

  • 在开始主只读刷新循环之前,您绝对想要写入已分配的内存(步骤2),否则您可能只是重复读取操作系统返回的相同的小型zero-mapped page以满足您的内存分配。
  • 您希望使用比LLC大小大得多的区域,因为外部缓存级别通常是物理地址,但您只能分配和访问虚拟地址。如果你只是分配一个LLC大小的区域,你通常不会完全覆盖每个缓存集的所有方式:一些集合将被过度表示(因此将被完全刷新),而其他集合的代表性不足因此,通过访问此内存区域,甚至不能刷新所有现有值。 2倍的过度分配使得几乎所有集合都具有足够的表示。
  • 你想避免优化器做一些聪明的事情,比如注意内存永远不会逃避函数并消除所有的读写操作。
  • 您希望在内存区域内随机迭代,而不是仅仅线性地进行迭代:一些设计(如最近的英特尔上的LLC)检测何时出现“流”模式,并从LRU切换到MRU,因为LRU是最差的这种负荷的替代政策。结果是无论你通过内存流多少次,你努力之前的一些“旧”行可以保留在缓存中。随机访问内存会使这种行为失败。
  • 您希望访问的不仅仅是LLC的内存量,这是因为(a)分配超过LLC大小(虚拟访问与物理缓存)的原因相同(b)因为随机访问需要更多访问才能获得更高的命中率每次设置足够的时间(c)缓存通常只是伪LRU,因此您需要的数量超过了您在精确LRU下期望的访问次数以清除每一行。

即使这不是万无一失的。上面未考虑的其他硬件优化或缓存行为可能导致此方法失败。您可能会对操作系统提供的页面分配非常不满,并且无法访问所有页面(您可以通过使用2MB页面来大大减轻这种情况)。我强烈建议您测试您的刷新技术是否足够:一种方法是在运行基准测试时使用CPU性能计数器测量缓存未命中数,并根据已知的工作集大小2查看数字是否有意义。

请注意,这会使E(独占)或S(共享)状态中的行保留所有级别的高速缓存,而不是M(已修改)状态。这意味着当这些行被基准测试中的访问替换时,不需要将这些行驱逐到其他缓存级别:它们可以简单地删除。 other answer中描述的方法将使大多数/所有行都处于M状态,因此您在基准测试中访问的每一行最初都会有一行驱逐流量。通过将步骤4更改为写入而不是读取,您可以使用上面的配方实现相同的行为。

在这方面,这里的方法本身都不比其他方法“更好”:在现实世界中,缓存级别将混合使用修改的和未修改的行,而这些方法将缓存留在连续体的两个极端。原则上,您可以使用全M和非M状态进行基准测试,并查看它是否重要:如果确实如此,您可以尝试评估缓存的实际状态通常是什么样的复制。


1请记住,LLC规模几乎每一代CPU都在增长(主要是因为核心数量在增加),所以如果需要面向未来,你需要留出一些增长空间。

2我只是把它扔出去就好像它很“容易”,但实际上可能很难取决于你的确切问题。

© www.soinside.com 2019 - 2024. All rights reserved.