在计算机中,内存复制经常而普遍。它们出现在联网应用、数据库应用、科学应用以及几乎您能想得到的其它任何应用和服务中。因为它们是如此的通用,所以程序员对于内存复制有点满不在乎,而且还采用了各种各样的编程技巧来完成复制。内存复制可以是整块内存的简单移动,也可以根本不是一个复制而类似一个访问模式。
一个 访问模式的形成是通过访问一个矩阵中的各栏。比如您需要 12x12 矩阵中所有第 12 个词。这 12 个词将代表矩阵的第一栏。通常,一栏访问只需要每栏元素的 32 或 64 字节;很少多于 64 字节。复杂矩阵每栏元素可能需要两个 64 字节值。访问间的字节数被称为一个 跨距,内存访问以一个跨距值作为参数。
|
|
在这个部分中,我们只观察块内存复制,这即使在第一年编程课程中也是非常普通的。测量内存复制速需要技巧,因为计算机有一级、二级和三级高速缓存(有时候有),所以测试必须考虑高速缓存的大小。我们只看最终结果,即数据移动有多快。关键在于理解代码路径长度而非系统缓冲或高速缓存的问题。这两个问题将在以后的文章中谈到。代码路径很重要,它显示字节能多快地被移动以及移动涉及的系统开销。
内存转移只需简单的编程技巧。简单的 for 循环和 memcpy() 库例程是最常用的机制。而结构分配和文件 IO 技巧则相对使用较少。
转移内存在整个性能图中只占据一小部分。当真的是占据一部分时,最好清楚操作系统和硬件作为一个组可以提供什么。
在一个栏中有太多方面要调查和涉及,因此,有必要缩小范围。这也就意味着要得到有用的结论,必须再扩大范围。一些检测参数包括:
- 跨距 -- 如上所述,常与矩阵访问有关
- 总体转移大小 -- 移动数据的总量
- 块尺寸 -- 在一次单独操作中移动的数据量(与下一参数紧密有关)
- 编程技巧 -- 如 memcpy、(char *, double *) 指针
- 系统页大小
- 翻译后备缓冲器 (TLB) 的大小
这里的目标是将总转移量定在 16 MB,同时将跨距定为 0,即作简单的块内存转移。块尺寸随编程技巧变化而变化。Windows 2000 下系统页大小为 4K,Linux 也是 4K。如果是相同的计算机,TLB 的大小也相同。因为只使用一个线程移动内存,所以无线程问题。
|
|
我们使用的程序叫做 memxfer5b.cpp,它已有过几个版本。它的使用信息如下:
memxfer5b 的使用信息
Usage: memxfer5b.exe [-f] [-w] [-s] [-p] 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 *" |
"-p" 选项之所以有用是因为在多数测试中首次复制比接下来的复制要运行地慢。即暗示高速缓存正在被装载。这里的测量法特别注意代码路径而非内存转移速度。因此,我们使用 "-p" 选项来“预先准备”内存。我们的目的是尽可能达到最快(最短时间)的内存转移。现实情况下,程序更有可能碰到的环境是,首次转移时高速缓存未做准备。尽管这样,如果我们在测试中选择能导致最佳性能的代码路径,我们就很有可能找到最优的生产代码性能。
Memxfer5b 能使用 7 种不同的内存转移技巧。“推荐的” memcpy() API 以及 6 个不同的指针类型在 Linux 和 Windows 上都可行。
Memxfer5b.cpp 可方便地编译下列任何一条命令:
gcc -O2 memxfer5b.cpp -o memxfer5b cl -O2 memxfer5b.cpp -o memxfer5b.exe
Memxfer5b.cpp 使用我在 介绍专栏中描述的相同的支持例程。在支持例程的列表中再加入一个名为 Malloc() 的例程。Malloc() 的作用和 malloc() 一样,但当无法分配内存时,它将打印错误消息并退出。就我们的目的来说,这已经足够的。我们不测量内存分配速度;这个测试中任何分配内存的失败只是说明程序里有一个错误,或者我们已经达到系统限制。这两种情况我们都不希望发生。(我的介绍专栏中也提到过 Malloc() 例程,但它不包含于任何源代码中。在这个部分中,也会提到 -- 请参阅 参考资料。)
先前,我们看了微软 C++ 编译器的各种选项,看有没有什么比 "-O2" 更好的。在我们的测试中,我们什么也没找到,于是放弃继续查找。但是,如果哪位读者在 cl.exe 用他或她喜欢的优化参数编译 memxfer5b.cpp,“并”产生更好的性能表现,请在讨论论坛上告诉我们所有人;点击文章顶部或底部的 讨论图标。群策群力会提高查找 cl.exe 参数空间的效率。
memxfer5b.cpp 的主循环是实际移动内存的部分,它通过调用定时例程将移动分类。Memxfer5b.cpp 可去块内存转移的其它区域。以后的版本将增加分离功能,这样我们就可模拟矩阵操作。
|
|
我们将在安装了 Windows 2000 Advanced Server Service Pack 1、Linux 2.2.16 和 Linux 2.4.4 的系统编译和运行测试。这些 Linux 在 Red Hat 7.0 环境下运行。在 Linux 上,我们将使用包括在 Red Hat 7.0 发行版中的 gcc。在 Windows 2000 上,我们将使用来自 Visual Studio 6.0 的 Microsoft C++ Version 12.00.8168。我们的测试系统将是 ThinkPad 600X Model 2645-9FU,576 MB 的内存和 12 GB 的硬盘。 这个 600X 在 Windows 2000 上是个 648 MHz 奔腾 III 机器,而在 Linux 上是 647.767 MHz。Windows 2000 和 Linux 决定那信息的“机制”是:
Windows 2000 到 MHz 的浏览路径为:
Start/ Settings/ Control Panel/ Administrative Tools/ Computer Management/ System Tools/ System Information/ System Summary
cat /proc/cpuinfo
程序以每秒兆字节为单位打印作为结果的内存速度。如果指定 "-s" 标记,那么运行 "cnt" 计算内存复制速度。否则,每次运行都被打印。我们的测试按如下方式进行:
memxfer5b -p -s -csv 16m 8 0 1 2 3 4 5 6 memxfer5b -p -s -csv 16m 8 0 1 2 3 4 5 6 memxfer5b -p -s -csv 16m 8 0 1 2 3 4 5 6 memxfer5b -p -s -csv 16m 8 0 1 2 3 4 5 6 |
这是我们八次运行中的四次。不要期望看到很多变化;重复的尝试将检验我们的期待,或者给我们所要期待的加一些限定范围。
|
|
结果显示在下表中。Linux 的新旧版本在内存转移上比 Windows 2000 显然快得多。还不清楚是怎么一回事,有待进一步研究。
memxfer5b -s -p -csv 16777216 8 | |||
方法 | 每秒兆字节的平均内存速度 | ||
Linux 2.2.16-22 | Linux 2.4.4 | Windows 2000 AS | |
memcpy | 173.644 | 179.417 | 132.077 |
char * | 169.683 | 169.000 | 93.494 |
short * | 170.065 | 172.333 | 96.156 |
int * | 170.136 | 172.648 | 102.507 |
long * | 170.066 | 172.050 | 123.498 |
__int64 * | 170.094 | 172.330 | 123.498 |
double * | 169.778 | 171.192 | 123.283 |
检查 Windows memxfer5b.exe 程序的汇编清单,发现当调用 memcpy API 时,Microsoft C++ 编译器使用 "rep movs" 指令。另外,它尽职地为 "char *" 做字符对字符移动,为 "short *" 做字对字移动,并且奇怪地为 "int *"、"long *"、"__int64 *" 和 "double *" 做双字对双字移动。(Memxfer5b 避免所有错误排列的限定条件。)
一个类似的 Linux 二进制检查显示(几乎)同样的代码。唯一例外是 gcc 实际使用 memcpy() 例程。两个编译器都只移动字节、16-bit 字和 32-bit 字。在 Linux 和 Windows 下生成的汇编代码很相似。
内存复制性能差异的一个可能性是 Windows 在后台比 Linux 做了更多的工作。为了测试这个理论,让我们写一个十分短小但计算密集的程序来计算一个单一的分形点。不带参数, fract2.cpp 重复分形公式,直到重复了 100,000,000 次或分形点“逃逸”。Fract2.cpp 是一个短浮点数(双)循环计算的简单程序。
结果如下:
操作系统 | 完成时间(秒) |
Linux 2.2.16-22 | 4.065 |
Linux 2.4.4 | 4.087 |
Windows 2000 AS | 4.300 |
完成时间暗示了相同硬件条件下,任何一种 linux 版本可以比 Windows 完成更多的计算。此外,当两个经过优化的编译程序被反编译后,我们发现产生的循环几乎完全一致,实际上,linux 的循环比 Windows 的多包含了两个指令。
fract2.cpp 运行时间不能说明内存复制速度上的巨大差别。而且,Fract2.cpp 可能说明了这些差别的一部分,但不是全部。
|
|
就我们研究的这一点而言,我们没有足够的信息来充分理解内存复制的异常。而且,我们只涉及了块内存转移的一个很小方面。因此,还不能对 Linux 和 Windows 的优劣做出公正的结论。我们可以断定,对于 16M 字节传输,在两种平台上使用 memcpy 都是个好主意。我们也可以断定,正如 fract2.cpp 所显示的,Windows 操作系统只有一小部分的系统额外开销。然而,最好让读者自己来评价这里使用的技术,以及提出如何在这两个小测试中使每种系统运行更高效的建议。
本文提出的问题比回答的问题更多,我们将在以后的文章里更仔细地考察内存性能,在此之前,是否一个系统的块内存移动性比另一个更高仍然是个未知的问题。
- 请阅读开始这一专栏的 介绍文章;它定义了 Ed 使用的测量工具。
- 获取本文提到的文件:
- memxfer5b.cpp 是测试程序;它可使用 7 种不同的内存转移技巧。
- memxfer5b.cpp 的主循环是真正移动内存的部分。
- fract2.cpp 是一个短浮点数(双)循环计算的简单程序。它重复分形公式直至 100,000,000 次重复或分形点"逃逸"。
- 在 developerWorks Linux zone 有更多的 Linux 参考资料,包括这些文章:
Edward Bradf |