curl 中減少記憶體分配操作
今天我在 libcurl 內部又做了 一個小改動 [1],使其做更少的 malloc。這一次,泛型連結串列函式被轉換成更少的 malloc (這才是連結串列函式應有的方式,真的)。
幾周前我開始研究記憶體分配。這很容易,因為多年前我們 curl 中就已經有記憶體除錯和日誌記錄系統了。使用 curl 的除錯版本,並在我的構建目錄中執行此 :
#!/bin/sh export CURL_MEMDEBUG=$HOME/tmp/curlmem.log ./src/curl ./tests/memanalyze.pl -v $HOME/tmp/curlmem.log
對於 curl 7.53.1,這大約有 115 次記憶體分配。這算多還是少?
記憶體日誌非常基礎。為了讓你有所瞭解,這是一個示例片段:
MEM getinfo.c:70 free((nil)) MEM getinfo.c:73 free((nil)) MEM url.c:294 free((nil)) MEM url.c:297 strdup(0x559e7150d616) (24) = 0x559e73760f98 MEM url.c:294 free((nil)) MEM url.c:297 strdup(0x559e7150d62e) (22) = 0x559e73760fc8 MEM multi.c:302 calloc(1,480) = 0x559e73760ff8 MEM hash.c:75 malloc(224) = 0x559e737611f8 MEM hash.c:75 malloc(29152) = 0x559e737a2bc8 MEM hash.c:75 malloc(3104) = 0x559e737a9dc8
然後,我對日誌進行了更深入的研究,我意識到在相同的程式碼行做了許多小記憶體分配。我們顯然有一些相當愚蠢的程式碼模式,我們分配一個結構體,然後將該結構新增到連結串列或雜湊,然後該程式碼隨後再新增另一個小結構體,如此這般,而且經常在迴圈中執行。(我在這裡說的是 我們,不是為了責怪某個人,當然大部分的責任是我自己……)
這兩種分配操作將總是成對地出現,並被同時釋放。我決定解決這些問題。做非常小的(小於 32 位元組)的分配也是浪費的,因為非常多的資料將被用於(在 malloc 系統內)跟蹤那個微小的記憶體區域。更不用說堆碎片了。
因此,將該雜湊和連結串列程式碼修復為不使用 malloc 是快速且簡單的方法,對於最簡單的 “curl ” 傳輸,它可以消除 20% 以上的 malloc。
此時,我根據大小對所有的記憶體分配操作進行排序,並檢查所有最小的分配操作。一個突出的部分是在 curl_multi_wait() 中,它是一個典型的在 curl 傳輸主迴圈中被反覆呼叫的函式。對於大多數典型情況,我將其轉換為 使用堆疊 [2]。在大量重複的呼叫函式中避免 malloc 是一件好事。
現在,如上面的 所示,同樣的 curl localhost 從 curl 7.53.1 的 115 次分配操作下降到 80 個分配操作,而沒有犧牲任何東西。輕鬆地有 26% 的改善。一點也不差!
由於我修改了 curl_multi_wait(),我也想看看它實際上是如何改進一些稍微一些的傳輸。我使用了 multi-double.c [3] 示例程式碼,新增了初始化記憶體記錄的呼叫,讓它使用 curl_multi_wait(),並且並行下載了這兩個 URL:
/512M
第二個檔案是 512 兆位元組的零,第一個檔案是一個 600 位元組的公共 html 頁面。這是 [4]。
首先,我使用 7.53.1 來測試上面的例子,並使用 memanalyze 指令碼檢查:
Mallocs: 33901 Reallocs: 5 Callocs: 24 Strdups: 31 Wcsdups: 0 Frees: 33956 Allocations: 33961 Maximum allocated: 160385
好了,所以它總共使用了 160KB 的記憶體,分配操作次數超過 33900 次。而它下載超過 512 兆位元組的資料,所以它每 15KB 資料有一次 malloc。是好是壞?
回到 git master,現在是 7.54.1-DEV 的版本 - 因為我們不太確定當我們釋出下一個版本時會變成哪個版本號。它可能是 7.54.1 或 7.55.0,它還尚未確定。我離題了,我再次執行相同修改的 multi-double.c 示例,再次對記憶體日誌執行 memanalyze,報告來了:
Mallocs: 69 Reallocs: 5 Callocs: 24 Strdups: 31 Wcsdups: 0 Frees: 124 Allocations: 129 Maximum allocated: 153247
我不敢置信地反覆看了兩遍。發生什麼了嗎?為了仔細檢查,我最好再執行一次。無論我執行多少次,結果還是一樣的。
在典型的傳輸中 curl_multi_wait() 被呼叫了很多次,並且在傳輸過程中至少要正常進行一次記憶體分配操作,因此刪除那個單一的微小分配操作對計數器有非常大的影響。正常的傳輸也會做一些將資料移入或移出連結串列和雜湊操作,但是它們現在也大都是無 malloc 的。簡單地說:剩餘的分配操作不會在傳輸迴圈中執行,所以它們的重要性不大。
以前的 curl 是當前示例分配運算元量的 263 倍。換句話說:新的是舊的分配運算元量的 0.37% 。
另外還有一點好處,新的記憶體分配量更少,總共減少了 7KB(4.3%)。
在幾個 G 記憶體的時代裡,在傳輸中有幾個 malloc 真的對於普通人有顯著的區別嗎?對 512MB 資料進行的 33832 個額外的 malloc 有什麼影響?
為了衡量這些變化的影響,我決定比較 localhost 的 HTTP 傳輸,看看是否可以看到任何速度差異。localhost 對於這個測試是很好的,因為沒有網路速度限制,更快的 curl 下載也越快。伺服器端也會相同的快/慢,因為我將使用相同的測試集進行這兩個測試。
我相同方式構建了 curl 7.53.1 和 curl 7.54.1-DEV,並執行:
curl /80GB -o /dev/null
下載的 80GB 的資料會盡可能快地寫到空裝置中。
我獲得的確切數字可能不是很有用,因為它將取決於機器中的 CPU、使用的 HTTP 伺服器、構建 curl 時的最佳化級別等,但是相對數字仍然應該是高度相關的。新程式碼對決舊程式碼!
7.54.1-DEV 反覆地30%!我的早期版本是 2200MB/秒增加到當前版本的超過 2900 MB/秒。
這裡的要點當然不是說它很容易在我的機器上使用單一核心以超過 20GB/秒的速度來進行 HTTP 傳輸,因為實際上很少有使用者可以透過 curl 做到這樣快速的傳輸。關鍵在於 curl 現在每個位元組的傳輸使用更少的 CPU,這將使更多的 CPU 轉移到系統的其餘部分來執行任何需要做的事情。或者如果裝置是行動式裝置,那麼可以省電。
關於 malloc 的成本:512MB 測試中,我使用舊程式碼發生了 33832 次或更多的分配。舊程式碼以大約 2200MB/秒的速率進行 HTTP 傳輸。這等於每秒 145827 次 malloc - 現在它們被消除了!600 MB/秒的改進意味著每秒鐘 curl 中每個減少的 malloc 操作能額外換來多傳輸 4300 位元組。
一點也不難,非常簡單。然而,有趣的是,在這個舊專案中,仍然有這樣的改進空間。我有這個想法已經好幾年了,我很高興我終於花點時間來實現。感謝我們的測試套件,我可以有相當大的信心做這個“激烈的”內部變化,而不會引入太可怕的迴歸問題。由於我們的 API 很好地隱藏了內部,所以這種變化可以完全不改變任何舊的或新的應用程式……
(是的,我還沒在版本中釋出該變更,所以這還有風險,我有點後悔我的“這很容易”的宣告……)
curl 的 git 倉庫從 7.53.1 到今天已經有 213 個提交。即使我沒有別的想法,可能還會有一次或多次的提交,而不僅僅是記憶體分配對效能的影響。
還有其他類似的情況麼?
也許。我們不會做很多效能測量或比較,所以誰知道呢,我們也許會做更多的愚蠢事情,我們可以收手並做得更好。有一個事情是我一直想做,但是從來沒有做,就是新增所使用的記憶體/malloc 和 curl 執行速度的每日“監視” ,以便更好地跟蹤我們在這些方面不知不覺的迴歸問題。
(關於我在 hacker news、Reddit 和其它地方讀到的關於這篇文章的評論)
有些人讓我再次執行那個 80GB 的下載,給出時間。我執行了三次新程式碼和舊程式碼,其執行“中值”如下:
舊程式碼:
real 0m36.705s user 0m20.176s sys 0m16.072s
新程式碼:
real 0m29.032s user 0m12.196s sys 0m12.820s
承載這個 80GB 檔案的伺服器是標準的 Apache 2.4.25,檔案儲存在 SSD 上,我的機器的 CPU 是 i7 3770K 3.50GHz 。
有些人也提到 alloca() 作為該補丁之一也是個解決方案,但是 alloca() 移植性不夠,只能作為一個孤立的解決方案,這意味著如果我們要使用它的話,需要寫一堆醜陋的 #ifdef。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69901823/viewspace-2997282/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 微軟的HotSpot C2可減少15%堆記憶體分配微軟HotSpot記憶體
- 通過減少記憶體使用改善.NET效能記憶體
- 使用String.intern減少記憶體使用記憶體
- Effective C#:儘量減少記憶體垃圾C#記憶體
- 字串池化,減少1/3記憶體佔用字串記憶體
- C中的記憶體分配模型記憶體模型
- JavaScript記憶體分配JavaScript記憶體
- JVM記憶體分配JVM記憶體
- java記憶體分配Java記憶體
- 谷歌Chrome瀏覽器引入省記憶體/省電模式:減少記憶體佔用谷歌Chrome瀏覽器記憶體模式
- JAVA物件在JVM中記憶體分配Java物件JVM記憶體
- 垃圾收集器與記憶體分配策略_記憶體分配策略記憶體
- win10系統下如何減少RAM記憶體使用Win10記憶體
- JVM 記憶體模型 記憶體分配,JVM鎖JVM記憶體模型
- Netty 中的記憶體分配淺析Netty記憶體
- GO slice 切片-在記憶體中如何分配Go記憶體
- 探索iOS記憶體分配iOS記憶體
- Java 記憶體分配策略Java記憶體
- java jvm 記憶體分配JavaJVM記憶體
- [C++]記憶體分配C++記憶體
- 動態記憶體分配記憶體
- python定時爬蟲啟用時如何減少記憶體?Python爬蟲記憶體
- java基礎-記憶體分配Java記憶體
- C語言-記憶體分配C語言記憶體
- java-方法記憶體分配Java記憶體
- go記憶體分配器Go記憶體
- Java 堆疊記憶體分配Java記憶體
- 記憶體分配策略學習記憶體
- 記憶體分配的確定記憶體
- weblogic的記憶體分配Web記憶體
- 記憶體分配方式 (轉)記憶體
- 記憶體的分配與釋放,記憶體洩漏記憶體
- C語言-記憶體管理之一[記憶體分配]C語言記憶體
- 減少.NET應用程式記憶體佔用的一則實踐記憶體
- 簡單理解動態記憶體分配和靜態記憶體分配的區別記憶體
- 關於C中記憶體操作記憶體
- linux記憶體管理(一)實體記憶體的組織和記憶體分配Linux記憶體
- 記憶體分配策略中,堆和棧的區別記憶體