You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程为何降低循环效率?未用锁仍出现性能下降

问题

我知道很多关于多线程拖慢程序的讨论,大多是因为线程对共享状态频繁加锁,但我根本没用到任何锁机制。

我写了个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";
}
分析与解决方案

核心原因

  1. 伪共享(False Sharing)
    out是存储std::vector<double>的容器,相邻子vector的内存大概率处于同一个CPU缓存行中。当多个线程同时修改不同的子vector时,会触发缓存行的频繁失效与同步——即使线程没有修改共享变量,缓存行的关联性也会导致性能损耗。band_size越小,线程数越多,这种冲突就越频繁,耗时自然越高。

  2. 极差的内存访问模式
    每个线程处理通道时,对原始data数组的访问是跳跃式的(每次步长为2*nchans),这种非连续的内存访问会导致CPU缓存命中率极低。单线程时缓存还能部分复用,多线程时多个线程同时乱序访问内存,缓存失效的情况会急剧增加,拖慢整体速度。

  3. 过度线程化的调度开销
    当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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 11:37:33