深刻理解計算機系統9個重點筆記

引言

深刻理解計算機系統,對我來講是部大塊頭。說實話,我沒有從頭至尾完完整整的所有看完,而是選擇性的看了一些我自認爲重要的或感興趣的章節,也從中獲益良多,看清楚了計算機系統的一些本質東西或原理性的內容,這對每一個想要深刻學習編程的程序員來講都是相當重要的。只有很好的理解了系統究竟是如何運行咱們代碼的,咱們才能針對系統的特色寫出高質量、高效率的代碼來。這本書我之後還須要多研究幾遍,今天就先總結下書中我已學到的幾點知識。程序員


 

重點筆記

  1. 編寫高效的程序須要下面幾類活動:算法

    • 選擇一組合適的算法和數據結構。這是很重要的,好的數據結構有時能幫助更快的實現某些算法,這也要求編程人員可以熟知各類經常使用的數據結構和算法。
    • 編寫出使編譯器可以有效優化以轉換成高效可執行的源代碼。所以,理解編譯器優化的能力和侷限性是很重要的。編寫程序方式中看上去只是一點小小的變更,都會引發編譯器優化方式很大的變化。有些編程語言比其餘語言容易優化得多。C語言的某些特性,例如執行指針運算和強制類型轉換的能力,使得編譯器很難對其進行優化。
    • 並行技術,針對處理運算量特別大的計算,將一個任務分紅多個部分,這些部分能夠在多核和多處理器的某種組合上並行地計算。

  2. 讓編譯器展開循環
    說到程序優化,不少人都會提到循環展開技術。如今編譯器能夠很容易地執行循環展開,只要優化級別設置的足夠高,許多編譯器都能例行公事的作到這一點。用命令行選項「-funroll-loops」調用gcc,會執行循環展開。

    編程

  3. 性能提升技術:數組

    • 高級設計,爲手邊的問題選擇適當的算法和數據結構,要特別警覺,避免使用會漸進地產生糟糕性能的算法或編碼技術。
    • 基本編碼原則。避免限制優化的因素,這樣編譯器就能產生高效代碼。
      • 消除連續的函數調用。在可能時將計算移到循環外,考慮有選擇的妥協程序的模塊性以得到更大效率。
      • 消除沒必要要的存儲器引用。引入臨時變量來保存中間結果,只有在最後的值計算出來時,才能將結果放到數組或全局變量中。
    • 低級優化。
      • 嘗試各類與數組代碼相對的指針形式。
      • 經過開展經過展開循環下降循環開銷。
      • 經過諸如迭代分割之類的技術,找到使用流水線化的功能單元的方法。

    說到性能提升,可能有人會有一些說法:服務器

    (1)不要過早優化,優化是萬惡之源;
    (2)花費不少時間所做的優化可能效果不明顯,不值得;
    (3)如今內存、CPU價格都這麼低了,性能的優化已經不是那麼重要了。
     ……網絡

    其實個人見解是:咱們也許沒必要特意把之前寫過的程序拿出來優化下,花費N多時間只爲提高那麼幾秒或幾分鐘的時間。可是,咱們在重構別人的代碼或本身最初開始構思代碼時,就須要知道這些性能提升技術,一開始就遵照這些基本原則來寫代碼,寫出的代碼也就不須要讓別人來重構以提升性能了。另外,有的很簡單的技術,好比說將與循環無關的複雜計算或大內存操做的代碼放到循環外,對於整個性能的提升真的是較明顯的。

    數據結構

  4. 如何使用代碼剖析程序(code profiler,即性能分析工具)來調優代碼?
    程序剖析(profiling)其實就是在運行程序的一個版本中插入了工具代碼,以肯定程序的各個部分須要多少時間。
    Unix系統提供了一個profiling叫GPROF,這個程序產生兩類信息:多線程

    首先,它肯定程序中每一個函數花費了多少CPU時間。
    其次,它計算每一個函數被調用的次數,以執行調用的函數來分類。還有每一個函數被哪些函數調用,自身又調用了哪些函數。併發

    使用GPROF進行剖析須要3個步驟,好比源程序爲prog.c。
    1)編譯: gcc -O1 -pg prog.c -o prog(只要加上-pg參數便可)
    2)運行:./prog
     會生成一個gmon.out文件供 gprof分析程序時候使用(運行比平時慢些)。
    3)剖析:gprof prog
     分析gmon.out中的數據,並顯示出來。

    剖析報告的第一部分列出了執行各個函數花費的時間,按照降序排列。
    剖析報告的第二部分是函數的調用歷史。
    具體例子可參考網上資料。
    數據結構和算法

    GPROF有些屬性值得注意:

    • 計時不是很準確。它的計時基於一個簡單的間隔計數機制,編譯過的程序爲每一個函數維護一個計數器,記錄花費在執行該函數上的時間。對於運行時間較長的程序,相對準確。
    • 調用信息至關可靠。
    • 默認狀況下,不顯示庫函數的調用。相反地,庫函數的時間會被計算到調用它們的函數的時間中。

  5. 靜態連接和動態連接一個很重要的區別是:動態連接時沒有任何動態連接庫的代碼和數據節真正的被拷貝到可執行文件中,反之,連接器只需拷貝一些重定位和符號表信息,便可使得運行時能夠解析對動態連接庫中代碼和數據的引用。

  6. 存儲器映射
    指的是將磁盤上的空間映射爲虛擬存儲器區域。Unix進程可使用mmap函數來建立新的虛擬存儲器區域,並將對象映射到這些區域中,這屬於低級的分配方式。
    通常C程序會使用malloc和free來動態分配存儲器區域,這是利用堆的方式。

  7. 形成堆利用率很低的主要緣由是碎片,當雖然有未使用的存儲器但不能用來知足分配請求時,就會發生這種現象。
    有兩種形式的碎片:內部碎片和外部碎片。二者的區別以下:

    • 內部碎片是在一個已分配的塊比有效載荷大時發生的。例如,有些分配器爲了知足對其約束添加額外的1字的存儲空間,這個1字的空間就是內部碎片。它就是已分配塊大小和它們的有效載荷大小之差的和。
    • 外部碎片是當空閒存儲器合計起來足夠知足一個分配請求,可是沒有一個單獨的空閒塊足夠大能夠來處理這個請求時發生的。

  8. 現代OS提供了三種方法實現併發編程:

    • 進程。用這種方法,每一個邏輯控制流都是一個進程,由內核來調度和維護。由於進程有獨立的虛擬地址空間,想要和其餘流通訊,控制流必須使用進程間通訊(IPC)。
    • I/O多路複用。這種形式的併發,應用程序在一個進程的上下文中顯示地調度它們本身的邏輯流。邏輯流被模擬爲「狀態機」,數據到達文件描述符後,主程序顯示地從一個狀態轉換到另外一個狀態。由於程序是一個單獨的進程,因此全部的流都共享一個地址空間。
    • 線程。線程是運行在一個單一進程上下文中的邏輯流,由內核進行調度。線程能夠看作是進程和I/O多路複用的合體,像進程同樣由內核調度,像I/O多路複用同樣共享一個虛擬地址空間。

    (1)基於進程的併發服務器
    構造併發最簡單的就是使用進程,像fork函數。例如,一個併發服務器,在父進程中接受客戶端鏈接請求,而後建立一個新的子進程來爲每一個新客戶端提供服務。爲了瞭解這是如何工做的,假設咱們有兩個客戶端和一個服務器,服務器正在監聽一個監聽描述符(好比描述符3)上的鏈接請求。下面顯示了服務器是如何接受這兩個客戶端的請求的。

    關於進程的優劣,對於在父、子進程間共享狀態信息,進程有一個很是清晰的模型:共享文件表,可是不共享用戶地址空間。進程有獨立的地址控件愛你既是優勢又是缺點。因爲獨立的地址空間,因此進程不會覆蓋另外一個進程的虛擬存儲器。可是另外一方面進程間通訊就比較麻煩,至少開銷很高。

    (2)基於I/O多路複用的併發編程
    好比一個服務器,它有兩個I/O事件:1)網絡客戶端發起鏈接請求,2)用戶在鍵盤上鍵入命令行。咱們先等待那個事件呢?沒有那個選擇是理想的。若是accept中等待鏈接,那麼沒法響應輸入命令。若是在read中等待一個輸入命令,咱們就不能響應任何鏈接請求(這個前提是一個進程)。
    針對這種困境的一個解決辦法就是I/O多路複用技術。基本思想是:使用select函數,要求內核掛起進程,只有在一個或者多個I/O事件發生後,纔將控制返給應用程序。
    I/O多路複用的優劣:因爲I/O多路複用是在單一進程的上下文中的,所以每一個邏輯流程都能訪問該進程的所有地址空間,因此開銷比多進程低得多;缺點是編程複雜度高。

    (3)基於線程的併發編程
    每一個線程都有本身的線程上下文,包括一個線程ID、棧、棧指針、程序計數器、通用目的寄存器和條件碼。全部的運行在一個進程裏的線程共享該進程的整個虛擬地址空間。因爲線程運行在單一進程中,所以共享這個進程虛擬地址空間的整個內容,包括它的代碼、數據、堆、共享庫和打開的文件。因此我認爲不存在線程間通訊,線程間只有鎖的概念。

    • 線程執行的模型。線程和進程的執行模型有些類似。每一個進程的生明週期都是一個線程,咱們稱之爲主線程。可是你們要有意識:線程是對等的,主線程跟其餘線程的區別就是它先執行。
      通常來講,線程的代碼和本地數據被封裝在一個線程例程中(就是一個函數)。該函數一般只有一個指針參數和一個指針返回值。
      在Unix中線程能夠是joinable(可結合)或者detached(分離)的。joinable能夠被其餘線程殺死,detached線程不能被殺死,它的存儲器資源有系統自動釋放。

    • 線程存儲器模型,每一個線程都有它本身的獨立的線程上下文,包括線程ID、棧、棧指針、程序計數器、條件碼和通用目的寄存器。每一個線程和其餘線程共享剩下的部分,包括整個用戶虛擬地址空間,它是由代碼段、數據段、堆以及全部的共享庫代碼和數據區域組成。不一樣線程的棧是對其餘線程不設防的,也就是說:若是一個線程以某種方式獲得一個指向其餘線程的指針,那麼它能夠讀取這個線程棧的任何部分。

  9. 什麼樣的變量多線程能夠共享,什麼樣的不能夠共享?
    有三種變量:全局變量、本地自動變量(局部變量)和本地靜態變量,其中本地自動變量每一個線程的本地棧中都存有一份,不共享。而全局變量和靜態變量能夠共享。

相關文章
相關標籤/搜索