欢迎光临本专栏,这个新的 Linux 专栏主要演示和比较了 Linux 和 Windows 2000 操作系统的性能。专栏作家 Ed Bradford 比较了操作系统级的特性,而不是应用程序,以便让人们了解每个操作系统的最佳性能特性。本文包含了源代码,在尽可能公平的环境中,它们表示每个平台的“最佳编程实例”。
在这个新的文章系列中,将主要讨论用于 Linux 和 Windows 2000 操作系统的高性能编程技术。我将演示实用且有效的编程实例,它们能够解决 Linux 和 Windows 2000 上出现的同一问题。问题解决之后,就至少可以对每个平台进行某一方面的性能测量。各个性能测试脚本和程序将会显示操作系统特性的速度。我的目标是演示如何得到每个操作系统可能的最佳性能,顺便比较一下这两个操作平台的性能。
性能测试概述
这些测试将检测内存速度、系统调用速度、输入输出总和、上下文切换速度和许多其它在这两种操作平台上通用的编程工具。但是,不会测量对 Windows 注册表的访问。本文中发表了源代码,也可以免费下载这些源代码。在这里追求的是富有建设性的评价。我的目的是首先展示最好的编程实例 -- 然后比较性能。十分欢迎读者在 讨论论坛上针对本文发表您的看法。
我们将 Linux 看作是两个操作系统:Linux 2.2.16 内核和 2.4.2 内核。Windows 2000 是指版本 Windows 2000 2195 系统,发行时,称作 Windows XP 内核。所有测试都基于完全相同的硬件。首选硬件是双引导 IBM ThinkPad 600X(带 576 MB 内存和两个 12-GB 磁盘)。
虽然很少用到面向对象的代码,但还是用 C++ 编写了程序。使用 C++ 的原因只是 C 具有很强的类型检查特点。在 Linux 上,使用 Red Hat 7.0 分发版所带的 gcc。在 Windows 2000 上,使用 Visual Studio 6.0 下的 Microsoft C++ 版本 12.00.8168。
|
测量实用程序
首篇文章定义了对 Windows 2000 和 Linux 进行测量和报告测量结果所需的实用例行程序。所列出的工具很少:用于测量时间的接口、返回描述操作系统的字符串的例程、简单 malloc() 内存分配例程的简单且有效的接口,以及处理大数的输入例程。(以下将详细讨论定时例程)。
这里的内存分配器称为 Malloc(int)。它所做的就是调用 malloc(int) 例程,如果 malloc(int) 失败,Malloc() 打印一条错误消息并退出。在测试内存分配性能时,没有用这个例程,但同时,它是流线形编码。这里使用 malloc() 是由于在 Windows 2000 和 Linux 中都有这个函数。Malloc() 在这两个系统中是公平而等同的。
接下来的一个例程是 atoik(char *)。这个例程与 atoi() 例程是相同的,但它带一个后缀 "k" 或 "m"。后缀 "k" 或 "m" 表示:对于 "k",将已解析的数字乘以 1024,对于 "m",将已解析的数字乘以 1024*1024。"k" 和 "m" 可以是大小写,并且可以给它们附加任何数字。Atoik() 只返回 32 位,所以当出现任何大于或等于 2 GB 的数字时将不能继续运行。当出现这个问题时,使用我们编写的 atoik64() 函数。这个例程在两个操作系统中是相同的。
|
测量时间
在这两个操作系统上如何测量时间?让我们看一下我们有几种选择。在 Windows 2000 中有两个 API 可以测量时间间隔。第一个是 GetTickCount()。这个函数报告自从系统启动后经过的毫秒数。GetTickCount() 是以“时钟报时信号”为颗粒度。这意味着只有当系统发出时钟报时信号时这个函数才会更新这个值。在 Windows 中,这个更新间隔为 10 毫秒。所以它的颗粒度不超过 10 毫秒或 10000 微妙。
Windows 2000 还有一个 QueryPerformanceCounter() API,它用来重新修正 64 位高分辨率性能计数器的当前值。调用 QueryPerformanceCounter() 的结果中的每次“报时”取决于 QueryPerformanceFrequency() 返回的值。频率是每秒计数器的增加量,所以秒可以表示成:
LARGE_INTEGER tim, freq; double seconds; QueryPerformanceCounter(&tim); QeryPerformanceFrequency(&freq); seconds = (double)tim / (double) freq; |
由于 GetTickCount() 的分辨率太低,我们将只使用 QueryPerformanceCounter。我们必须要注意,如果计时短到与 QueryPerformanceCounter() API 的开销一样时,那么我们的结果可能时不可靠的。下面我们将测量计时例程的开销。
在 Linux 上,使用 gettimeofday() API。只有这个 API 可以满足我们在次毫秒级时间的需求。
选择完要使用的 API 后,需要定义自己的 API,这样可以使程序在不知道主机操作系统的情况下也可以使用这些 API。我们选用下面的接口来实现这些功能:
void tstart(); void tend(); double tval(); |
当调用 Tstart() 时,它记录静态内存中的时间值。当调用 Tend() 时,它记录静态内存中的时间值。Tval() 采用 tstart 和 tend 时间值,将它们转换为双精度数,然后减去它们,以返回双精度形式的结果。这个接口在 Linux 和 Windows 上很容易实现,它执行了所需要的计时功能。
Linux 和 Windows 2000 下计时例程的实现如下。由于不能避免对系统的依赖性,所以我们的目标是在尽可能地减小条件定义的情况下,编写最佳的代码。以下是计时例程的清单。
#ifdef _WIN32 static LARGE_INTEGER _tstart, _tend; static LARGE_INTEGER freq; void tstart(void) { static int first = 1; if(first) { QueryPerformanceFrequency(&freq); first = 0; } QueryPerformanceCounter(&_tstart); } void tend(void) { QueryPerformanceCounter(&_tend); } double tval() { return ((double)_tend.QuadPart - (double)_tstart.QuadPart)/((double)freq.QuadPart); } #else static struct timeval _tstart, _tend; static struct timezone tz; void tstart(void) { gettimeofday(&_tstart, &tz); } void tend(void) { gettimeofday(&_tend,&tz); } double tval() { double t1, t2; t1 = (double)_tstart.tv_sec + (double)_tstart.tv_usec/(1000*1000); t2 = (double)_tend.tv_sec + (double)_tend.tv_usec/(1000*1000); return t2-t1; } #endif |
最后一个的例程是 "char *ver()"。这个简单的函数返回一个描述当前操作系统环境的字符串。正如在源代码中所见,它在每个操作平台上都完全不同。在该例程结尾处是用于测试的条件性定义的 main() 例程。编译过程如下:
gcc -DMAIN -O2 ver.cpp -o ver 或 cl -DMAIN -O2 ver.cpp -o ver.exe |
ver.exe 程序用于将操作系统版本信息记录到输出文件。
#ifdef _WIN32 #include |
上面定义的计时函数可以满足我们的需要。开始使用它们之前,应该知道它们要执行多久。实际上,我们只需要知道 tstart() 和 tend() 要执行多长时间。由于这两个函数在形式上是一样的,因此只需要计算其中一个的执行时间。在 Windows 2000 和 Linux 中,使用 time-timers.cpp 程序对计时函数进行计时分析。请注意,这里只列出了 main() 例程。实际程序包括所有计时器函数和前面清单中的 atoik() 源代码。
char *applname; int main(int ac, char *av[]) { long count = 100000; long i; double t; char *v = ver(); char *q; applname = av[0]; if(strrchr(applname,SLASHC)) applname = strrchr(applname,SLASHC) + 1; if(ac > 1) { count = atoik(av[1]); ac--; av++; if(count < 0) count = 100000; } tstart(); for(i = 0; i < count; i++) tend(); tend(); t = tval(); printf("%s: ",applname); printf("%d calls to tend() = %8.3f seconds %8.3f usec/call/n", count, t, (t/( (double) count ))*1E6); return 0; } |
一切就绪。现在编译 time-timers.cpp 程序,方式如下:
在 LINUX 上 gcc -O2 time-timers.cpp -o time-timers 在 Windows 2000 上 cl -O2 time-timers.cpp -o time-timers.exe |
这个程序只使用一个可选变量。缺省情况下,程序调用 tend() 函数 100,000 次。重复运行该程序可保证重新生成时间结果。我们使用了缺省计数,运行了该程序 10 次。在 Linux 2.2.16、Linux 2.4.2 和 Windows 2000 的表中分别显示了结果。
在同一台 Thinkpad 上的 Linux 2.2.16、Linux 2.4.2 和 Windows 2000 中,我运行了以下脚本。事实上,最初我使用了 Linux 2.4.2 的对称多处理 (SMP) 版本。我无意中使用了 SMP 版本进行构建和测试。发现这个情况后,我还构建了单处理器版本并用其进行了测试。下面总结了这两个版本的结果(如果有兴趣)。
ver > time-timers.out for i in 1 2 3 4 5 6 7 8 9 10 do time-timers 1m done >> time-timers.out for i in 1 2 3 4 5 6 7 8 9 10 do time-timers 1m done >> time-timers.out |
这个脚本将把对 tend() 的一百万次调用运行 20 遍。结果如下:
Linux 2.2.16 | Linux 2.4.2 | Linux 2.4.2 SMP | Windows 2000 |
0.740 usec | 0.729 usec | 0.806 usec | 1.945 usec |
我能够得出的唯一结论是在 Winsows 2000 里的 QueryPerformanceCounter() 系统调用要比同一硬件上的 gettimeofday() API 慢得多。对于我们的目的来讲,计时例程的 2 微秒的颗粒度是足够的。在 1 毫秒测量时间里,只有千分之二是实际的测量开销。即 0.2%,对我们的目的来说是可接受的范围。
|
结束语