首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

Linux 和 Windows 上的高性能编程技术-调度线程

Linux 和 Windows 上的高性能编程技术-调度线程

线程调度线程提供了一种有用的编程技术,以将工作分割成一些独立的部分。正确使用线程的程序可以运行在多处理器上,其中每个线程可以拥有自己的 CPU。单 CPU 系统上的任何慢进程都有可能在 N 路多处理器中用 1/N 时间来执行。当然,这种加速只会发生在“令人烦恼的并行”问题中,但是这种问题的确存在。
题外话:列表生成在探究本月介绍的程序的详细信息之前,需要跑一下题,去了解用来加速测试员(也就是我)生产力的编程技术。该程序的以前版本支持单输入参数。为了对一定范围的值执行测试,就需要多次执行程序。生成和维护包含程序执行方式列表的脚本文件是恼人的。而且,如果希望许多(成百)个测试用例,仅仅是脚本文件的生成就会变得很繁琐。
为了将测试用例的生成由手工改为自动,我编写了一个列表生成的编程实用程序。如果我是一位有经验的面向对象的专家,将该程序表示为对象是很理想的。但是,我觉得没有很大的必要来要求信息隐藏和自动内存分配。因此,我使用 C 而不是 C++ 对象来创建该实用程序。
就接口而言,它具有一个 C 结构、一个从字符串 [         getlist() ] 来初始化结构的 API、一个重新启动列表 [         restart_list() ] 的 API 和一个获取列表下一个成员的 API。列表由零终止。      
由于一些原因,该软件不是完全通用的。一个原因就是它不能返回零作为列表成员。还有,它不支持任意的列表。最后,就是它不完全检查所有可能的不一致性。但它支持用一个任意非负数作为起点来生成算术和几何级数。
利用列表生成实用程序,您可以不在命令行上输入数字,而是输入一个数字或一列数字。语法如下所示:
1
begin[,end[,increment[,type]]]




表 1. 列表示例命令行输入操作fract4b 4运行 fract4b 4 秒;由单个参数传递期望的结果。fract4b 4,8分别运行 fract4b 4 秒、5 秒、6 秒、7 秒和 8 秒(1 是缺省的增量)。fract4b 4,32,2以 2 秒为增量,分别运行 fract4b 4 秒到 32 秒;0 是缺省类型,并且是算术级数。fract4b 1,128,1,1以 1 秒、2 秒、4 秒、8 秒、16 秒 …… 的方式,分别运行 fract4b  1 秒到 128 秒。类型 1 意味着几何级数。2 是唯一可用的乘数。fract4b 1,32,2,1运行 fract4b 1 秒到 32 秒,其中前后两个数之间保持一个间隔,将当前数乘以 2 以得到当前数之后的第二个数,并尽可能将前后两个数之间分成两个间隔。因此对于 1,32,2,1 而言,我们将得到 1,2,3,4,6,8,12,16,24,32
为了使用该实用程序,在将要解析数字时,仅将字符串传递给         getlist() 而不是         atof() 。         Getlist() 获取 genlist_t 结构的地址和一个字符串作为参数。它解析字符串并填入该结构。我已经注意到要确保不完善的规范正确地填写。但是,我确实发现仅仅注意到这一点是不够的。在真正大量使用该代码之前,需要对其做一些微小的改进。但是,对于我们的用途而言,它已经能胜任了。清单 1 显示了一段         getlist() 的用法。      
清单 1. Getlist 列表初始程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    char *defaulttimelist   = "4,4,1,0";
    char *defaultthreadlist = "1,1,1,0";
    getlist(defaulttimelist,   &timelist);
    getlist(defaultthreadlist, &threadlist);
=================================================
    else if(equaln(av[1], "-t",2)) {
        if(av[1][2]) {
            getlist(&av[1][2],&threadlist);
        }
        else {
            ac--;
            av++;
            getlist(av[1],&threadlist);
        }
        ac--;
        av++;
    }
=================================================




