跳转到主要内容

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

当您初次登录到 developerWorks 时,将会为您创建一份概要信息。您在 developerWorks 概要信息中选择公开的信息将公开显示给其他人,但您可以随时修改这些信息的显示状态。您的姓名(除非选择隐藏)和昵称将和您在 developerWorks 发布的内容一同显示。

所有提交的信息确保安全。

  • 关闭 [x]

当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

所有提交的信息确保安全。

  • 关闭 [x]

RunTime: 上下文切换,第 2 部分

Linux 和 Windows 上的高性能编程技术

Edward Bradford 博士现在为 IBM Software Group 管理 Microsoft Premier Support,并且每周还为 “Linux 和 Windows 2000 软件开发者”撰写新闻简报。可以通过 egb@us.ibm.com与他联系。

简介: 本月 Ed 研究了调度程序的两种行为。第一种行为是向调度程序的切换决策添加更多选项后的反应。第二种行为通过在多个线程中执行相同的工作负载来说明公平性。本文提供了源代码,因此您可以自己进行尝试。

发布日期: 2001 年 10 月 19 日
级别: 初级
访问情况 : 1606 次浏览
评论: 


上个月的专栏文章通过在 Windows 和 Linux 上使用最佳原语,研究了纯上下文切换次数。根据那些结果,Windows 中的上下文切换时间只有 Linux 的一半。我具此得出结论:Windows 中选择线程来运行的算法得到了很好的优化。

本月的专栏文章将研究两个问题。首先,我们研究通过向调度区域插入多个额外的锁来为调度程序算法提供多种选择。这里,我们仍然只研究切换算法而不让处理器完成任何实际工作。最后,在第二个问题中,我们研究用线程完成实际工作。我的工作负载由对一个分形点的 CPU 密集型计算组成。我让每个线程计算一个分形点,然后观察,在一段给定时间之后每个线程能完成多少次迭代。这么多线程都会获得等量的时间吗?

还记得吗?上个月的专栏文章显示了使用适当的处理同步原语可以对执行上下文切换所需的时间造成巨大差异。在每种平台上选择最佳原语来测量上下文切换时间能够得到相当公平的比较结果。尽管上个月的程序不完成任何工作,但它确实演示了两个系统中的上下文切换速度。

缩短任务完成时间

第一个程序是 csfast5a.cpp(请参阅 参考资料来下载它),它对上一篇专栏文章中的 csfast5.cpp 进行了少量修改。本月,将让 csfast5a 来启动任意数量的线程,其中每个线程都会锁定和解锁(使用进程同步原语)给定的次数,并在完成后记录其使用的时间。我将研究如果可以在任一时间执行多个线程,是否可以用更少时间完成更多工作。例如,32 个线程,每个都要锁定和解锁 50000 次,总共就需要 1600000 次调用。这个测试仍然不完成任何有用的工作;第二个程序将完成实际操作。

要测量每个线程的执行时间,必须增强我们的计时原语以使之成为是线程安全的。基本的更改要求是为 tstart()tend() 找到存储计时信息的地方。先前,它们仅被存储在单一全局内存位置中。显然,在可能执行数百个线程的情形中,这样是不会奏效的。您可以更改计时例程 tstart()tend() ,使它们成为线程安全的,如下所示:

清单 1. 线程安全的计时

        #ifdef _WIN32
    #define INT64   __int64
        static INT64 freq; // GLOBAL
        static int tfirst = 1;

        void tstart(INT64 *t)
        {
            if(tfirst) {
                QueryPerformanceFrequency(&freq);
                tfirst = 0;
            }
            QueryPerformanceCounter(t);
        }
        void tend(INT64 *t)
        {
            QueryPerformanceCounter(t);
        }
        double tval(INT64 *t1, INT64 *t2)
        {
            return ((double)t2->QuadPart -
                        (double)t1->QuadPart)/((double)freq.QuadPart);
        }
    #else
        void tstart(struct timeval *t)
        {
            gettimeofday(t, NULL);
        }
        void tend(struct timeval *t)
        {
            gettimeofday(t,NULL);
        }
    #endif
    

