运行时: 块内存复制,第 2 部分

内存块传送性能测试
本文对比了不同操作系统下多种内存块传送方法的性能,重点分析了4字节至64MB大小的内存块,并探讨了memcpy与其他编程技术的优劣。

我的 前一专栏专注于 16-MB 的内存块,这次我将讨论大小范围在 4 字节到 64 MB 之间的内存块。先前,我检验了各种执行内存传送的方法并确定使用系统提供的 memcpy() 例程是一个很不错的主意(至少在学会其它更好的方法之前,我会一直这样认为)。

在此处描述的测试中,我运行了几次传送以确保数据是可再生的。我的测试仅在内存为 576 MB 的 ThinkPad 600X (650 MHz) 上运行。没有在其它的双重引导系统上运行过。

我鼓励您在双重引导机器上运行这些测试并报告测试结果。另外,我还鼓励您对程序中的编程方法提出批评和提高性能的建议。本文中,我们的目的是演示最好的编程实践,而不是为证明一个系统比另一个好。

测试内存复制时间

我将使用与上个月几乎相同的程序。仍然使用一个开销小的、简单的源代码管理系统,我将程序重命名为 memxfer5c.cpp

我所做的更改考虑到了小于 32 个字节的内存块。由于部分循环展开是通过 "double *" 传送进行,所以上个月的程序只允许传送大于或等于 32 个字节。Memxfer5c.cpp 只是仅仅不用 "double *" 方法传送小于 32 字节的内存块。另外,还纠正了用法信息中的一个错误。

现在,让我们检查一下块大小和编程技术。我们的测试运行的测试脚本是 test2c.sh。它主要由一长列带有不同块大小和循环计数的 memxfer5c 命令行组成。

Memxfer5c.cpp 的用法信息几乎相同:


memxfer5c 用法信息
Usage:memxfer5c.exe [-f] [-w] [-s] [-p] [-csv] size cnt [method]
        -f flag says to malloc and free of the "cnt" times.
        -w = set process min and max working set size to "size"
        -s = silent; only print averages
        -p = prep; "freshen" cache before; -w disables
        -csv = print output in CSV format
        methods:
         0:     "memcpy (default)"
         1:     "char *"
         2:     "short *"
         3:     "int *"
         4:     "long *"
         5:     "__int64 *"
         6:     "double *"

测试脚本文件中一个典型的命令行如下所示:

     
	  memxfer5c -csv -s -p 4 64m 0 1 2 3 4 5 6

这条命令的意思是按用逗号隔开的列表格式( "-csv" )输出数据,汇总信息( "-s" ),“准备”高速缓存( "-p" ),使用 4 字节传送,传送 6400 万次,并为用法信息中列出的每种方法执行这种测试。

Memxfer5c.cpp 使用以下命令进行编译:

     gcc -O2 memxfer5c.cpp -o memxfer5c
    或
     cl -O2 memxfer5c.cpp -o memxfer5c.exe

(注意:本系列的 介绍专栏中有对 memxfer5c.cpp 使用的支持例程的描述。)

既然 memxfer5c.cpp 程序可用,而且与上个月的程序几乎没什么差别,所以就不再提供程序清单。我将显示测试运行的图形化结果。使用以下命令运行这个测试:

bash test2c.sh a

参数 a 是一个讨厌的小东西,它强迫我考虑这个测试做什么工作。如果脚本不带参数运行,它只在打印一条用法信息后便退出。任何参数都会起作用。

我们在配备 576 MB 内存和 12-GB 硬盘的 ThinkPad 600X Model 2645-9FU 上运行此脚本。600X 是一台 648 MHz 的 Pentium III 机器。(我的 前一专栏展示了如何在 Windows 和 Linux 下查找处理器型号和以 MHz 为单位的速率。) 测试过的操作系统有:

  • Linux 2.2.16-22
  • Linux 2.4.4
  • Windows 2000 SP1

由于 Linux 内核存在于 Red Hat 7.0 环境中,所以我们使用包含在 Red Hat 7.0 分发版(版本 2.96)中的 gcc。在 Windows 2000 上,我们使用 Visual Studio 6.0 中的 Microsoft C++(版本 12.00.8168)。

运行的一次典型输出 ― 展开各栏后 ― 如下所示:

    memxfer5c.exe -s -p -csv 4 67108864 Win2k
        "memcpy", 4,268435456,   5.862,  45.789
        "char *", 4,268435456,   3.243,  82.771
        "short *",4,268435456,   3.139,  85.507
        "int *",  4,268435456,   2.721,  98.667
        "long *", 4,268435456,   2.720,  98.672
        "memcpy", 4,268435456,   5.862,  45.795
        "memcpy", 4,268435456,   5.858,  45.828

只有信息中的第 1、第 2、和最后一栏在绘图时被用到。另外两栏被用于验证最后一栏的正确性。例如,从上面的“memcpy”行,我们可以看到:

		45.792 = 268435456/(5.862*1E6)

我使用的是舍位过的数字,可以验证数字 45.792 的正确性,精确到四位有效数字。对于绘图工作,这已经足够了。

看一下数据。前 3 个图(用 Microsoft Excel 制作的)分别代表了每种操作系统上的运行,并显示了这些系统间非常相似的表现。


图 1. Linux 2.2.16-22
Linux 2.2.16-22 

图 2. Linux 2.4.4
Linux 2.4.4 

图 3. Win2k
Win2k 

我还为每个方法绘了一张图,用来显示不同的操作系统:


图 4. Linux 和 Win2k 中的 Memcpy
Memcpy Linux 和 Win2k 

