上次使用的缓存行与不同的缓存行

时间:2013-12-11 09:48:44

标签: c performance optimization x86 cpu-cache

假设缓存行是64字节宽,我有两个数组ab填充缓存行,并且还与缓存行对齐。我们还假设两个数组都在L1缓存中,所以当我从它们读取时,我没有得到缓存未命中。

float a[16];  //64 byte aligned e.g. with __attribute__((aligned (64)))
float b[16];  //64 byte aligned

我读过a[0]。我的问题是,现在阅读a[1]比阅读b[0]更快吗? 换句话说,从上次使用的缓存行读取是否更快?

这套是否重要?我们现在假设我有一个32 kb的L1数据缓存,这是4路。因此,如果ab相隔8192个字节,则它们会在同一个集合中结束。这会改变我的问题的答案吗?

另一种问我问题的方法(这是我真正关心的)是关于阅读矩阵。

换句话说,假设矩阵M适合L1缓存并且64字节对齐且已经在L1缓存中,这两个代码选项中的哪一个将更有效。

float M[16][16]; //64 byte aligned

版本1:

for(int i=0; i<16; i++) {
    for(int j=0; j<16; j++) {
        x += M[i][j];
    }
}

第2版:

for(int i=0; i<16; i++) {
    for(int j=0; j<16; j++) {
        x += M[j][i];
    }
}

编辑:为了使SSE / AVX更清晰,我们假设我使用AVX一次性读取a的前八个值(例如使用_mm256_load_ps())。从a读取接下来的八个值会比从b读取前八个值更快(回想一下a和b已经在缓存中,因此不会出现错误)?

编辑:我对Intel Core 2和Nehalem以来的所有处理器感兴趣,但我目前正在使用Ivy Bridge处理器并计划很快使用Haswell。

4 个答案:

答案 0 :(得分:3)

使用当前的英特尔处理器,加载两个不同的缓存行(两者都在L1缓存中)之间没有性能差异,其他条件相同。如果float a[16], b[16];最近加载了a[0]a[1]a[0]在同一个缓存行中,并且b[1]最近未加载但仍在L1缓存中,那么将会在没有其他因素的情况下加载a[1]b[0]之间没有性能差异。

可能导致差异的一件事是,如果最近有一个存储到某个地址,它与正在加载的值之一共享一些位,尽管整个地址不同。英特尔处理器会比较一些地址位,以确定它们是否可能与当前正在进行的存储匹配。如果位匹配,则某些Intel处理器会延迟加载指令,以便给处理器时间来解析整个虚拟地址并将其与存储的地址进行比较。但是,这是偶然的影响,并非特定于a[1]b[0]

理论上,看到代码的编译器在短时间内连续加载a[0]a[1]也可能会进行一些优化,例如用一条指令加载它们。我上面的评论适用于硬件行为,而不是C实现行为。

使用二维数组场景,只要整个数组M在L1缓存中,就应该没有区别。但是,当数组超过L1缓存时,数组的列遍历因性能问题而臭名昭着。出现问题是因为地址通过地址中的固定位映射到高速缓存中的集合,并且每个高速缓存集只能容纳有限数量的高速缓存行,例如4。这是一个问题场景:

  • 数组M的行长度是距离的倍数,导致地址映射到相同的缓存集,例如4096字节。例如,在数组float M[1024][1024];中,M[0][0]M[1][0]相隔4096个字节并映射到相同的缓存集。
  • 在遍历数组的列时,您可以访问M[0][0]M[1][0]M[2][0]M[3][0],依此类推。每个元素的缓存行都会加载到缓存中。
  • 在列中继续操作时,您可以访问M[8][0]M[9][0],依此类推。由于每个缓存集都使用与前一个缓存集相同的缓存集,并且缓存集只能容纳四行,因此包含M[0][0]等的早期行将从缓存中逐出。
  • 当您完成列并通过阅读M[0][1]开始下一列时,数据不再位于L1缓存中,并且所有加载都必须从L2缓存中获取数据(或者如果您还打败L2,则会更糟以同样的方式缓存)。

答案 1 :(得分:1)

在任何一种情况下,获取a[0]然后a[1]b[0]都应该达到2次缓存访问权限。你没有说你正在使用哪个uArch但是我不熟悉任何进一步“缓存”L1上面的完整缓存行(在内存单元中的任何位置)的机制,我不认为这样的机制可行(至少不是任何合理的价格)。

假设您阅读a[0]然后a[1],并希望再次为该行保存访问L1的工作 - 您的硬件不仅要将完整的缓存行保留在内存单元,以防它再次被访问(不知道多少是一个常见的情况,所以这个功能可能不是努力),但也保持它作为缓存的逻辑扩展可以窥探,以防一些其他核心试图修改这两个读取之间的a[1](x86允许wb内存)。实际上,它甚至可以是同一线程上下文中的存储,并且您必须防范(因为当今大多数常见的x86 CPU都在无序执行加载)。如果你没有同时维护这些(也可能是其他安全措施) - 你打破了一致性,如果你这样做 - 你已经创建了一个与你的L1已经完成相同的怪物逻辑,只是为了节省1-2个周期访问。

然而,即使两个选项都需要相同数量的缓存访问,也可能有其他考虑因素影响其效率,例如L1银行业务,相同集访问限制,懒惰LRU更新等。所有这些都取决于你确切的机器实现。

如果您不仅仅关注内存/缓存访问效率,您的编译器应该能够对访问连续内存位置进行矢量化访问,这仍然会导致相同的访问,但执行BW会更轻。我认为任何体面的编译器都应该能够以这个大小展开你的循环,并将连续访问组合成一个向量,但你可以通过使用选项1来帮助它(特别是如果还有写入或其他有问题的指令在中间将编译编译器的工作)

修改

因为您还要求在L2中拟合矩阵 - 这简化了问题 - 在这种情况下,使用相同的行多次,如选项1更好,因为它允许您击中L1,而另一种方法是不断从L2中获取,这样可以降低延迟和带宽。这是loop tiling / blocking

背后的基本原则

答案 2 :(得分:0)

空间位置为王,因此版本#1更快。一个好的编译器甚至可以使用SSE / AVX对读取进行矢量化。

CPU会重新排列读数,因此无论哪个是第一个都无关紧要。在无序CPU中,如果两个高速缓存行的方式相同,那么它应该非常重要。

对于大型矩阵,保持位置更为重要,因此L1缓存仍然很热(缓存丢失较少)。

答案 3 :(得分:0)

虽然我不直接知道你的问题的答案(其他人可能对处理器架构有更多的了解),你有没有尝试/是否有可能通过某种形式的{{3}自己找到答案。 }}?

您可以通过某些功能获得高分辨率计时器,例如benchmarking(假设您在Windows上)或操作系统等效,然后按x次迭代您要测试的读数,然后再次获得高分辨率计时器以获得读取的平均时间。

对于不同的读取再次执行此过程,您应该能够比较不同类型读取的平均读取时间,这应该回答您的问题。这并不是说答案在不同的处理器上会保持不变。