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

thread_local对std::array与自定义Buffer类性能影响差异的原因探究

问题:thread_local优化std::array和自定义Buffer类表现相反的原因

我尝试通过将类的成员变量改为thread_local来优化代码,但结果却适得其反。编写了测试代码复现该问题:

#include <atomic>
#include <cassert>
#include <chrono>
#include <cstring>
#include <random>
#include <stdexcept>
#include <string>
#include <thread>

void bind_core(int cpu_core) {
  cpu_set_t cpuset;
  CPU_ZERO(&cpuset);
  CPU_SET(cpu_core, &cpuset);
  if (0 != pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset)) {
    throw std::runtime_error("Failed to set CPU affinity");
  }
}

struct Bench {
private:
  const char *name_;
  size_t repeat_;
  std::chrono::time_point<std::chrono::system_clock> start_;

public:
  Bench(const char *name, size_t repeat)
      : name_(name), repeat_(repeat), start_(std::chrono::system_clock::now()) {
  }
  ~Bench() {
    printf("Bench: %s %luns\n", name_,
           (std::chrono::system_clock::now() - start_).count() / repeat_);
  }
  template <typename Func> void operator()(Func &&f) const {
    for (size_t i{}; i < repeat_; i++) {
      f(i);
    }
  }
};

inline uint64_t random64() {
  static std::random_device rd;
  static std::default_random_engine re{rd()};
  static std::uniform_int_distribution<uint64_t> distribution;
  return distribution(re);
}

/// \todo async
inline void generate_random_data(void *buf, size_t len, uint n_thread = 1) {
  if (!n_thread)
    throw std::invalid_argument("invalid argument n_thread: 0");

  std::atomic_uint todo = n_thread;
  std::vector<std::thread> thrds;
  for (int i = 0; i < n_thread; ++i) {
    size_t step = len / n_thread;
    assert(!(step % 8));
    thrds.emplace_back([=, &todo] {
      uint64_t *tmp = (uint64_t *)((char *)(buf) + i * step);
      uint64_t *upper = (uint64_t *)((char *)(buf) + (i + 1) * step);
      for (; tmp < upper; ++tmp) {
        auto v = random64();
        memcpy(tmp, &v, sizeof v);
      }
      todo--;
    });
  }
  for (auto &th : thrds) {
    th.join();
  }
}
std::array<std::array<char, 4096>, 65536> random_data;
template <typename T> inline void doNotOptimize(T &&value) {
  asm volatile("" : "+m,r"(value) : : "memory");
}

constexpr auto buf_size = 4000;
constexpr auto copy_size = 4000;
constexpr auto repeat = 10'000'000;

template <size_t SIZE> struct Buffer {
  std::array<char, SIZE> buf;
  char *ptr;
  Buffer() : ptr(buf.data()) {}

  constexpr auto size() const { return SIZE; }
  char *data() { return ptr; }
  void append(const char *data, size_t size) {
    memcpy(buf.data(), data, copy_size);
  }
};

struct TestArr {
  std::array<char, buf_size> buf;
  char padding[4096 - buf_size];

  void run(size_t i) {
    doNotOptimize(memcpy(&buf[0], random_data[i & 65535].data(), copy_size));
  }
};

struct TestArrTL {
  static thread_local std::array<char, buf_size> buf;
  char *p = buf.data();

  void run(size_t i) {
    doNotOptimize(memcpy(&buf[0], random_data[i & 65535].data(), copy_size));
    doNotOptimize(p += copy_size);
  }
};
thread_local std::array<char, buf_size> TestArrTL::buf;

struct TestBuf {
  Buffer<buf_size> buf;
  void run(size_t i) {
    doNotOptimize(memcpy(buf.data(), random_data[i & 65535].data(), copy_size));
  }
};
struct TestBufTL {
  static thread_local Buffer<buf_size> buf;
  void run(size_t i) {
    doNotOptimize(memcpy(buf.data(), random_data[i & 65535].data(), copy_size));
  }
};
thread_local Buffer<buf_size> TestBufTL::buf;

