进行上下文切换需要多长时间?

旧巷老猫 提交于 2020-08-12 03:13:14

进行上下文切换需要多长时间?

 
这是一个有趣的问题,我愿意花一些时间在上面。StumbleUpon的某个人提出了这样的假设:随着Nehalem架构(以Intel i7出售)的所有改进,上下文切换将更快。您将如何设计一个测试以凭经验找到该问题的答案?上下文切换到底有多贵?(tl;博士回答:非常昂贵

阵容

2011年4月21日更新:我添加了“至尊” Nehalem和低压Westmere。
2013年4月1日更新:添加了英特尔Sandy Bridge E5-2620。
我已经测试了4种不同的CPU:
  • Intel 5150(Woodcrest,基于旧的“ Core”架构,2.67GHz)。5150是双核,因此该机器总共有4个可用核。内核:2.6.28-19-服务器x86_64。
  • Intel E5440 (Harpertown,基于Penrynn架构,2.83GHz)。E5440是四核,因此该机器总共有8个核。内核:2.6.24-26-server x86_64。
  • Intel E5520(Gainestown,基于Nehalem架构,又名i7,2.27GHz)。E5520是四核,并且启用了超线程,因此该计算机总共有8个核或16个“硬件线程”。内核:2.6.28-18-通用x86_64。
  • Intel X5550(Gainestown,基于Nehalem架构,又名i7,2.67GHz )。X5550是四核的,并且启用了超线程功能,因此该计算机总共有8个核或16个“硬件线程”。注意:X5550在“服务器”产品系列中。该CPU的价格是前一个CPU的3倍。内核:2.6.28-15-服务器x86_64。
  • Intel L5630(Gulftown,基于Westmere架构,又名i7,2.13GHz )。L5630是四核,并启用了超线程,因此该计算机总共有8个核或16个“硬件线程”。注意:L5630是“低压” CPU。在同等价格下,该CPU理论上比非低压CPU的功能低16%。内核:2.6.32-29-server x86_64。
  • Intel E5-2620(Sandy Bridge-EP,基于Sandy Bridge架构,又名E5,2Ghz)。E5-2620是六核的,具有超线程功能,因此该计算机总共有12个核或24个“硬件线程”。内核:3.4.24 x86_64。
据我所知,所有CPU都设置为恒定的时钟频率(没有Turbo Boost或其他花哨的功能)。所有的Linux内核都是由Ubuntu构建和分发的。

第一个想法:使用系统调用(失败)

我的第一个想法是连续进行多次廉价的系统调用,花费多长时间并计算每个系统调用花费的平均时间。这些天来Linux上最便宜的系统调用似乎是gettid。事实证明,这是一种幼稚的方法,因为如今系统调用实际上不再引起完整的上下文切换,内核可以通过“模式切换”(从用户模式切换到内核模式,然后再回到用户模式)来摆脱困境。这就是为什么当我运行第一个测试程序时,vmstat上下文切换数量没有明显增加的原因。但这测试也很有趣,尽管这并不是我最初想要的。

源代码:timesyscall.c结果:
  • 英特尔5150:105ns / syscall
  • 英特尔E5440:87ns / syscall
  • 英特尔E5520:58ns / syscall
  • 英特尔X5550:52ns / syscall
  • 英特尔L5630:58ns / syscall
  • 英特尔E5-2620:67ns / syscall
现在很好,更昂贵的CPU性能明显更好(但是请注意,Sandy Bridge的成本略有增加)。但这并不是我们真正想知道的。因此,为了测试上下文切换的成本,我们需要强制内核取消调度当前进程,然后调度另一个进程。为了对CPU进行基准测试,我们需要让内核在一个紧密的循环中不执行任何操作。你会怎么做?

第二个想法: futex

我这样做的方法是滥用futexRTFM)。 futex是大多数线程库用于特定于Linux的低级原语,用于实现阻止操作,例如等待竞争的互斥对象,用完许可的信号量,条件变量和好友。如果您想了解更多信息,请阅读Ulrich Drepper 撰写的Futexes Are Tricky。无论如何,有了futex,就很容易挂起和恢复进程。我的测试所做的是,它派生了一个子进程,而父级和子级轮流等待futex。当父母等待时,孩子将其唤醒并继续等待,直到父母唤醒并继续等待。某种乒乓球:“我叫醒你,你叫醒我……”。

源代码:timectxsw.c 结果:
  • 英特尔5150:〜4300ns /上下文切换
  • 英特尔E5440:〜3600ns /上下文切换
  • 英特尔E5520:〜4500ns /上下文切换
  • Intel X5550:〜3000ns /上下文切换
  • Intel L5630:〜3000ns /上下文切换
  • 英特尔E5-2620:〜3000ns /上下文切换
注意:这些结果包括futex系统调用的开销。

现在,您必须一粒盐地获得这些结果。微型基准除了上下文切换外什么也不做。实际上,上下文切换非常昂贵,因为它会占用CPU缓存(如果您拥有L1,L2,L3和TLB,请记住,不要忘记TLB!)。

CPU亲和力

