多线程代码中的高速缓存友好的数组迭代模式

时间:2014-08-30 19:28:52

标签: c++ arrays multithreading caching c++11

我有一段代码迭代结构数组,对其中的每个元素进行更改:

const size_t nThreads = 4;

struct foo {
  int a = 0;
  float b = 10;
};

void process(foo * array, size_t i, size_t end) {
  for (; i < end; i++) array[i].a += array[i].b;
}

int main() {
  foo fooArray[512];
  process(fooArray, 0, 512);
  return 0;
}

现在,我计划利用其他代码无法访问fooArray并且每个元素彼此独立,以便在process()的不同部分上分割fooArray,每个部分在自己的线程中。

我知道通常每个核心都有自己独立的(仅限L1)缓存,当它们具有重叠的缓存区域时,一个核心缓存中的写入必须与指向它的其他缓存同步,从而导致数据停顿。我也知道通常通过FSB访问RAM是同步的,这也会导致在不同内核频繁出现高速缓存丢失的情况下出现停顿。

然后问题是: 考虑到所有这一切,考虑到我希望最小化缓存未命中和数据停顿,访问fooArray的最佳模式是什么:还是通过切片?

按片:

void process(foo * array, size_t i, size_t end) {
  for (; i < end; i++) array[i].a += array[i].b;
}

for (int i = 0; i < nThreads; i++) {
  std::thread t = std::thread([fooArray, i]() {
    process(fooArray, i * 512/nThreads, (i+1) * 512/nThreads -1);
  }
  t.join();
}

在这种情况下,每个线程将遍历数组的一个片段(线程t1将经历0到127,t2将从128到255,t3从256到383以及t4从384到511)。我相信这会通过内核之间的缓存同步来最小化数据停顿,因为每个片可能由不同的核心缓存处理,但如果所有内核都试图在同一时间内获取缓存行,则可能会因FSB同步访问而增加停顿。

条纹:注意i+=nThreads

 void process(foo * array, size_t i, size_t end) {
  for (; i < end; i+=nThreads) array[i].a += array[i].b;
}

for (int i = 0; i < nThreads; i++) {
  std::thread t = std::thread([fooArray, i]() {
    process(fooArray, i, 512);
  }
  t.join();
}

在这种情况下,t1将在2,5,10,1,13,...,509,t3上超过元素0,4,8,12,...,508,t1,超过2,6,10, 14,...,510和t4超过3,7,11,15,...,511。我相信这会最小化FSB访问,因为每次缓存未命中只会触发对RAM的访问一次,但由于缓存同步导致数据停止增加。

1 个答案:

答案 0 :(得分:0)

您可以参考以下SO链接,其中包含有关此概念的重要信息以及专家对此主题的各种信息的链接:https://stackoverflow.com/a/23172120/2724703

在处理与性能相关的问题时,我们应始终自行测量执行时间并了解可能影响的各种参数。对于所有三种情况,首先应该测量时间

话虽如此,我认为您的&#34; By slices&#34; 方法会因为以下原因而优于其他方法:

因此,要在我们的程序中实现缓存友好逻辑,我们应该尽可能地尝试线性内存访问。这确实会受到缓存机制的影响,因为具有L1缓存的内存概率会显着提高。在这种情况下,线程对数组段的线性访问将远远优于另一个。现在,如果我们的数据集很大并且不适合L1缓存,则此概念适用。在任何一种情况下,整个数据都可以很容易地适应L1缓存。

因此,如果我们的工作/逻辑是实质性的,那么这种技术会更合适,在这种情况下,这种复杂性可能不会带来任何好处(有时可能会增加执行时间)。可以了解这些概念,但实际上,如果我们通过测量发现这会导致瓶颈,我们应该只尝试优化我们的代码。

我认为由于内核之间的缓存同步导致数据停顿不应该有任何问题。这种开销应该同样适用于这两种情况。