int main() {
  generate_random_data(random_data.data(), 4096 * 65536, 8);
  printf("data ready\n");

  std::thread t1([] {
    bind_core(1);
    Bench("TestArr", repeat)([](auto i) { TestArr().run(i); });
  });
  std::thread t2([] {
    bind_core(2);
    Bench("TestArrTL", repeat)([](auto i) { TestArrTL().run(i); });
  });
  std::thread t3([] {
    bind_core(3);
    Bench("TestBuf", repeat)([](auto i) { TestBuf().run(i); });
  });
  std::thread t4([] {
    bind_core(4);
    Bench("TestBufTL", repeat)([](auto i) { TestBufTL().run(i); });
  });
  t1.join();
  t2.join();
  t3.join();
  t4.join();
  bind_core(5);
  Bench("TestBufTL_main", repeat)([](auto i) { TestBufTL().run(i); });
}

测试输出

data ready
Bench: TestBuf 391ns
Bench: TestArrTL 399ns
Bench: TestBufTL 460ns
Bench: TestArr 482ns
Bench: TestBufTL_main 339ns

从结果可见,thread_local对std::array和自定义Buffer类的影响截然不同:它提升了std::array的性能,却导致Buffer类性能下降。我猜测这可能与Buffer的非constexpr构造函数有关,但无法理解其具体影响逻辑。

测试环境

  • OS:Description: Ubuntu 22.04.3 LTS
  • 内核:5.15.146.1-microsoft-standard-WSL2
  • 架构:x86_64
  • 编译器:gcc-11.4.0
  • 编译参数:-O3

原因分析与解决方法

1. std::array作为thread_local的性能优势

std::array是聚合类型,它的构造是constexpr且无运行时副作用的。声明为thread_local时,GCC会直接将其分配到线程的TLS区域,不需要运行时构造函数调用——线程启动时内存直接完成初始化,甚至编译期就能确定布局。

在TestArrTL中,每次构造TestArrTL对象只是获取thread_local std::array的地址,无额外初始化开销。对比TestArr每次构造都要在栈上分配4096字节空间(含padding),thread_local版本避免了重复的栈内存分配/释放,因此性能提升。

2. 自定义Buffer类的thread_local性能瓶颈

你的Buffer类包含std::array和char*成员,构造函数并非constexpr。对于非聚合类型的thread_local变量,GCC需要在线程首次访问该变量时执行构造函数,这会引入一次性初始化检查开销:
每次访问TestBufTL::buf时,编译器会插入代码检查该线程是否已初始化此变量,未初始化则调用构造函数。虽然检查是原子操作且仅执行一次,但在基准测试中,每次run调用都会触发这个检查(即便已初始化),额外的分支和内存访问会带来性能损耗。

而TestBuf版本中,Buffer对象在栈上构造,-O3优化下,构造函数的ptr赋值会被完全内联,甚至编译期就能确定ptr地址,无额外开销。

3. TestBufTL_main性能异常的原因

主线程中TestBufTL_main性能优于TestBuf,是因为主线程的thread_local变量初始化仅发生一次,且后续访问的初始化检查会被编译器优化掉(主线程无并发,编译器可推断初始化状态)。而其他线程是并发执行,编译器无法完全消除检查分支,开销得以保留。

解决方法

将Buffer的构造函数改为constexpr,使其成为可编译期初始化的类型,消除运行时初始化检查:

template <size_t SIZE> struct Buffer {
  std::array<char, SIZE> buf;
  char *ptr;
  constexpr Buffer() : ptr(buf.data()) {}

  constexpr auto size() const { return SIZE; }
  constexpr char *data() { return ptr; }
  void append(const char *data, size_t size) {
    memcpy(buf.data(), data, copy_size);
  }
};

修改后,GCC会将thread_local Buffer视为可静态初始化的变量,消除运行时检查,性能即可和thread_local std::array持平。

内容的提问来源于stack exchange,提问作者陈泽霖

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 09:27:12