The Old New Thing——讀《Windows編程啟示錄》有感

來源:互聯網
上載者:User

by Fei Tian

最近拜讀了Raymond Chen的The Old New Thing(中譯名《Windows編程啟示錄》),作者Chen是微軟Windows部門的資深軟體工程師。總體感覺,這是本很有價值的書,它的價值體現在對Windows這個系統的細節剖析——關於Windows,我想很多windows開發人員很推崇的一本書是《Windows核心編程》,《核心編程》這本書某種程度上可以看成作業系統教科書的翻版,進程、線程、同步、記憶體、IO一章一章的寫,確實是win編程的不錯工具參考書。但《啟示錄》這本書就是另外一種風格了,作者關心的是win系統的一些有趣的細節,諸如win視窗管理及其內在機制、VC++編譯器、向後相容性、國際化編程、win安全機制等等,並以風趣的語言將其成文成章,給人另外一種別樣的參考:"原來,Windows這樣設計是有原因的!"因此對於看管了經典教科書的win開發人員來說,讀一下這本書,應當會在細節方面有很好的收穫。


廢話少說,步入正題:按照我的理解整本書可以分成兩部分:windows設計問題和windows開發問題,前一部分屬於比較淺顯比較迷人的,作者從win95開始娓娓道來,解釋了很多win UI的設計原因——諸如為什麼有個Start按鈕,為什麼關機選項要放在Start中等。後一部分則屬於比較難懂的,也是適合開發人員好好閱讀的一部分,牽扯到win系統很多的實現細節,其中我最欣賞第十一章——General Software Issues,如作者所言:Although these topics may take Windows as their starting point, they are nonetheless applicable to software development in general,有些體會想跟大家分享。

很多程式員寫程式時都會想到的一點就是拚命最佳化——時間上和空間上,但你有沒有這種感覺,你絞盡腦汁想出來的一種最佳化策略,最終卻花費了更大的時間成本?Chen就給我們舉了一個很簡單的例子——試想一個簡單的小程式, 目的是獲得當前函數的返回地址。這樣的程式可以通過編譯器內嵌函式_ReturnAddress很容易實現:


左邊代碼是c++程式,右邊是相應的彙編代碼。這個時候有聰明者可能會想:為什麼要這麼麻煩?一條Pop指令不就搞定了嘛:


上面程式,pop指令將ESP直接送入currentInstruction,兩條指令V.S.四條指令,顯然兩條指令勝出!然而,事實剛好相反。

實際上,x86的處理器隱藏了很多我們在處理器手冊中也不一定能查閱到的東西——比如除了大家耳熟能詳的程式調用棧,處理器為了最佳化還會為call/ret指令"量身定製"一個"返回預測"棧:當遇到call指令時,處理器將應當返回的地址(即ESP)壓入"返回預測"棧(當然也會壓入正常的調用棧),當遇到ret指令時,做相應的出棧(Pop)操作,同時處理器會做投機:它認為"返回預測"棧的棧頂即為應當返回的地址,因此首先將返回地址移入返回預測棧的棧頂地址,如果有問題再做相關的錯誤修正操作。這樣第二種方法的弊端就出來了:它額外添加了一條call指令而沒有相應的ret指令,當然這樣正確性肯定是沒問題的,但導致的後果就是返回預測棧的棧頂是——L1! Pop指令對返回預測棧不起作用,這樣當前程式返回時,處理器會簡單的將返回地址設定為L1——之後就是不斷的Page Fault 處理壓棧的出錯代碼 Page Fault 處理壓棧的出錯代碼…自然會花費更長的時間!

Return address
predictor stack:

 

L1

->

caller1

->

caller2

->

caller3

->

...

Actual stack:

 

caller1

->

caller2

->

caller3

->

caller4

->

...


 

                   

作者提到的另外一個"敏感問題"是記憶體泄露——malloc\calloc\relloc\free\delete…種種讓人頭大的東西,作者提到,導致一個server效能變低乃至崩潰的常見原因就是換頁(Paging),為什麼會換頁?記憶體不夠用了唄,為什麼記憶體不夠用?記憶體泄露了唄!

這裡我們管理記憶體的時候經常有的一種思維方式就是:分配記憶體時,分配最大的,回收記憶體時,回收最大的(如果比當前保管的記憶體地區大),這樣雙管齊下,保證能有有效記憶體供我使用,可這樣的效能呢?想想OS課中講過的Best Fit Allocation, Worst Fit Allocation? 作者給出了這種想法的一個簡單實現:


左邊可以認為是個簡單的堆管理器,右邊則是該管理器分配、回收記憶體的代碼,GetBuffer是向管理器請求記憶體,ReturnBuffer是向管理器返回記憶體(即回收),順帶著提一下原書中的一個錯誤:GetBuffer的第二行代碼 LocalSize(m_pCache)>=cb, 原書中印成了<=cb J

這種簡單穩妥的想法,實際中卻被證明記憶體大量被浪費,原因即在於實際中的記憶體請求多為較小塊的請求,堆管理器回收的記憶體也多為小塊記憶體。對於小塊的記憶體請求,如果給其分配最大的可用記憶體塊,會造成大量浪費。但當偶爾一個大塊記憶體請求到來時,很可能的情況是沒有新的記憶體可供使用——原來的大塊記憶體都被分配一滿足到更有機率出現的小塊請求了,這樣需要重新開闢新的大塊記憶體!即使之前分配過的大塊記憶體在某個時刻被返回了,之後的記憶體請求也很有可能是小塊的,這樣這一大塊的記憶體地區又被浪費,如此周而復始,肯定會造成大量的記憶體泄露(其實照這麼說,不能嚴格的算泄露,畢竟記憶體還是被穩妥的管理著,但因為這些也都發生在malloc calloc free這些函數的執行過程中,姑且這麼叫了吧^_^)

解決辦法呢?很簡單,回收記憶體時,只回收較小的記憶體塊就可以了,一個簡單的修改:


注意到紅色標註的部分:我們只回收比較小的記憶體,有大塊的記憶體需要回收我們還不保留,只是簡單的free掉。這樣做的好處是什麼呢?我們不需要很大塊的記憶體,這樣做只保證管理器當前擁有的空閑記憶體塊不那麼大,這樣可以保證對於大部分小塊的記憶體請求即能滿足又不會有記憶體浪費,只有對於大塊記憶體請求才需要進行重新分配,但重新分配的大塊記憶體使用量完畢被收集時,因為很大,按照當前方法就被簡單的free掉了。這就是一種"對症下藥"的方法,殺雞焉用牛刀?對於大部分的小塊記憶體請求,為什麼非要分配最大塊的記憶體來滿足?小塊的就行了J

總之,在《Windows編程啟示錄》中,類似這樣的例子數不勝數,這本書確實給我們提供了很多全新的視角,讓我們更清晰的瞭解到Windows系統的一些細節。遺憾的是我現在還沒有看完,只有期待空閑時間比較多的時候再細細拜讀,也希望有時間有興趣的朋友能好好讀一下,相信能給您帶來很多啟發~

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.