How long does it take to make a context switch(上下文切换需要花费多长时间)

https://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html
这是一个非常有趣的问题,我非常乐意花点时间来研究这个问题。StumbleUpon的某个人提出了一个假设,即随着Nehalem架构(以Intel i7)的所有改进,上下文切换将会更快。你将如何设计一个测试,以实证地找到这个问题的答案?上下文切换到底有多昂贵?(答案:代价非常昂贵)

The lineup


2011年4月21日更新:我添加了一个“极端”Nehalem和一个低电压Westmere。
2013年4月1日更新:增加了英特尔Sandy Bridge E5-2620。
我测试了4代不同的cpu:

  • 一组英特尔5150 (Woodcrest,基于旧“核心”架构,2.67GHz)CPU。5150是一个双核架构,所以机器总共有4个可用核。Kernel:2.6.28-19-server x86_64。
  • 一组英特尔E5440 (Harpertown,基于Penrynn架构,2.83GHz)CPU。E5440是一个四核架构CPU,所以机器总共有8个核。Kernel:2.6.24-26-server x86_64。
  •  一组英特尔E5520 (Gainestown,基于Nehalem架构,又名i7, 2.27GHz)CPU。E5520是一个四核的,并且启用了超线程,所以机器总共有8个核或16个“硬件线程”。Kernel:2.6.28-18-generic x86_64。
  • 一组英特尔X5550 (Gainestown,基于Nehalem架构,又名i7, 2.67GHz)CPU。X5550是一个四核,并启用了超线程,所以机器总共有8个核或16个“硬件线程”。注意:X5550是在intel的“服务器”产品线。这个CPU比前一个贵3倍。Kernel:2.6.28-15-server x86_64。
  • 一组特尔L5630 (Gulftown,基于Westmere架构,又名i7, 2.13GHz)。L5630是一个四核,并启用了超线程,因此机器总共有8个核或16个“硬件线程”。注意:L5630是一个“低电压”CPU。在相同的价格下,这个CPU在理论上比非低压CPU的功率低16%。Kernel:2.6.32-29-server x86_64。
  •  一组Intel E5-2620 (Sandy Bridge- ep,基于Sandy Bridge架构,又称E5, 2Ghz)CPU。E5-2620是一个六核,有超线程,所以机器总共有12个核,或24个“硬件线程”。Kernel:3.4.24 x86_64。

所有的cpu都设置为一个固定的时钟速率(没有Turbo Boost或任何花哨的东西)。所有的Linux内核都是由Ubuntu构建和发布的(采用Ubuntu系统)。

First idea: with syscalls (fail) 


