多线程为何降低循环效率?未用锁仍出现性能下降
我知道很多关于多线程拖慢程序的讨论,大多是因为线程对共享状态频繁加锁,但我根本没用到任何锁机制。
我写了个TCP客户端,接收的字节数组里是多通道时序数据,样本按通道交错排列(比如第1个C1样本、第1个C2样本、第2个C1样本、第2个C2样本),用小端short编码,需要把每个通道的数据存到std::vector里。数据量很大,通道数有几百个,本来想着拆分数据用多线程处理能提速,结果反而更慢了。
我写了复现代码,而且当band_size减小时,每个线程的平均耗时还会增加:
#include <iostream> #include <thread> #include <chrono> #include <vector> char data[1048576]; int main() { for (auto& d : data) { d = std::rand(); } auto nchans = 769; std::vector<std::vector<double>> out(nchans, std::vector<double>()); auto band_size = nchans/12; std::vector<std::thread> threads; std::atomic_uint total_ns = 0; for (auto c = 0; c < nchans; c += band_size) { threads.emplace_back([&, c] { auto start = std::chrono::steady_clock::now(); for (auto subc = 0; subc < band_size && c + subc < nchans; ++subc) { auto channel = c + subc; for (auto i = 2 * (c + subc); i + 1 < sizeof(data); i += 2 * nchans) { short sample = data[i] + (data[i + 1] << 8); out[channel].push_back(sample); } } total_ns += (std::chrono::steady_clock::now() - start).count(); }); } for (auto& promise : threads) { promise.join(); } std::cout << "done " << (total_ns / threads.size()) / 1e6 << "ms"; }
核心原因
伪共享(False Sharing)
out是存储std::vector<double>的容器,相邻子vector的内存大概率处于同一个CPU缓存行中。当多个线程同时修改不同的子vector时,会触发缓存行的频繁失效与同步——即使线程没有修改共享变量,缓存行的关联性也会导致性能损耗。band_size越小,线程数越多,这种冲突就越频繁,耗时自然越高。极差的内存访问模式
每个线程处理通道时,对原始data数组的访问是跳跃式的(每次步长为2*nchans),这种非连续的内存访问会导致CPU缓存命中率极低。单线程时缓存还能部分复用,多线程时多个线程同时乱序访问内存,缓存失效的情况会急剧增加,拖慢整体速度。过度线程化的调度开销
当band_size过小时,创建的线程数会远超CPU核心数。线程的创建、上下文切换开销会超过并行计算带来的收益,导致整体性能下降。
优化方案
- 消除伪共享
给每个子vector添加缓存行填充,确保每个子vector独占一个缓存行。比如定义带填充的结构体:
struct ChannelBuffer { std::vector<double> data; char padding[64 - sizeof(std::vector<double>) % 64]; }; std::vector<ChannelBuffer> out(nchans);
这样相邻的ChannelBuffer不会共享缓存行,避免线程间的缓存冲突。
- 重构内存访问模式
先通过单线程将交错排列的原始数据转换为按通道连续存储的结构,再分线程处理连续内存:
// 先计算样本总数 int num_samples = sizeof(data) / (2 * nchans); // 预处理:将交错数据转为通道连续 std::vector<short> channel_data(nchans * num_samples); for (int i = 0; i < num_samples; ++i) { for (int c = 0; c < nchans; ++c) { int src_idx = 2*(i*nchans + c); short sample = data[src_idx] + (data[src_idx+1] << 8); channel_data[i*nchans + c] = sample; } } // 多线程处理连续内存 for (auto c = 0; c < nchans; c += band_size) { threads.emplace_back([&, c] { int end_c = std::min(c + band_size, nchans); for (int channel = c; channel < end_c; ++channel) { auto& vec = out[channel]; vec.reserve(num_samples); for (int i = 0; i < num_samples; ++i) { vec.push_back(channel_data[i*nchans + channel]); } } }); }
连续内存访问能充分利用CPU预取机制,大幅提升缓存命中率。
- 控制线程数量
将线程数限制在CPU核心数的1~2倍,比如:
auto thread_count = std::thread::hardware_concurrency(); auto band_size = (nchans + thread_count - 1) / thread_count;
避免过度调度带来的开销。
- 预分配内存
提前计算每个通道的样本数量,调用reserve()为out的子vector预分配内存,避免push_back()时频繁的内存分配与拷贝。
内容的提问来源于stack exchange,提问作者HahaHortness

