memset 的实现分析

简介:   memset 是 msvcrt 中的一个函数,其作用和用途是显而易见的,通常是对一段内存进行填充,就其作用本身不具有任何歧义性。但就有人一定要纠结对数组的初始化一定要写成如下形式:     int a[...] = { 0 };   int a[100] = { 1, 2 };     而认为如下使用 memset 的写法不明就里的被其排斥和拒绝:     memset(a, 0, sizeof(a));     这种看法首先是毫无道理的,在代码风格,可读性,可维护性上根本不构成一个命题,且 memset 在开发中的使用是非常常见的。

  memset 是 msvcrt 中的一个函数,其作用和用途是显而易见的,通常是对一段内存进行填充,就其作用本身不具有任何歧义性。但就有人一定要纠结对数组的初始化一定要写成如下形式:

 

  int a[...] = { 0 };

  int a[100] = { 1, 2 };

 

  而认为如下使用 memset 的写法不明就里的被其排斥和拒绝:

 

  memset(a, 0, sizeof(a));

 

  这种看法首先是毫无道理的,在代码风格,可读性,可维护性上根本不构成一个命题,且 memset 在开发中的使用是非常常见的。这种错误观点来自于对代码风格和语言的僵硬理解,之后我们将看到在编译器处理后两者的等效性。

 

  【补充】在讨论之前,需要先明确一个基本常识,即 memset 中提供的那个填充值的参数,是以字节为单位填充内存,因此实际的 memset 处理中只把它当作字节处理(即只有 0-7 bit 重要,高位被忽略),将其低位字节扩展成 32 位(例如参数值为 0x12345678,则实际被扩展成 0x78787878),然后用 rep stosd 填充。因此 memset 不能像循环赋值一样,完成对内存完成 4 bytes 为周期的周期性填充(而只能把所有字节都赋值为相同值),但汇编语言可以。

  因此,假设有一个整数数组 a[],如果把所有元素赋值为 0,可以用 memset (a, 0, sizeof ( a )); // 这可能是 memset 使用中最常见的情况

  如果把所有元素赋值为 -1 ( signed ) / 最大值 (unsigned) , 可以用 memset (a, 0xFF, sizeof ( a ));

  如果要把所有元素赋值为任意一个常数值,则 memset 不能达到要求,需要用高级语言的循环进行赋值。

 

  -- hoodlum1980 on 2014年6月19日 补充。

 

  本文讨论的前提条件是:操作系统平台为 windows 系统,编译器为 VS2005 中的 VC,编译输出选项主要为 Release,反汇编工具为 VC 本身和 IDA。下面将给出一些经过实际观察和分析得到的基本结论,

 

  (1)在数组被声明时提供初始化列表(且语言上仅能在声明时提供),其语法定义时对于缺省元素将使用 0 填充。在 MSVC 编译器的 release 输出中,将后续元素使用 memset 进行初始化。

 

  (2)对数组用循环初始化时(这里假设数组元素类型为 int),编译器将其处理为 rep stosd 指令。

 

  这个情况的汇编代码比较简单,因此忽略。根据这一点可以看到,不论在代码风格层面还是运行效率层面,认为使用初始化列表优于 memset 都是一种毫无理由的主观臆测。事实上,两者在运行效率上等效,且代码风格上不存在优劣之分。所以,当程序员对结构体,数组进行初始化时,不需要在这里产生犹豫。后面我们还会看到,对数组用循环的方法初始化,和调用 memset 初始化,在多数条件下的等效性。

 

  (3)memset 的实现。

 

  这里分析 memset 这个函数在汇编语言层面的实现方式。首先,memset 的原型如下:

  

  void* __cdecl memset (void* _Dst, int _Val, size_t _Size);

 

  第二个参数虽然为 int 类型,但是函数针对的目标是字节,所以它实际上提供的是一个字节的值。首先给出该函数的常规实现过程(后面我们将分析在 CPU 支持 sse2 时的分支)的基本结论:

 

  (3.1)如果 _Dst 没有对齐到 DWORD,则先把前面未对齐部分(1~3 bytes),以字节为单位循环设置。

  (3.2)主要循环部分 rep stosd 串存储指令,以 DWORD (4 bytes) 为基本单位循环设置。

  (3.3)如果还有一些字节(1~3 bytes)未被设置,则以字节为单位循环设置。

 

  以上是 memset 的方法的过程,后面我们将看到当 CPU 支持 SSE2 时的分支和上述步骤相同,只是第二步中基本单位的粒度更大(128 bit / 16 bytes)。

  下面给出的是 memset 在 IDE 中的汇编代码,来自于 Micrsoft Visual Studio X\VC\crt\src\intel\memset.asm 的内容(下面的汇编代码在以字节为单位时使用的是 MOV [EDI], AL, 而在实际编译结果中是 rep stosb):

 

        CODESEG

    extrn   _VEC_memzero:near
    extrn   __sse2_available:dword

        public  memset
memset proc \
        dst:ptr byte, \
        value:byte, \
        count:dword

        OPTION PROLOGUE:NONE, EPILOGUE:NONE

        .FPO    ( 0, 3, 0, 0, 0, 0 )

        mov     edx,[esp + 0ch] ; edx = "count"
        mov     ecx,[esp + 4]   ; ecx points to "dst"

        test    edx,edx         ; 0?
        jz      short toend     ; if so, nothing to do

        xor     eax,eax
        mov     al,[esp + 8]    ; the byte "value" to be stored