新的计时例程对每个线程进行计时,其中存储计时信息的位置是分配给每个线程的内存的一部分。

这个测试要求 32 个线程各执行 50000 次锁定/解锁操作。每个线程将进行 50000 次锁定/解锁调用,并在其线程内存中保存其启动和完成时间。当一切完成后,主线程检查所有信息并计算所有 32 个线程的平均时间 — 任一线程所花费的最长时间和最短时间。然后,打印结果并终止程序。

第一个案例是 32 个线程和 33 个锁。 最初,所有线程都被锁定,并等待释放下一个线程的锁。为了使操作进行,您可以添加一个额外的锁,以便列表中最后一个线程实际上可以获得额外的锁。当它成功地获得一个锁,它释放自己的锁,唤醒前一个线程,这个线程也做同样的事。因为区域中只有一个额外锁,所以每次只能运行一个线程。大多数获得锁的尝试都会引起操作系统中的阻塞。

使用一个额外锁时,我们的测量结果显示 Windows 2000 中每个线程完成 50000 次锁定/解锁调用平均花费 8.4 秒。Linux 中的平均时间为 12.3 秒。Windows 和 Linux 的最长时间和最短时间平均都在 10 毫秒以内。这意味着给定数量的工作在线程之间是均匀分布的。还记得吗?这里的工作很琐细,并且常常会导致操作系统发生阻塞。

在提供图形结果之前,您应该确切地知道锁定尝试每隔多久才会实际成功。我们打算对它们进行计数。既然锁定是一种系统调用原语,其行为不会因阻塞而异,那么我们怎么能做到这一点呢?还记得吗?在上一篇专栏文章中您了解了在 650 MHz TP600X 上,当使用优化原语时,上下文切换可以在任何情况下在 2 至 10 微秒中完成。显然,这种特定的计时将因处理器而异。我现在仍在同一机器上工作,因此两种计时结果是相关的。

我使用这种试探法:如果对 lock() 的一次调用花费的时间超过 10 微秒,则锁定该程序。这种不精确的试探法取决于处理器的速度。但是,我的 TP600X 是我在上一篇专栏文章中使用的同一机器,因此 10 微秒现在仍是合适的(完成该任务的唯一可靠方法是检测操作系统)。 因此,csfast5a.cpp 也使用这种试探法对每个线程实际阻塞的次数进行计数。计数的代码看起来如下:

清单 2. 在操作系统中对阻塞计数

                tstart(&ts);
        Lock(k1);
        tend(&te);
        tv = tval(&ts,&te);
        if(tv < 1e-5) { // 10 microseconds
            noswitch++;
            // no context switch
        }
        else {
            switched++;
            // must have context switched.
        }
        

请记住,这个特定的计算只是给出了大致的想法。不检测内核,就难以进行更精确的测量。


图 1. 完成时间 vs. 额外锁(32 个线程)
完成时间 vs. 额外锁

我对 32 个线程和 1 至(8192-32)个额外锁运行了测试。我绘制了 32 至 64 之间所有的点,然后只是绘制了大于 64 的 2 的 n 次幂的点。我在 Windows 2000 Advanced Server(无 Service Pack)和 Red Hat 7.3 Linux 2.4.18-2 上运行了该测试。WIN2KAS-64 和 RH73-64 分别是在 Windows 和 Linux 上测试的 32 至 64 之间的点集(本工作中不存在 64 位处理器)。图 1 显示了当有更多锁时,执行 50000 次锁定/解锁操作的时间会减少。图 2 演示了略有不同的观点,其中发生阻塞的可能性随着锁数的增加而增长。因为我程序中的线程连续地请求锁定,所以完成时间方面的改进比率与锁数的增加相比是缓慢的。 但是,该图清晰地显示了增加额外锁带来了有用的结果。(注:对于两幅图而言,csfast5a.cpp 都是用来演示向池添加资源的。Linux 中使用了互斥锁 (Mutex),Windows 中使用临界段 (Critical Section)。)


图 2. 不阻塞的可能性(32 个线程)
不阻塞的可能性

测量实际工作

