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

Posted

技术标签:

【中文标题】如何估计线程上下文切换开销?【英文标题】:How to estimate the thread context switching overhead? 【发布时间】:2010-09-23 05:10:43 【问题描述】:

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

因此我的问题是双重的:

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

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

有谁知道找出给定线程的上下文切换次数(开/关)的方法吗?

【问题讨论】:

我相信线程切换在很大程度上取决于“内存”的数量并声明单个线程“包含”。如果你所有的线程在巨大的位图上做很多工作,线程切换可能会非常昂贵。简单地增加单个计数器的线程具有非常小的线程切换开销。 接受的答案是错误的。由于缓存失效,上下文切换代价高昂。当然,如果您仅使用计数器增量对线程切换进行基准测试,它似乎很快,但这是一个不切实际的毫无价值的基准测试。当上下文只是计数器寄存器时,它甚至不是真正的上下文切换。 【参考方案1】:

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

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

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

【讨论】:

【参考方案2】:

我怀疑您是否可以在任何现有平台的网络上找到此开销。存在太多不同的平台。开销取决于两个因素:

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

其他因素包括转换的发生方式。当

    线程已使用其所有时间片。当一个线程启动时,它可能会运行一段给定的时间,然后才必须将控制权交还给决定下一个是谁的内核。

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

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

这3种场景理论上可能有不同的线程切换时间。例如。我希望最后一个最慢,因为对 sleep() 的调用意味着 CPU 被归还给内核,内核需要设置一个唤醒调用,以确保线程在大约它请求睡眠的时间,然后必须将线程从调度进程中取出,一旦线程被唤醒,它必须再次将线程添加到调度进程中。所有这些陡峭的斜坡都需要一些时间。所以实际的 sleep-call 可能比切换到另一个线程所需的时间更长。

我认为,如果您想确定,您必须进行基准测试。问题是您通常必须让线程进入睡眠状态,或者您必须使用互斥锁同步它们。休眠或锁定/解锁互斥体本身就有开销。这意味着您的基准测试也将包括这些开销。如果没有强大的分析器,以后很难说实际切换使用了多少 CPU 时间以及睡眠/互斥调用使用了多少。另一方面,在现实生活中,你的线程要么休眠,要么通过锁同步。纯粹测量上下文切换时间的基准是综合基准,因为它不模拟任何现实生活场景。如果基准基于现实生活场景,它们会更加“现实”。如果这个结果在现实生活中的 3D 应用程序中永远无法实现,那么 GPU 基准测试告诉我理论上我的 GPU 每秒可以处理 20 亿个多边形有什么用?知道一个现实生活中的 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 并不算太糟糕,即使我们有锁定和条件等待。我猜如果没有这些东西,每秒线程切换的次数至少会增加一倍。

【讨论】:

“不幸的是,我对 Windows 编程一无所知...我只能为您提供一些 POSIX 的源代码”的哪一部分。你不明白吗? 不,我完全理解,但你的回答对提出原始问题的人没有帮助,重点是帮助那些提出问题的人。【参考方案3】:

上下文切换的问题在于它们有固定的时间。 GPU 在线程之间实现了 1 个周期的上下文切换。下面的例子不能线程化 在 CPU 上:

double * a; 
...
for (i = 0; i < 1000; i ++)

    a[i] = a[i] + a[i]

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

线程池使用的所有封装都必须计入线程切换时间,因为这就是全部归结为(最后)。

阿特马普里

【讨论】:

【参考方案4】:

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

【讨论】:

【参考方案5】:

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

【讨论】:

.9 NANOSECONDS?你确定吗? ... 您的代码似乎正在计算毫秒/开关*1000-> 微秒。 @IraBaxter 不是纳秒,1000us==1ms 1000ms==1s 每毫秒超过 1000 个开关??你确定吗? 鉴于它现在是 CFS,它可能需要重新测试...... @Scott:检查消息编辑历史。它曾经说“纳秒”。【参考方案6】:

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

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

一、编码方式(伪代码):

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 中的活动进程,然后进行迁移。

【讨论】:

【参考方案7】:

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

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

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

【讨论】:

【参考方案8】:

虽然您说您不想编写测试应用程序,但我这样做是为了在 ARM9 Linux 平台上进行先前的测试,以了解开销是多少。只有两个线程会 boost::thread::yield() (或者,你知道)并增加一些变量,大约一分钟后(没有其他正在运行的进程,至少没有做某事),应用程序打印它每秒可以进行多少次上下文切换。当然,这并不完全准确,但关键是两个线程都相互让出 CPU,而且速度如此之快,以至于不再考虑开销是没有意义的。 因此,只需继续编写一个简单的测试,而不是过多考虑可能不存在的问题。

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

哦,我记得有一个在 Windows CE 4.X 上运行的应用程序,其中我们也有四个线程,有时需要频繁切换,而且从未遇到性能问题。我们还尝试在完全没有线程的情况下实现核心线程,并没有看到性能提升(GUI 只是响应慢了很多,但其他一切都一样)。也许您可以尝试相同的方法,方法是减少上下文切换的数量或完全删除线程(仅用于测试)。

【讨论】:

谢谢,我需要的是切换时间最短的确认。 用不填充缓存的进程对上下文切换进行基准测试是没有意义的。【参考方案9】:

我不知道,但您在 windows mobile 中有常用的性能计数器吗?您可以查看上下文切换/秒之类的内容。我不知道是否有专门测量上下文切换时间的方法。

【讨论】:

以上是关于如何估计线程上下文切换开销?的主要内容,如果未能解决你的问题,请参考以下文章

《java并发编程的艺术》学习小结

Java线程的上下文切换与线程状态

操作系统-线程

Java线程的上下文切换与线程状态

使用 ThreadPoolExecutor 时看不到 CPU Bound 任务的上下文切换开销

docker容器中查看线程上下文切换次数为0