原子fetch_add与增加性能



下面的代码展示了多线程编程的好奇心。特别是单个线程中std::memory_order_relaxed增量与常规增量的性能。我不明白为什么fetch_add(宽松(单线程比常规增量慢两倍。

static void BM_IncrementCounterLocal(benchmark::State& state) {
  volatile std::atomic_int val2;
  while (state.KeepRunning()) {
    for (int i = 0; i < 10; ++i) {
      DoNotOptimize(val2.fetch_add(1, std::memory_order_relaxed));
    }
  }
}
BENCHMARK(BM_IncrementCounterLocal)->ThreadRange(1, 8);
static void BM_IncrementCounterLocalInt(benchmark::State& state) {
  volatile int val3 = 0;
  while (state.KeepRunning()) {
    for (int i = 0; i < 10; ++i) {
      DoNotOptimize(++val3);
    }
  }
}
BENCHMARK(BM_IncrementCounterLocalInt)->ThreadRange(1, 8);

输出:

      基准测试时间(纳秒( 中央处理器(纳秒(迭代次数      ----------------------------------------------------------------------      BM_IncrementCounterLocal/线程:1  59 60 11402509                                      BM_IncrementCounterLocal/线程:2 30 61 11284498                                      BM_IncrementCounterLocal/线程:4 19 62 11373100                                      BM_IncrementCounterLocal/线程:8 17 62 10491608      BM_IncrementCounterLocalInt/线程:1  31 31 22592452                                      BM_IncrementCounterLocalInt/线程:2 15 31 22170842                                      BM_IncrementCounterLocalInt/线程:4 8 31 22214640                                      BM_IncrementCounterLocalInt/线程:8 9 31 21889704 

对于volatile int,编译器必须确保它不会优化和/或重新排序变量的任何读/写。

对于fetch_add,CPU必须采取预防措施,使读-修改-写操作是原子的。

这是两个完全不同的要求:原子性要求意味着 CPU 必须与计算机上的其他 CPU 通信,确保它们不会在自己的读取和写入之间读取/写入给定的内存位置。如果编译器使用比较和交换指令编译fetch_add,它实际上会发出一个短循环,以捕获其他 CPU 修改了两者之间的值的情况。

对于volatile int没有必要进行此类通信。相反,volatile要求编译器不发明任何读取:volatile是为与硬件寄存器的单线程通信而设计的,其中读取值的简单行为可能会产生副作用。

本地版本不使用原子学。 (它使用volatile的事实是一个红鲱鱼 - volatile在多线程代码中基本上没有任何意义(。

原子

版本使用原子(! 实际上只有一个线程将用于访问变量这一事实对 CPU 来说是不可见的,而且编译器也没有发现它并不感到惊讶。 (没有必要浪费开发人员的精力来确定将std::atomic_int转换为int是否安全,而这几乎永远不会。 如果他们不需要从多个线程访问它,没有人会写atomic_int

因此,原子版本将难以确保增量实际上是原子的,坦率地说,我很惊讶它只慢了 2 倍 - 我本来期望更像 10 倍。

最新更新