【发布时间】:2017-11-17 13:59:36
【问题描述】:
我考虑过并行化一个程序,以便在第一阶段将项目分类到桶中,以并行工作者的数量为模,这样可以避免第二阶段的冲突。并行程序的每个线程使用std::atomic::fetch_add在输出数组中保留一个位置,然后使用std::atomic::compare_exchange_weak更新当前桶头指针。所以它是无锁的。但是,我对多个线程为单个原子而苦苦挣扎的性能表示怀疑(我们所做的那个fetch_add,因为桶头数等于线程数,因此平均而言没有太多争用),所以我决定测量这个。代码如下:
#include <atomic>
#include <chrono>
#include <cstdio>
#include <string>
#include <thread>
#include <vector>
std::atomic<int64_t> gCounter(0);
const int64_t gnAtomicIterations = 10 * 1000 * 1000;
void CountingThread() {
for (int64_t i = 0; i < gnAtomicIterations; i++) {
gCounter.fetch_add(1, std::memory_order_acq_rel);
}
}
void BenchmarkAtomic() {
const uint32_t maxThreads = std::thread::hardware_concurrency();
std::vector<std::thread> thrs;
thrs.reserve(maxThreads + 1);
for (uint32_t nThreads = 1; nThreads <= maxThreads; nThreads++) {
auto start = std::chrono::high_resolution_clock::now();
for (uint32_t i = 0; i < nThreads; i++) {
thrs.emplace_back(CountingThread);
}
for (uint32_t i = 0; i < nThreads; i++) {
thrs[i].join();
}
auto elapsed = std::chrono::high_resolution_clock::now() - start;
double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
printf("%d threads: %.3lf Ops/sec, counter=%lld\n", (int)nThreads, (nThreads * gnAtomicIterations) / nSec,
(long long)gCounter.load(std::memory_order_acquire));
thrs.clear();
gCounter.store(0, std::memory_order_release);
}
}
int __cdecl main() {
BenchmarkAtomic();
return 0;
}
这是输出:
1 threads: 150836387.770 Ops/sec, counter=10000000
2 threads: 91198022.827 Ops/sec, counter=20000000
3 threads: 78989357.501 Ops/sec, counter=30000000
4 threads: 66808858.187 Ops/sec, counter=40000000
5 threads: 68732962.817 Ops/sec, counter=50000000
6 threads: 64296828.452 Ops/sec, counter=60000000
7 threads: 66575046.721 Ops/sec, counter=70000000
8 threads: 64487317.763 Ops/sec, counter=80000000
9 threads: 63598622.030 Ops/sec, counter=90000000
10 threads: 62666457.778 Ops/sec, counter=100000000
11 threads: 62341701.668 Ops/sec, counter=110000000
12 threads: 62043591.828 Ops/sec, counter=120000000
13 threads: 61933752.800 Ops/sec, counter=130000000
14 threads: 62063367.585 Ops/sec, counter=140000000
15 threads: 61994384.135 Ops/sec, counter=150000000
16 threads: 61760299.784 Ops/sec, counter=160000000
CPU 为 8 核 16 线程(Ryzen 1800X @3.9Ghz)。 因此,在使用 4 个线程之前,每秒所有操作线程的总数会急剧下降。然后缓慢下降,并略有波动。
那么这种现象是否常见于其他 CPU 和编译器?是否有任何解决方法(除了求助于单线程)?
【问题讨论】:
-
同步总是有开销。 Amdahl 定律告诉我们并行化并不能解决所有问题。如果您可以运行多个单线程进程并执行相同的工作,那么您应该尝试一下。
-
我没有看到将线程数增加到大于 4 的任何实际好处(在这种情况下)。原因可能是您的 CPU 将在上下文中花费越来越多的时间当线程数增加时切换/同步线程。
-
@AbdusSalamKhazi 程序除了切换和同步之外没有做任何事情。
-
请注意,单线程实现根本不使用原子,并且比单线程版本快很多。
for (int64_t i = 0; i < iters; i++) { non_atomic_count++; }可以将计数器保存在整个循环的寄存器中,然后再存储。编译器甚至可以将其优化为non_atomic_count += iters。 (实际上是编译器are allowed to do that with your atomic version,但目前没有。) -
无论如何,就像其他人所说的那样,您只是在测量高争用同步开销随着线程的增加而增加。必须有一些独立的工作,每个线程可以做的并行化是值得的,但是你同步。
标签: c++ multithreading performance atomic lock-free