我的第一个想法是连续多次进行一个廉价的系统调用,计算它所花费的时间,并计算每个系统调用所花费的平均时间。现在Linux上最便宜的系统调用似乎是[gettid](https://man7.org/linux/man-pages/man2/gettid.2.html)。事实证明,这是一种幼稚的方法,因为现在系统调用实际上不再导致完整的上下文切换,内核可以通过“模式切换”(从用户模式切换到内核模式,然后再返回到用户模式)。这就是为什么当我运行第一个测试程序时,vmstat没有显示出明显的上下文切换数量增加。但是这个测试也很有趣,尽管它不是我最初想要的。
[timesyscal.c](https://github.com/tsuna/contextswitch/blob/master/timesyscall.c) 的测试结果:

  • - Intel 5150: 105ns/syscall
  • - Intel E5440: 87ns/syscall
  • - Intel E5520: 58ns/syscall
  • - Intel X5550: 52ns/syscall
  • - Intel L5630: 58ns/syscall
  • - Intel E5-2620: 67ns/syscall

注意:这些结果包括futex系统调用的开销。
现在你必须对这些结果持保留态度。微基准只做上下文切换。在实践中,上下文切换是昂贵的,因为它会破坏CPU缓存(如果有L1、L2、L3,还有TLB——别忘了TLB!)

CPU affinity


在SMP环境中很难预测,因为根据任务是否从一个核心迁移到另一个核心,性能可能会有很大差异(特别是如果迁移是跨物理cpu)。我再次运行基准测试,但这次我将进程/线程固定在单个核心(或“硬件线程”)上。性能的加速是戏剧性的。

[cpubench.sh](https://github.com/tsuna/contextswitch/blob/master/cpubench.sh) 执行结果:

  • - Intel 5150: ~1900ns/process context switch, ~1700ns/thread context switch
  • - Intel E5440: ~1300ns/process context switch, ~1100ns/thread context switch
  • - Intel E5520: ~1400ns/process context switch, ~1300ns/thread context switch
  • - Intel X5550: ~1300ns/process context switch, ~1100ns/thread context switch
  • - Intel L5630: ~1600ns/process context switch, ~1400ns/thread context switch
  • - Intel E5-2620: ~1600ns/process context switch, ~1300ns/thread context siwtch

性能提升:5150:66%,E5440: 65-70%, E5520: 50-54%, X5550: 55%, L5630: 45%, E5-2620: 45%

线程切换和进程切换之间的性能差距随着CPU的更新而增加。(5150: 7-8%, E5440: 5-15%, E5520: 11-20%, X5550: 15%, L5630: 13%, E5-2620: 19%)总的来说,从一个任务切换到另一个任务的代价仍然非常高。请记住,这些人工测试完全不进行计算,所以它们在L1d和L1i中可能有100%的缓存命中率。在现实世界中,由于缓存污染,在两个任务(线程或进程)之间切换通常会导致更高的代价。但我们稍后再谈这个。

Threads vs. processes


再得到上述数字之后,我很快并不赞同Java中的一些做法,因为在Java中创建大量线程是相当常见的,并且在此类应用程序中线程上下文切换的成本变得很高。有人反驳说,是的,Java使用了很多线程,但是在Linux 2.6的NPTL中,线程变得更快、更便宜了。他们说,在同一个进程的两个线程之间切换时,通常不需要做TLB刷新。这是真的,你可以去检查Linux内核的源代码(在mmu_context.h中switch_mm):
```
static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next,
                             struct task_struct *tsk)
{
       unsigned cpu = smp_processor_id();

       if (likely(prev != next)) {
               [...]
               load_cr3(next->pgd);
       } else {
               [don't typically reload cr3]
       }
}
```
在这段代码中,内核希望在具有不同内存结构的任务之间进行切换,在这种情况下它会更新CR3, CR3是保存页面表指针的寄存器。写入CR3会自动导致x86上的TLB刷新。
是在实践中,使用默认内核调度器和繁忙的服务器类型工作负载,跳过对load_cr3的调用的代码路径是相当少见的。另外,不同的线程往往有不同的工作集,所以即使您跳过这一步,您仍然会污染L1/L2/L3/TLB缓存。我重复上述基准与2线程而不是2流程(来源:timetctxsw.c),但是结果并不显著不同(这取决于很多不同调度和运气,但在许多运行平均通常只有100 ns更快切换线程如果你不设置一个定制的CPU关联)。

Indirect costs in context switches: cache pollution


上述结果与罗切斯特大学的一些人发表的一篇论文相符:量化语境转换的成本。在一台未指定的Intel Xeon上(这篇论文写于2007年,所以CPU可能不是太旧),它们最终的平均时间是3800秒。它们使用我想到的另一种方法,即向管道写入/读取1个字节,以阻塞/解除几个进程的阻塞。我认为(ab)使用futex会更好,因为futex本质上向用户领域暴露了一些调度接口。
本文还解释了由于缓存干扰而导致的上下文切换的间接代价。在特定的工作集大小之外(在他们的基准测试中大约是L2缓存大小的一半),上下文切换的成本显著增加(增加了2个数量级)。

我认为这是一个更现实的期望。线程之间不共享数据可以获得最佳性能,但这也意味着每个线程都有自己的工作集,**当线程从一个核心迁移到另一个核心(或者更糟的是,跨物理cpu)时,缓存污染将是非常昂贵的。**不幸的是,当应用程序的活动线程比硬件线程多得多时,这种情况就会一直发生,就是为什么**不创建比可用硬件线程更多的活动线程**是如此重要,因为在这种情况下,Linux调度器更容易在内核上重新调度它们最后使用的线程(“弱亲和性”)。
话虽如此,现在我们的cpu有更大的缓存,甚至可以有L3缓存。

  • - 5150: L1i & L1d = 32K each, L2 = 4M
  • - E5440: L1i & L1d = 32K each, L2 = 6M
  • - E5520: L1i & L1d = 32K each, L2 = 256K/core, L3 = 8M (same for the X5550)
  • - L5630: L1i & L1d = 32K each, L2 = 256K/core, L3 = 12M
  • - E5-2620: L1i & L1d = 64K each, L2 = 256K/core, L3 = 15M

注意,在E5520/X5550/L5630(“i7”)和Sandy Bridge E5-2520的情况下,L2缓存很小,但每个核心有一个L2缓存(启用HT后,每个硬件线程提供128K)。L3缓存为每个物理CPU上的所有核心共享。
拥有更多核心固然很好,但这也增加了你的任务被重新安排到不同核心的可能性。内核必须“迁移”缓存线,这是昂贵的。我推荐阅读Ulrich Drepper所著的[《What Every Programmer Should Know About Main Memory》](https://akkadia.org/drepper/cpumemory.pdf),以了解更多关于这一方法的工作原理以及所涉及的性能损失。

那么上下文切换的代价如何随着工作集的大小而增加呢?这一次,我们将使用另一个微基准测试,[timectxswws.c](https://github.com/tsuna/contextswitch/blob/master/timectxswws.c),它接受要使用的页面数作为工作集的参数。这个基准测试与之前用于测试两个进程之间上下文切换成本的基准测试完全相同,只是现在每个进程在工作集中执行一个memset,这个memset在两个进程之间共享。开始之前,基准测试将计算在请求的工作集大小中覆盖所有页面所需的时间。这段时间将从考试所花费的总时间中扣除。这试图估计跨上下文切换覆盖页面的开销。
这里是5150的结果:

正如我们所看到的,一旦我们的工作集大于L1d (32K)所能容纳的容量,编写4K页面所需的时间就会增加一倍以上。每次上下文切换的时间会随着工作集大小的增加而不断增加,但超过某个点之后,基准测试就会由内存访问控制,不再实际测试上下文切换的开销,而只是测试内存子系统的性能。
相同的测试,但这次有CPU关联性(两个进程固定在同一个核心上): 

哇,看这个!当将两个进程固定在同一个核心上时,速度要快一个数量级!工作集是共享的,因为工作集完全适合在4米L2缓存和从L2高速缓存线路只需要转移到L1d,而不是转移从核心核心(可能在两个物理CPU,它是昂贵得多比在同一个CPU)。

注意,这一次我讨论了更大的工作集大小,因此在X轴上使用对数尺度。

:所以,i7的上下文切换速度更快,但时间也就这么长。真正的应用程序(尤其是Java应用程序)往往具有大型工作集,因此在进行上下文切换时通常要付出最高的代价。关于i7年使用的Nehalem架构的其他观察:

  • - 从L1到L2几乎是不明显的。用工作集写一个适合L1d (32K)的页面大约需要130纳秒,适合L2 (256K)的页面只需要180纳秒。在这方面,Nehalem上的L2更像是一个“L1.5”,因为它的延迟根本无法与之前CPU代的L2相比。
  • - 一旦工作集增加到1024K以上,写一个页面所需的时间就会跳到750ns。我的理论是,1024K = 256页=核心TLB的一半,这是由两个hyperthread共享的。因为现在两个超线程都在争夺TLB项,CPU核心一直在进行页表查找。

说到TLB, Nehalem有一个有趣的架构。每个核都有一个64项“L1d TLB”(没有“L1i TLB”)和一个统一的512项“L2TLB”。两者都是在两个超线程之间动态分配的。

Virtualization


我想知道使用虚拟化时的开销是多少。我对双E5440重复了基准测试,一次是在普通的Linux安装中,一次是在VMware ESX服务器中运行相同的安装。结果是,在使用虚拟化时进行上下文切换的平均成本要高出2.5到3倍。我猜测这是由于这样的事实,guest操作系统不能更新页表本身,因此,当它试图改变它,hypervisor干预,导致额外的2次上下文切换(一个内部管理程序,一个出去,回到guest操作系统)。
这可能解释了为什么英特尔添加了EPT Nehalem(扩展页表),因为它使来宾操作系统修改自己的页表的程序,和CPU能够做端到端内存地址转换,完全在硬件(虚拟地址客户物理地址到物理地址)。

Parting words


上下文切换非常昂贵。我的经验是,它将花费你大约30µ的CPU开销。这似乎是一个很好的最坏情况近似。
如果应用程序创建了过多的线程,而这些线程又在不断地争夺CPU时间(例如Apache的HTTPd或许多Java应用程序),那么仅仅为了在不同线程之间来回切换,就会浪费大量的CPU周期。我认为优化CPU使用的最佳点是拥有与硬件线程相同数量的工作线程,并以异步/非阻塞方式编写代码。异步代码往往受到CPU的限制,因为任何可能阻塞的东西都被延迟到稍后,直到阻塞操作完成。这意味着异步/非阻塞应用程序中的线程更有可能在内核调度器抢占它们之前使用它们的全时间量。如果可运行线程的数量与硬件线程的数量相同,那么内核很可能会重新调度同一内核上的线程,这将极大地提高性能。
另一个严重影响服务器类型工作负载的隐性成本是,在切换出去之后,即使您的进程变成可运行的,它也必须在内核的运行队列中等待,直到有一个CPU内核可用为止。Linux内核通常使用HZ=100进行编译,这要求给进程分配10ms的时间片。如果你的线程已经被切换掉,但是几乎立刻就可以运行了,并且在它之前还有另外两个线程在运行队列中等待CPU时间,那么在最坏的情况下,你的线程可能需要等待20毫秒才能获得CPU时间。因此,根据运行队列的平均长度(反映在平均负载中),以及线程在再次切换之前通常运行的时间,这可能会对性能产生相当大的影响。
:假设NPTL或Nehalem体系结构在真实的服务器类型工作负载中降低了上下文切换的成本,这是一种错觉。默认的Linux内核在保持CPU亲和性方面做得不好,即使是在空闲机器上。您必须研究其他调度程序,或使用任务集或cpuet自己来控制关联。如果您在同一台服务器上运行多个不同的cpu密集型应用程序,那么在应用程序之间手动划分内核可以帮助您获得非常显著的性能提升。
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值