代码之家  ›  专栏  ›  技术社区  ›  Ignas Limanauskas

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

  •  65
  • Ignas Limanauskas  · 技术社区  · 17 年前

    我正试图通过实时截止日期来提高线程化应用程序的性能。它运行在Windows Mobile上,用C/C++编写。我怀疑频繁的线程切换可能会导致有形的开销,但既不能证明也不能反驳。众所周知,缺乏证据并不是相反的证明:)。

    因此,我的问题是双重的:

    • 如果存在,我在哪里可以找到切换线程上下文成本的实际测量值?

    • 在不花费时间编写测试应用程序的情况下,有什么方法可以估算现有应用程序中的线程切换开销?

    • 有人知道一种方法来找出给定线程的上下文开关(开/关)的数量吗?

    9 回复  |  直到 17 年前
        1
  •  28
  •   SH' Mecki    8 年前

    我怀疑你在任何现有平台的网络上都找不到这种开销。存在太多不同的平台。开销取决于两个因素:

    • CPU,因为在不同的CPU类型上,必要的操作可能更容易或更难
    • 系统内核,因为不同的内核必须在每个交换机上执行不同的操作

    其他因素包括转换是如何发生的。当发生以下情况时,可以进行切换

    1. 线程已经使用了它所有的时间量。当一个线程启动时,它可能会运行一段给定的时间,然后必须将控制权返回给内核,由内核决定下一个是谁。

    2. 线程被抢占。当另一个线程需要CPU时间并且具有更高的优先级时,就会发生这种情况。例如,处理鼠标/键盘输入的线程可以是这样的线程。无论什么线索 拥有 CPU现在,当用户键入或单击某些内容时,他不想等到当前线程的时间量完全用完,他想看到系统立即做出反应。因此,一些系统会使当前线程立即停止,并将控制权返回给具有更高优先级的其他线程。

    3. 线程不再需要CPU时间,因为它阻塞了某些操作,或者只是调用sleep()(或类似方法)停止运行。

    理论上,这3种情况可能有不同的线程切换时间。例如,我预计最后一个线程最慢,因为调用sleep()意味着CPU被还给内核,内核需要设置一个唤醒调用,以确保线程在请求睡眠的时间后被唤醒,然后它必须将线程从调度进程中取出,一旦线程被唤醒,它必须再次将线程添加到调度进程中。所有这些浸泡都需要一些时间。因此,实际的睡眠调用可能比切换到另一个线程所需的时间长。

    我认为,如果你想确定,你必须进行基准测试。问题是,你通常必须要么让线程休眠,要么必须使用互斥同步它们。睡眠或锁定/解锁静音本身就有开销。这意味着您的基准测试也将包括这些开销。如果没有强大的分析器,很难在以后说出实际切换使用了多少CPU时间,以及睡眠/互斥调用使用了多少时间。另一方面,在现实生活中,你的线程要么休眠,要么通过锁同步。纯粹衡量上下文切换时间的基准是一种综合基准,因为它不模拟任何现实生活场景。如果基准基于现实生活场景,则更“现实”。告诉我,如果在现实生活中的3D应用程序中永远无法实现这一结果,那么理论上我的GPU每秒可以处理20亿个多边形的GPU基准有什么用?知道一个现实生活中的3D应用程序一秒钟可以处理多少个多边形,不是更有趣吗?

    不幸的是,我对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
    

    超过100000并不算太糟糕,尽管我们有锁定和条件等待。我想,如果没有这些东西,一秒钟内至少有两倍的线程切换是可能的。

        2
  •  14
  •   ctacke    17 年前

    你无法估计它。你需要测量它。而且它会因设备中的处理器而异。

    有两种相当简单的方法来衡量上下文切换。一个涉及代码,另一个不涉及。

    首先,代码方式(伪代码):

    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有几个博客,搜索引擎可以找到。

    非代码途径是使用远程内核跟踪器。安装eVC 4.0或Platform Builder的eval版本以获取它。它将以图形方式显示内核正在做的一切,您可以使用提供的游标功能直接测量线程上下文切换。同样,我确信Sue也有一篇关于使用Kernel Tracker的博客文章。

    综上所述,你会发现CE进程内线程上下文切换非常非常快。进程切换是昂贵的,因为它需要交换RAM中的活动进程,然后进行迁移。

        3
  •  12
  •   OregonGhost    17 年前

    虽然你说你不想编写一个测试应用程序,但我在ARM9 Linux平台上进行了之前的一次测试,以找出开销是多少。只有两个线程可以增强::thread::yield()(或者,你知道)并增加一些变量,大约一分钟后(没有其他正在运行的进程,至少没有任何进程可以做点什么),应用程序会打印出它每秒可以做多少上下文切换。当然,这并不完全准确,但关键是两个线程都将CPU交给了对方,而且速度太快了,再考虑开销就没有意义了。 因此,只需编写一个简单的测试,而不是过多思考可能不存在的问题。

    除此之外,您可以尝试使用性能计数器建议的1800。

    哦,我记得在Windows CE 4.X上运行的一个应用程序,在那里我们还有四个线程,有时会进行密集的切换,并且从未遇到性能问题。我们还尝试在没有线程的情况下实现核心线程,但没有看到性能的提高(GUI的响应速度要慢得多,但其他一切都是一样的)。也许你可以尝试同样的方法,要么减少上下文切换的数量,要么完全删除线程(仅用于测试)。

        4
  •  7
  •   bobah    15 年前

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

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

    除非你的线程只是递增一个计数器,否则接受的答案是错误的。当然,在这种情况下不涉及缓存刷新。在不填充缓存的情况下对上下文切换进行基准测试是没有意义的。

        5
  •  6
  •   Soroush    14 年前

    我的 50 lines of C++ 显示Linux(QuadCore Q6600)的上下文切换时间约为0.9us(2个线程为0.75us,50个线程为0.95)。在这个基准测试中,线程在获得一定时间后立即调用yield。

        6
  •  6
  •   bokan    7 年前

    上下文切换很昂贵,根据经验,它需要花费30秒的CPU开销 http://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html

        7
  •  5
  •   Tim Ring    17 年前

    我只试着估计过一次,那是486!结果是,处理器上下文切换需要大约70条指令才能完成(请注意,许多操作系统api调用以及线程切换都会发生这种情况)。我们计算出,在DX3上,每个线程切换大约需要30us(包括操作系统开销)。我们每秒进行的几千次上下文切换占用了5-10%的处理器时间。

    我不知道这将如何转化为多核、多ghz的现代处理器,但我想,除非你完全过度使用线程切换,否则它的开销可以忽略不计。

    请注意,创建/删除线程比激活/停用线程更占用CPU/OS资源。对于多线程应用程序,一个好的策略是使用线程池并根据需要激活/停用。

        8
  •  4
  •   Jared Burrows    11 年前

    上下文切换的问题在于它们有固定的时间。GPU实现了线程之间的1周期上下文切换。例如,以下内容不能进行螺纹连接 在CPU上:

    double * a; 
    ...
    for (i = 0; i < 1000; i ++)
    {
        a[i] = a[i] + a[i]
    }
    

    因为它的执行时间远低于上下文切换成本。在Core i7上,这段代码 大约需要1微秒(取决于编译器)。因此,上下文切换时间确实很重要,因为它定义了如何线程化小作业。我想这也为有效测量上下文切换提供了一种方法。检查数组(在上面的示例中)必须有多长时间,这样与单线程线程相比,线程池中的两个线程将开始显示出一些真正的优势。这可能很容易变成100000个元素,因此在同一个应用程序中,有效的上下文切换时间将在20us的范围内。

    线程池使用的所有封装都必须计入线程切换时间,因为这就是它的最终结果。

    阿特马普里

        9
  •  1
  •   1800 INFORMATION    17 年前

    我不知道,但你们有windows mobile中常见的性能计数器吗?你可以看看上下文切换/秒之类的东西。但我不知道是否有一个专门测量上下文切换时间的东西。