1
#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;
}

我使用上面的代码来演示虚假共享对性能的影响。但令我惊讶的是,填充似乎根本没有加速程序。有趣的是,如果两者a都是b原子变量,则有明显的改进。有什么不同?

4

1 回答 1

2

当同一高速缓存行中的 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
于 2018-06-24T18:24:03.133 回答