`
netcome
  • 浏览: 467468 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

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

阅读更多

我的 前一专栏专注于 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,并且每周

分享到:
评论

相关推荐

    内存管理内存管理内存管理

    返回的每块内存的起始处首先要有这个结构: 清单 3. 内存控制块结构定义 struct mem_control_block { int is_available; int size; }; 现在,您可能会认为当程序调用 malloc 时这会引发问题 —— 它们...

    操作系统(内存管理)

    因此, malloc 返回的每块内存的起始处首先要有这个结构: 清单 3. 内存控制块结构定义 struct mem_control_block { int is_available; int size; }; 现在,您可能会认为当程序调用 malloc 时这会引发...

    游戏画面就弹出内存不能为read修复工具

    程序需要一块内存用以储存数据时,就需要使用操作系统提供的「功能函数」来申请,如果内存分配成功,函数就会将所新开辟的内存区地址返回给应用程序,应用程序就可以通过这个地址使用这块内存。这就是「动态内存分配...

    Java常见面试问题整理.docx

    Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。 2.方法区(元空间):方法区用于存储已被虚拟机加载的类信息、常量、...

    操作系统精髓与设计原理答案

    步骤2:3->AC 步骤3:5940->IR;步骤4:3+2=5->AC 步骤5:7006->IR:步骤6:AC->设备 6 1.2、本章中用6步来描述图1.4中的程序执行情况,请使用MAR和MBR扩充这个描述。 答案:1. a. PC中包含第一条指令的...

    C++初学者指南(原版)

    第 2 章:数据类型和运算符简介 编程语言的核心在于其数据类型和运算符。不出您所料,C++ 支持大量数据类型和运算符,使其适合的编程范围非常广泛。此模块对 C++ 基本数据类型及其最常用运算符进行探讨。我们还将...

    c++ 面试题 总结

    当程序运行时需要从内存中读出这段程序的代码。代码的位置必须在物理内存中才能被运行,由于现在的操作系统中有非常多的程序运行着,内存中不能够完全放下,所以引出了虚拟内存的概念。把哪些不常用的程序片断就放入...

    Oracle9i的init.ora参数中文说明

    否则在运行 PL/SQL V3 时将接受某些 PL/SQL V2 行为。 值范围: TRUE | FALSE 默认值: FALSE plsql_native_make_utility: 说明: 指定 make 实用程序 (如 UNIX 中的 make 或 gmake, 即 GNU make) 的完整路径名。要...

    计算机基础计算机系统知识点.doc

    o " " " " " " "1:计算机系统由硬件系统和软件系统组成 " "2:计算机硬件系统包括:运算器、控制器、存储器、输入设备、输出设 " "备 " "3:控制器是整个计算机系统的控制中心,指挥计算机各部分协调工作 " "4:...

    《计算机操作系统》期末复习指导

    (2)按选定的算法,从后备作业队列中选出一部分(多道)或一个作业投入运行; (3)为被选中的作业做好运行前的准备工作。例如建立相应的执行进程和分配系统资源; (4)作业运行结束的善后处理工作。 ...

    (重要)AIX command 使用总结.txt

    <2>运行which_fileset命令, 根据文件查找对应的文件集. 例如: #which_fileset iostat /usr/bin/iostat bos.acct 5.1.0.0 运行lslpp -f 命令, 查看指定文件集中包含的文件: #lslpp -f bos.acct //出于AIX系统...

    网管教程 从入门到精通软件篇.txt

    该信息在运行 fixboot 和 fixmbr 命令时非常有用。  map 命令仅在使用故障恢复控制台时才可用。  Map [ arc]  参数  arc  指示 map 命令显示高级 RISC 计算 (ARC)设备名称而不是设备名称。以下是 ARC 设备...

    Visual C#2010 从入门到精通(Visual.C#.2010.Step.By.Step).完整去密码锁定版 I部分

    第i部分 visual c#和visual studio 2010概述 第1章 欢迎进入c#编程世界 3 1.1 开始在visual studio 2010环境中编程 3 1.2 编写第一个程序 8 1.3 使用命名空间 12 1.4 创建图形应用程序 15 第1章快速参考 22 ...

    StrongOD v0.2.6

    2,CPU DUMP 窗口,如果选中一个内存块的第一个字节,Infoline会显示异常 增加: 如果断点窗口没有任何断点,则不显示菜单 [2007.11.14 v0.10] 增加创建进程模式 本插件提供了3种方式来启动进程: 1,Normal 和...

Global site tag (gtag.js) - Google Analytics