特定阵列大小的性能修正

时间:2015-08-04 21:15:27

标签: c++ memory optimization intel

我遇到以下代码的问题,我无法理解问题出在哪里。然而问题是只有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 访问)分配给给定的struct参数“ aTuple ”。我首先将给定结构的单个字段指定为参数,然后一直继续到所有字段。也就是说,每个方法分配的字段比前一个字段多一个。使用索引(aIdx)从0开始调用方法“deref_ *”到数组的MAX大小(顺序,数组具有相同的大小)。该索引用于访问数组元素,如代码所示 - 非常简单。

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

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

我非常感谢你身边的任何暗示。

1 个答案:

答案 0 :(得分:2)

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

来自OP的更新:这是DTLB未命中。当您的工作集适合缓存时,缓存库冲突通常只是一个问题。被限制为每个时钟而不是2个8B读取不应该阻止CPU跟上主存储器,甚至单线程。 (8B * 3GHz = 24GB/s,大约等于主存储器顺序读取带宽。)

认为有一个perf计数器,您可以使用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仍然存在错误依赖问题,但缓存库冲突问题已完全消失。