如何估算线程上下文切换的开销?

66

我试图提高实时截止期限下多线程应用程序的性能。它在Windows Mobile上运行,使用C/C++编写。我怀疑频繁的线程切换可能会导致明显的开销,但既不能证明也不能否定。众所周知,缺乏证据并不是相反证据。

因此我的问题有两个方面:

  • 如果存在的话,我该在哪里找到任何关于线程上下文切换成本的实际测量数据?

  • 在不花费时间编写测试应用程序的情况下,如何估算现有应用程序中的线程切换开销?

  • 是否有人知道一种方法可以查找给定线程的上下文切换(开/关)次数?


5
我认为线程切换严重依赖于每个线程所包含的“内存”和状态数量。如果你的所有线程都在大型位图上执行大量工作,那么线程切换可能会非常昂贵。而一个简单递增单个计数器的线程则具有非常小的线程切换开销。 - CodingBarfield
被接受的答案是错误的。上下文切换之所以昂贵,是因为缓存失效。当然,如果你只对线程切换进行基准测试,并且使用计数器递增,它看起来很快,但这是一个不现实的毫无价值的基准测试。当上下文只是计数器寄存器时,它甚至不是真正的上下文切换。 - bokan
9个回答

30
我怀疑你在网络上找不到任何现有平台的这种开销。因为存在太多不同的平台。这个开销取决于两个因素:
- CPU,因为必要的操作可能在不同的CPU类型上更容易或更困难。 - 系统内核,因为不同的内核将在每次切换时执行不同的操作。
其他因素包括切换发生的方式。切换可以在以下情况下发生:
- 线程已经使用了所有时间片。当线程启动时,它可能会在返回控制权给内核之前运行一定的时间量,内核会决定谁是下一个。 - 线程被抢占。当另一个线程需要CPU时间并具有更高的优先级时,就会发生这种情况。例如,处理鼠标/键盘输入的线程可能是这样的线程。无论当前线程拥有CPU的时间还是其他线程,当用户键入某些内容或单击某些内容时,他都不想等待当前线程的时间片完全用完,他希望立即看到系统反应。因此,某些系统将使当前线程立即停止,并将控制权返回给具有更高优先级的其他线程。 - 线程不再需要CPU时间,因为它正在阻塞某些操作或只是调用sleep()(或类似的操作)以停止运行。
理论上,这3种情况可能具有不同的线程切换时间。例如,我期望最后一种情况最慢,因为对sleep()的调用意味着CPU被归还给内核,内核需要设置一个唤醒调用,以确保线程在请求的大约时间后被唤醒,然后必须将线程从调度过程中取出,并且一旦线程被唤醒,它必须再次将线程添加到调度过程中。所有这些步骤都需要一定的时间。因此,实际的sleep-call可能比切换到另一个线程所需的时间更长。
我认为,如果你想确定,你必须进行基准测试。问题是,你通常必须将线程置于睡眠状态,或者你必须使用互斥锁进行同步。睡眠或锁定/解锁互斥锁本身也具有开销。这意味着你的基准测试也将包括这些开销。如果没有强大的分析器,很难事后说出实际切换使用了多少CPU时间,以及睡眠/互斥调用使用了多少CPU时间。另一方面,在实际场景中,你的线程也将通过睡眠或锁定进行同步。如果基准测试仅纯粹测量上下文切换时间,则是一种合成基准测试,因为它不模拟任何实际场景。如果一个GPU基准测试告诉我我的GPU理论上可以处理20亿个多边形/秒,那有什么用呢?如果这个结果在实际的3D应用程序中永远无法实现,那么了解一个真实的3D应用程序可以让GPU每秒处理多少个多边形会更有趣。

很不幸,我对Windows编程一无所知。我可以用Java或者C#为Windows编写应用程序,但是在Windows上使用C/C++却让我感到很困难。我只能提供一些适用于POSIX系统的源代码。

#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <pthread.h>
#include <sys/time.h>
#include <unistd.h>

uint32_t COUNTER;
pthread_mutex_t LOCK;
pthread_mutex_t START;
pthread_cond_t CONDITION;

