下面的代码展示了多线程编程的好奇心。特别是单个线程中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 倍。