特定数组大小的性能扭曲

Performance detoriation for certain array sizes

本文关键字:性能 数组      更新时间:2023-10-16

我对下面的代码有一个问题,我不明白问题在哪里。然而,问题只发生在V2英特尔处理器上,而不是V3。考虑下面的c++代码:

struct Tuple{
  size_t _a; 
  size_t _b; 
  size_t _c; 
  size_t _d; 
  size_t _e; 
  size_t _f; 
  size_t _g; 
  size_t _h; 
};
void
deref_A(Tuple& aTuple, const size_t& aIdx) {
  aTuple._a = A[aIdx];
}
void
deref_AB(Tuple& aTuple, const size_t& aIdx) {
  aTuple._a = A[aIdx];
  aTuple._b = B[aIdx];
}
void
deref_ABC(Tuple& aTuple, const size_t& aIdx) {
  aTuple._a = A[aIdx];
  aTuple._b = B[aIdx];
  aTuple._c = C[aIdx];
}
....
void
deref_ABCDEFG(Tuple& aTuple, const size_t& aIdx) {
  aTuple._a = A[aIdx];
  aTuple._b = B[aIdx];
  aTuple._c = C[aIdx];
  aTuple._d = D[aIdx];
  aTuple._e = E[aIdx];
  aTuple._f = F[aIdx];
  aTuple._g = G[aIdx];
}

注意A, B, C,…, G是简单数组(全局声明)。数组用整数填充。

方法"deref_*",简单地从数组(通过index - aIdx访问)中分配一些值给给定的结构参数"aTuple"。我首先将给定结构体的单个字段作为参数赋值,然后继续对所有字段进行赋值。也就是说,每个方法比前一个多分配一个字段。方法"deref_*"被调用,索引(aIdx)从0开始,到数组的最大大小(顺便说一下,数组具有相同的大小)。索引用于访问数组元素,如代码所示——非常简单。

现在,考虑这个图(http://docdro.id/AUSil1f),它描述了数组大小从2000万个(size_t = 8字节)整数开始到24 m (x轴表示数组大小)的性能。

对于包含2100万个整数(size_t)的数组,如果方法涉及至少5个不同的数组(即deref_ACDE…G),则性能会下降,因此您将在图中看到峰值。对于包含22 m整数的数组,性能会再次提高。我想知道为什么这只发生在21米的数组大小?只有当我在CPU服务器上进行测试时才会发生这种情况:英特尔(R) Xeon(R) CPU E5-2690 v2 @ 3.00GHz,而不是Haswell,即v3。显然,这是英特尔已知的问题,已经解决了,但我不知道它是什么,以及如何改进v2的代码。

我将非常感谢您的任何提示。

我怀疑您可能看到缓存-银行冲突。Sandybridge/Ivybridge (Xeon Exxxx v1/v2)有它们,Haswell (v3)没有。

OP更新:DTLB未命中。缓存库冲突通常只在工作集适合缓存时才会成为问题。将每个时钟的读取限制为一个8B而不是两个,不应该阻止CPU跟上主内存,即使是单线程的。(8B * 3GHz = 24GB/s,约等于主存顺序读带宽)

认为有一个性能计数器,你可以用perf或其他工具检查。

引用Agner Fog的微架构文档(章节9.13):

缓存库冲突

数据缓存中每连续128字节或两条缓存行为分为8组,每组16字节。做两个是不可能的如果两个内存地址有,则在相同的时钟周期内读取内存相同的银行号码,即,如果两个地址中的第4 - 6位是相同。

; Example 9.5. Sandy bridge cache
mov eax,  [rsi]         ; Use bank 0, assuming rsi is divisible by 40H
mov ebx,  [rsi+100H]    ; Use bank 0. Cache bank conflict
mov ecx,  [rsi+110H]    ; Use bank 1. No cache bank conflict

改变数组的总大小会改变具有相同索引的两个元素之间的距离,如果它们的布局或多或少是首尾相连的。

如果你让每个数组对齐到不同的16B偏移量(模128),这将有助于一些SnB/IvB。对每个数组中相同索引的访问将位于不同的缓存库中,因此可以并行进行。实现这一点可以像分配128b对齐的数组一样简单,在每个数组的开始处增加16*n个额外字节。(将最终释放的指针与解引用的指针分开跟踪会很麻烦。)

如果你写结果的元组与读的地址相同,取4096的模,你也会得到一个错误的依赖。(也就是说,从其中一个数组中读取数据可能需要等待元组的存储。)详见Agner Fog的文档。我没有引用这部分,因为我认为缓存-银行冲突是更可能的解释。Haswell仍然有错误依赖的问题,但是cache-bank冲突的问题已经完全解决了。