第7-8章的評論

== 第7章: 內存與資源管理 ==
前3章主要是一些 wrapper,這章開始要分析並選擇一些數據結構和演算法問題,去配合某些需求,例如著重避免memory fragmentation。
之後所說的,功能其實是除錯的部份,例如偵測及查找內存泄漏和Socket泄漏。
文中可以理解那些範例的開發過程,當中不乏許多「突發奇想」和幽默(可見 P. 272, 274)。
=== 程序bug/問題 ===
P. 313 「oid」->「void」
除了在析構函式了用Lock,暫未發現其他問題。
=== 文中錯誤 ===
P.268, P. 269 「不泄露」 -> 「不泄漏」 三處
P. 269 「很多解釋型高級語言可以在運行前主動分析程序,對大型的內存申請實行預分配制度。」
既然是解釋型,就不能在運行前主動分析程序吧。
P. 279 「Malloc (指一個成員函數)是內存池最重要的功能,在實際使用中需要完全替代 C 語言的 malloc 以及 C++ 語言 new 的內存分配功能」」
實際上 malloc 和 (多個 overloading) operator new 是不同的。示範的代碼也沒有替代 new 的功能 (如 overriding operator new 或使用 replacement new)。
P. 294 「Modeify」-> 「Modify」
P. 301 「內存池不具備自動調用構造函數和析構函數功能,因此,無法替代C++完成對象的創建和摧毁......」
終於反駁了 P. 279。
P. 302 「......業務交易通常是與業務綁定的......」我估計原意應該是「與Session綁定的」
=== 其他意見 ===
P. 269 「......筆者突發奇想,......我們自然可以很輕易地知道是那個模塊在申請內存,為甚麼不把這個信息記綠下來以幫助 debug 呢?」
這個我估計是很常用的手法。不同的是,書中的方法是另外建一個數組 map: pointer -> char[124] 去儲存這個資訊。很多時候可以把這個資料放到記憶塊的 header 裡,那樣就不用按書裡的方法做 O(n) 搜尋。而且,通常是放 char * 和 unsigned,記錄 __FILE__ 和 __LINE__,不用strcpy及額外的緩衝區。參見下面 P.274。
P. 271 書中用不同大小的內存池去分配內存,釋放後放到內存池的單鏈表去。而內存塊的大小是16, 32, 64, ..., 1MiB。不同內存塊大小的內存池是按需要新成的,而每次申請內存要遍歷這個內存池單鏈。因為這單鏈最長也只是17個,建議改為17個元素的數組,當申請內存時直接計算應使用那個內存池即可。這樣可以減少很多不必要的遍歷。
P.272 「在實做中筆者發現一個問題,即鏈表效率不高......筆者經過思考,發現一個問題......經筆者測試,內在塊的申請和釋放吞吐量"隊列"管理方式下每秒僅5萬次左右,一旦使用"棧"方式管理,迅速提升到40-50萬次,提升了整整一個數量級。」
是甚麼優化這麼神奇呢? 原來這整頁內容就是說明一個優化,就是不用把新的節點加到那單鏈表的最後(「需要循環遍歷到鏈表尾部進行掛鏈操作」),而可以插在頭部。這就是所謂"隊列"和"棧"方式管理。
但其實,那個池是一個集合(set),把節點加到前後都沒關係,而且一般的單鏈都會實現前後加節點都只需 O(1) 。這個讓我想起 http://en.wikipedia.org/wiki/Sorting_algorithm#Inefficient.2Fhumorous_sorts
P. 274 「經過分析,筆者突發奇想,概然我們內存池管理的就是內存塊,就有存儲能力,為甚麼我們不能利用內存塊做一點自己的管理數據存儲呢?」
看到這裡,又「突發奇想」,我有點無語了。大部分內存分配也會加入額外信息吧。原來這裡重新發明了 free list (http://en.wikipedia.org/wiki/Free_list)。結合 P. 269 的建議就可以了。
P. 306 注冊和反注冊一個 CSocket 對象到 CSocketRegister 對象要用 O(n) 時間,即加入 n 個 socket 要 O(n^2) 時間,這種效能可接受嗎?
因此,書中 P.318-P.320解釋,n 並不是池的大小,是數組內最後一個使用中的元素的索引(而不是使用中元素數目, 即比這個值還要大),又由於池最前的元素被重用的概率最高,所以「這部份檢索成本很低、也滿足了絕大多數的高頻應用需求。」我只可以說,O(n) 還是 O(n),或許平均會好一點。
對於 socket 最多只有 65536 個,最簡單的方法是建立一個 65536 個元素的數組。而書中的目的也只是記錄 socket 的說明文字,最簡單是 char* socketInfo[65536]。由 Socket 的使用者決定是靜態文字還是動態新成的文字(由使用者方管理)。那麼,注冊和反注冊都是O(1)。
這是利用空間換取時間。但因為 socket 應該不是分散 (sparse) 的,舊的號碼會重用 (有錯請指正),那當該系統真的需要同時使用 n 個 socket,必須 n 個地址記錄額外的資訊。因此,char* socketInfo[MAX_SOCKET] 應該是合理的。
== 第8章: 隊列 ==
這章應該是全書最長的一章,接近100頁。它介紹幾種隊列:
- 動態Buffer類 CTonyBuffer (每次更新都申請新內存塊,可以想像是比 std::queue<std::vector<char> > 還慢的容器)
- 靜態Buffer類 CTonyBuffer (和上面的同名,元素固定大小,每次dequeue會向前 "memcpy" 的容器, STL 沒有類比)
- CTonyPopBuffer (這個比較像一個訊息 serializer/deserailizer,序列化做一個 binary buffer)
- CTonyXiaoMemoryQueue (PopBuffer 的變種,用鏈表指向各個訊息, 相似於 std::queue<char *>)
- CTonyXiaoMemoryQueueWithLock (上面類的 thread-safe wrapper)
隊列是多綫程編程的重要工具。但是那前 4 個類都沒有考慮多綫程的,最後一個僅是 wrapper。
我開始覺得 std::queue<char> 很強,因為它已經能取代近100頁的篇幅。
我失望應該可以吧。我為甚麼不去研究 Intel Threading Building Block (TBB) 的 concurrent_queue<T> 的代碼呢? 而花幾小時看這近 100頁呢?
concurrent_queue<T>可是可以多個綫程同時從隊列壓入或者彈出元素的啊。
// concurrency_queue<T> 已修正為 concurrent_queue<T>,謝 Chen Shou
=== 程序bug/問題 ===
P. 340, 354, 375
// 這是從後向前Move,因此,直接調用memecpy 完成
memcpy(m_pData, m_pData + nBuytes, m_nDataLength - nBytes);
這並不是跨平台可行的,參閱其他意見 P.338-339
=== 文中錯誤 ===
P. 327 「GetFirstLenght」 -> 「GetFirstLength」
=== 其他意見 ===
P. 328 「在 DeleteFirst 動作時,需要將后續的數據向前Move,使用成本還是很高的」
文中使用一個數組,Dequeue時要移動數組內所有元素,即 O(n)。可使用 Circular Queue,Enqueue/Pop 都是 O(1)。
P. 332 「正是因為筆者近年來深入思考多綫程並行開發環境時感覺到上述限制因素,這才強行扭轉了靜態編程思想,逐漸回到動態編程思想上來。」
書中有很多「思想」,這些詞彙很難明白。其實內文是指使用固定大小的內存(棧裡的變量)還是在堆裡用 malloc() 申請內存。
有個建議,其實可以用棧裡固定大小的數組,再把數組交給一個動態內存分配器(可以是不同種類的)。這樣可以不做成跨綫程的 memory fragmentation,又不用使用全局的分配器 (就不用locking)。
另一個簡單的動態分配棧的方法是使用 alloca/_alloca/_malloca 函數,不過這種函數只能申請不能手動釋放。
P. 333 文中的 CTonyBuffer 類,基本上的功能是把數組加入到緩衝區的頭或尾,每次都重新申請新緩衝區,把舊的緩衝區拷過去。
我認為,用這種做法倒不如把這類變成一個 immutable 類,這種類無需鎖就能為 thread-safe。見 http://en.wikipedia.org/wiki/Immutable_object
P. 338-339 用兩頁說明 memcpy() 的問題「但在 C 語言中,memcpy 一般屏蔽了拷貝的順序細節......」
其實是因為當目的範圍和內源範圍重疊時,memcpy 的行為沒有定義,所以才有一個叫memmove的函數。http://en.wikipedia.org/wiki/Memcpy#Functions
P. 360 「......但近期的一些開發郤多用靜態類來實現,這沒有甚麼道理,完全是實踐的感覺。程序設計是一門實踐性的科學,建議各位讀者在以後的開發中不要迷信理論,不要人云亦云,一切設計筆者都建議具體問題具體分析,以自己的判斷為準。」
也許我們不能用純理性的思維去開發軟件,當中的設計決策需要使用 heristic 或經驗,但我覺得,不能全靠感覺。
如果有幾個選擇,以書中的例子來說,有兩個介面相同等性能特性不一樣的類,就拿去模擬測試吧。
我所認識的「科學」與上文有頗大出入,相對「以自己的判斷為準」,我認為以科學化的實驗為準才是應當追求的。
P. 370, 371,... 這章中有很多這類代碼 (其實之前的幾章也有):
if (a > b)
return true;
else
return false;
我個人會認為這麼寫比較清楚
return a > b;
尤其是書中有時候會在 if 成功時 return false 的,那就更難讀了。
P. 383-384 「......對效率做了很多測試和思考。其中,筆者最主要關注的效率就是如何規避遍歷循環。......大家可以推論一下,我們僅僅添加了一個指針變量加速因子 m_pLast,就立即將一個隊列的 AddLast 操作由 O(n) 提升到 O(1)……」
又是看了一大堆敍述、插圖,原來這裡優化了在單鏈表尾能 O(1) 插入節點。而且,那 m_pLast 還可能會是 NULL,屆時要遍歷(worst case O(n))。
P. 384 「如果我們使用通用的鏈表類來完成 MemQueue,這個效率是不是一定不如我們自己設計的這個管理模型。」
這是關於 m_pLast 的結語。我只能說,std::list::push_back() 和 std::deque::push_back() 都是 O(1) 的。
P. 395 又繼續寫三行 m_pLast 有多美好。
P. 407-413 這麼多的篇幅是描述用一個類封裝另一個類,加入鎖。所以就是每個函數調用Lock,再調用封裝物件的同名函數,再調用 Unlock。同樣的函數有14個。
這又花費7頁。
待續......
前3章主要是一些 wrapper,這章開始要分析並選擇一些數據結構和演算法問題,去配合某些需求,例如著重避免memory fragmentation。
之後所說的,功能其實是除錯的部份,例如偵測及查找內存泄漏和Socket泄漏。
文中可以理解那些範例的開發過程,當中不乏許多「突發奇想」和幽默(可見 P. 272, 274)。
=== 程序bug/問題 ===
P. 313 「oid」->「void」
除了在析構函式了用Lock,暫未發現其他問題。
=== 文中錯誤 ===
P.268, P. 269 「不泄露」 -> 「不泄漏」 三處
P. 269 「很多解釋型高級語言可以在運行前主動分析程序,對大型的內存申請實行預分配制度。」
既然是解釋型,就不能在運行前主動分析程序吧。
P. 279 「Malloc (指一個成員函數)是內存池最重要的功能,在實際使用中需要完全替代 C 語言的 malloc 以及 C++ 語言 new 的內存分配功能」」
實際上 malloc 和 (多個 overloading) operator new 是不同的。示範的代碼也沒有替代 new 的功能 (如 overriding operator new 或使用 replacement new)。
P. 294 「Modeify」-> 「Modify」
P. 301 「內存池不具備自動調用構造函數和析構函數功能,因此,無法替代C++完成對象的創建和摧毁......」
終於反駁了 P. 279。
P. 302 「......業務交易通常是與業務綁定的......」我估計原意應該是「與Session綁定的」
=== 其他意見 ===
P. 269 「......筆者突發奇想,......我們自然可以很輕易地知道是那個模塊在申請內存,為甚麼不把這個信息記綠下來以幫助 debug 呢?」
這個我估計是很常用的手法。不同的是,書中的方法是另外建一個數組 map: pointer -> char[124] 去儲存這個資訊。很多時候可以把這個資料放到記憶塊的 header 裡,那樣就不用按書裡的方法做 O(n) 搜尋。而且,通常是放 char * 和 unsigned,記錄 __FILE__ 和 __LINE__,不用strcpy及額外的緩衝區。參見下面 P.274。
P. 271 書中用不同大小的內存池去分配內存,釋放後放到內存池的單鏈表去。而內存塊的大小是16, 32, 64, ..., 1MiB。不同內存塊大小的內存池是按需要新成的,而每次申請內存要遍歷這個內存池單鏈。因為這單鏈最長也只是17個,建議改為17個元素的數組,當申請內存時直接計算應使用那個內存池即可。這樣可以減少很多不必要的遍歷。
P.272 「在實做中筆者發現一個問題,即鏈表效率不高......筆者經過思考,發現一個問題......經筆者測試,內在塊的申請和釋放吞吐量"隊列"管理方式下每秒僅5萬次左右,一旦使用"棧"方式管理,迅速提升到40-50萬次,提升了整整一個數量級。」
是甚麼優化這麼神奇呢? 原來這整頁內容就是說明一個優化,就是不用把新的節點加到那單鏈表的最後(「需要循環遍歷到鏈表尾部進行掛鏈操作」),而可以插在頭部。這就是所謂"隊列"和"棧"方式管理。
但其實,那個池是一個集合(set),把節點加到前後都沒關係,而且一般的單鏈都會實現前後加節點都只需 O(1) 。這個讓我想起 http://en.wikipedia.org/wiki/Sorting_algorithm#Inefficient.2Fhumorous_sorts
P. 274 「經過分析,筆者突發奇想,概然我們內存池管理的就是內存塊,就有存儲能力,為甚麼我們不能利用內存塊做一點自己的管理數據存儲呢?」
看到這裡,又「突發奇想」,我有點無語了。大部分內存分配也會加入額外信息吧。原來這裡重新發明了 free list (http://en.wikipedia.org/wiki/Free_list)。結合 P. 269 的建議就可以了。
P. 306 注冊和反注冊一個 CSocket 對象到 CSocketRegister 對象要用 O(n) 時間,即加入 n 個 socket 要 O(n^2) 時間,這種效能可接受嗎?
因此,書中 P.318-P.320解釋,n 並不是池的大小,是數組內最後一個使用中的元素的索引(而不是使用中元素數目, 即比這個值還要大),又由於池最前的元素被重用的概率最高,所以「這部份檢索成本很低、也滿足了絕大多數的高頻應用需求。」我只可以說,O(n) 還是 O(n),或許平均會好一點。
對於 socket 最多只有 65536 個,最簡單的方法是建立一個 65536 個元素的數組。而書中的目的也只是記錄 socket 的說明文字,最簡單是 char* socketInfo[65536]。由 Socket 的使用者決定是靜態文字還是動態新成的文字(由使用者方管理)。那麼,注冊和反注冊都是O(1)。
這是利用空間換取時間。但因為 socket 應該不是分散 (sparse) 的,舊的號碼會重用 (有錯請指正),那當該系統真的需要同時使用 n 個 socket,必須 n 個地址記錄額外的資訊。因此,char* socketInfo[MAX_SOCKET] 應該是合理的。
== 第8章: 隊列 ==
這章應該是全書最長的一章,接近100頁。它介紹幾種隊列:
- 動態Buffer類 CTonyBuffer (每次更新都申請新內存塊,可以想像是比 std::queue<std::vector<char> > 還慢的容器)
- 靜態Buffer類 CTonyBuffer (和上面的同名,元素固定大小,每次dequeue會向前 "memcpy" 的容器, STL 沒有類比)
- CTonyPopBuffer (這個比較像一個訊息 serializer/deserailizer,序列化做一個 binary buffer)
- CTonyXiaoMemoryQueue (PopBuffer 的變種,用鏈表指向各個訊息, 相似於 std::queue<char *>)
- CTonyXiaoMemoryQueueWithLock (上面類的 thread-safe wrapper)
隊列是多綫程編程的重要工具。但是那前 4 個類都沒有考慮多綫程的,最後一個僅是 wrapper。
我開始覺得 std::queue<char> 很強,因為它已經能取代近100頁的篇幅。
我失望應該可以吧。我為甚麼不去研究 Intel Threading Building Block (TBB) 的 concurrent_queue<T> 的代碼呢? 而花幾小時看這近 100頁呢?
concurrent_queue<T>可是可以多個綫程同時從隊列壓入或者彈出元素的啊。
// concurrency_queue<T> 已修正為 concurrent_queue<T>,謝 Chen Shou
=== 程序bug/問題 ===
P. 340, 354, 375
// 這是從後向前Move,因此,直接調用memecpy 完成
memcpy(m_pData, m_pData + nBuytes, m_nDataLength - nBytes);
這並不是跨平台可行的,參閱其他意見 P.338-339
=== 文中錯誤 ===
P. 327 「GetFirstLenght」 -> 「GetFirstLength」
=== 其他意見 ===
P. 328 「在 DeleteFirst 動作時,需要將后續的數據向前Move,使用成本還是很高的」
文中使用一個數組,Dequeue時要移動數組內所有元素,即 O(n)。可使用 Circular Queue,Enqueue/Pop 都是 O(1)。
P. 332 「正是因為筆者近年來深入思考多綫程並行開發環境時感覺到上述限制因素,這才強行扭轉了靜態編程思想,逐漸回到動態編程思想上來。」
書中有很多「思想」,這些詞彙很難明白。其實內文是指使用固定大小的內存(棧裡的變量)還是在堆裡用 malloc() 申請內存。
有個建議,其實可以用棧裡固定大小的數組,再把數組交給一個動態內存分配器(可以是不同種類的)。這樣可以不做成跨綫程的 memory fragmentation,又不用使用全局的分配器 (就不用locking)。
另一個簡單的動態分配棧的方法是使用 alloca/_alloca/_malloca 函數,不過這種函數只能申請不能手動釋放。
P. 333 文中的 CTonyBuffer 類,基本上的功能是把數組加入到緩衝區的頭或尾,每次都重新申請新緩衝區,把舊的緩衝區拷過去。
我認為,用這種做法倒不如把這類變成一個 immutable 類,這種類無需鎖就能為 thread-safe。見 http://en.wikipedia.org/wiki/Immutable_object
P. 338-339 用兩頁說明 memcpy() 的問題「但在 C 語言中,memcpy 一般屏蔽了拷貝的順序細節......」
其實是因為當目的範圍和內源範圍重疊時,memcpy 的行為沒有定義,所以才有一個叫memmove的函數。http://en.wikipedia.org/wiki/Memcpy#Functions
P. 360 「......但近期的一些開發郤多用靜態類來實現,這沒有甚麼道理,完全是實踐的感覺。程序設計是一門實踐性的科學,建議各位讀者在以後的開發中不要迷信理論,不要人云亦云,一切設計筆者都建議具體問題具體分析,以自己的判斷為準。」
也許我們不能用純理性的思維去開發軟件,當中的設計決策需要使用 heristic 或經驗,但我覺得,不能全靠感覺。
如果有幾個選擇,以書中的例子來說,有兩個介面相同等性能特性不一樣的類,就拿去模擬測試吧。
我所認識的「科學」與上文有頗大出入,相對「以自己的判斷為準」,我認為以科學化的實驗為準才是應當追求的。
P. 370, 371,... 這章中有很多這類代碼 (其實之前的幾章也有):
if (a > b)
return true;
else
return false;
我個人會認為這麼寫比較清楚
return a > b;
尤其是書中有時候會在 if 成功時 return false 的,那就更難讀了。
P. 383-384 「......對效率做了很多測試和思考。其中,筆者最主要關注的效率就是如何規避遍歷循環。......大家可以推論一下,我們僅僅添加了一個指針變量加速因子 m_pLast,就立即將一個隊列的 AddLast 操作由 O(n) 提升到 O(1)……」
又是看了一大堆敍述、插圖,原來這裡優化了在單鏈表尾能 O(1) 插入節點。而且,那 m_pLast 還可能會是 NULL,屆時要遍歷(worst case O(n))。
P. 384 「如果我們使用通用的鏈表類來完成 MemQueue,這個效率是不是一定不如我們自己設計的這個管理模型。」
這是關於 m_pLast 的結語。我只能說,std::list::push_back() 和 std::deque::push_back() 都是 O(1) 的。
P. 395 又繼續寫三行 m_pLast 有多美好。
P. 407-413 這麼多的篇幅是描述用一個類封裝另一個類,加入鎖。所以就是每個函數調用Lock,再調用封裝物件的同名函數,再調用 Unlock。同樣的函數有14個。
這又花費7頁。
待續......
有关键情节透露