; Special case large block zeroing using SSE2 support
    test    al,al ; memset using zero initializer?
    jne     dword_align
    cmp     edx,0100h ; block size exceeds size threshold?
    jb      dword_align
    cmp     DWORD PTR __sse2_available,0 ; SSE2 supported?
    je      dword_align

    jmp     _VEC_memzero ; use fast zero SSE2 implementation
    ; no return

; Align address on dword boundary
dword_align:

        push    edi             ; preserve edi
        mov     edi,ecx         ; edi = dest pointer

        cmp     edx,4           ; if it's less then 4 bytes
        jb      tail            ; tail needs edi and edx to be initialized

        neg     ecx
        and     ecx,3           ; ecx = # bytes before dword boundary
        jz      short dwords    ; jump if address already aligned

        sub     edx,ecx         ; edx = adjusted count (for later)
adjust_loop:
        mov     [edi],al
        add     edi,1
        sub     ecx,1
        jnz     adjust_loop

dwords:
; set all 4 bytes of eax to [value]
        mov     ecx,eax         ; ecx=0/0/0/value
        shl     eax,8           ; eax=0/0/value/0

        add     eax,ecx         ; eax=0/0val/val

        mov     ecx,eax         ; ecx=0/0/val/val

        shl     eax,10h         ; eax=val/val/0/0

        add     eax,ecx         ; eax = all 4 bytes = [value]

; Set dword-sized blocks
        mov     ecx,edx         ; move original count to ecx
        and     edx,3           ; prepare in edx byte count (for tail loop)
        shr     ecx,2           ; adjust ecx to be dword count
        jz      tail            ; jump if it was less then 4 bytes

        rep     stosd
main_loop_tail:
        test    edx,edx         ; if there is no tail bytes,
        jz      finish          ; we finish, and it's time to leave
; Set remaining bytes

tail:
        mov     [edi],al        ; set remaining bytes
        add     edi,1

        sub     edx,1           ; if there is some more bytes
        jnz     tail            ; continue to fill them

; Done
finish:
        mov     eax,[esp + 8]   ; return dest pointer
        pop     edi             ; restore edi

        ret

toend:
        mov     eax,[esp + 4]   ; return dest pointer

        ret
memset.asm

 

  上面的代码相对简单,这里就不详细解释了。可以看到有一个名为 _VEC_memset 的标签(是一个具体函数)在满足条件时接管了此函数。即当同时满足:(1)_Val 为 0;(2) CPU 支持 SSE2,(3)_Size 达到某个阈值(这里是256字节)时,memset 将会跳转到 _VEC_memzero 分支。

  关于 SSE2,我将引用 Intel 的文档内容简要介绍如下:

 

  SSE2 全称是 Streaming SIMD Extention2, SIMD 全称是 Single-Instruction, Multiple-Data,是 Intel MMX 技术支持的一种单指令多数据运行模型,其目的为提高多媒体和通讯应用程序的性能。

 

  由于多媒体数据处理的特征是,常见在大量的小元素(BYTE,WORD,DWORD 等)组成的连续数据上进行相同的操作,所以可以在一条指令中提高数据吞吐能力来提高效率(即每次把多个数据打包成一组进行相同的并行操作),即 SIMD。(我的解释性评论,2014年5月3日补充 -- hoodlum1980)

 

  SSE2 在 Pentium 4 和 Intel Xeon 处理器中引入,提高了 3-D 图形,视频编码解码,语音识别,互联网,科学技术和工程应用程序的性能。提供 128-bit 的数据类型和相关指令,8 个 128-bit XMM 寄存器(XMM0~XMM7)。后面可以看到,当 CPU 支持 SSE2 时,memset 将采用 SSE2 进行批量设置,每条指令可赋值 16 Bytes。

 

  通过 CPUID.01H (EAX=01H) 指令,如果 EDX.SSE2 [ bit 26 ] = 1,则支持 SSE2 扩展。

 

  memset 是 msvcrt.dll (这个 Dll 有名称不同的多个版本)中的一个导出函数,但如果写一个简单的程序作为观察,编译器将不会让目标程序导入对应的 Dll,而是把 memset 直接插入到目标程序的代码段。

  下面给出的是 _VEC_memzero 的汇编代码:

 

; void* _VEC_memzero(void* _Dst, int _Val(=0), size_t _Size); 
 _VEC_memzero    proc near               ; CODE XREF: memset+27
目录
相关文章
|
6月前
|
C语言
sizeof
【6月更文挑战第17天】
68 1
|
7月前
模拟实现memcpy,memmove,memset,memcmp
memcpy void * memcpy ( void * destination, const void * source, size_t num );
29 1
|
7月前
|
存储 C语言
关于sizeof介绍与分享给大家介绍
关于sizeof介绍与分享给大家介绍
|
编译器 C++
memset的坑
memset 作为对内存初始化的函数,还是有不少坑和误区的,今天就来对这个函数作一个总结。
120 0
|
C++
关于sizeof相关注意点
strlen是专门用来求字符串长度的,统计的是’\0’之前出现的字符个数,一定要找到’\0’才能结束,所以如果没有\0可能会存在越界访问问题
100 0
C++学习——memset函数详解
C++学习——memset函数详解
292 0
|
存储 编译器 C语言
C/C++ 基础之 sizeof 使用(一)
C/C++ 基础之 sizeof 使用
610 0
|
存储 编译器 C++
C/C++ 基础之 sizeof 使用(二)
C/C++ 基础之 sizeof 使用
142 0