上下文切换的开销是多少?

时间:2014-02-19 17:24:41

标签: linux performance operating-system virtualization context-switch

最初我认为上下文切换的开销是TLB被刷新。但是我刚刚在维基百科上看到了:

http://en.wikipedia.org/wiki/Translation_lookaside_buffer

  

2008年,英特尔(Nehalem)[18]和AMD(SVM)[19]都推出了   标签作为TLB条目的一部分和检查的专用硬件   查找期间的标记。即使这些没有得到充分利用,也是如此   设想将来,这些标签将识别地址   每个TLB条目所属的空间。 因此上下文切换不会   导致刷新TLB - 但只是改变了标签   当前地址空间到新任务的地址空间的标记。

以上是否确认较新的Intel CPU TLB在上下文切换时没有刷新?

这是否意味着现在在上下文切换中没有真正的开销?

(我试图理解上下文切换的性能损失)

4 个答案:

答案 0 :(得分:17)

当维基百科知道in its Context switch文章时," 上下文切换是存储和恢复进程的状态(上下文)的过程,以便稍后可以从同一点恢复执行时间。&#34 ;.我假设在同一操作系统的两个进程之间切换上下文,而不是用户/内核模式转换(系统调用),它更快,不需要TLB刷新。

因此,OS内核需要大量时间将当前正在运行的进程的执行状态(所有,实际上所有,寄存器和许多特殊控制结构)保存到内存中,然后加载其他进程的执行状态(读入从记忆里)。如果需要,TLB flush将为交换机增加一些时间,但它只是总开销的一小部分。

如果您想查找上下文切换延迟,则lmbench基准工具http://www.bitmover.com/lmbench/与LAT_CTX测试http://www.bitmover.com/lmbench/lat_ctx.8.html

我无法找到nehalem的结果(phoronix套件中是否有lmbench?),但对于core2 and modern Linux,上下文切换可能需要5-7微秒。

对于上下文切换,还有1-3微秒的低质量测试http://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html的结果。无法从他的结果中获得非冲刷TLB的确切效果。

更新 - 您的问题应该是虚拟化,而不是流程上下文切换。

RWT在their article about Nehalem" Inside Nehalem:英特尔的未来处理器和系统中说。 TLB,页表和同步" 2008年4月2日David Kanter,Nehalem将TLID添加到TLB以更快地制作虚拟机/主机交换机(vmentry / vmexit):

  

Nehalem的TLB条目也通过引入“虚拟处理器ID”或VPID而巧妙地改变。每个TLB条目都缓存虚拟到物理地址转换......该转换特定于给定进程和虚拟机。只要处理器在虚拟客户机和主机实例之间切换,英特尔的旧CPU就会刷新TLB,以确保进程只访问允许触摸的内存。 VPID跟踪TLB中给定转换条目与哪个VM相关联,以便当VM退出并重新进入时,不必为了安全而刷新TLB。 ....通过降低VM转换的开销,VPID有助于提高虚拟化性能;英特尔估计,与Merom(即65nm Core 2)相比,Nehalem的往返VM转换延迟为40%,比45nm Penryn低约三分之一。

此外,您应该知道,在您在问题中引用的片段中," [18]"链接是" G. Neiger,A。Santoni,F。Leung,D。Rodgers和R. Uhlig。英特尔虚拟化技术:高效处理器虚拟化的硬件支持。英特尔技术期刊,10(3)。",因此这是有效虚拟化(快速访客 - 主机交换机)的功能。

答案 1 :(得分:8)

如果我们计算高速缓存失效(我们通常应该这样,并且实际上是上下文切换成本的最大贡献者),由于上下文切换导致的性能损失可能很大:

https://www.usenix.org/legacy/events/expcs07/papers/2-li.pdf(诚然有点过时,但我能找到的最好的)使其在100K-1M CPU周期范围内。从理论上讲,在最糟糕的情况下,多插槽服务器盒具有32M L3每插槽高速缓存,由64字节高速缓存行组成,完全随机访问,并且主RAM的L3 / 100周期的典型访问时间为40个周期,惩罚可以达到30M + CPU周期(!)。

根据个人经验,我会说它通常在几十K周期的范围内,但根据具体情况,它可能会有一个数量级的差异。

答案 2 :(得分:3)

让我们将任务转换的成本分为“直接成本”(任务转换代码本身的成本)和“间接成本”(TLB遗漏的成本等)。

直接费用

对于直接成本,这主要是保存上一个任务的状态(对用户空间可见的体系结构),然后为下一个任务加载状态的成本。视情况而定,这主要是因为它可能包含FPU / MMX / SSE / AVX状态,也可能不包含,而FPU / MMX / SSE / AVX状态可能会添加多达KiB的数据(尤其是涉及AVX时-例如,AVX2自身为512字节,而AVX 512本身超过2 KiB。)

