memset 是 msvcrt 中的一個函數,其做用和用途是顯而易見的,一般是對一段內存進行填充,就其做用自己不具備任何歧義性。但就有人必定要糾結對數組的初始化必定要寫成以下形式:程序員
int a[...] = { 0 };windows
int a[100] = { 1, 2 };數組
而認爲以下使用 memset 的寫法不明就裏的被其排斥和拒絕:less
memset(a, 0, sizeof(a));ide
這種見解首先是毫無道理的,在代碼風格,可讀性,可維護性上根本不構成一個命題,且 memset 在開發中的使用是很是常見的。這種錯誤觀點來自於對代碼風格和語言的僵硬理解,以後咱們將看到在編譯器處理後二者的等效性。函數
【補充】在討論以前,須要先明確一個基本常識,即 memset 中提供的那個填充值的參數,是以字節爲單位填充內存,所以實際的 memset 處理中只把它看成字節處理(即只有 0-7 bit 重要,高位被忽略),將其低位字節擴展成 32 位(例如參數值爲 0x12345678,則實際被擴展成 0x78787878),而後用 rep stosd 填充。所以 memset 不能像循環賦值同樣,完成對內存完成 4 bytes 爲週期的週期性填充(而只能把全部字節都賦值爲相同值),但彙編語言能夠。工具
所以,假設有一個整數數組 a[],若是把全部元素賦值爲 0,能夠用 memset (a, 0, sizeof ( a )); // 這多是 memset 使用中最多見的狀況
oop
若是把全部元素賦值爲 -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