第二个程序 fract5.cpp(请参阅 参考资料以下载它)启动了给定数量的线程并执行“伪”实际的工作。这个驻留在内存的工作计算分形点。每个线程可以在给定的已分配的内存块上可选地执行内存复制操作。这个测试不研究向系统增加内存负载所造成的影响。我们试图找到在出现资源缺乏之前必须运行多少线程。当线程无法得到计算机的执行上下文时,资源缺乏就发生了。

我不会寻找完整的资源缺乏,而是更愿意清晰地演示线程资源缺乏。为了轻松地演示这一点,fract5.cpp 程序需要用上一篇专栏文章中演示的自动命令行生成选项。以下是用于我的测试的命令串:

fract5-1 -s -t 1,1024,2,1 4

表 1 解释了这条“神秘”的命令。我将程序命名为 fract5-1 是因为它在 Windows 上是使用 -O1 优化选项编译的。本月,Windows 出现了反常的行为,其中 -O2 编译器优化选项比 -O1 慢。在 Linux 上,-O2 产生了最佳效果(别让程序名上的“-1”干扰了您)。

表 1. 命令字符串说明

选项 说明
-s打印摘要
C-t 1,1024,2,1运行 1、2、3、4、6、8、12、16、24、32、48、64、96、128、192、256、384、512、768、1024 个线程
4每次运行将持续 4 秒

表 2 说明了使用下列命令字符串的 fract5.cpp 程序的输出:

fract5-2 -s -t 1,192,2,1 4

输出是:

4,1,0,0,23.4412,4.0199,2,0

其格式是:

seconds,threads,unscheduled-threads,tmem,iter/usec,total_time,yields,yields2

表 2. fract5.cpp 输出说明

输出 参数 说明
4seconds线程执行的秒数
1threads运行中的线程数
0unscheduled-threads完成 0 个分形循环的线程数
0tmem memset() 操作中使用的内存量
23.4412iter/usec每微秒进行的总计迭代数
4.0199total_time运行花费的时间。取决于 Windows 上的方式(后台处理或应用程序性能调优),程序可以花费大量时间来将执行线程分配给那些试图变得“礼貌”的线程。
2yieldsfract5.cpp 中有两个地方,每个线程在其中等待来自主线程的信号进行启动。线程将到达集合点并让出处理器。如果必须多次做这一点,则线程运行得比主循环更频繁。Yields 是在启动主循环之前对在第一个位置处的让出数的计数。
0yields2第二个位置是在主循环终止之后。Yields2 是当主线程可能已经运行时这个线程的运行次数的计数。

表 3 显示了在 Linux 和 Windows 2000 Advanced Server 上运行同一测试的结果。该表表明对于 4 秒钟运行,在 Windows 上运行大约 96 个线程时,线程资源缺乏就开始了,在 Linux 上则为 128 个线程。请再次注意,对于现有的 Red Hat 7.3,Linux 对于每个进程仅支持 256 个线程。

还请注意,Windows 已经经过改进,在迭代/微秒比率方面略胜一筹。我们应该料到这一点。先前小于 5% 的差异在统计意义上应该被认为是无关紧要的。尽管我确实重新运行先前的程序以向自己保证任何事情都没有更改,但内循环中的代码不同了,从而允许不同编译器使用更多优化选项。

表 3. 在 Linux 和 Windows 2000 Advanced Server 上运行 fract5.cpp 的结果

Red Hat 7.3 Windows 2000 Advanced Server
seconds threads unscheduled-threads iter/usec total_time yields yields2 unscheduled-threads iter/usec total_time yields yields2
41023.4414.02020024.0643.99720
42023.4404.02040024.0714.00240
43023.4384.02060024.0654.00260
44023.4354.02080024.0644.00180
46023.4194.020120024.0614.001120
48023.4274.020160024.0704.001160
412023.4224.020240024.0664.000240
416023.4204.020320024.0714.000320
424023.4264.020480024.0643.998650
432023.4234.020640024.0653.9971000
448023.4434.017960024.0654.0041520
464023.4224.0201280024.0625.0032420
496023.3594.02023401924.0696.0684770
41282923.4224.01935805424.0615.7426940
41929223.4324.020592011324.0666.08314990
425617924.0635.95123810
438430723.9246.03650330
451242224.0466.75184490
476868724.0386.731183190
4102492524.0378.107327984

