我的 前一专栏专注于 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

图 2. Linux 2.4.4

图 3. Win2k

我还为每个方法绘了一张图,用来显示不同的操作系统:
图 4. Linux 和 Win2k 中的 Memcpy

图 5. Linux 和 Win2k 中的 "Char"

图 6. Linux 和 Win2k 中的 "Short"

图 7. Linux 和 Win2k 中的 "Int"

图 8. Linux 和 Win2k 中的 "Long"

图 9. Linux 和 Win2k 中的 "_int64"

图 10. Linux 和 Win2k 中的 "Double"

这些图有一些古怪的地方。首先,在 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 移动内存的速度好象更快。
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文.
- 请单击文章顶部或底部的 讨论,参与本专栏系列的 讨论论坛。
- 研究内存传送程序的源代码、测试脚本以及带有图和原始数据的 Excel 电子表格:
- 请阅读本系列开头的 介绍文章;它定义了 Ed 使用的测量工具。
- 请阅读 Ed 的本系列中的 前一专栏。
- 请在 developerWorks Linux 专区查找更多的 Linux 参考资料,包括以下文章:
![]() | ||
| ![]() | Edward Bradford 博士现在为 IBM Software Group 管理 Microsoft Premier Support,并且每周 |