请注意,存在“惰性状态加载”机制,可以避免加载(部分或全部)FPU / MMX / SSE / AVX状态的成本,并避免在未加载状态时保存该状态的成本;并且出于性能原因可以禁用此功能(如果几乎所有任务都使用该状态,则“需要加载使用状态的成本”陷阱/异常超出了您在任务切换期间避免执行此操作所节省的费用)或出于安全原因(例如,因为Linux中的代码确实“保存了使用情况”而不是“保存然后清除,如果使用了”,并将属于一个任务的数据保留在寄存器中,而这些任务可以通过推测性执行攻击由其他任务获得)。

还有其他一些成本(更新统计信息,例如“上一个任务使用的CPU时间”),确定新任务是否使用与旧任务相同的虚拟地址空间(例如,同一进程中的不同线程),等

间接费用

从本质上来说,间接成本是CPU拥有的所有“类似于缓存”的东西的有效性的损失-缓存本身,TLB,更高级别的分页结构缓存,所有分支预测的内容(分支方向,分支目标,返回缓冲区) )等。

间接成本可以分为3个原因。一种是间接成本,其发生是因为事物被任务开关完全冲洗掉了。过去,这主要是由于在任务切换期间刷新了TLB导致的TLB丢失。请注意,即使使用PCID,也可能发生这种情况-限制为4096个ID(并且使用“融化缓解”时,这些ID成对使用-对于每个虚拟地址空间,一个ID用于用户空间,而另一个ID用于用户空间。对于内核),这意味着当使用的虚拟地址空间超过4096(或2048)时,内核必须回收先前使用的ID,并刷新所有TLB以重新使用该ID。但是,现在(伴随所有推测性执行安全性问题),内核可能会刷新其他内容(例如分支预测的内容),以使信息不会从一项任务泄漏到另一项任务,但是我真的不知道Linux是否做了或没有做。不支持这种“类似缓存”的东西(我怀疑它们主要是试图防止数据从内核泄漏到用户空间,并最终防止数据从一个任务泄漏到另一个任务)。

间接成本的另一个原因是容量限制。例如,如果L2缓存最多只能缓存256 KiB的数据,而先前的任务使用的缓存多于256 KiB的数据;那么L2高速缓存将充满对下一个任务无用的数据,并且由于“最近最少使用”,已将下一个任务要缓存(并且以前已缓存)的所有数据清除了。这适用于所有“类似缓存”的东西(包括TLB和更高级的页面结构缓存,即使使用PCID功能也是如此)。

间接成本的另一个原因是将任务迁移到另一个CPU。这取决于哪个CPU-例如如果将任务迁移到同一内核中的其他逻辑CPU,则两个CPU可能会共享很多类似“缓存”的内容,并且迁移成本可能会相对较小;如果将任务迁移到其他物理程序包中的CPU,则两个CPU都不会共享“类似于缓存”的内容,并且迁移成本可能会相对较大。

请注意,间接成本幅度的上限取决于任务的作用。例如,如果任务使用大量数据,则间接成本可能相对较高(大量缓存和TLB未命中),并且如果任务使用少量数据,则间接成本可以忽略不计(非常少的缓存和TLB未命中。)

不相关

请注意,PCID功能有其自己的成本(与任务开关本身无关)。特别;当在一个CPU上修改页面翻译时,可能需要使用称为“多CPU TLB击落”的方法使其他CPU上的页面翻译无效,这相对昂贵(涉及IPI /处理器间中断,该中断会打乱其他CPU,并且成本“低数百每个CPU的周期数)。没有PCID,您可以避免其中的一些。例如,如果没有PCID,对于在一个CPU上运行的单线程进程,您知道没有其他CPU可以使用相同的虚拟地址空间,因此知道您不需要执行“多CPU TLB击倒” ”,并且如果多线程进程仅限于单个NUMA域,则“多CPU TLB击落”仅需要涉及该NUMA域内的CP​​U。使用PCID时,您将不能依靠这些技巧,并且会产生更高的开销,因为没有经常避免“多CPU TLB击落”。

