我应该"绑定""旋转"线程到某个核心吗?

jav*_*red 10 c++ multithreading low-latency

我的应用程序包含几个"旋转"的延迟关键线程,即从不阻塞.这样的线程预计占用一个CPU核心的100%.然而,现代操作系统似乎经常将线程从一个核心转移到另一个核心.因此,例如,使用此Windows代码:

void Processor::ConnectionThread()
{
    while (work)
    {
        Iterate();
    }
}
Run Code Online (Sandbox Code Playgroud)

我没有在任务管理器中看到"100%占用"核心,整体系统负载为36-40%.

但如果我把它改成这个:

void Processor::ConnectionThread()
{
    SetThreadAffinityMask(GetCurrentThread(), 2);
    while (work)
    {
        Iterate();
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我确实看到其中一个CPU核心被100%占用,整体系统负载也降低到34-36%.

这是否意味着我应该倾向于SetThreadAffinityMask"旋转"线程?如果我SetThreadAffinityMask在这种情况下改进延迟添加?我还应该为"旋转"线程做些什么来改善延迟?

我正在将我的应用程序移植到Linux,所以这个问题更多的是关于Linux,如果这很重要.

upd发现此幻灯片显示将忙等待线程绑定到CPU可能会有所帮助:

在此输入图像描述

Jas*_*son 6

将任务固定到特定处理器通常会为任务提供更好的性能.但是,这样做时需要考虑很多细微差别和成本.

强制关联时,可以限制操作系统的调度选择.您增加剩余任务的cpu争用.所以一切的系统上的其他人受到影响,包括操作系统本身.您还需要考虑,如果任务需要跨内存进行通信,并且亲和力设置为不共享缓存的cpu,则可以大大增加跨任务的通信延迟.

设置任务cpu亲和性的最大原因之一是有益的,因为它提供了更可预测的缓存和tlb(转换后备缓冲区)行为.当任务切换cpus时,操作系统可以将其切换到无法访问最后一个cpu缓存或tlb的cpu.这可能会增加任务的缓存未命中.特别是跨任务进行通信的问题,因为在更高级别的缓存和最差的内存中进行通信需要更多的时间.要测量linux上的缓存统计信息(一般性能),我建议使用perf.

在尝试修复亲和力之前,最好的建议是衡量.量化延迟的好方法是使用rdtsc指令(至少在x86上).这将读取cpu的时间源,通常会提供最高的精度.跨事件测量将提供大约纳秒的准确度.

volatile uint64_t rdtsc() {
   register uint32_t eax, edx;
   asm volatile (".byte 0x0f, 0x31" : "=d"(edx), "=a"(eax) : : );
   return ((uint64_t) edx << 32) | (uint64_t) eax;
}
Run Code Online (Sandbox Code Playgroud)
  • 注意 - rdtsc指令需要与加载栅栏组合以确保所有先前的指令已完成(或使用rdtscp)
  • 另请注意 - 如果rdtsc在没有不变时间源的情况下使用(在linux上grep constant_tsc /proc/cpuinfo,您可能会在频率变化时获得不可靠的值以及任务是否切换cpu(时间源)

所以,总的来说,设置亲和力确实会降低延迟,但这并不总是正确的,并且当你这样做时会有非常严重的成本.

一些额外的阅读......


Sur*_*urt 6

如果这是代码中最重要的事情,那么在大多数情况下运行锁定到单个内核的线程会为该线程提供最佳延迟.

原因(R)是

  • 您的代码可能在您的iCache中
  • 分支预测器已调整为您的代码
  • 您的数据可能已准备好在您的dCache中
  • TLB指向您的代码和数据.

除非

  • 你正在运行一个SMT系统(例如超线程),在这种情况下,邪恶的双胞胎将通过使你的代码被淘汰来帮助你,你的分支预测器将调整到它的代码,它的数据将推动你离开dCache ,您的TLB受其使用影响.
    • 成本未知,每个缓存错过成本〜4ns,〜15ns和~75ns的数据,这很快就可以达到几个1000ns.
    • 它保存了上面提到的每个原因R,它仍然存在.
    • 如果邪恶的双胞胎也只是旋转成本应该低得多.
  • 或者你的核心允许中断,在这种情况下你会遇到同样的问题
    • 你的TLB被冲洗了
    • 如果驱动程序编程良好,那么在上下文切换时你需要1000ns-20000ns的命中率,大多数应该在低端.
  • 或者您允许操作系统切换您的进程,在这种情况下,您遇到与中断相同的问题,就在范围的高端.
    • 切换也可能导致线程暂停整个切片,因为它只能在一个(或两个)硬件线程上运行.
  • 或者您使用导致上下文切换的任何系统调用.
    • 根本没有磁盘IO.
    • 只有async IO else.
  • 拥有比核心更多的活动(非暂停)线程会增加出现问题的可能性.

因此,如果您需要不到100ns的延迟来防止应用程序爆炸,则需要防止或减少SMT,中断和核心任务切换的影响.完美的解决方案是具有静态调度实时操作系统.这是一个几乎完美匹配你的目标,但如果你的主要完成服务器和桌面编程,它是一个新的世界.

将线程锁定到单个核心的缺点是:

  • 它将花费一些总吞吐量.
    • 作为一些可能已经运行的线程,如果上下文可以被切换.
    • 但在这种情况下延迟更重要.
  • 如果线程获得上下文切换,则可能需要一些时间才能调度一个或多个时间片,通常为10-16ms,这在本申请中是不可接受的.
    • 将其锁定到核心及其SMT将减轻此问题,但不能消除它.每个添加的核心将减轻问题.
    • 将其优先级设置得更高将减轻问题,但不能消除它.
    • 使用SCHED_FIFO和最高优先级的调度将阻止大多数上下文切换,中断仍然可以像某些系统调用一样导致临时切换.
    • 如果你有一个多CPU设置,你可以通过cpuset获得其中一个CPU的独占所有权.这可以防止其他应用程序使用它.

使用带有SCHED_FIFO的pthread_setschedparam和在SU中运行的最高优先级并将其锁定到核心及其邪恶双胞胎应该确保所有这些的最佳延迟,只有实时操作系统才能消除所有上下文切换.

其他链接:

讨论中断.

您的Linux可能会接受您使用SCHED_FIFO调用sched_setscheduler,但这需要您拥有自己的PID,而不仅仅是TID或您的线程是协作式多任务处理. 这可能并不理想,因为所有线程都只是"自愿"切换,从而降低了内核调度的灵活性.

100ns的进程间通信