最初我认为上下文切换的开销是TLB被刷新。但是我刚刚在维基百科上看到了:
http://en.wikipedia.org/wiki/Translation_lookaside_buffer
2008年,英特尔(Nehalem)[18]和AMD(SVM)[19]都推出了 标签作为TLB条目的一部分和检查的专用硬件 查找期间的标记。即使这些没有得到充分利用,也是如此 设想将来,这些标签将识别地址 每个TLB条目所属的空间。 因此上下文切换不会 导致刷新TLB - 但只是改变了标签 当前地址空间到新任务的地址空间的标记。
以上是否确认较新的Intel CPU TLB在上下文切换时没有刷新?
这是否意味着现在在上下文切换中没有真正的开销?
(我试图理解上下文切换的性能损失)
答案 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域内的CPU。使用PCID时,您将不能依靠这些技巧,并且会产生更高的开销,因为没有经常避免“多CPU TLB击落”。
当然,与ID管理相关的还有一些成本(例如,找出哪个ID可自由分配给新创建的任务,在任务终止时撤销ID,某种重新使用ID的“最近最少使用”的系统当虚拟地址空间多于ID等时。
由于这些成本,在某些情况下,使用PCID的成本必定会超过“由于任务切换而导致的TLB丢失少”带来的好处(使用PCID会使性能变差)。
答案 3 :(得分:0)
注意:正如布伦丹(Brendan)在其评论中指出的那样。这个答案的目的是回答细节上下文切换对Windows服务器/桌面性能的总体影响是什么,包括Windows,Linux,Solaris等操作系统开销的操作系统开销。。
找出答案的最佳方法当然是对其进行基准测试。这里的问题是每秒上下文切换数与CPU时间之间的关系是指数的。换句话说,这是O(n 2 )成本。意味着我们有一个最大限制,根本不能超过。
下面的基准代码使用了一些不安全的变量,等等。。。这不是重点。
每个线程完成的实际工作最少。从理论上讲,每个线程每秒应生成1000个上下文切换。
有理由说2560个线程应该生成大约2%的CPU,但是在2300个线程上(在我的Core i7-4790K 4 Core + 4超线程Core台式机上),CPU达到100%。
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();
}
}
}