在SMP环境中,事情很难预测,因为性能可能会根据任务是否从一个核心迁移到另一个核心(尤其是跨物理CPU的迁移)而发生巨大变化。我再次运行基准测试,但是这次我将进程/线程固定在单个内核(或“硬件线程”)上。性能提升是惊人的。

源代码:cpubench.sh结果:
  • 英特尔5150:〜1900ns /进程上下文切换,〜1700ns /线程上下文切换
  • 英特尔E5440:〜1300ns /进程上下文切换,〜1100ns /线程上下文切换
  • 英特尔E5520:〜1400ns /进程上下文切换,〜1300ns /线程上下文切换
  • Intel X5550:〜1300ns /进程上下文切换,〜1100ns /线程上下文切换
  • Intel L5630:〜1600ns /进程上下文切换,〜1400ns /线程上下文切换
  • 英特尔E5-2620:〜1600ns /进程上下文切换,〜1300ns /线程上下文切换
性能提升: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%的高速缓存命中率。在现实世界中,由于高速缓存污染,在两个任务(线程或进程)之间切换通常会导致更高的惩罚。但是我们稍后会再讲。

线程与进程

在得出上述数字之后,我迅速批评了Java应用程序,因为在Java中创建大量线程是相当普遍的,并且在此类应用程序中上下文切换的成本很高。有人反驳说,是的,Java使用大量线程,但是使用Linux 2.6中的NPTL,线程已变得更快,更便宜。他们说,通常,在同一进程的两个线程之间切换时,无需执行TLB刷新。没错,您可以检查Linux内核的源代码(switch_mm位于中mmu_context.h):
静态嵌入式void switch_mm(struct mm_struct * prev,struct mm_struct * next, 结构task_struct * tsk) { unsigned cpu = smp_processor_id(); 如果(可能(上一个!=下一个)){ [...] load_cr3(next-> pgd); }其他{ [通常不重新加载cr3] } }
在此代码中,内核期望在具有不同内存结构的任务之间切换,在这种情况下,内核将更新CR3CR3是保存指向页表的指针的寄存器。写入CR3会自动在x86上导致TLB刷新。

不过实际上,在默认的内核调度程序和繁忙的服务器类型工作负载的情况下,很少会跳过跳过对的调用的代码路径load_cr3。另外,不同的线程倾向于具有不同的工作集,因此,即使您跳过此步骤,仍然会污染L1 / L2 / L3 / TLB缓存。我使用2个线程而不是2个进程重新运行了上述基准测试(来源:timetctxsw.c),但结果并没有明显的不同(这取决于调度和运气,但是平均而言,在许多运行中,如果未设置自定义CPU亲和力,则线程之间的切换通常仅快100ns)。

上下文切换中的间接成本:缓存污染

以上结果与罗切斯特大学一群人发表的论文相符:量化上下文切换的成本。在未指定的Intel Xeon(该论文写于2007年,因此CPU可能不太旧)上,它们的平均时间为3800ns。他们使用了我想到的另一种方法,该方法涉及向管道写入/从管道读取1个字节,以阻塞/解除阻塞几个进程。我认为(ab)使用futex会更好,因为futex实质上是向用户区公开了一些调度接口。

本文继续说明了上下文切换所涉及的间接成本,这是由于缓存干扰引起的。除了一定的工作集大小(大约是基准测试中L2缓存大小的一半)之外,上下文切换的成本也急剧增加(增加了2个数量级)。

我认为这是一个更现实的期望。不在线程之间共享数据会导致最佳性能,但这也意味着每个线程都有其自己的工作集,并且当一个线程从一个内核迁移到另一个内核时(或更糟糕的是,跨物理CPU迁移),缓存污染将是昂贵。不幸的是,当应用程序的活动线程比硬件线程多时,这种情况一直在发生。这就是为什么不创建比可用硬件线程更多的活动线程如此重要的原因,因为在这种情况下,Linux调度程序可以更轻松地在其上次使用的内核上重新调度相同的线程(“弱亲和力”)。

话虽这么说,如今,我们的CPU具有更大的缓存,甚至可以拥有L3缓存。
  • 5150:L1i和L1d = 32K,L2 = 4M
  • E5440:L1i和L1d = 32K,L2 = 6M
  • E5520:L1i和L1d = 32K,L2 = 256K /核心,L3 = 8M(与X5550相同)
  • L5630:L1i和L1d每个= 32K,L2 = 256K /核心,L3 = 12M
  • E5-2620:L1i和L1d每个= 64K,L2 = 256K /核心,L3 = 15M
请注意,对于E5520 / X5550 / L5630(市售为“ i7”)和Sandy Bridge E5-2520,L2缓存很小,但是每个内核只有一个L2缓存(启用HT,这使每个硬件线程128K)。L3缓存由每个物理CPU上的所有内核共享。

拥有更多核心是很棒的选择,但同时也增加了将您的任务重新安排到其他核心的机会。内核必须“迁移”高速缓存行,这很昂贵。我建议阅读Ulrich Drepper的《每个程序员应该了解的有关主内存知识》(是的,再来一次!),以了解有关它如何工作以及涉及的性能损失的更多信息。

