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

欢迎光临本专栏,这个新的 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() 返回的值。频率是每秒计数器的增加量,所以秒可以表示成:


用 QueryPerformanceCounter() 计算秒
     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() 例程。编译过程如下:


将 ver.cpp 编译成为程序
     gcc -DMAIN -O2 ver.cpp -o ver
    或
     cl -DMAIN -O2 ver.cpp -o ver.exe

ver.exe 程序用于将操作系统版本信息记录到输出文件。


Ver.cpp - 打印操作系统版本
   #ifdef _WIN32
    #include 
    #else
    #include 
    #endif
    #include 
    int ver_underbars = 0;
    char *ver()
    {
        char *q;
    #ifdef _WIN32
        static char verbuf[256];
    #else
        static char verbuf[4*SYS_NMLN + 4];
    #endif
    #ifdef _WIN32
        OSVERSIONINFO VersionInfo;
        VersionInfo.dwOSVersionInfoSize = sizeof(VersionInfo);
        if(GetVersionEx(&VersionInfo)) {
            if(strlen(VersionInfo.szCSDVersion) > 200)
                VersionInfo.szCSDVersion[100] = 0;
            sprintf(verbuf, "Windows %d.%d build%d PlatformId %d SP=/"%s/"",
                VersionInfo.dwMajorVersion,
                VersionInfo.dwMinorVersion,
                VersionInfo.dwBuildNumber,
                VersionInfo.dwPlatformId,
                VersionInfo.szCSDVersion);
        }
        else {
            strcpy(verbuf, "WINDOWS UNKNOWN");
        }
    #else
        struct utsname ubuf;
        if(uname(&ubuf)) {
            strcpy(verbuf, "LINUX UNKNOWN");
        }
        else {
            sprintf(verbuf,"%s %s %s %s",
                ubuf.sysname,
                ubuf.release,
                ubuf.version,
                ubuf.machine);
        }
    #endif
        // Substitute an underbar for white space. Makes output
        // easier to parse.
        if(ver_underbars) {
            for(q = verbuf; *q; q++)
                if(*q == ' '  || *q == '/t' || *q == '/n' ||
                   *q == '/r' || *q == '/b' || *q == '/f')
                    *q = '_';
        }
        return verbuf;
    }
    // gcc -DMAIN ver.cpp -o ver -- produces a simple test program.
    #ifdef MAIN
    int main(int ac, char *av)
    {
        if(ac > 1) ver_underbars = 1;
        printf("%s/n", ver());
        return 0;
    }
    #endif

上面定义的计时函数可以满足我们的需要。开始使用它们之前,应该知道它们要执行多久。实际上,我们只需要知道 tstart() 和 tend() 要执行多长时间。由于这两个函数在形式上是一样的,因此只需要计算其中一个的执行时间。在 Windows 2000 和 Linux 中,使用 time-timers.cpp 程序对计时函数进行计时分析。请注意,这里只列出了 main() 例程。实际程序包括所有计时器函数和前面清单中的 atoik() 源代码。


time-timers.cpp - 计时定时器的程序
    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 程序,方式如下:


编译 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 版本进行构建和测试。发现这个情况后,我还构建了单处理器版本并用其进行了测试。下面总结了这两个版本的结果(如果有兴趣)。


运行 running time-timers 的脚本
   
    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%,对我们的目的来说是可接受的范围。





回页首


结束语

 

你可能感兴趣的:(Linux 和 Windows 2000上的高性能编程技术)