一旦填入了 genlist_t 结构,就会通过调用         next_list() API 使用它。由于此处实现的列表具有状态,所以要重用列表,必须调用         restart_list() API。清单 2 显示了它们的用法。      
清单 2. 使用列表
1
2
3
4
5
6
7
8
9
10
11
12
    nthreads = next_list(&threadlist);
    if(nthreads == 0) {
        return 0; // exiting from main.
    }
===========================================
    restart_list(&timelist);
    for(;;) {
        timedtestflag = 0;
        nseconds = next_list(&timelist);
        if(nseconds == 0)
            break;
===========================================




该实用程序的有用之处在于,现在只用一个“简洁的单行程序”就可以表达较长的测试。
1
fract4b -s -t 1,256,2,1 1,32,2,1




这会产生大约 15 x 10 = 150 次测试。结果是程序生成能满足广度和深度优化搜索所需的足够数据。这种编程能力将支持对优越性能和拙劣性能的自动搜索。
执行工作的线程程序本月,我们的程序执行实际工作。分形计算就是“令人烦恼的并行问题”中的一个。它也具有大量的浮点运算,并且内存需求很小。清单 3 显示了 fract4b.cpp 中执行分形计算的那部分代码。
清单 3. 分形计算循环
1
2
3
4
5
6
7
8
9
10
11
INT64 ull;
x = a;
y = b;
for(ull = 0; timedtestflag == 1 ; ull++) {
    x1 = x*x - y*y;
    y1 = -2 * x * y;
    if( (x1*x1 + y1*y1) > 1.0)
        break;
    x = x1 + c;
    y = y1 + d;
}




计算代码包含在一个名为         fractwork() 的子例程中。Fractwork 构成了每个线程的全部职能。尽管多个线程计算完全相同的分形点,但是浮点计算单元并不知道这一点,而是独立执行每个计算。使用浮点计算单元需要保存和恢复浮点环境。对于非浮点计算的应用程序,无需有保存和恢复浮点环境的额外开销,这使与上下文切换相关的开销也较少。      
Fract4b.cpp 将启动与命令行中指定的数目一样多的线程,并且按指定的时间长度运行程序。如果未指定“-s”(摘要)标志,它会打印出每秒的总迭代次数,以及每个线程完成了多少次迭代。对每个线程迭代次数的计数对于呈现调度程序的公平性是很有用的。
让出处理器在编写这个程序的过程中,“让出处理器”的概念是必需的。当程序想等待某个事件发生而又不想用同步原语(信号、互斥锁、临界段)阻塞其它程序时,或者当它们等待 I/O 时,就会让出处理器。在 Fract4b.cpp 中,我选用了内存变量作为标志(“timedtestflag”)以控制所有线程的运行。当“timedtestflag”指示为不要运行时,线程会使用操作系统支持的让出原语让出处理器。
在 Windows 和 Linux 中,有几个机制使线程让出处理器。对于 Windows,可以使用信号或互斥锁,或者任何其它阻塞系统所支持的调用。如果只要让出处理器,则有两个 API 可以使用:         Sleep() 和         SwitchToThread() 。本月的这个程序带有一个命令行选项,可以选择这两个 API 中的一个。我的经验告诉我,就性能而言,它们没什么区别。      
Linux 也带有两个可用于让出处理器的 API。我的经验表明,这两个 API 有实际的差别。         sched_yield() API 似乎不能实现手册页中暗示的允诺。当我运行带有越来越多线程的 fract4b.cpp 时,4 秒测试花费的时间越来越长。调试打印出来的内容显示一些线程正在让位给主线程,但是主线程并未在运行。因此,上下文切换程序在运行主线程之前会运行同一个线程两次或更多次。      
新闻组中似乎已经记述过了这个问题,但是大多数的讨论都已经是一年多前的事情了。最新的 Linux 内核可能没有这个问题,但是 Red Hat 7.2 分发版却有该问题。为了解决这个问题,我使用了         nanosleep() API。它支持亚微秒进程和线程暂挂。我对其进行编程以休眠 1 纳秒,所有低效的让出都消失了。然后,我冒险尝试了一下,将其编程为进行 0 纳秒 nanosleep,结果一样。很显然,0 纳秒的 nanosleep 如实地将线程放到了调度程序队列的末尾。Fract4b.cpp 还通过“-sched_yield”选项支持选择         nanosleep() 或         sched_yield() ,以演示这两种让出技术之间的区别(如果有任何区别的话)。      
性能诧异在编写这个程序时,我原本期望得到大体类似于我在 中得到的结果,在那篇文章中,我展示了 Linux 的 CPU 性能大约要优于 Windows 5%。但在测试过程中,我发现现在 Windows 可以比 Linux 提供更佳的性能。自从那篇专栏文章中的 fract2.cpp 发表以来,分形算法已经得以“成熟”。我检查了源代码,发现这两个程序间的唯一区别是:fract4b.cpp 程序使用了线程,而以前的版本未使用。在迷惑了一阵子后,我尝试用不同的优化进行编译。下面是在 IBM Thinkpad 770X 上得到的结果:      
表 2. 优化结果优化级别迭代/秒gcc     fract4b.cpp -lpthread3.00gcc -O1 fract4b.cpp -lpthread11.30gcc -O2 fract4b.cpp -lpthread6.80
就像您看到的,我错以为“-O2”始终都能提供最佳性能。这里就是一个这种想法不正确的例子。本专栏文章中的测试和性能结果是以一个正常人所能获得的最佳性能为基础的。在这个例子中,Linux 上使用了“-01”优化。Windows 2000 未显示这些异常;我对它使用了“-O2”优化。
fract4b.cpp该程序按照指定的时间长度,用指定的线程个数计算一个分形点。我没有双引导的多处理器系统可以测试 fract4b.cpp,但是,结果可能会很有趣。如果谁有既可引导 Windows 又可引导 Linux 的多处理器,那么我很想知道运行与下面测试相同的测试所得到的结果。
我运行了以下测试:
1
fract4b -s -t 1,256,2,1    8




这里,我将线程数参数化,以便查看当添加了更多的线程时总性能是否有差异。因为 Red Hat 7.2 的缺省最大线程为 256,所以将 Linux 上测试的线程限制为 192 个。由于主线程和计时器线程都包含在该计数中,所以计算用的线程就不到 256 个。Windows 2000 Advanced Server SP2 和 Windows XP Professional 都对指定的 1024 个线程成功地进行了测试。
结果下图中显示的结果表明,在分形计算中,Linux 看上去仍然可以提供多出 5% 到 7% 的性能。这一限定测试的特点是计算量大、对内存需求较小、无 I/O 以及无线程间的活动。
图 1. 线程数 VS. 性能本图中所有的测试都是在同一台 ThinkPad 600X 计算机(650 MHz,576 MB 内存)上运行的。
Windows 调度程序我们已经尝试了去理解 Windows 调度程序。Advanced Server 能以“后台服务”方式或“应用程序”方式运行。在 GUI 中,在“My Computer”图标上单击鼠标右键,选择 Properties,然后选择 Advanced 选项卡来选择方式。在 Advanced 面板上,选择“Performance Options”。可以在该页面上选择后台服务或应用程序调度方式。上图表明在 Windows 2000 Advanced Server 中后台服务和应用程序方式的结果几乎没有区别。
Windows XP 中有等效的机制来选择调度的优先级。在 Advanced 选项卡上的 Performance 标题下,可以看到“Settings”按钮而不是“Performance”按钮。可以在那里选择“Let Windows choose what's best for my computer”。另外,您可以选择“Adjust for best appearance”,“Adjust for best performance”或者可以选择一些定制设置。
所有定制设置都与机器的屏幕行为有关。上图是在两种设置下运行的。第一种是“Windows choose”,而第二种是“Adjust for best performance”。在测试运行的过程中,图形界面不会受到干扰,并且我发现这两个运行之间没什么区别。
总的来说,Windows 2000 Advanced Server 和 Windows XP 表现相同,且能得到预期的结果。Linux 也得到了预期的结果,但是性能优于 Windows 5% 左右。
返回列表