性能殺手:”潛伏”的memset
性能殺手:”潛伏”的memset
【memset性能陷進】
memset是大家常用的函數,而且一般的編程書籍都會諄諄告誡大家:申請內存後要初始化,防止使用未經初始化的內存導致不可預知的結果,所以我們一般都會按照如下方式編寫代碼:
char* buffer = (char*)malloc(1024);
memset(buffer, 0x00, 1024);
代碼看起來很標準,也很美觀,但這裏卻隱藏著一個陷進:memset的性能並不高!如果你的內存在1K左右,可能還察覺不到,但如果是1M,那性能影響就很明顯了,我們來看實測數據(Redhat EL5 Intel(R) Xeon(R) CPU E5620 2.40GHz, gcc 4.1.2):
內存大小 |
重複次數 |
時間 |
1K |
100000 |
14ms |
1M |
1000 |
60ms |
10M |
1000 |
725ms |
所以如果你在代碼裏循環調用memset,或者每次流程處理都要調用memset,而且每次都要memset 1M以上,那麼你就要小心了,1000次的memset 1M就能夠消耗你100多ms的時間,對於高並發高性能的係統來說,這個時間是非常可觀的,這就意味著即使不考慮業務處理,1秒鍾你最多也不可能處理超過10000次請求。
【“潛伏”的memset】
但這隻是memset的第一個陷阱,還有更加隱蔽的第二個陷阱,而且這個陷阱你從代碼上根本看不出和memset有什麼關係!下麵我們就來看這個“潛伏”的memset究竟是如何潛伏的。
代碼很簡單,就是在棧內存中申請緩衝區,然後再賦值:
char buffer[1024] = {0};
這一行代碼很簡單,和上麵代碼不同的地方就在於一個在堆內存中申請,一個在棧內存中申請。但就是這麼一行簡單的代碼,卻隱藏了一個陷阱:初始化的時候調用了memset。我們用ltrace工具來看看:
===============================代碼==================================== int main(){ char buffer[1024 * 1024] = {0}; } ==============================ltrace 輸出================================ __libc_start_main(0x8048444, 1, 0xbff9b0a4, 0x80484b0, 0x80484a0 <unfinished ...> memset(0xbfe9afec, '/000', 1048576) = 0xbfe9afec +++ exited (status 236) +++ |
可能會有同學會說,你全部都初始化為0了,當然慢了,如果我隻初始化第一個元素,是不是會沒有這個問題呢?
多說無益,代碼驗證:
===============================代碼==================================== int main(){ char buffer[1024 * 1024] = {0,}; } ==============================ltrace 輸出================================ __libc_start_main(0x8048444, 1, 0xbfb54f84, 0x80484b0, 0x80484a0 <unfinished ...> memset(0xbfa54ecc, '/000', 1048576) = 0xbfa54ecc +++ exited (status 204) +++ |
從上麵的ltrace輸出可以看出,不管你是全部設置為0,還是隻設置第一個,都會調用memset,而且memset的長度都是1024*1024,難道這行代碼和括號裏的值沒有關係,無論怎樣都會調用memset?我們再來驗證一下:
===============================代碼==================================== int main(){ char buffer[1024 * 1024] = {}; } ==============================ltrace 輸出================================ __libc_start_main(0x8048444, 1, 0xbff0e844, 0x80484b0, 0x80484a0 <unfinished ...> memset(0xbfe0e78c, '/000', 1048576) = 0xbfe0e78c +++ exited (status 140) +++ |
再驗證括號裏設置其它值:
===============================代碼==================================== int main(){ char buffer[1024 * 1024] = {100}; } ==============================ltrace 輸出================================ __libc_start_main(0x8048444, 1, 0xbfea51b4, 0x80484b0, 0x80484a0 <unfinished ...> memset(0xbfda50fc, '/000', 1048576) = 0xbfda50fc +++ exited (status 252) +++ |
經過4次代碼驗證,確實證實了不管{}裏麵是什麼內容,都會調用memset,將所有內存設置為0。
實際上上麵的代碼裏麵還有一個陷阱,下麵這兩行語句的效果是一樣的,第一行並不是將所有的內存位都設置為0,而隻是設置第0個內存位為0.
char buffer[1024 * 1024] = {0};
char buffer[1024 * 1024] = {0,};
因為隱藏調用了memset,所以設置為0的時候還看不出,我們換一個值就看的很清楚了:
============================代碼======================================= #include <stdio.h> int main(){ char buffer[1024 * 1024 ] = {100}; printf("%d/n", *buffer); printf("%d/n", *(buffer+1)); } =============================輸出====================================== 100 0 |
各種賦值的情況我們已經驗證過了,當然也要驗證一下不賦值的情況,下麵是驗證情況:
===============================代碼==================================== int main(){ char buffer[1024 * 1024 ]; *buffer = 'A'; printf("%d/n", *buffer); } ==============================ltrace 輸出================================ __libc_start_main(0x8048444, 1, 0xbfbab8f4, 0x80484b0, 0x80484a0 <unfinished ...> printf("%d/n", 6565 ) = 3 +++ exited (status 3) +++ |
從ltrace結果可以清楚的看出:即使使用了內存,這次也並沒有調用memset了。
除了char類型外,其它的類型也都是這樣的,有興趣的同學可以自己嚐試一下,這裏就不貼詳細的代碼了。
【總結匯總】
代碼 |
作用 |
性能(ms/times) |
是否隱含調用memset |
char buffer[1024 * 1024] = {0}; |
申請棧空間,並將第一個元素設置為0 |
75/1000 |
是 |
char buffer[1024 * 1024] = {0,}; |
75/1000 |
||
char buffer[1024 * 1024] = {}; |
申請棧空間,不設置任何元素 |
75/1000 |
|
char buffer[1024 * 1024] = {100}; |
申請棧空間,並將第一個元素設置為100 |
75/1000 |
|
char buffer[1024 * 1024 ]; |
隻申請棧空間 |
5/1000000 |
否 |
綜合前麵的代碼分析和性能測試,建議大家在編寫代碼的時候不要用char buffer[1024 * 1024] = XXX;這種方式,而要使用char buffer[1024 * 1024];除非確定一定要memset。
有的同學就會問到:既然這樣,難道我們就不用memset了?
我的理解是:memset 1k以下,次數也不多,則可以用;否則除非你確定一定要memset,否則就不要用,你可以用其它替代方法。
例如:
1)如果內存是用來存放字符串的,你完全可以使用sprintf等函數,讓其自動添加結束符;或者自己記錄字符串的長度,在最後手動設置字符串結束符。
2)對於C++的struct來說,可以定義默認構造函數來完成初始化。
附:
使用Visual studio 2008測試,發現有同樣的問題,初步估計是不是哪個標準定義了,但找了幾個C/C++/POSIX的標準初看了一下,內容太多,都是英文的,一時半會看不出來,有興趣有時間的同學可以去研究一下。
===========================================================================================
2014.10.10補充: 經過CSDN網友 @土雞笨蛋 的提醒,然後自己試了一下,在gcc編譯時加上 O2或者O3 優化參數,就不會隱含調用memset了
非常感謝@土雞笨蛋 的測試和補充,讓這個知識點更全麵了,由此也引出兩個值得注意的點:
1)Debug模式和Release模式處理不一樣,因此可能出現Debug模式下沒有問題,正式發布上線後就有問題
2)研發開發的時候為了方便可以用Debug模式,但給測試同學測試的時候一定要用Release模式,否則就可能出現上述問題
歡迎轉載,轉載請注明出處:https://blog.csdn.net/yunhua_lee/article/details/6381866
最後更新:2017-04-02 06:51:38