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
上面的代碼相對簡單,這里就不詳細解釋了。可以看到有一個名為 _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+27j ; _VEC_memzero+7Dp var_10 = dword ptr -10h var_C = dword ptr -0Ch var_8 = dword ptr -8 var_4 = dword ptr -4 arg_0 = dword ptr 8 ;void* _Dst; arg_8 = dword ptr 10h ;size_t _Size; push ebp mov ebp, esp sub esp, 10h mov [ebp+var_4], edi ; 保護 EDI 寄存器 mov eax, [ebp+arg_0] ; 以下是計算 EDI = _Dst % 16; cdq ; 把EAX有符號擴展到 Quadword (EDX:EAX) mov edi, eax xor edi, edx sub edi, edx and edi, 0Fh xor edi, edx sub edi, edx test edi, edi jnz short loc_4085A5; if(_Dst % 16 != 0) goto... mov ecx, [ebp+arg_8] mov edx, ecx and edx, 7Fh mov [ebp+var_C], edx cmp ecx, edx jz short loc_40858A sub ecx, edx push ecx push eax call fastzero_I ; 調用 fastzero_I 進行設置(SSE2) add esp, 8 mov eax, [ebp+arg_0] mov edx, [ebp+var_C] loc_40858A: ; 處理尾端的零散字節 test edx, edx jz short loc_4085D3 add eax, [ebp+arg_8] sub eax, edx mov [ebp+var_8], eax xor eax, eax mov edi, [ebp+var_8] mov ecx, [ebp+var_C] rep stosb mov eax, [ebp+arg_0] jmp short loc_4085D3 loc_4085A5: ; 處理未對齊到 128-bit 的首端的零散字節 neg edi add edi, 10h ; mov [ebp+var_10], edi xor eax, eax mov edi, [ebp+arg_0] ; EDI = _Dst; mov ecx, [ebp+var_10]; ECX = 16 - (_Size % 16); rep stosb mov eax, [ebp+var_10] mov ecx, [ebp+arg_0] mov edx, [ebp+arg_8] add ecx, eax sub edx, eax push edx push 0 push ecx call _VEC_memzero; _Dst 已經對齊,再次調用自身 add esp, 0Ch mov eax, [ebp+arg_0] loc_4085D3: ; CODE XREF: _VEC_memzero+41j ; _VEC_memzero+58j mov edi, [ebp+var_4] mov esp, ebp pop ebp retn
上面的代碼,和前面提到的三部是基本一致的。但它主要是完成(3.1)和(3.3)部分,對應與(3.1)為處理不能達到對齊粒度(16 Bytes)的那些零散字節(1~15 Bytes),對應於(3.3)是處理結尾的零散字節(1~127 Bytes)。中間已經對齊到 oword(這里我將稱其為八字,由 16 bytes 組成)的部分,是通過調用 fastzero_I (其處理的內存塊以 128 bytes 為一個基本單位循環處理,即循環體每次采用連續 8 條指令設置 128 Bytes)來完成的。
下面先給出上面的匯編代碼翻譯到 C 語言的代碼:

