避免错误共享以提高性能

时间:2018-06-24 16:44:41

标签: c++ concurrency atomic false-sharing

#include <iostream>
#include <future>
#include <chrono>

using namespace std;
using namespace std::chrono;

int a = 0;
int padding[16]; // avoid false sharing
int b = 0;

promise<void> p;
shared_future<void> sf = p.get_future().share();

void func(shared_future<void> sf, int &data)
{
  sf.get();

  auto t1 = steady_clock::now();
  while (data < 1'000'000'000)
    ++data;
  auto t2 = steady_clock::now();

  cout << duration<double, ratio<1, 1>>(t2 - t1).count() << endl;
}

int main()
{
  thread th1(func, sf, ref(a)), th2(func, sf, ref(b));
  p.set_value();
  th1.join();
  th2.join();

  return 0;
}

我使用上面的代码演示了错误共享对性能的影响。但是令我惊讶的是,填充似乎根本没有加快程序的速度。有趣的是,如果ab都是原子变量,则有明显的改进。有什么区别?

1 个答案:

答案 0 :(得分:1)

相同缓存行中的2个原子变量通过不同的线程使用读-修改-写(RMW)操作递增时,错误共享是最好的检测方法。 为此,每个CPU必须在增量操作期间刷新存储缓冲区并锁定高速缓存行,即:

  • 锁定缓存行
  • 从L1缓存中读取值到寄存器
  • 寄存器内的增量值
  • 写回一级缓存
  • 解锁缓存行

即使在全面的编译器优化下,单个高速缓存行在CPU之间不断跳动的影响也很明显。 强制两个变量都位于不同的缓存行中(通过添加填充数据)可能会导致性能显着提高,因为每个CPU都可以完全访问其自己的缓存行。 锁定高速缓存行仍然是必要的,但是在获得对高速缓存行的读写访问上不会浪费任何时间。

如果两个变量都是纯整数,则情况会有所不同,因为递增整数会涉及纯负载并存储(即,不是原子RMW操作)。
在没有填充的情况下,内核之间的高速缓存行跳动的效果可能仍然很明显,但是范围更小,因为不再涉及高速缓存行锁定。 如果您进行全面优化编译,则整个while循环可能将被单个增量替换,并且将不再有任何区别。

在4核X86上,我得到以下数字:

atomic int, no padding, no optimization: real 57.960s, user 114.495s

atomic int, padding, no optimization: real 10.514s, user 20.793s

atomic int, no padding, full optimization: real 55.732s, user 110.178s

atomic int, padding, full optimization: real 8.712s, user 17.214s

int, no padding, no optimization: real 2.206s, user 4.348s

int, padding, no optimization: real 1.951s, user 3.853s

int, no padding, full optimization: real 0.002s, user 0.000s

int, padding, full optimization: real 0.002s, user 0.000s