第7-8章的評論

2010-02-03 03:29:35   来自: Milo
0 bug的评论   2 star rating2 star rating   2


  == 第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頁。
  
  待續......
你认为这篇评论: 27

2010-02-03 03:31:04 食人树

  太高深我不懂,佩服你严谨的态度

2010-02-03 03:46:44 Tinyfool

  你的7-8评论,验证了我的想法,买本书学他的经验,怕是经验还没学到,路子先被带野了。那哥们视野不够宽阔,当然宽阔了也就不敢随便喊0bug了。Bloch写过一篇blog叫”几乎所有的二分搜索和归并排序实现都是错的“ ,真牛都是跟你这样很谦虚的。

2010-02-03 06:18:53 init.d

  其实看到这里,我反倒开始佩服肖老师了,完全是“实践”出来的“科学”啊~~ 肖老师大概从来没有好好读过文档,一切都是以自己的代码为标杆吧,真是“彪悍的代码不需要解释”,佩服佩服~~

2010-02-03 06:20:00 init.d

  P. 340, 354, 375
    // 這是從後向前Move,因此,直接調用memecpy 完成
  
  这里是memcpy,milo激动了,多打一个e
  XD

2010-02-03 08:21:49 西北

  先赞再看

2010-02-03 08:54:55 西北

  佩服Milo读书的态度,肖老师应该学学侯捷老师的气量

2010-02-03 08:56:20 lzprgmr_闭关

  Milo同学最大的贡献在于指出了这本异常凶险的书中可能给你造成伤害的地方,从而让你在读这本书时不会误入歧途。但话说回来,你有那么多的书可以选,为什么不走阳光道,偏走独木桥?

2010-02-03 09:01:13 当仁不让的老孙

  “彪悍的代码不需要解释”
  
  肖老师属于热血江湖派,想想也挺有意思。

2010-02-03 09:16:14 那谁

  “笔者突发奇想”也许是出现最多的非技术词汇了…

2010-02-03 09:32:59 猛禽

  难道肖老师是一位靠突发奇想写程序的大师?果然飚汗啊

2010-02-03 09:51:53 [已注销]

  我觉得Milo看这样一本书真是受折磨了

2010-02-03 09:54:57 锲而不舍求妹汁

  好多“突发奇想”,哈哈,确实让人感觉很“山寨”啊!
  
    - CTonyPopBuffer (這個比較像一個訊息 serializer/deserailizer,序列化做一個 binary buffer)
    - CTonyXiaoMemoryQueue (PopBuffer 的變種,用鏈表指向各個訊息, 相似於 std::queue<char *>)
    - CTonyXiaoMemoryQueueWithLock (上面類的 thread-safe wrapper)
  ============================================
  把自己的id用在程序命名上,还真是有点吃苍蝇的感觉。
    
    尤其是書中有時候會在 if 成功時 return false 的,那就更難讀了。
  ===========================================
  要怪国内很多地方没有一个比较严谨的编码规范让人遵守,以及开发人员半路出家却缺少正规的培训。
  
    P. 269 「很多解釋型高級語言可以在運行前主動分析程序,對大型的內存申請實行預分配制度。」
    既然是解釋型,就不能在運行前主動分析程序吧。
  ==================================================
  这点我有点疑问:假设某种解释型语言的工作流程是读取分析源代码,生成opcode,解释运行opcode这三步,是不是在前两步中就可以完成一些优化工作,在第三步解释运行opcode时可以保证已经是当前最优化的代码了?

2010-02-03 10:03:05 Milo

  2010-02-03 06:20:00 init.d
    P. 340, 354, 375
      // 這是從後向前Move,因此,直接調用memecpy 完成
    
    这里是memcpy,milo激动了,多打一个e
    XD
  
  不是我的錯, 原文這裡就是寫錯的.

2010-02-03 10:05:34 Milo

  2010-02-03 08:56:20 Dbger
    Milo同学最大的贡献在于指出了这本异常凶险的书中可能给你造成伤害的地方,从而让你在读这本书时不会误入歧途。但话说回来,你有那么多的书可以选,为什么不走阳光道,偏走独木桥?
  
  之前也看過一些不錯的內地原創書籍。讀了當當的介紹、目錄、書評等就買來看看了。「買錯了」是我的錯吧。

2010-02-03 10:06:13 老赵

  看上去0 bug老师是山寨来的……

2010-02-03 10:09:16 lzprgmr_闭关

  >>之前也看過一些不錯的內地原創書籍。讀了當當的介紹、目錄、書評等就買來看看了。「買錯了」是我的錯吧。
  呵呵,一个人买错了,但让大家受益应该是很有价值的(从总体上来说:))。 我上面的意思其实是:对于后来者,既然知道这本书的凶险,不如去读其他已经经过证明的书~~~
  
  

