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

为何线程休眠会引发缓存性能问题?

单线程实时程序的缓存性能异常问题

我在单线程实时程序中遇到了诡异的性能问题,最终简化出以下最小复现示例。程序接收一个含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个物理核心)上。由于每个核心拥有独立的缓存,这一点是关键诱因。性能劣化的曲线与经典缓存层级阶梯图高度吻合:
性能劣化

问题

线程休眠究竟如何损害缓存性能?该性能问题的成因是什么?


问题解答

核心成因:线程调度切换+缓存一致性协议的开销

  1. 线程休眠引发调度迁移
    当线程调用sleep_for进入休眠后,Windows调度器会将该线程标记为非就绪状态,此时其他线程会占用原核心。当休眠结束后,调度器很可能将该线程重新调度到另一个物理核心上——而非之前运行的核心。

  2. 跨核心缓存的一致性开销
    每个物理核心拥有独立的L1、L2缓存,L3缓存通常为所有核心共享(不同架构有差异)。当线程切换到新核心后:

  • 输出数组的数据仍存在于原核心的缓存中,且处于**已修改(Dirty)**状态(上一轮循环写入过);
  • 新核心要写入输出数组时,根据MESI等缓存一致性协议,必须先向原核心发送消息,将Dirty状态的缓存行写回内存,再从内存加载这些缓存行到新核心缓存;
  • 这个过程会产生巨大延迟:内存加载速度比本地缓存慢几十到上百倍,直接导致性能暴跌,且表现出类似缓存层级阶梯的延迟曲线。
  1. 无休眠时的调度特性
    如果没有休眠,线程会持续处于运行状态,Windows调度器倾向于将线程保留在同一个核心上(减少调度开销),此时输出数组的数据始终在本地核心缓存中,写入操作完全命中缓存,性能稳定。

  2. 手动刷新缓存的作用
    调用_mm_clflush会强制将输出数组的缓存行写回内存并失效,原核心缓存中不再保留Dirty状态的数据。当线程切换到新核心后,只需从内存加载干净数据,避免了跨核心的一致性同步开销——虽然内存加载也有延迟,但相比跨核心同步,延迟更稳定,不会出现突发性能暴跌。

解决方案建议

  • 绑定线程到固定核心:通过SetThreadAffinityMask将线程绑定到单个物理核心,彻底避免核心切换带来的缓存一致性开销;
  • 优化内存访问模式:如果无法绑定核心,可考虑将输出数组设计为临时内存,或在每次循环前主动清理缓存(如示例中的_mm_clflush);
  • 调整休眠策略:使用更精准的休眠方式(如Windows的QueryPerformanceCounter配合自旋等待),减少调度器迁移线程的概率,但该方式对实时程序兼容性较差。

内容的提问来源于stack exchange,提问作者greenlagoon

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 19:49:49