深入理解計算機系統,對我來講是部大塊頭。說實話,我沒有從頭到尾完完全整的全部看完,而是選擇性的看了1些我自認為重要的或感興趣的章節,也從中獲益很多,看清楚了計算機系統的1些本質東西或原理性的內容,這對每一個想要深入學習編程的程序員來講都是相當重要的。只有很好的理解了系統究竟是如何運行我們代碼的,我們才能針對系統的特點寫出高質量、高效力的代碼來。這本書我以后還需要多研究幾遍,今天就先總結下書中我已學到的幾點知識。
編寫高效的程序需要下面幾類活動:
讓編譯器展開循環
說到程序優化,很多人都會提到循環展開技術。現在編譯器可以很容易地履行循環展開,只要優化級別設置的足夠高,許多編譯器都能例行公事的做到這1點。用命令行選項“-funroll-loops”調用gcc,會履行循環展開。
性能提高技術:
說到性能提高,可能有人會有1些說法:
(1)不要過早優化,優化是萬惡之源;
(2)花費很多時間所作的優化可能效果不明顯,不值得;
(3)現在內存、CPU價格都這么低了,性能的優化已不是那末重要了。
……
其實我的看法是:我們或許沒必要特地把之前寫過的程序拿出來優化下,花費N多時間只為提升那末幾秒或幾分鐘的時間。但是,我們在重構他人的代碼或自己最初開始構思代碼時,就需要知道這些性能提高技術,1開始就遵照這些基本原則來寫代碼,寫出的代碼也就不需要讓他人來重構以提高性能了。另外,有的很簡單的技術,比如說將與循環無關的復雜計算或大內存操作的代碼放到循環外,對全部性能的提高真的是較明顯的。
如何使用代碼剖析程序(code profiler,即性能分析工具)來調優代碼?
程序剖析(profiling)其實就是在運行程序的1個版本中插入了工具代碼,以肯定程序的各個部份需要多少時間。
Unix系統提供了1個profiling叫GPROF
,這個程序產生兩類信息:
首先,它肯定程序中每一個函數花費了多少CPU時間。
其次,它計算每一個函數被調用的次數,以履行調用的函數來分類。還有每一個函數被哪些函數調用,本身又調用了哪些函數。
使用GPROF進行剖析需要3個步驟,比如源程序為prog.c。
1)編譯: gcc -O1 -pg prog.c -o prog
(只要加上-pg參數便可)
2)運行:./prog
會生成1個gmon.out文件供 gprof分析程序時候使用(運行比平時慢些)。
3)剖析:gprof prog
分析gmon.out中的數據,并顯示出來。
剖析報告的第1部份列出了履行各個函數花費的時間,依照降序排列。
剖析報告的第2部份是函數的調用歷史。具體例子可參考網上資料。
GPROF有些屬性值得注意:
靜態鏈接和動態鏈接1個很重要的區分是:動態鏈接時沒有任何動態鏈接庫的代碼和數據節真實的被拷貝到可履行文件中,反之,鏈接器只需拷貝1些重定位和符號表信息,便可使得運行時可以解析對動態鏈接庫中代碼和數據的援用。
存儲器映照
指的是將磁盤上的空間映照為虛擬存儲器區域。Unix進程可使用mmap函數來創建新的虛擬存儲器區域,并將對象映照到這些區域中,這屬于低級的分配方式。
1般C程序會使用malloc和free來動態分配存儲器區域,這是利用堆的方式。
造成堆利用率很低的主要緣由是碎片,當雖然有未使用的存儲器但不能用來滿足分配要求時,就會產生這類現象。
有兩種情勢的碎片:內部碎片和外部碎片。二者的區分以下:
現代OS提供了3種方法實現并發編程:
(1)基于進程的并發服務器
構造并發最簡單的就是使用進程,像fork函數。例如,1個并發服務器,在父進程中接受客戶端連接要求,然后創建1個新的子進程來為每一個新客戶端提供服務。為了了解這是如何工作的,假定我們有兩個客戶端和1個服務器,服務器正在監聽1個監聽描寫符(比如描寫符3)上的連接要求。下面顯示了服務器是如何接受這兩個客戶真個要求的。
關于進程的優劣,對在父、子進程間同享狀態信息,進程有1個非常清晰的模型:同享文件表,但是不同享用戶地址空間。進程有獨立的地址控件愛你既是優點又是缺點。由于獨立的地址空間,所以進程不會覆蓋另外一個進程的虛擬存儲器。但是另外一方面進程間通訊就比較麻煩,最少開消很高。
(2)基于I/O多路復用的并發編程
比如1個服務器,它有兩個I/O事件:1)網絡客戶端發起連接要求,2)用戶在鍵盤上鍵入命令行。我們先等待那個事件呢?沒有那個選擇是理想的。如果accept中等待連接,那末沒法響應輸入命令。如果在read中等待1個輸入命令,我們就不能響應任何連接要求(這個條件是1個進程)。
針對這類窘境的1個解決辦法就是I/O多路復用技術。基本思想是:使用select函數,要求內核掛起進程,只有在1個或多個I/O事件產生后,才將控制返給利用程序。
I/O多路復用的優劣:由于I/O多路復用是在單1進程的上下文中的,因此每一個邏輯流程都能訪問該進程的全部地址空間,所以開消比多進程低很多;缺點是編程復雜度高。
(3)基于線程的并發編程
每一個線程都有自己的線程上下文,包括1個線程ID、棧、棧指針、程序計數器、通用目的寄存器和條件碼。所有的運行在1個進程里的線程同享該進程的全部虛擬地址空間。由于線程運行在單1進程中,因此同享這個進程虛擬地址空間的全部內容,包括它的代碼、數據、堆、同享庫和打開的文件。所以我認為不存在線程間通訊,線程間只有鎖的概念。
線程履行的模型。線程和進程的履行模型有些類似。每一個進程的生明周期都是1個線程,我們稱之為主線程。但是大家要成心識:線程是對等的,主線程跟其他線程的區分就是它先履行。
1般來講,線程的代碼和本地數據被封裝在1個線程例程中(就是1個函數)。該函數通常只有1個指針參數和1個指針返回值。
在Unix中線程可以是joinable(可結合)或detached(分離)的。joinable可以被其他線程殺死,detached線程不能被殺死,它的存儲器資源有系統自動釋放。
線程存儲器模型,每一個線程都有它自己的獨立的線程上下文,包括線程ID、棧、棧指針、程序計數器、條件碼和通用目的寄存器。每一個線程和其他線程同享剩下的部份,包括全部用戶虛擬地址空間,它是由代碼段、數據段、堆和所有的同享庫代碼和數據區域組成。不同線程的棧是對其他線程不設防的,也就是說:如果1個線程以某種方式得到1個指向其他線程的指針,那末它可以讀取這個線程棧的任何部份。
甚么樣的變量多線程可以同享,甚么樣的不可以同享?
有3種變量:全局變量、本地自動變量(局部變量)和本地靜態變量,其中本地自動變量每一個線程的本地棧中都存有1份,不同享。而全局變量和靜態變量可以同享。