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,提问作者陈泽霖

