内存复制
在计算机中,内存复制经常而普遍。它们出现在联网应用、数据库应用、科学应用以及几乎您能想得到的其它任何应用和服务中。因为它们是如此的通用,所以程序员对于内存复制有点满不在乎,而且还采用了各种各样的编程技巧来完成复制。内存复制可以是整块内存的简单移动,也可以根本不是一个复制而类似一个访问模式。
一个 访问模式的形成是通过访问一个矩阵中的各栏。比如您需要 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
Linux 显示 MHz 的命令是:
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 操作系统只有一小部分的系统额外开销。然而,最好让读者自己来评价这里使用的技术,以及提出如何在这两个小测试中使每种系统运行更高效的建议。
本文提出的问题比回答的问题更多,我们将在以后的文章里更仔细地考察内存性能,在此之前,是否一个系统的块内存移动性比另一个更高仍然是个未知的问题。
参考资料
关于作者
相关推荐
4. **Java堆**(Heap): 这是JVM中最大的一块内存区域,所有对象实例和数组都在这里分配内存。堆内存是所有线程共享的,垃圾收集器的主要工作区域。Java堆可以被划分为新生代(Young Generation)和老年代(Tenured ...
4. **内存监控**:内存监控是通过Java管理扩展API(JMX)提供的Runtime/MemoryMXBean和MemoryPoolMXBean来实现,可以监控整个堆内存和各个内存池的使用情况。 接着,文档介绍了堆内存的监控和设置参数: 1. **堆...
共享内存是一种多进程通信方法,其中一个进程创建一块内存区域,并将其设置为可由其他进程访问。这种方式使得数据可以直接在内存中传递,而无需通过文件或网络接口,因此速度非常快。 在Codesys中,我们可以使用其...
这意味着新对象和原始对象共享同一块内存空间,对其中一个对象的修改会影响到另一个。如果原始对象包含的是值类型字段,那么这些字段会被逐个复制到新对象中;但如果包含的是引用类型字段,如列表或自定义对象,那么...
6. **内存管理**:在复制大量数据时,可能需要考虑内存缓冲区的使用,以提高效率。 7. **异常处理**:为了确保程序的健壮性,可以使用`try-catch`块来捕获可能出现的异常,如文件未找到、磁盘空间不足等。 8. **...
- **堆(Heap)**:这是JVM中最大的一块内存,用于存储对象实例和数组。堆被划分为新生代(Young Generation)和老年代(Old Generation)两个部分,进一步细分为Eden空间、Survivor空间(From和To)。 - **运行...
而缓冲区复制则是在内存中开辟一块区域作为缓冲,一次性读取和写入更多数据,提高了效率。在C++中,可以利用`std::ifstream`和`std::ofstream`配合缓冲区(如`std::streambuf`)实现高效复制。 C++源码实现文件复制...
- **复制算法**: 将可用内存分为两块相同大小的空间,每次只使用其中一块,回收时将存活的对象复制到另一块空间。 - **标记-整理算法**: 在标记阶段标记出所有需要回收的对象,然后将存活的对象向一端移动,清理出...
首先,我们需要了解C#中的命名空间`System.Runtime.InteropServices`,这个命名空间提供了与操作系统交互的接口,包括对共享内存的操作。我们通常会使用`Marshal`类来分配和管理非托管内存,以及`IntPtr`类型来存储...
因此, malloc 返回的每块内存的起始处首先要有这个结构: 清单 3. 内存控制块结构定义 struct mem_control_block { int is_available; int size; }; 现在,您可能会认为当程序调用 malloc 时这会引发...
- **堆 (Heap)**: 存放对象实例,是JVM管理的最大一块内存。GC主要发生在堆中,通常会将堆细分为多个子区域,以便更高效地进行垃圾回收。 - **方法区域**: Hotspot JVM中的永久代(Permanent Generation),存放每个...
返回的每块内存的起始处首先要有这个结构: 清单 3. 内存控制块结构定义 struct mem_control_block { int is_available; int size; }; 现在,您可能会认为当程序调用 malloc 时这会引发问题 —— 它们...
4. Java 堆:它是 Java 虚拟机管理的内存中最大的一块,是被所有线程共享的一块内存区域,它在虚拟机启动时创建,它的唯一目的是存储对象实例,几乎所有的对象都在这里分配内存。Java 堆也是垃圾收集器管理的主要...
`Marshal.Copy`函数可以用来从进程内存复制数据到缓冲区,以便进一步分析。 对于特定数据类型的查找,例如寻找浮点数或整数,可以使用`BitConverter`类将字节数组转换为相应类型。一旦找到目标数据,我们就可以使用...
Apache的内存池管理机制主要体现在APR(Apache Portable Runtime)库中,这是一个为Apache提供跨平台支持的基础运行库。内存池的概念是Apache内存管理的核心,它提供了一种高效的内存分配和回收方式。 - **基本概念...
1. **减少内存分配次数**:通过预先分配一大块内存,后续需要时直接从中获取,避免了频繁调用`malloc`和`free`所带来的性能开销。 2. **自动回收内存**:当内存池被销毁时,所有未释放的内存将自动回收,这有助于...
4. **堆(Heap)**:是所有线程共享的一块内存区域,主要用于存放对象实例和数组。Java垃圾收集器主要管理的就是堆内存,通过新生代、老年代划分进行不同策略的垃圾回收。 5. **方法区(Method Area)**:也被称为...
用途:将设备数组中的二维内存复制到主机上。 **1.5.16 cudaMemcpyArrayToArray** 用途:在设备数组之间复制内存。 **1.5.17 cudaMemcpy2DArrayToArray** 用途:在设备数组之间复制二维内存。 **1.5.18 ...
4. **堆(Heap)**:这是所有线程共享的一块内存区域,主要用于对象实例的存储。Java垃圾收集器管理堆内存,进行对象的分配和回收。 5. **方法区(Method Area)**:也被称为“永久代”或“元空间”,存储类和接口...