为何线程休眠会引发缓存性能问题?
我在单线程实时程序中遇到了诡异的性能问题,最终简化出以下最小复现示例。程序接收一个含100万个float的输入数组,执行若干计算后将结果写入另一个100万个float的输出数组:
#include <random> #include <chrono> #include <array> #include <iostream> #include <thread> #include <Windows.h> constexpr int no_vals = 1'000'000; std::array<float, no_vals> in; std::array<float, no_vals> out; inline float func(float in) { float val; for (int p = 0; p < 10; p++) { val = in + p; val *= val; val = sqrtf(val); } val /= 10.1f; return val; } int main() { //生成-10.f到+10.f的随机float输入 std::random_device dev; std::mt19937 rng(dev()); std::uniform_real_distribution<float> random(-10.f, 10.f); for (auto& f : in) f = random(rng); //将线程绑定到CPU核心0(测试时可开启/关闭): //if (!SetThreadAffinityMask(GetCurrentThread(), 0b0001)) return -1; int no_loops = 3000; while (no_loops-- > 0) { //将所有输出值从缓存中刷新(测试时可开启/关闭): for (int n = 0; n < no_vals; n++) _mm_clflush(&out[0] + n); auto time_start = std::chrono::steady_clock::now(); for (int n = 0; n < no_vals; n++) out[n] = func(in[n]); auto time_end = std::chrono::steady_clock::now(); double time_microseconds = (double)std::chrono::duration_cast<std::chrono::nanoseconds> (time_end - time_start).count() / 1000.0; std::cout << time_microseconds << "," << GetCurrentProcessorNumber() << std::endl; //休眠1毫秒(测试时可开启/关闭): std::this_thread::sleep_for(std::chrono::milliseconds(1)); } }
现象描述
线程休眠会触发严重的缓存相关性能问题,且问题与之前写入的输出数组直接相关:
- 若将输出数组从缓存中手动刷新,性能问题消失;
- 若去掉线程休眠,性能问题也不会出现,但这对需要稳定帧率的实时程序不可行。
测试结果显示性能波动明显:
进一步观察发现,在3000次循环中,Windows会频繁将线程调度到不同的逻辑处理器(共8个,对应4个物理核心)上。由于每个核心拥有独立的缓存,这一点是关键诱因。性能劣化的曲线与经典缓存层级阶梯图高度吻合:
问题
线程休眠究竟如何损害缓存性能?该性能问题的成因是什么?
核心成因:线程调度切换+缓存一致性协议的开销
线程休眠引发调度迁移
当线程调用sleep_for进入休眠后,Windows调度器会将该线程标记为非就绪状态,此时其他线程会占用原核心。当休眠结束后,调度器很可能将该线程重新调度到另一个物理核心上——而非之前运行的核心。跨核心缓存的一致性开销
每个物理核心拥有独立的L1、L2缓存,L3缓存通常为所有核心共享(不同架构有差异)。当线程切换到新核心后:
- 输出数组的数据仍存在于原核心的缓存中,且处于**已修改(Dirty)**状态(上一轮循环写入过);
- 新核心要写入输出数组时,根据MESI等缓存一致性协议,必须先向原核心发送消息,将Dirty状态的缓存行写回内存,再从内存加载这些缓存行到新核心缓存;
- 这个过程会产生巨大延迟:内存加载速度比本地缓存慢几十到上百倍,直接导致性能暴跌,且表现出类似缓存层级阶梯的延迟曲线。
无休眠时的调度特性
如果没有休眠,线程会持续处于运行状态,Windows调度器倾向于将线程保留在同一个核心上(减少调度开销),此时输出数组的数据始终在本地核心缓存中,写入操作完全命中缓存,性能稳定。手动刷新缓存的作用
调用_mm_clflush会强制将输出数组的缓存行写回内存并失效,原核心缓存中不再保留Dirty状态的数据。当线程切换到新核心后,只需从内存加载干净数据,避免了跨核心的一致性同步开销——虽然内存加载也有延迟,但相比跨核心同步,延迟更稳定,不会出现突发性能暴跌。
解决方案建议
- 绑定线程到固定核心:通过
SetThreadAffinityMask将线程绑定到单个物理核心,彻底避免核心切换带来的缓存一致性开销; - 优化内存访问模式:如果无法绑定核心,可考虑将输出数组设计为临时内存,或在每次循环前主动清理缓存(如示例中的
_mm_clflush); - 调整休眠策略:使用更精准的休眠方式(如Windows的
QueryPerformanceCounter配合自旋等待),减少调度器迁移线程的概率,但该方式对实时程序兼容性较差。
内容的提问来源于stack exchange,提问作者greenlagoon