图 5. Linux 和 Win2k 中的 "Char"
Char Linux 和 Win2k 

图 6. Linux 和 Win2k 中的 "Short"
Short Linux 和 Win2k 

图 7. Linux 和 Win2k 中的 "Int"
Int Linux 和 Win2k 

图 8. Linux 和 Win2k 中的 "Long"
Long Linux 和 Win2k 

图 9. Linux 和 Win2k 中的 "_int64"
_int64 Linux 和 Win2k 

图 10. Linux 和 Win2k 中的 "Double"
Double Linux 和 Win2k 

这些图有一些古怪的地方。首先,在 Linux 上使用 "long *" 好象比使用 "int *" 更好。既然 int 和 long 变量在 Linux 和 Windows 上的长度相同,肯定是什么地方出错了。main(方法 3 和方法 4 )的相关部分的反汇编如下所示:


方法 3 和方法 4("int *" 和 "long *")反汇编
    0x8048ec0 :  xor    %esi,%esi
    0x8048ec2 :  cmp    %edi,%esi
    0x8048ec4 :  mov    0xffffffdc(%ebp),%ecx
    0x8048ec7 :  mov    0xffffffd8(%ebp),%edx
    0x8048eca :  jae    0x8048f87 
    0x8048ed0 :  mov    (%edx),%eax
    0x8048ed2 :  add    $0x4,%esi
    0x8048ed5 :  mov    %eax,(%ecx)
    0x8048ed7 :  add    $0x4,%edx
    0x8048eda :  add    $0x4,%ecx
    0x8048edd :  cmp    %edi,%esi
    0x8048edf :  jb     0x8048ed0 
    long *
    0x8048ee8 :  xor    %esi,%esi
    0x8048eea :  cmp    %edi,%esi
    0x8048eec :  mov    0xffffffdc(%ebp),%ecx
    0x8048eef :  mov    0xffffffd8(%ebp),%edx
    0x8048ef2 :  jae    0x8048f87 
    0x8048ef8 :  mov    (%edx),%eax
    0x8048efa :  add    $0x4,%esi
    0x8048efd :  mov    %eax,(%ecx)
    0x8048eff :  add    $0x4,%edx
    0x8048f02 :  add    $0x4,%ecx
    0x8048f05 :  cmp    %edi,%esi
    0x8048f07 :  jb     0x8048ef8 

如您所见,代码是同样的。我将 memxfer5c.cpp 源文件复制到 x.cpp,并开始忙于一些琐事。我首先做的是验证正确的方法确实被执行了。然后,我尝试切换方法 3 和方法 4 的顺序,结果是 "int *" 的速度变快了,而 "long *" 的速度却变慢了。使用相同的两个值,但通过交换代码的位置,性能也交换了。我得出的唯一结论就是代码一定是处于高速缓存的边界上。就象我们在上面看到的,没有两部分代码交叉的页边界。

第 2 点神秘之处是与 Windows 比较时 Linux 在 "int *" 和 "long *" 上的表现。在使用 "char *" 、 "short *" 和 "int *" 时,Windows 比例适中。每张图显示在内存传送性能方面 Windows 大约是 Linux 的两倍。Linux 在进行 4 字节传送时速度达到 600 MB/秒左右便停顿,无法再上升。这种现象的原因委实不清楚。

另外让人觉得好奇的一点就是 Linux 和 Windows 的 memcpy 图中的“锯齿状图形”。当使用的块大小在 4K 到 32K 之间时,Linux 图中就会出现这种“锯齿状图形”。这里是这个区域(锯齿状图形)的放大后的图像,没有使用块大小轴上的记录刻度。


图 11. Linux 和 Win2k 中的 Memcpy
 

这种表现看起来象是高速缓存级别之间的差频。系统设计者可能不得不解释为什么 Linux 会这样。





回页首


关于编译器优化的注意事项

读者 Matteo Ianeselli 指出了 gcc 编译器的“-funroll-loops”选项。因为我们输入限制循环次数的变量,所以我不清楚编译器如何展开这些循环中的一个循环。根据我在命令行输入的值,编译器可能已经把循环过分展开。我使用“-funroll-loops”选项在 ThinkPad 770X 上运行一个快速测试。结果如下所示:

  • 对于小于 1.5 MB 的传送,展开循环比不使用这个选项展开的速度稍慢。
  • 对于大于 1.5 MB 的传送,展开循环产生的加速大约是 135/131。

我先前已经看过 Microsoft C++ 编译器上的各种选项以查看是否有比“-O2”更好的选项。在我的测试中,我没有发现。我又放弃了继续搜索。但是,如果我们的读者用他/她最喜欢的 cl.exe 上的优化参数编译 memxfer5c.cpp,“并且”使性能得到了提高,请通过 讨论论坛告诉我们。当众多人参与时,搜索 cl.exe 的参数空间的效率就会更高。





回页首


结论

这次好象我提出的问题比答案还多。除上面提出的奇怪现象外,我的结果看起来都是在预料之中的。

先前,我慎重地下了结论:在 Linux 和 Windows 下使用 memcpy() 是一个好主意。这个月的测量肯定了这个结论。Memcpy() 产生的结果比 Windows 和 Linux 上任何其它的方法产生的结果都要好。将 Windows 与 Linux 相比较,Windows 在使用 4 字节指针传送内存和传送小于 200 KB 内存时速度更快。传送的内存小于 10 KB 时,局部展开的 "double *" 方法在 Windows 上也更快。在所有其它的情况下,Linux 移动内存的速度好象更快。



参考资料



关于作者

Edward Bradford 博士现在为 IBM Software Group 管理 Microsoft Premier Support,并且每周

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值