那么上下文切换的成本如何随着工作集的大小而增加?这次,我们将使用另一个微基准timectxswws.c,该参数将用作工作集的页面数作为参数。该基准与先前用于测试两个流程之间的上下文切换成本的基准完全相同,只是现在每个流程都memset在工作集上进行操作,并在两个流程之间共享。在开始之前,基准测试将以要求的工作集大小覆盖所有页面花费的时间。然后将此时间从测试花费的总时间中扣除。这试图估计跨上下文切换覆盖页面的开销

以下是5150的结果:如我们所见,一旦工作集大于L1d(32K)的容量,编写4K页所需的时间将增加一倍以上。随着工作集大小的增加,每个上下文切换的时间不断增加,但是超过某个特定点时,基准测试将由内存访问控制,并且不再实际测试上下文切换的开销,它只是测试内存的性能子系统。

相同的测试,但是这次具有CPU亲和力(两个进程都固定在同一内核上):哇,看这个!这是一个数量级将两个进程固定在同一内核上时,速度更快!由于工作集是共享的,因此工作集完全适合4M L2缓存,并且缓存行仅需要从L2传输到L1d,而不是从核心到核心传输(可能跨越2个物理CPU,这要昂贵得多)而不是同一CPU内)。

现在是i7处理器的结果:请注意,这次我涵盖了较大的工作集大小,因此涉及了X轴上的对数刻度。

因此,是的,i7上的上下文切换速度更快,但是持续的时间如此之长。实际应用程序(尤其是Java应用程序)往往具有较大的工作集,因此在进行上下文切换时通常会付出最高的代价。有关i7中使用的Nehalem架构的其他观察结果:
  • 从L1到L2几乎是不明显的。使用适合L1d(32K)的工作集来写页面大约需要130ns,而适合L2d(256K)的页面只需要180ns。在这方面,Nehalem上的L2更像是“ L1.5”,因为它的延迟时间根本无法与上一代CPU的L2相提并论。
  • 一旦工作集增加到1024K以上,写页面所需的时间就跳到750ns。我的理论是1024K = 256页=核心TLB的一半,这是两个HyperThreads共享的。因为现在两个超线程都在争夺TLB条目,所以CPU核心一直在进行页表查找。
说到TLB,Nehalem具有有趣的架构。每个内核都有一个64项“ L1d TLB”(没有“ L1i TLB”)和一个统一的512项“ L2TLB”。两者都在两个HyperThreads之间动态分配。

虚拟化

我想知道使用虚拟化时会有多少开销。我重复了双E5440的基准测试,一次是在正常的Linux安装中,一次是在VMware ESX Server中运行相同的安装。结果是,使用虚拟化时进行上下文切换平均要贵2.5到3倍。我的猜测是,由于来宾操作系统无法更新页面表本身,因此,在尝试更改页面表时,系统管理程序会进行干预,这会导致额外的2次上下文切换(其中一个进入系统管理程序,一个下车,回到客户操作系统)。

这可能可以解释为什么英特尔添加了EPT(扩展页表)),因为它使来宾OS无需管理程序就可以修改其自己的页表,并且CPU能够完全通过硬件(虚拟地址为“访客物理”地址到物理地址)。

分词

上下文切换非常昂贵。我的经验法则是,这将花费您大约30µs的CPU开销。这似乎是一个很好的最坏情况下的近似值。创建太多线程而一直在争夺CPU时间的应用程序(例如Apache的HTTPd或许多Java应用程序)可能会浪费大量CPU周期,只是为了在不同线程之间来回切换。我认为,最佳CPU使用的好处是与硬件线程具有相同数量的工作线程,并以异步/非阻塞方式编写代码。异步代码倾向于受CPU约束,因为任何将阻塞的内容都会被推迟到稍后,直到阻塞操作完成为止。这意味着异步/非阻塞应用程序中的线程更有可能在内核调度程序抢占它们之前使用其全时量子。而且,如果可运行线程的数量与硬件线程的数量相同,则内核很有可能在同一内核上重新调度线程,从而极大地提高了性能。

另一个严重影响服务器类型工作负载的隐性成本是,在关闭服务器后,即使您的进程变得可运行,它也必须在内核的运行队列中等待,直到有可用的CPU内核为止。Linux内核通常使用编译HZ=100,这需要为进程分配10ms的时间片。如果您的线程已关闭但几乎立即可以运行,并且在运行队列中还有其他两个线程在等待CPU时间,则在最坏的情况下,您的线程可能必须等待长达20毫秒才能获得CPU时间。因此,取决于运行队列的平均长度(反映在平均负载中)以及线程在再次退出之前通常运行多长时间,这可能会严重影响性能。

可以想象,在实际服务器类型的工作负载中,NPTL或Nehalem体系结构使上下文切换的成本降低了。默认的Linux内核即使在闲置的计算机上也无法很好地保持CPU亲和力。您必须探索替代的调度程序,或者自己使用tasksetcpuset控制相似性。如果您在同一服务器上运行多个不同的CPU密集型应用程序,则在应用程序之间手动对核心进行分区可以帮助您获得非常可观的性能提升。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!