当然,与ID管理相关的还有一些成本(例如,找出哪个ID可自由分配给新创建的任务,在任务终止时撤销ID,某种重新使用ID的“最近最少使用”的系统当虚拟地址空间多于ID等时。

由于这些成本,在某些情况下,使用PCID的成本必定会超过“由于任务切换而导致的TLB丢失少”带来的好处(使用PCID会使性能变差)。

答案 3 :(得分:0)

注意:正如布伦丹(Brendan)在其评论中指出的那样。这个答案的目的是回答细节上下文切换对Windows服务器/桌面性能的总体影响是什么,包括Windows,Linux,Solaris等操作系统开销的操作系统开销。

找出答案的最佳方法当然是对其进行基准测试。这里的问题是每秒上下文切换数与CPU时间之间的关系是指数的。换句话说,这是O(n 2 )成本。意味着我们有一个最大限制,根本不能超过。

下面的基准代码使用了一些不安全的变量,等等。。。这不是重点。

每个线程完成的实际工作最少。从理论上讲,每个线程每秒应生成1000个上下文切换。

  • 将以下代码转储到.NET控制台应用程序中,并在perfmon中查看结果。
  • 向Perfmon添加两个计数器:处理器->%处理器时间系统->每秒上下文切换。在8核计算机上,128个线程从这些线程完成的工作中产生大约0.1%的CPU开销。

有理由说2560个线程应该生成大约2%的CPU,但是在2300个线程上(在我的Core i7-4790K 4 Core + 4超线程Core台式机上),CPU达到100%。

  • 2048个线程-每秒200万个上下文切换:CPU占40%
  • 2300个线程-每秒230万个上下文切换:100%的CPU

perfmon graph

static void Main(string[] args)
{
    ThreadTestClass ThreadClass;
    bool Wait;
    int Counter;
    Wait = true;
    Counter = 0;
    while (Wait)
    {
        if (Console.KeyAvailable)
        {
            ConsoleKey Key = Console.ReadKey().Key;
            switch (Key)
            {
                case ConsoleKey.UpArrow:
                    ThreadClass = new ThreadTestClass();
                    break;
                case ConsoleKey.DownArrow:
                    SignalExitThread();
                    break;
                case ConsoleKey.PageUp:
                    SleepTime += 1;
                    break;
                case ConsoleKey.PageDown:
                    SleepTime -= 1;
                    break;
                case ConsoleKey.Insert:
                    for (int I = 0; I < 64; I++)
                    {
                        ThreadClass = new ThreadTestClass();
                    }
                    break;
                case ConsoleKey.Delete:
                    for (int I = 0; I < 64; I++)
                    {
                        SignalExitThread();
                    }
                    break;
                case ConsoleKey.Q:
                    Wait = false;
                    break;
                case ConsoleKey.Spacebar:
                    Wait = false;
                    break;
                case ConsoleKey.Enter:
                    Wait = false;
                    break;
            }
        }
        Counter += 1;
        if (Counter >= 10)
        {
            Counter = 0;
            Console.WriteLine(string.Concat(@"Thread Count: ", NumThreadsActive.ToString(), @" - SleepTime: ", SleepTime.ToString(), @" - Counter: ", UnSafeCounter.ToString()));
        }
        System.Threading.Thread.Sleep(100);
    }
    IsActive = false;
}

public static object SyncRoot = new object();
public static bool IsActive = true;
public static int SleepTime = 1;
public static long UnSafeCounter = 0;
private static int m_NumThreadsActive;
public static int NumThreadsActive
{
    get
    {
        lock(SyncRoot)
        {
            return m_NumThreadsActive;
        }
    }
}
private static void NumThreadsActive_Inc()
{
    lock (SyncRoot)
    {
        m_NumThreadsActive += 1;
    }
}
private static void NumThreadsActive_Dec()
{
    lock (SyncRoot)
    {
        m_NumThreadsActive -= 1;
    }
}
private static int ThreadsToExit = 0;
private static bool ThreadExitFlag = false;
public static void SignalExitThread()
{
    lock(SyncRoot)
    {
        ThreadsToExit += 1;
        ThreadExitFlag = (ThreadsToExit > 0);
    }
}

private static bool ExitThread()
{
    if (ThreadExitFlag)
    {
        lock (SyncRoot)
        {
            ThreadsToExit -= 1;
            ThreadExitFlag = (ThreadsToExit > 0);
            return (ThreadsToExit >= 0);
        }
    }
    return false;
}

public class ThreadTestClass
{
    public ThreadTestClass()
    {
        System.Threading.Thread RunThread;
        RunThread = new System.Threading.Thread(new System.Threading.ThreadStart(ThreadRunMethod));
        RunThread.Start();
    }

    public void ThreadRunMethod()
    {
        long Counter1;
        long Counter2;
        long Counter3;
        Counter1 = 0;
        NumThreadsActive_Inc();
        try
        {
            while (IsActive && (!ExitThread()))
            {
                UnSafeCounter += 1;
                System.Threading.Thread.Sleep(SleepTime);
                Counter1 += 1;
                Counter2 = UnSafeCounter;
                Counter3 = Counter1 + Counter2;
            }
        }
        finally
        {
            NumThreadsActive_Dec();
        }
    }
}