您也可以通过运行大量线程(从 1 秒至 128 秒)来观察线程资源缺乏。显然,运行 1 秒,操作系统可能来不及运行所有的 250 个线程。(我再次选择用 250 个线程来直接比较 Windows 和 Linux。也许某天,Linux 的分发商会将缺省最大线程数增加到 1024 左右。) 表 4 演示了下列命令的运行:

fract5-1 -s -t 250 1,128,2,1

表 4. 运行 fract5.cpp 以演示资源缺乏的结果

Red Hat 7.3 Windows 2000 Advanced Server
seconds threads unscheduled-threads threads unscheduled-threads
1250233250222
2250216250212
3250200250229
4250165250216
6250154250190
8250158250145
12250109250157
1625067250137
242503625044
32250325035
4825002500
6425002500

最后,您可以下载并编译 fract5.cpp,并在任何多处理器机器上运行下列命令:

fract5 -s -t 1,32 1

您将非常清楚地了解到,您的系统上有多少处理器。我唯一一次这样运行的例子是在有四个物理 CPU 的 Intel“Foster”机器上。运行显示性能提高了八倍(因为每个物理处理器有两个虚拟处理器)。我建议您自己尝试这种效果。


结束语

本文描述的两个程序:csfast5a.cpp 演示了如何用线程来测量上下文切换次数,fract5.cpp 完成了“伪”实际工作。第一个程序 csfast5a.cpp 演示了当线程准备好运行时通过增加成功可能性来减少总执行时间。我们通过增加每个线程所需的资源池(锁)来做到这一点。第二个程序 fract5.cpp 完成“实际”工作并演示了如果您有许多线程要在很短时间内运行,则有些线程可能不会被调度。在 1 至 64 秒的时间内运行大量线程确切地演示了这一点。

建议您下载 csfast5a.cpp 和 fract5.cpp(请参阅 参考资料),并在您的硬件上运行它们。在真正的多处理器机器上运行这两个程序,并将您的结果发送给我。请记得告诉我您的 CPU 的型号和数目。


参考资料

  • 下载本专栏文章中提到的程序的源代码:

  • 阅读 Ed 先前的“RunTime”专栏文章:

  • developerWorksLinux 专区找到更多 Linux 文章

关于作者

Edward G. Bradford 博士

Edward Bradford 博士现在为 IBM Software Group 管理 Microsoft Premier Support,并且每周还为 “Linux 和 Windows 2000 软件开发者”撰写新闻简报。可以通过 egb@us.ibm.com与他联系。

关于报告滥用的帮助

报告滥用

谢谢! 此内容已经标识给管理员注意。


关于报告滥用的帮助

报告滥用

报告滥用提交失败。 请稍后重试。


developerWorks:登录


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 使用条款

 


当您初次登录到 developerWorks 时,将会为您创建一份概要信息。您在 developerWorks 概要信息中选择公开的信息将公开显示给其他人,但您可以随时修改这些信息的显示状态。您的姓名(除非选择隐藏)和昵称将和您在 developerWorks 发布的内容一同显示。

请选择您的昵称:

当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

(长度在 3 至 31 个字符之间)


单击提交则表示您同意developerWorks 的条款和条件。 使用条款.

 


为本文评分

评论

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=156599
ArticleTitle=RunTime: 上下文切换,第 2 部分
publish-date=10192001
author1-email=egb@us.ibm.com
author1-email-cc=

标签

Help
使用 搜索 文本框在 My developerWorks 中查找包含该标签的所有内容。

使用 滑动条 调节标签的数量。

热门标签 显示了特定专区最受欢迎的标签(例如 Java technology,Linux,WebSphere)。

我的标签 显示了特定专区您标记的标签(例如 Java technology,Linux,WebSphere)。

使用搜索文本框在 My developerWorks 中查找包含该标签的所有内容。热门标签 显示了特定专区最受欢迎的标签(例如 Java technology,Linux,WebSphere)。我的标签 显示了特定专区您标记的标签(例如 Java technology,Linux,WebSphere)。