【问题标题】:Performance detoriation for certain array sizes某些数组大小的性能恶化
【发布时间】:2015-08-06 20:12:49
【问题描述】:

以下代码有问题,我不明白问题出在哪里。但是,该问题仅发生在 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_*”,简单地将数组中的一些值(通过索引访问 - aIdx)分配给给定的结构参数“aTuple”。我首先将给定结构的单个字段作为参数分配,然后一直继续到所有字段。也就是说,每个方法都比前一个方法多分配一个字段。方法“deref_*”被调用,索引(aIdx)从 0 开始,到数组的 MAX 大小(顺便说一下,数组具有相同的大小)。索引用于访问数组元素,如代码所示——非常简单。

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

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

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

【问题讨论】:

  • v3 引入了change in carche management,通过将核心所需的数据放在一起来减少读/写延迟。这也许可以解释您的观察结果
  • Christophe,但是为什么只有 21 m 的数组大小会出现性能下降?
  • 您是否使用类似perf 的方式对性能计数器进行了采样?
  • 这可能是缓存关联性问题。你能转储数组 A...G 的起始地址吗?您是在单核上运行测试还是并行运行多个实例?
  • 保罗,它不是缓存关联性,我最初也是这样。我已经向数组添加了填充,但结果相同。数组的大小不是 2 的幂,已知会导致缓存关联性问题。最后,我发现问题是 L1 中的 TLB 未命中。

标签: c++ memory optimization intel


【解决方案1】:

我怀疑您可能会看到缓存库冲突。 Sandybridge/Ivybridge (Xeon Exxxx v1/v2) 有,Haswell (v3) 没有。

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

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

引用 Agner Fog's microarchitecture doc(第 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 仍然存在错误依赖问题,但缓存库冲突问题完全消失了。

【讨论】:

  • 不是缓存库问题。用 VTUNE 分析代码后发现是 DTBL 未命中。
  • 有趣。同时访问新页面的所有数组都比交错更糟糕?这就是时间与大小发生冲突的原因吗?顺便说一句,Haswell 确实有改进的 L2 TLB,但与 Sandy/Ivybridge 相同的 L1 DTLB。 realworldtech.com/haswell-cpu/5
  • @3.14: 嗯,另外,我应该意识到缓存组冲突可能不是问题,因为每个周期读取一次 8B 应该足以跟上主内存的速度(因为你的数据集比 L3 缓存大几倍)。我认为这可能会导致未完成请求的并行性降低,或者其他什么。
  • 我也尝试了您的解决方案(16 B 偏移),以及我提出的其他一些想法,但无济于事。直到最近我们才安装了 VTUNE,然后我可以清楚地看到问题,并且是 DTLB,这也有道理,因为 L1 DTLB 只有 4-way associative。现在,为什么 HASWELL 处理器不会出现这种效果,坦率地说,我不知道,也许那里的 DTLB 包含更多条目,但是你说它们在 L1 和 SandyBridge 中是一样的。或者,由于 L2 的改进,v3 中的 page-walk 更快,或者可能是其他原因......
  • @3.14:Haswell 确实有改进的 TLB。这是一个 2 级系统,第 2 级将容量和关联性提高了一倍,并增加了对大 (2MB) 页面的支持。与其对 L1 DTLB 未命中进行页面遍历,不如从 L2 TLB 快速获取。 L1 DTLB 必须非常快,并且需要 3 个读取端口,因此使第 2 级更大比改进 L1 更容易。我上次评论中那个 Realwordtech URL 的详细信息。
猜你喜欢
  • 2016-07-28
  • 2012-02-01
  • 2017-03-18
  • 1970-01-01
  • 2017-12-04
  • 2013-04-27
  • 1970-01-01
  • 2021-06-09
  • 2019-12-14
相关资源
最近更新 更多