void * threads (
    void * unused
) {
    // Wait till we may fire away
    pthread_mutex_lock(&START);
    pthread_mutex_unlock(&START);

    pthread_mutex_lock(&LOCK);
    // If I'm not the first thread, the other thread is already waiting on
    // the condition, thus Ihave to wake it up first, otherwise we'll deadlock
    if (COUNTER > 0) {
        pthread_cond_signal(&CONDITION);
    }
    for (;;) {
        COUNTER++;
        pthread_cond_wait(&CONDITION, &LOCK);
        // Always wake up the other thread before processing. The other
        // thread will not be able to do anything as long as I don't go
        // back to sleep first.
        pthread_cond_signal(&CONDITION);
    }
    pthread_mutex_unlock(&LOCK); //To unlock
}

int64_t timeInMS ()
{
    struct timeval t;

    gettimeofday(&t, NULL);
    return (
        (int64_t)t.tv_sec * 1000 +
        (int64_t)t.tv_usec / 1000
    );
}


int main (
    int argc,
    char ** argv
) {
    int64_t start;
    pthread_t t1;
    pthread_t t2;
    int64_t myTime;

    pthread_mutex_init(&LOCK, NULL);
    pthread_mutex_init(&START, NULL);   
    pthread_cond_init(&CONDITION, NULL);

    pthread_mutex_lock(&START);
    COUNTER = 0;
    pthread_create(&t1, NULL, threads, NULL);
    pthread_create(&t2, NULL, threads, NULL);
    pthread_detach(t1);
    pthread_detach(t2);
    // Get start time and fire away
    myTime = timeInMS();
    pthread_mutex_unlock(&START);
    // Wait for about a second
    sleep(1);
    // Stop both threads
    pthread_mutex_lock(&LOCK);
    // Find out how much time has really passed. sleep won't guarantee me that
    // I sleep exactly one second, I might sleep longer since even after being
    // woken up, it can take some time before I gain back CPU time. Further
    // some more time might have passed before I obtained the lock!
    myTime = timeInMS() - myTime;
    // Correct the number of thread switches accordingly
    COUNTER = (uint32_t)(((uint64_t)COUNTER * 1000) / myTime);
    printf("Number of thread switches in about one second was %u\n", COUNTER);
    return 0;
}

输出

Number of thread switches in about one second was 108406

虽然我们使用了锁和条件等待,但是超过100,000并不算太糟糕。我猜如果没有这些东西,每秒钟可能会有至少两倍的线程切换。


17
你理解不了“Unfortunately I know nothing of Windows programming...I can only offer you some source code for POSIX.”这句话的哪个部分?我只能提供一些适用于POSIX的源代码,对于Windows编程我一无所知。 - Mecki
7
我完全理解,但是你的回答并没有帮助到那位提出原问题的人,而我们的目的在于帮助那些提问的人。 - ctacke

14

无法估计它,您需要测量它。而它将根据设备中的处理器而变化。

有两种相当简单的方法来测量上下文切换。其中一种涉及代码,另一种则不需要。

首先是使用代码的方式(伪代码):

DWORD tick;

main()
{
  HANDLE hThread = CreateThread(..., ThreadProc, CREATE_SUSPENDED, ...);
  tick = QueryPerformanceCounter();
  CeSetThreadPriority(hThread, 10); // real high
  ResumeThread(hThread);
  Sleep(10);
}

ThreadProc()
{
  tick = QueryPerformanceCounter() - tick;
  RETAILMSG(TRUE, (_T("ET: %i\r\n"), tick));
}

显然,使用循环和平均值会更好。请记住,这不仅仅是测量上下文切换。您还在测量ResumeThread的调用,而且不能保证调度程序会立即切换到另一个线程(尽管优先级为10应该有助于增加它将切换的机率)。
通过钩入调度程序事件,CeLog可以提供更准确的测量结果,但是做起来远非简单,并且文档也不是很充分。如果您真的想走这条路,Sue Loh在其博客中写了一些相关内容可以通过搜索引擎找到。
在不编写代码的情况下,使用Remote Kernel Tracker也是一种选择。安装eVC 4.0或Platform Builder的评估版即可获得它。它会显示内核正在执行的所有操作,并且您可以使用提供的光标功能直接测量线程上下文切换。同样,我确定Sue也在她的博客中写过有关使用Kernel Tracker的内容。
所有这些说法都是,您会发现CE进程内线程上下文切换非常快。昂贵的是进程切换,因为它需要在RAM中交换活动进程,然后进行迁移。

如果您的应用程序没有执行填充缓存的实际工作,那么测量上下文切换就没有意义。(请查看我在底部的答案)。 - bokan

12