2010-02-03 10:19:27 Milo

  @missdeer
      P. 269 「很多解釋型高級語言可以在運行前主動分析程序,對大型的內存申請實行預分配制度。」
      既然是解釋型,就不能在運行前主動分析程序吧。
    ==================================================
    这点我有点疑问:假设某种解释型语言的工作流程是读取分析源代码,生成opcode,解释运行opcode这三步,是不是在前两步中就可以完成一些优化工作,在第三步解释运行opcode时可以保证已经是当前最优化的代码了?
  
  這點可能要在看文中的上文下理。文中上認為解釋型語言可以做到這些優化,之後說編釋型語言不可以。我覺得以優化的能力來說,這應該是相反的。
  
  至於解釋型語言中,運行前分析並預分配的可行性,我的說法可能是太武斷的。但一般解釋型語言都是動態語言,這種靜態分析能否可能或有用,是一個可以討論的地方。
  
  如果有意見可以提出,我會修正。

2010-02-03 10:34:38 微子

  @Tinyfool ```Bloch写过一篇blog叫”几乎所有的二分搜索和归并排序实现都是错的“ ''', 我最近也写过归并排序, 比较次数比较大, 估计有问题, 求文章.
  
  看这Review, 倒是能学点东西, 这书能引发这样的评论, 也是种价值.

2010-02-03 10:36:15 [已注销]

  感觉上书的作者对一些相关的概念一知半解,又不去学习前人的经验,自己闭门造车才造成了很多突发奇想的代码。大概只有半桶水才会自称0 BUG吧。 浪费了Milo很多时间。

2010-02-03 10:59:40 锲而不舍求妹汁

    這點可能要在看文中的上文下理。文中上認為解釋型語言可以做到這些優化,之後說編釋型語言不可以。我覺得以優化的能力來說,這應該是相反的。
    
    至於解釋型語言中,運行前分析並預分配的可行性,我的說法可能是太武斷的。但一般解釋型語言都是動態語言,這種靜態分析能否可能或有用,是一個可以討論的地方。
  
  ============================================
  哦,我不是针对书上的问题说的。我对编译原理不熟,对现在主流的动态解释型语言的具体实现也不了解,只是看到这个话题就有刚才的疑惑。
  
  我前面说的优化,也是偏向指令流的优化,而不是内存使用。这个可能偏离了你跟本书作者讨论的主题。

2010-02-03 11:56:51 贾里

  汗。。看来我的路子也很野。。看来对技术要更严谨一些才行。

2010-02-03 12:19:43 Milo

  關於評論中的 socket 個數問題 MAX_SOCKET,陳碩指出,Linux 下可以用 getrlimit(RLIMIT_NOFILE,&rlim),取得目前進程的最大可使用 file descriptornumber。
  
  http://www.kernel.org/doc/man-pages/online/pages/man2/getrlimit.2.html

2010-02-03 12:50:40 陈硕

  @Milo
  这个对于 POSIX 操作系统都是成立的:
  http://www.opengroup.org/onlinepubs/000095399/functions/getrlimit.html
  
  同时,以特权级别运行的进程(比如 root 用户),可以自己修改这个数值。

2010-02-03 13:56:03 ydntlk

  @Milo
  为O bug做的Errata和Review的工作,看来是可以产生“带有费马注释的丢番图的《算术》”的巨著呢。山寨式的workaround配上比较正规的Review,倒是相得益彰。

2010-02-03 19:30:16 bakey

  很佩服milo老师的严谨和扎实的理论基础啊,读了这个书评真是受益匪浅

2010-02-03 19:59:08 RednaxelaFX

  @missdeer
        P. 269 「很多解釋型高級語言可以在運行前主動分析程序,對大型的內存申請實行預分配制度。」
        既然是解釋型,就不能在運行前主動分析程序吧。
      ==================================================
      这点我有点疑问:假设某种解释型语言的工作流程是读取分析源代码,生成opcode,解释运行opcode这三步,是不是在前两步中就可以完成一些优化工作,在第三步解释运行opcode时可以保证已经是当前最优化的代码了?
    
    這點可能要在看文中的上文下理。文中上認為解釋型語言可以做到這些優化,之後說編釋型語言不可以。我覺得以優化的能力來說,這應該是相反的。
    
    至於解釋型語言中,運行前分析並預分配的可行性,我的說法可能是太武斷的。但一般解釋型語言都是動態語言,這種靜態分析能否可能或有用,是一個可以討論的地方。
    
    如果有意見可以提出,我會修正。
  
  =======================================================
  
  那个……看到关键字了,来插入讨论。
  静态/动态主要指的是事情发生的时机:静态的事情发生在程序运行前,动态的东西发生在程序运行过程中。另外也有事后发生的活动(如postmortem),这里暂时不讨论。
  这就带来了有趣的问题:compile-time与run-time是什么关系?是static与dynamic的关系吗?如果是的话,dyanmic compilation/adaptive compilation的概念就很诡异了;特别是像PyPy这种极端模糊了compile-time与run-time的实现,由于编译是在运行时进行的,它可以把run-time value当作compile-time constant来用,生成非常高效的代码。
  
  我想表达的是,“编译”未必是在运行时进行的工作,未必是静态的。如果把“编译型语言(的实现)”定义为“在运行时不再对程序语义或行为模式做分析的语言(的实现)”的话,那么剩下的就都是“解释型语言(的实现)”,不幸的是这些“解释型语言(的实现)”当中很可能有非常先进的动态编译器,可以有选择性的生成非常高效的代码。
  
  > 假设某种解释型语言的工作流程是读取分析源代码,生成opcode,解释运行opcode这三步
  想想看,Java的一般实现就是这样的:
  第一步,javac或者别的编译器将Java源码parse为AST用于分析;
  第二步,javac或者别的编译器由AST生成JVM字节码;
  第三步,某个JVM实现读取JVM字节码开始“解释执行”程序。
  
  现在的CPython也是这么实现的:
  第一步,python内部的源码到字节码编译器将Python源码parse为AST用于分析,随后简单优化;
  第二步,python内部的源码到字节码编译器由AST生成Python bytecode;
  第三步,python内部的字节码解释器开始解释执行程序。
  
  使用类似的实现方式的语言实现非常多。步骤很相似,从整个语言实现来看,上面提到的三步中负责头两步的可以称为前端,负责后一步称为后端。每个部分内里也可能存在更细粒度的前端与后端。于是Java的一般实现不过就是把前端暴露出来给用户直接用,把后端包装在JVM里;而CPython则是把前端后端都包装在一个python解释器里。对Sun的桌面版Java实现来说,这在1.3开始变得更贴切:Sun的javac会忽略-O开关,在javac内部几乎不做优化,使它非常像一个native compiler的纯粹的前端,而JVM字节码则对应到native compiler的intermediate representation,只不过更强调portable与compact的特征。
  普通程序员关心的是他们何时启动了一个程序,而不关心那个到底是他们写的user program本身还是准备执行user program的“解释器”。对许多人来说,javac是Java的“编译时”,JVM是Java的“运行时”;python则整个就是Python的“运行时”而没有“编译时”。由于有这种表象上的差异,Java更多被看作编译或半编译型语言,而Python被看作解释型语言。
  之前正好简单记了一些相关观点在blog里,可惜最近JavaEye动荡,那帖也消失了。诶。诶诶。
  
  关于对大块对象的处理,许多高效VM中这都是一个关注点。微软的CLR中有专门的大对象堆(LOH),在分配空间时发现对象大小超过某个阈值就会将它分配到LOH上,这个堆与GC堆中最老的一个分代同级,较少收集,尽量不做复制(CLRv2的话LOH是不compact的)。至于预读,Sun的HotSpot在编译一些热的方法时可能生成prefetch系指令试图降低cache miss对性能的影响。不过这些分析都是“run-time”做的就是了……

2010-02-03 20:49:20 Milo

  @RednaxelaFX
  現時的確很難分辨一些語言的那個部份是 compile-time,在加上可以在run-time 新成代碼 compile/JIT 的可能。
  
  你所說的 LOH 應該都是 run-time 時跟據物件的情況 (例如大小、生存時間等) 去做的優化。想請教,在你認識的例子裡,有在 compile-time 時可以進行內存分配的優化嗎?

2010-02-03 22:19:09 RednaxelaFX

  @Milo
  > 你所說的 LOH 應該都是 run-time 時跟據物件的情況 (例如大小、生存時間等) 去做的優化。想請教,在你認識的例子裡,有在 compile-time 時可以進行內存分配的優化嗎?
  
  LOH的使用从user program角度来看是compile-time做的决定,run-time做的执行。以CLR为例,在加载assembly的时候所有的类型信息就被VM记录下来了,包括对象的大小,这就已经决定了后面某个类型的对象是在普通heap还是LOH上分配了;allocator在处理空间分配时会查询这些类型信息,看对象有多大,超过了threshold就分配到LOH上。
  
  
  不知道哪些可以算在这个答案中呢,试着举些例子。
  
  有一种很典型的就是escape analysis +scalar replacement + stack allocation的优化,在IBM J9与Oracle JRockit里都已经实现并应用已久,Sun的HotSpot也已实验性实现。
  JVM spec说Java object逻辑长都是在heap上分配空间的;实际实现中Java对象却有机会在栈上分配空间。JVM内的编译器在分析代码时(对user program来说这是compile-time)可能会发现某些局部变量从来没有离开过其所在方法的作用域,也就是说该方法调用结束后那个对象就变成unreachable的了,于是这些局部变量所指向的对象的reachable周期就跟方法调用的顺序可以关联起来,满足LIFO顺序,也就可以在调用栈上分配空间。这就覆盖了escape analysis与stack allocation优化。Scalar replacement是说通过escape analysis得知某个对象没有逃逸出方法的作用域之后,可以不把那个对象看作整体来分配空间,而是将其中的field拆出来单独考虑(拆成scalar)。这样如果可以发现其中某些field根本没被用过的话,相应的分配也可以省下来了。
  可以看JRockit的一个实际例子:http://blogs.oracle.com/ohrstrom/2009/05/pulling_a_machine_code_rabbit.html << 留意文中最后一组测试代码的int test(int)
  
  Escape analysis本身是业界常用的辅助优化的技巧之一。GCC里也有实现。随便抓了一篇相关的介绍:http://developer.apple.com/mac/library/documentation/DeveloperTools/gcc-4.0.1/gccint/Alias-analysis.html
  不过我不太清楚GCC对malloc/new的优化做得如何,还没仔细钻过这方面。相对来说我还是对VM的实现技巧更感兴趣,而这些技巧中有大量的都是动态编译相关。
  
  因为分配空间的操作有点复杂,很多编译器都是生成library call而不是生成直接分配空间的代码来处理的。但在带有JIT的系统里,为了更多的压榨性能,JIT很可能会将分配空间的代码也inline到申请空间的方法里。例如说HotSpot在编译热方法时会把对象分配的fast path给inline到被编译的方法中,在slow path里才做library call。

2010-02-03 22:37:02 RednaxelaFX

  @Milo
  哦对了,编译器可以影响空间分配的一点是object layout。很多C++编译器都是按照源码中field的声明顺序来分配它们的,意味着如果有alignment要求的话,中间的padding可能会浪费一些空间。而如果不需要按声明顺序来,给编译器更大的自由的话,它可能会为每个类新计算出更紧凑的layout。
  举例的话又要拿HotSpot来了,可以参考这篇:http://www.codeinstructions.com/2008/12/java-objects-memory-structure.html
  严格说HotSpot里的object layout不是编译器自己决定的,不过……我只是想说可以做这种优化。
  
  还有诸如automatic object inlining之类的也是空间分配相关的优化,是需要编译器配合完成的。

2010-02-03 23:31:44 陈硕

  @RednaxelaFX 很多C++编译器都是按照源码中field的声明顺序来分配它们的
  这个是 C++ 标准要求的,应该所有编译器都照做。不然没法做到(与 C 的)二进制兼容。假如编译器 A 按 abc 排,编译器 B 按 bac 排,那么二者编译出来的库就不能链接到一起。另外一个例子是 sockets API 里的那些用来传参数的结构体,其 layout 必须和 C 语言一字(节)不差,否则没法调用 sockets syscalls。这是 C++ 生存的要素。

2010-02-04 00:35:06 RednaxelaFX

  @Milo
  C++的数据类型要跟C达到ABI兼容的话,那个类型必须是POD才行。例如说带有虚方法的类就不是POD了。所以单从兼容性看的话原本也是只需要保证POD与C的ABI兼容。至于calling convention和name mangling不也得靠extern "C"么。感觉这个倒还不是大问题,本来很多优化就是以“Not cheating unless you get caught”的思想去做的,只要表象维持了语义,在与外界的接触面上符合规范就OK。

2010-02-04 02:19:08 尼采

  @Milo
    
    在C/C++中,我可以想到有compile-time 有內存分配優化可能的都是stack相關的,如RednaxelaFX 所說的 object layout ,我想到的另一個例子是把object 放在 register 而不是stack,前題是那個object 足夠細小。
    
    但是heap object呢??能動手的地方不多吧???

2010-02-04 03:05:42 Kouya

      P. 269 「很多解釋型高級語言可以在運行前主動分析程序,對大型的內存申請實行預分配制度。」
      既然是解釋型,就不能在運行前主動分析程序吧。
    ==================================================
    这点我有点疑问:假设某种解释型语言的工作流程是读取分析源代码,生成opcode,解释运行opcode这三步,是不是在前两步中就可以完成一些优化工作,在第三步解释运行opcode时可以保证已经是当前最优化的代码了?
  ====================
  其实解释器可以做优化工作的,这点在很多模拟器实现上都有证明。
  
  twitter观光团继续路过~

2010-02-04 03:16:49 尼采

  @Kouya
  我想Milo 大大的重點是,"運行前"這三句字吧。
  
  
  
  
  

2010-02-04 09:57:16 RednaxelaFX

  @尼采
      
      在C/C++中,我可以想到有compile-time 有內存分配優化可能的都是stack相關的,如RednaxelaFX 所說的 object layout ,我想到的另一個例子是把object 放在 register 而不是stack,前題是那個object 足夠細小。
      
      但是heap object呢??能動手的地方不多吧???
  
  =======================================
  
  escape analysis、point-to之类的分析确保对象“地址”没有泄漏的时候才有可能做scalar replacement,因为这种前提下对象中域的相对位置没有关系;变为scalar后,对象中的域就跟普通的局部变量的scalar一样可以得到优化,包括可能分配到register上。
  
  C++的实现中对heap object的优化我是缺乏了解……
  能想到的是,malloc/new返回的都是指针,要对指针相关做优化肯定要做alias analysis;如果要对C++的heap object做stack allocation,应该要在同一函数内能看到确定性配对的malloc/free或new/delete(这个配对如果是在RAII的scope object里写的而被inline到了caller这边也可以),不然无法判断heap object的生命周期跟函数调用顺序的关系,无法确定满足LIFO顺序就没办法做stack allocation。

2010-02-04 11:59:06 锲而不舍求妹汁

    不过我不太清楚GCC对malloc/new的优化做得如何,还没仔细钻过这方面。
  ===============================================
  前些天我刚好帮人做过一个简单的测试,在P4M 1.86GHz的CPU,1.5G RAM,Windows Xp SP3中使用VC2008,Intel C++11,Open watcom 1.8,Borland C++ 5.5,MinGW GCC 4.4.0以及Digital Mars C/C++ 8.5,重复malloc(1000)和free共100000次,都使用单线程的CRT库,结果显示,Open Watcom最快,大约耗时1s,Borland C++最慢,大约18s,其他的几个差不多,都在3s左右。也许使用不同的命令行参数,结果还会有差异,我都是使用bjam的默认配置。
  最后也没有得出什么有用的结论,我只是猜测各个编译器套件的CRT实现的差异引起此番结果。

2010-02-04 13:50:11 微子

  如 glibc 的 Malloc 可以用环境变量和 mallopt 配置, 对速度影响很明显, 很多平台的 malloc 在不同大小时, 采用不同的方法, 也对速度有显著的影响.
  
  missdeer 能测试下不同大小的 Malloc/free 的速度么?

2010-02-04 20:39:53 肖舸

   我的新书《0 bug -- C/C++商用工程之道》近期已经面市,经过出版社统计,上市一个月20天左右,销售共计2500多册(出版社数据),这在专业性技术书籍中,应该还是比较乐观的。我作为作者本人,也衷心感谢各位读者的鼓励和支持,我将一如既往地努力进行后续版本的修订和写作,为大家提供更好的精品书籍。
  
   应该说,我写这本书,还是有一定目的的。
  
   目前的社会上,讲C和C++语言的书籍汗牛充栋,但是,我发现有个问题都没有讲,就是“并行计算”。
  
   很多学习计算机软件编程的朋友,在学校中学习到了很多很好的知识,但是,就笔者所知(可能是笔者孤陋寡闻),确实很少有大学开设《并行计算》这门课程。
  
   但我们知道,现实社会中,目前32位多任务操作系统,如Windows、Linux、Unix等操作系统已经完全普及,哪怕连很小的嵌入式设备,如手机上,都开始使用多任务开发环境,这就要求现代程序员必须具有并行程序设计能力,但无疑,目前大多数程序员缺乏这种能力。最直观的例子就是Intel等CPU厂商,为了自己的多核CPU好销,已经开始在各地区开设程序员进修班,以各种形式向大家培训并行程序开发技能,以便解决整个业界无法适应多核多任务开发环境的需求这个问题。
  
   我也是在这种情况下考虑写作本书的,本书虽然定位于0bug,无错化开发,宣导商业化的务实开发思维,但这仅仅是一个方面,传统单任务程序设计的无错化方法,其实已经有很多参考资料了,笔者认为再“炒剩饭”没有多大意义。笔者从第一天开始写作本书,就致力于解决大家关心的一个核心问题:“如何书写无错化的并行程序?”,这应该说是本书的核心宗旨。
  
   这么做的意义显而易见,由于现在是互联网的社会,网络化开发采用C/S模型,但是,大量的书籍讲了Client端的开发和设计,缺很少,甚至没有书籍来描述服务器端的设计技能。但偏偏目前业界几乎所有的应用,都已经逐渐网络化,在未来的集中式和分布式运行模型下,大量的设计需求要求程序员具有多任务服务器的设计能力,这是一个现实的情况。
  
   现在,哪怕一个很小的嵌入式家用路由器,其实都要求具有7*24小时的连续服务能力。这很好理解,大家想想自己家里的小路由器,买回来连通后,是不是很少断电和关机?
  
   所以我作为作者,才认为这本书有这么大的现实意义,我是商用程序员,做事情讲究务实,我写书,也希望切切实实帮助现在的大多数程序员解决自己身边,现在就遇到的问题,因此我从这个角度切入,写作本书。据某些读者朋友反映,本书是:“目前为止我知道的惟一一本关注服务器端程序设计的 C++ 书。而且又是国人的原创作品,十分难能可贵。”
  
   不过呢,俗话说,林子大了,什么鸟都有,网络作为现实社会的一个剪影,确确实实什么人都有的。本书出来后,一直受到很多不必要的干扰,这个,我在博文《关于《0 bug-C/C++商用工程之道》一书出版前后的故事》中,已经有了详细论述,此处就不再详述。
  
   但是,我作为作者,也考虑了,不能把提批评意见的朋友都一竿子打死,这也不是客观的科学态度。毕竟,所谓“枪手”之说,我的猜测居多,并没有什么铁证,只能参考一下。因此,我又静下心来,详细阅读了一些读者,特别是一些“大牛”级的读者的批评意见。希望能找出一些真正属于自己的错误,好修订本书。这也是本着为读者负责的态度。
  
   但是,很奇怪,我发现不管是这些大牛,还是一些小牛,批评的意见大体有个总体思想,就是本书不符合C++开发的主流规范,显得“不标准”,“野路子”。这差不多也是目前批评本人和本书最主流的意见了。这让我这个作者莫名其妙,先不论这些读者是好心还是恶意,也不论他们批评的意见是否正确,但这些意见显然我无法接受。
  
   原因很简单,请大家看本书的书名《0 bug -- C/C++商用工程之道》,这表明,本书源代码其实是用C和C++这两种语言开发完成的,并且,在本书中任何一处提到开发语言的时候,C一定排在C++语言前面,如“C和C++无错化开发方法”。
   就我这个作者本人而言,在平时工作中,我比较喜欢同时使用C和C++两种语言混合开发,当然,以C为主。这一来是工程有时候需要,二来是我个人开发习惯,毕竟,我是先学会C语言,后来才学习的C++语言。
  
   我想这也很好理解,从第一天开始,C和C++语言编译器就是兼容多种语言的,我们知道,几乎所有版本的C和C++语言编译器,都支持内嵌式汇编开发,这是合理的,因为工业控制中很多高速端口操作,需要汇编来完成。所以现代的C和C++编译器,也有逐渐大一统的趋势,无论是VC还是gcc,均支持混合语言编程,可以说,目前我们的主流编译器,基本上都兼容三种语言,C、C++和汇编。
  
   因此,请各位读者就事论事,在阅读过程中,不要用纯正C++语言来考察本书的源代码,本来就不是主要用C++语言开发的。
  
   其实,我分析了一下,在我的源代码中,C和C++的比例,差不多8:2,即80%左右使用C语言开发的。对于C++语言,我本人倾向于“有限使用”,这个呢,是我的习惯,我习惯于到具体功能点的实现,使用C模式,因为即使是C++语言,函数内部都还是OP的,即面向过程的,这符合实际需要,但在模块组织上,我比较喜欢使用C++的对象概念来封装,因为确实方便。
  
   实际上,我曾经想过,是不是以本书定名就定为“C语言商用工程之道”,但显然也不现实,因为书中确实提到了对象。
  
   根据我个人经验呢,这比较符合现代商用系统的开发模型,一个较为大型的系统,尤其是网络相关,很多时候,系统是多种语言的混合体,有C和C++的,还有PHP和Java的,客户端开发还经常使用JavaScript和C#等,这些,我认为都是合理的。这毕竟是一个全方位满足客户需求的综合开发时代。
  
   另外,我这里也说说本书的源代码,很多读者知道,本书虽然内部包含大量带来,甚至包含一个并行开发工程库,但我并没有提供源代码下载服务,也不提供源码光盘,很多朋友都问我为什么,其实我是有原因的。
  
   1、本书定位为一本工程实战书籍,我认为更多是讲思路,讲解法,即share我本人的一些经验,帮助大家在以后的工程开发中,能有一些解决思路,能解决具体问题。因此,我没有认为本书的源代码有多重要。
  
   2、本书的源代码,我本人认为它是一种语言,由C和C++语言代码,以及相关注释,以及相关文字描述共同组成的一门语言,是程序员写给程序员看的一门语言,是用来讲清楚问题的,不是拿来就用的。就好比我们上学时的伪代码,是描述逻辑使用的,仅仅是我这个作者图省事,把自己的代码直接拿出来,做了注释就写到书里了。
  
   3、我本意是写书,不是做开源,如果做开源,我直接在网上开个库供大家下载好了,不需要写书这么麻烦的。
  
   4、本书中代码,最少的都有两年无故障连续运营历史,最多的有9年。但我仍然不认为这些代码就一点bug都没有。只能说,在过去的工程环境下,没有暴露出bug而已。这在书中已经说明了的。
  
   5、本书讨论的0bug,我有专文说明,一来,0bug我认为是程序员应该有的一种追求,是目标,其实我本人也没有做到的,但二来,本书讨论的0bug,可能比大多数人讨论的严厉一点点,即产品卖出钱了,你把钱揣到包包里面,并且落袋为安,不会因为维护再花出去,这个叫做0bug。我想我已经说得够清楚了,此处再次重申一下,就不劳大家不断争论了。
  
   此处呢,我作为作者,在此做个郑重声明,也希望各位读者和准读者朋友能精确看清楚本书的定位,以及写作的目标,有的放矢地购买和阅读本书,以便更好地学有所获。
  
  ==================================================
  
  (以下文字,纯属虚构,如有雷同,实属巧合)
  
  From: Mxxx Yxx <xxxxxx@gmail.com>
  Date: 2010-2-2 12:28
  Subject: Re: 关于程序中需要用锁的原因
  To: xxxx <xxxxxx@gmail.com>
  
  
  x先生你好
  
  謝謝賜教。
  我對於硬體的知識很有限。我以為cache是其中一個可能做成不同步的原因。
  不同的 architecture 下的同步機制會有出入,行為會有出入,所以我認為編程時應該使用平台提供的方法,而不要去假設一些行為。
  RISC 的 load/hit/store 是會造成不同步,但書中說的不一致的例子是: 一個 32-bit data 寫到
  memory,只寫了16-bit,另一個 thread 就去讀取。這個情況我覺得在現代的系統裡應該不存在的。
  所以我才建議,如果不需要就不要寫一些底層的機制,讓讀者明白一組內存/設備要同步時使用同步機制便可以了。
  如果願意, 閣下可以把這討論放到豆瓣, 我轉載也可以的。
  再次感謝x先生的來信指導。
  
  在 2010年2月2日上午11:45,xxxx <xxxxxx@gmail.com> 寫道:
  > 昨天看了您在豆瓣上的书评,以及后面的争论。关于0bug的
  > 有个技术问题跟您讨论一下。
  > 您说的关于程序中需要用锁的原因,是由于smp系统中存在cache。我觉得,这个论断是不正确的。
  > 大多数现代的smp系统,包括多核、多CPU系统,应该都是在硬件解决了这个问题。
  > 比如,小规模的系统,用总线侦听协议,如:MESI协议。
  > 而大规模系统,则用目录协议来解决这个问题。
  > 所以一般来说,在软件实现的互斥锁中,并不会有一个显式的cache同步指令。
  > 即便是在没有cache的单CPU、单核系统中,也可能存在多线程之间的数据不一致的问题。
  > 例如:我们有一个简单程序,一个线程循环执行i++,另外一个线程执行i--,两个线程的循环次数相等,这两个线程的循环次数足够的大的时候,运行完毕之后,i的结果可能不等于初始值。
  > 这是因为,某些RISC体系的CPU,load和store指令是分开的,当一个线程执行了load之后,如果被另外一个线程打断,此时就会出现我们不期望的结果。
  > 即便在x86上,也可能出现,因为对多数语言并不会去定义,i++这条语句应该对应一条什么样的汇编指令。
  > 尽管大多数x86上的编译器会把i++编译成一条inc mem[imm]指令,但这个是不保证的。程序正确性不应依赖于某个特定编译器。
  >
  
  
  下面是MSN的讨论记录:
  
  xx 说:
  打扰
  xx 说:
  昨天看了您在豆瓣上的书评,以及后面的争论。
  xx 说:
  关于0bug的
  xx 说:
  有个技术问题跟您讨论一下。
  xx 说:
  您说的关于程序中需要用锁的原因,是由于smp系统中存在cache。我觉得,这个论断是不正确的。
  xx 说:
  大多数现代的smp系统,包括多核、多CPU系统,应该都是在硬件解决了这个问题。
  xx 说:
  比如,小规模的系统,用总线侦听协议,如:MESI协议。
  xx 说:
  而大规模系统,则用目录协议来解决这个问题。
  xx 说:
  所以一般来说,在软件实现的互斥锁中,并不会有一个显式的cache同步指令。
  xx 说:
  即便是在没有cache的单CPU、单核系统中,也可能存在多线程之间的数据不一致的问题。
  xx 说:
  例如:我们有一个简单程序,一个线程循环执行i++,另外一个线程执行i--,两个线程的循环次数相等,这两个线程的循环次数足够的大的时候,运行完毕之后,i的结果可能不等于初始值。
  xx 说:
  这是因为,某些RISC体系的CPU,load和store指令是分开的,当一个线程执行了load之后,如果被另外一个线程打断,此时就会出现我们不期望的结果。
  xx 说:
  即便在x86上,也可能出现,因为对多数语言并不会去定义,i++这条语句应该对应一条什么样的汇编指令。
  xx 说:
  尽管大多数x86上的编译器会把i++编译成一条inc mem[imm]指令,但这个是不保证的。
  xx 说:
  程序正确性不应依赖于某个特定编译器。
  xx 说:
  稍等,去开会。
  xx 说:
  回来再讨论。
  xx 说:
  hi
  xx 说:
  关于为什么要用锁的问题,可否这样解答。
  xx 说:
  在多线程环境下,多个线程之间共享内存中的对象。
  xx 说:
  程序运行的正确性,依赖每个线程对于内存对象的更改操作的原子性。
  xx 说:
  用锁的目的,就是为了保证原子性。
  xx 说:
  而非原子更改操作产生的原因是多方面的,如:肖先生所说的原因,多个byte分开操作,是一方面的原因。
  xx 说:
  您说的smp体系下cache不一致性的问题,是导致这个问题的另外一个原因。
  xx 说:
  究竟应该用何种类型的锁,是由编程环境所定义的内存一致性模型决定的。
  xx 说:
  对象原子性被破坏的问题,是表现在不同层次上的。
  xx 说:
  他们之间又是相互联系的,如:底层内存访问的不一致,可能导致编程语言和操作系统层面不一致。
  xx 说:
  例如,您说的cache一致性问题,可能被传导到上层编程语言的层面。
  Mxxx 说:
  要正確說明為甚麼要用同步機制, 可能是挺困難的. 我有空的看看相關書籍的說法吧.
  xx 说:
  至于究竟应该用那种类型的锁,是由编程环境决定的,例如:如:PowerPC体系结构采用“释放一致性”模型,是比较松散的。
  Mxxx 说:
  atomicity 和 consistency 好像是兩個不同的 quality
  xx 说:
  而其模型,则要严格些。
  xx 说:
  是的,这是不同的概念。
  xx 说:
  似乎你对中文技术词汇,不是很熟悉
  Mxxx 说:
  小時候主要看台灣的, 現在又看內地的, 都混亂了
  xx 说:
  关于那本书,我没看过
  xx 说:
  但是那个人,我认识
  xx 说:
  我觉得要全面评价一本书籍,是比较困难的事情。
  Mxxx 说:
  這當然了, 不然就不會有評論家了.
  xx 说:
  您之所以产生“失望”的感觉,是因为那本书不太适合您。
  Mxxx 说:
  我希望可以比較客觀地在總結裡寫本書的優缺點
  xx 说:
  如果让一个成年人去读儿童读物,就会比较“失望”
  xx 说:
  当然,也并不是说儿童读物没有价值。
  Mxxx 说:
  其實我看前三章時, 真的感到書裡的不嚴謹, 只是靠經驗去解決問題, 也叫讀者這麼做.
  xx 说:
  这是他的习惯,我并不赞同。
  xx 说:
  但,返回头来说,这本书可能会让另外一些人学习到一些经验。也未尝不是好事。
  xx 说:
  我相信,每个人读书,对其内容都是选择性的读。不会是全盘接受的。
  xx 说:
  如果肖先生能够再谦虚一点,也许更好。
  xx 说:
  打扰了。
  
  --
  Mxxx Yxx
  
  

2010-02-05 08:46:24 锲而不舍求妹汁

  不知道楼上的----同学想表达什么意思

2010-02-05 10:15:24 肖舸

    话说某日,我正在归妹位看书。
    
    突然,一声怒喝,某人左手板砖,右手大刀片子,身上挂着要你命3000,奔着我就过来了,吓我一跳。
    
    他身后,还跟着一群打了鸡血的小公鸡,一个个鸡冠子竖老高,红得发紫,亢奋得嗷嗷直叫。这叫助威团。
    
    我说大事不好,正待摆出架势接招。
    
    没想到某人行至中途,突然刀锋一转,直接砍到无妄位的电线杆子上去了。
    
    可怜的电线杆,脑门上挂了个“C”。
    
    随后,板砖,脏水,烂西红柿,臭鸡蛋... ...
    
    电线杆惨不忍睹!
    
    我看了一下,觉得无趣,就走了... ...
    
    一个星期以后,我突发奇想,就回去看看。
    
    电线杆子还在,某人还在继续劈砍,不过,显然后继乏力。
    
    助威团呢,还剩下小鸡两三只。鸡冠子也垂下来了。毕竟,从生理学上讲,雄起得太久,会钙化的,呵呵。
    
    我看了一下,觉得无趣,就又走了... ...
    
    ... ...
    
    ... ...
    
    “收工!”,随着导演一声令下,摄影棚一片忙乱。
    
    “碰”,道具关闭了电闸,灯火通明的摄影棚暗了下来。
    
    人声渐行渐远,摄影棚安静下来。
    
    墙边的小门被微风吹开,一缕路灯的灯光,射了进来。
    
    凄白的灯光,射到摄影棚中心的道具“C”电线杆上。
    
    杆下靠坐着某人,嘴里喃喃地说:
    
    “哥拍的不是C,哥只是想成为传说... ...”
    
    
    
    ----仅以此文,纪念2010年春,某人以C++拍C,以及替伪代码debug的神勇之战
  

2010-02-05 15:31:57 锲而不舍求妹汁

  @微子 @RednaxelaFX
  花了点时间做了点测试,Windows下编译器内存分配释放性能对比,见链接:http://www.missdeer.com/?p=862

2010-03-26 16:26:50 葡萄

  看了《0Bug》作者的行为,我真的无语了。。。冷静一些不好吗?

2010-05-28 16:09:02 老土匪

  学习。 发现自己很多不足

2010-06-06 12:37:08 欲三更

  1.看了评论,我觉得作者有义务拿着大喇叭到每一个读者耳朵边上喊10遍:内存有重叠的时候不能memcpy,只能memmove!!!
  
  2.各种tony,各种缓冲,但是搞来搞去其实就是一种缓冲(事实上我觉得队列根本不能算作缓冲,它起不到缓冲的效果,只是按顺序摆放一些东西而已),怎么作者开发了这么多年高端程序,就没有遇到要用环形缓冲或者双缓冲的时候么?另外我没看过原书,但是看书评的介绍似乎作者对多线程缓冲应用介绍的并不多,就是加了一把锁wrapper了一下,但是实际情况是,如果某个新手真有必要徒手写一个多线程缓冲,那么最可能出错的地方,除了各种缓冲机制的实现,可能就是多线程访问和线程结束的处理了。单单把单线程模型加一把锁,你让人家取数据的线程怎么访问呢?难不成要sleep一会看看有没有数据?
  
  3.其实我还挺佩服这本书的作者的,就说那个free list,虽然是已经很通行的用法,但是在完全对此无知的情况下要“突发奇想”想出来,那还真要一点头脑,类似的还有double-check。


在哪儿买这本书?   · · · · · · 

> 0 bug

0 bug
作者: 肖舸
副标题: C/C++商用工程之道
isbn: 7121098482
书名: 0 bug
页数: 561
定价: 68.00元
出版年: 2010-1

Milo的其他评论   · · · · · ·