void* _VEC_memzero(void* _Dst, int _Val, size_t _Size) { int remain, count, i; BYTE *pBytes; //(2.1)處理起始位置未對齊到 128-bit 的字節; remain = ((int)_Dst) % 16; if(remain != 0) { count = 16 - remain1; pBytes = (BYTE*)_Dst; for(i = 0; i < count; i++) { pBytes[i] = 0; } _VEC_memzero(pBytes + count, 0, _Size - count); return _Dst; } remain = _Size & 127; //(2.2)利用 SSE2 擴展快速初始化 if(remain != _Size) { fastzero_I(_Dst, _Size); } //(2.3)處理結尾剩余的字節 if(remain != 0) { pBytes = (BYTE*)(_Dst) + _Size - remain; for(i = 0; i < remain; i++) { pBytes[i] = 0; } } return _Dst; }
上面的代碼,和使用 rep stosd 的方式相同,只是需要地址對齊的基本單位粒度更大。下面給出實現了的(3.2)的 fastzero_I 函數的匯編代碼。可以看到這個函數也是使用循環來處理的,假設我們把 oword (128-bit,16Bytes)看做一行,則下面的循環每次處理 8 行(128 bytes)。
這是一種擴充循環體的寫法,加大跳轉之間的跨度,以減小因跳轉帶來的性能懲罰,提高 CPU 流水線效率。當然,以現在的 CPU 技術來說,程序員或許不必顯示的這樣寫,CPU 執行時也可能有能力得到相同的優化結果。(2014年5月3日補充 --hoodlum1980)
因為此函數沒有觸碰 EAX,所以認為其原型為 void fastzero_I ( void* _Dst, size_t _Size );

; ; void fastzero_I(void* _Dst, size_t _Size); ; fastzero_I proc near var_4 = dword ptr -4 arg_0 = dword ptr 8 arg_4 = dword ptr 0Ch push ebp mov ebp, esp sub esp, 4 mov [ebp+var_4], edi mov edi, [ebp+arg_0] mov ecx, [ebp+arg_4] shr ecx, 7 pxor xmm0, xmm0 jmp short loc_408514 lea esp, [esp+0] nop loc_408514: ; CODE XREF: fastzero_I+16j ; fastzero_I+4Ej movdqa oword ptr [edi], xmm0 movdqa oword ptr [edi+10h], xmm0 movdqa oword ptr [edi+20h], xmm0 movdqa oword ptr [edi+30h], xmm0 movdqa oword ptr [edi+40h], xmm0 movdqa oword ptr [edi+50h], xmm0 movdqa oword ptr [edi+60h], xmm0 movdqa oword ptr [edi+70h], xmm0 lea edi, [edi+80h] dec ecx jnz short loc_408514 mov edi, [ebp+var_4] mov esp, ebp pop ebp retn
上面的代碼中,ECX 和 EDI 寄存器依然作為循環次數和目標地址索引來使用,和串操作中的用法相同,只是這里用的是 movdqa 指令,所以需要編譯器“手工”更新 ECX 和 EDI 寄存器。
同時,可以看出在 _VEC_memzero 中調用 fastzero_I 的幾個前提條件是:
(a).CPU支持 SSE2(因為使用了 SSE2 擴展的指令和寄存器)。
(b)._Dst 已經對齊到 16-byte,即需滿足 _Dst & 0xF = 0。否則將引發 (GP#, general-protection) 異常。
(c)._Size 大於等於 128 bytes。(因為 fastzero_I 中的循環體每次設置 128 Bytes)。
總結上面的代碼,可以得到如下結論:
memset 在常規條件下以 DWORD 為粒度對內存設置,在特定條件下(當要對內存初始化為 0 ,且需要初始化的內存達到某個閾值,且 CPU 支持 SSE2),則使用 SSE2 特性進行快速初始化。
(4)總結:
(3.1)對數組使用初始化列表,或 memset 兩者在底層上可能等效。(msvc編譯器將前者處理為后者)。
(3.2)對數組用循環初始化,和使用 memset 初始化相比,很有可能等效。即使不等效(memset 調用了 SSE2 擴展),也不可能達到成為一個優化命題和關注點。
(3.3)如果一定要說有點區別,那就是如果是對一個整數數組用初始化列表或者循環初始化,那么編譯器不需要考慮地址對齊的問題(因為編譯器必然把數組分配到對齊的地址),而 memset 則需要考慮傳入的地址是否已對齊到某個基本粒度,並對此未對齊部分作處理。
(3.4)當對一個隨機數據組成的內存塊進行清零操作,memset 看起來仿佛是唯一正確的可選方式(如果所在平台無此函數,則可以用手寫循環替代)。聲明數組時提供初始化列表,聲明后再調用 memset 或者使用循環初始化(顯然,在能夠使用 memset 時,循環寫法在高級語言層面不如前者簡潔),無論是代碼規范還是性能層面,這些寫法都不存在值得強調的絕對優劣關系。也就是說,“盡可能避免使用 memset ”這種說法是一種無根據、不負責任、臆測性的個人主觀結論。(2014年5月3日 補充 -- hoodlum1980)
所以綜上,我認為糾纏哪個寫法正確或者更正確是毫無意義的。例如數組的聲明位置,有人認為應該采用另其生存周期盡可能短的原則,而把數組聲明在生命周期更小的循環體中:
while(scanf(...) != EOF)
{
int a[1000] = 0;
...
}
這個問題同樣不成為一個值得討論的命題(編譯器在轉換時,將函數臨時變量的分配和釋放時機集中發生在函數的起始和返回,這是自然的處理方式)。生命周期更短的變量,反而因此而不利於調試。這里一個主要問題在於變量的聲明和使用越接近則對程序員越有利,因此C++等其他語言都已經去除了變量必須在函數開始位置全部聲明的限制。
可以看到,編譯器在優化時很聰明,乃至於會超出我們的預期。以至於為了寫出能夠觀察編譯器行為的測試代碼,有時你不得不動點腦筋。例如,如果寫一個函數對數據進行循環的初始化,則編譯器會把它內聯。如果你寫了一些在編譯器看來沒有用處的代碼和變量,則編譯器會把它們全都去掉。有些局部變量,也可能不會出現在棧上(被編譯器優化掉或者暫存於寄存器)。
例如,除數為常量的除法或取余,會被轉化為整數乘法和移位操作(如果需要移位的話),例如 x = y / 10 會被等效為:
x = ( y * 0x 6666 6667) >> 34;
例如,計算 x = y * 9 + 17;
lea eax, [ecx + ecx * 8 + 11h]
(5)參考資料:
(5.1)Intel 64 and IA-32 Architectures Software Developer's Manual Volume 1: Basic Architecture;
(5.2)Source Code Optimization. (Felix von Leitner, Code Blau GmbH), October 2009;
【補充討論】
ZeroMemory / RtlZeroMemory 宏(分別在 <winbase.h> 和 <winnt.h> 中定義)的定義是調用 memset 函數。
SecureZeroMemory / RtlSecureZeroMemory 宏為一個強制 inline 函數,目的是為了保證不會被編譯器優化掉。在 MSDN 中舉了下面的例子來說明這樣做的意義。下面的代碼片段范例來自於 MSDN:
如果下面的代碼中使用 ZeroMemory,由於編譯器認為 szPassword 在結束生命周期前沒有被任何代碼讀取,所以可能會把 ZeroMemory 完全優化掉。這樣密碼內容將會遺留在棧上,導致風險。
// 以下代碼來自於 MSDN 文檔: WCHAR szPassword[MAX_PATH]; // Retrieve the password if (GetPasswordFromUser(szPassword, MAX_PATH)) UsePassword(szPassword); // Clear the password from memory SecureZeroMemory(szPassword, sizeof(szPassword));
--hoodlum1980 2014-6-19 補充。
參考資料:
(1)SecureZeroMemory(@MSDN), ms-help://MS.VSCC.v80/MS.MSDN.v80/MS.WIN32COM.v10.en/memory/base/securezeromemory.htm