尽管你说你不想编写一个测试应用程序,但我在之前的ARM9 Linux平台测试中完成了这个任务,以找出额外开销。其中只有两个线程执行boost::thread::yield()(或其他操作)并增加一些变量,经过一分钟左右(没有其他正在运行的进程,至少没有做任何事情的进程),该应用程序打印出每秒可以执行多少次上下文切换。当然,这并不是真正的精确数据,但重点是两个线程彼此让出CPU,而且速度非常快,所以再考虑额外开销就没有意义了。

因此,请简单地编写一个简单的测试,而不要过于思考可能不存在的问题。

除此之外,您还可以像1800建议的那样使用性能计数器。

哦,我记得在运行Windows CE 4.X的应用程序中,我们也有四个线程进行了密集的切换,但从未遇到过性能问题。我们还尝试过完全不使用线程来实现核心线程功能,但没有看到性能改善(GUI响应速度慢了很多,但其他所有内容都是相同的)。也许您可以尝试相同的方法,通过减少上下文切换次数或完全删除线程(仅用于测试)来解决问题。


2
谢谢,这个肯定切换时间很短的说法正是我需要的。 - Ignas Limanauskas
除非您的应用程序只是递增计数器,否则这个答案是错误的。上下文切换非常昂贵。不是因为CPU操作,而是因为在切换到另一个线程时所有缓存都无效了。您必须使用真实工作对填充每个线程的缓存进行基准测试。 - bokan

11

上下文切换非常昂贵。这不是因为CPU操作本身,而是因为缓存失效。如果您有一个密集的任务正在运行,它会填充CPU缓存,包括指令和数据,还有内存预取、TLB和RAM将优化工作区域的一些方面。

当您更改上下文时,所有这些缓存机制都将被重置,新线程将从“空白”状态开始。

接受的答案是错误的,除非您的线程只是递增计数器。当然,在这种情况下没有涉及缓存刷新。在没有像真实应用程序那样填充缓存的情况下,对上下文切换进行基准测试毫无意义。


8

我的50行C++代码展示了在Linux系统(QuadCore Q6600)中,上下文切换时间约为0.9微秒(2个线程为0.75微秒,50个线程为0.95微秒)。在此基准测试中,当线程获得时间量子时,它们立即调用yield。


3
.9 纳秒?你确定吗?...<翻找代码>你的代码似乎在计算毫秒/每次转换*1000-> 微秒。 - Ira Baxter
@IraBaxter,那不是纳秒,1000微秒==1毫秒,1000毫秒==1秒。 - Scott 混合理论
考虑到现在是CFS,它可能需要重新测试... - bobah
但是,每毫秒>1000个开关,这是肯定的,在CFS中会更多。 - bobah
1
@Scott:请检查消息编辑历史记录。它曾经写着“纳秒”。 - Ira Baxter
显示剩余2条评论

7

5
我只尝试过一次估算这个,那时候是在一台486上!结果是处理器上下文切换需要完成大约70条指令(请注意,这种情况发生在许多操作系统API调用以及线程切换中)。我们计算出,在DX3上每个线程切换需要大约30微秒(包括操作系统开销)。我们每秒进行的几千次上下文切换吸收了处理器时间的5-10%。
我不知道这将如何转化为多核、多GHz的现代处理器,但我猜想,除非你完全过度使用线程切换,否则它是可以忽略不计的开销。
请注意,与激活/停用线程相比,线程创建/删除更耗费CPU /操作系统资源。对于高度线程化的应用程序,一个好的策略是使用线程池并根据需要激活/停用线程。

4
上下文切换的问题在于它们有一个固定的时间。GPU在线程之间实现了1个周期的上下文切换。例如,以下内容无法在线程上运行在CPU上:
double * a; 
...
for (i = 0; i < 1000; i ++)
{
    a[i] = a[i] + a[i]
}

因为其执行时间远小于上下文切换成本,在Core i7上,此代码大约需要1微秒(取决于编译器)。因此,上下文切换时间确实很重要,因为它定义了可以如何将小作业线程化。我想这也提供了一种有效测量上下文切换的方法。检查数组(在上面的示例中)必须有多长,以便与单线程相比,线程池中的两个线程开始显示出真正的优势。这可能很容易变成10万个元素,因此在同一个应用程序内,有效的上下文切换时间将在20us左右。
所有线程池使用的封装都必须计算到线程切换时间中,因为这就是最终结果。
Atmapuri

0

我不确定,但是你在Windows Mobile中是否有通常的性能计数器?你可以查看诸如每秒上下文切换之类的内容。虽然我不知道是否有一个特定的测量上下文切换时间的计数器。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接