深入理解计算机系统9个重点笔记
引言
深入理解計算機(jī)系統(tǒng),對我來說是部大塊頭。說實話,我沒有從頭到尾完完整整的全部看完,而是選擇性的看了一些我自認(rèn)為重要的或感興趣的章節(jié),也從中獲益良多,看清楚了計算機(jī)系統(tǒng)的一些本質(zhì)東西或原理性的內(nèi)容,這對每個想要深入學(xué)習(xí)編程的程序員來說都是至關(guān)重要的。只有很好的理解了系統(tǒng)到底是如何運行我們代碼的,我們才能針對系統(tǒng)的特點寫出高質(zhì)量、高效率的代碼來。這本書我以后還需要多研究幾遍,今天就先總結(jié)下書中我已學(xué)到的幾點知識。
重點筆記
編寫高效的程序需要下面幾類活動:
選擇一組合適的算法和數(shù)據(jù)結(jié)構(gòu)。這是很重要的,好的數(shù)據(jù)結(jié)構(gòu)有時能幫助更快的實現(xiàn)某些算法,這也要求編程人員能夠熟知各種常用的數(shù)據(jù)結(jié)構(gòu)和算法。
編寫出使編譯器能夠有效優(yōu)化以轉(zhuǎn)換成高效可執(zhí)行的源代碼。因此,理解編譯器優(yōu)化的能力和局限性是很重要的。編寫程序方式中看上去只是一點小小的變動,都會引起編譯器優(yōu)化方式很大的變化。有些編程語言比其他語言容易優(yōu)化得多。C語言的某些特性,例如執(zhí)行指針運算和強(qiáng)制類型轉(zhuǎn)換的能力,使得編譯器很難對其進(jìn)行優(yōu)化。
并行技術(shù),針對處理運算量特別大的計算,將一個任務(wù)分成多個部分,這些部分可以在多核和多處理器的某種組合上并行地計算。
讓編譯器展開循環(huán)
說到程序優(yōu)化,很多人都會提到循環(huán)展開技術(shù)。現(xiàn)在編譯器可以很容易地執(zhí)行循環(huán)展開,只要優(yōu)化級別設(shè)置的足夠高,許多編譯器都能例行公事的做到這一點。用命令行選項“-funroll-loops”調(diào)用gcc,會執(zhí)行循環(huán)展開。
性能提高技術(shù):
高級設(shè)計,為手邊的問題選擇適當(dāng)?shù)乃惴ê蛿?shù)據(jù)結(jié)構(gòu),要特別警覺,避免使用會漸進(jìn)地產(chǎn)生糟糕性能的算法或編碼技術(shù)。
基本編碼原則。避免限制優(yōu)化的因素,這樣編譯器就能產(chǎn)生高效代碼。
消除連續(xù)的函數(shù)調(diào)用。在可能時將計算移到循環(huán)外,考慮有選擇的妥協(xié)程序的模塊性以獲得更大效率。
消除不必要的存儲器引用。引入臨時變量來保存中間結(jié)果,只有在最后的值計算出來時,才能將結(jié)果放到數(shù)組或全局變量中。
低級優(yōu)化。
嘗試各種與數(shù)組代碼相對的指針形式。
通過開展通過展開循環(huán)降低循環(huán)開銷。
通過諸如迭代分割之類的技術(shù),找到使用流水線化的功能單元的方法。
說到性能提高,可能有人會有一些說法:
(1)不要過早優(yōu)化,優(yōu)化是萬惡之源;
(2)花費很多時間所作的優(yōu)化可能效果不明顯,不值得;
(3)現(xiàn)在內(nèi)存、CPU價格都這么低了,性能的優(yōu)化已經(jīng)不是那么重要了。
……
其實我的看法是:我們也許不必特地把以前寫過的程序拿出來優(yōu)化下,花費N多時間只為提升那么幾秒或幾分鐘的時間。但是,我們在重構(gòu)別人的代碼或自己最初開始構(gòu)思代碼時,就需要知道這些性能提高技術(shù),一開始就遵守這些基本原則來寫代碼,寫出的代碼也就不需要讓別人來重構(gòu)以提高性能了。另外,有的很簡單的技術(shù),比如說將與循環(huán)無關(guān)的復(fù)雜計算或大內(nèi)存操作的代碼放到循環(huán)外,對于整個性能的提高真的是較明顯的。
如何使用代碼剖析程序(code profiler,即性能分析工具)來調(diào)優(yōu)代碼?
程序剖析(profiling)其實就是在運行程序的一個版本中插入了工具代碼,以確定程序的各個部分需要多少時間。
Unix系統(tǒng)提供了一個profiling叫GPROF,這個程序產(chǎn)生兩類信息:
首先,它確定程序中每個函數(shù)花費了多少CPU時間。
其次,它計算每個函數(shù)被調(diào)用的次數(shù),以執(zhí)行調(diào)用的函數(shù)來分類。還有每個函數(shù)被哪些函數(shù)調(diào)用,自身又調(diào)用了哪些函數(shù)。
使用GPROF進(jìn)行剖析需要3個步驟,比如源程序為prog.c。
1)編譯:?gcc -O1 -pg prog.c -o prog(只要加上-pg參數(shù)即可)
2)運行:./prog
會生成一個gmon.out文件供 gprof分析程序時候使用(運行比平時慢些)。
3)剖析:gprof prog
分析gmon.out中的數(shù)據(jù),并顯示出來。
剖析報告的第一部分列出了執(zhí)行各個函數(shù)花費的時間,按照降序排列。
剖析報告的第二部分是函數(shù)的調(diào)用歷史。具體例子可參考網(wǎng)上資料。
GPROF有些屬性值得注意:
計時不是很準(zhǔn)確。它的計時基于一個簡單的間隔計數(shù)機(jī)制,編譯過的程序為每個函數(shù)維護(hù)一個計數(shù)器,記錄花費在執(zhí)行該函數(shù)上的時間。對于運行時間較長的程序,相對準(zhǔn)確。
調(diào)用信息相當(dāng)可靠。
默認(rèn)情況下,不顯示庫函數(shù)的調(diào)用。相反地,庫函數(shù)的時間會被計算到調(diào)用它們的函數(shù)的時間中。
靜態(tài)鏈接和動態(tài)鏈接一個很重要的區(qū)別是:動態(tài)鏈接時沒有任何動態(tài)鏈接庫的代碼和數(shù)據(jù)節(jié)真正的被拷貝到可執(zhí)行文件中,反之,鏈接器只需拷貝一些重定位和符號表信息,即可使得運行時可以解析對動態(tài)鏈接庫中代碼和數(shù)據(jù)的引用。
存儲器映射
指的是將磁盤上的空間映射為虛擬存儲器區(qū)域。Unix進(jìn)程可以使用mmap函數(shù)來創(chuàng)建新的虛擬存儲器區(qū)域,并將對象映射到這些區(qū)域中,這屬于低級的分配方式。
一般C程序會使用malloc和free來動態(tài)分配存儲器區(qū)域,這是利用堆的方式。
造成堆利用率很低的主要原因是碎片,當(dāng)雖然有未使用的存儲器但不能用來滿足分配請求時,就會發(fā)生這種現(xiàn)象。
有兩種形式的碎片:內(nèi)部碎片和外部碎片。兩者的區(qū)別如下:
內(nèi)部碎片是在一個已分配的塊比有效載荷大時發(fā)生的。例如,有些分配器為了滿足對其約束添加額外的1字的存儲空間,這個1字的空間就是內(nèi)部碎片。它就是已分配塊大小和它們的有效載荷大小之差的和。
外部碎片是當(dāng)空閑存儲器合計起來足夠滿足一個分配請求,但是沒有一個單獨的空閑塊足夠大可以來處理這個請求時發(fā)生的。
現(xiàn)代OS提供了三種方法實現(xiàn)并發(fā)編程:
進(jìn)程。用這種方法,每個邏輯控制流都是一個進(jìn)程,由內(nèi)核來調(diào)度和維護(hù)。因為進(jìn)程有獨立的虛擬地址空間,想要和其他流通信,控制流必須使用進(jìn)程間通信(IPC)。
I/O多路復(fù)用。這種形式的并發(fā),應(yīng)用程序在一個進(jìn)程的上下文中顯示地調(diào)度它們自己的邏輯流。邏輯流被模擬為“狀態(tài)機(jī)”,數(shù)據(jù)到達(dá)文件描述符后,主程序顯示地從一個狀態(tài)轉(zhuǎn)換到另一個狀態(tài)。因為程序是一個單獨的進(jìn)程,所以所有的流都共享一個地址空間。
線程。線程是運行在一個單一進(jìn)程上下文中的邏輯流,由內(nèi)核進(jìn)行調(diào)度。線程可以看做是進(jìn)程和I/O多路復(fù)用的合體,像進(jìn)程一樣由內(nèi)核調(diào)度,像I/O多路復(fù)用一樣共享一個虛擬地址空間。
(1)基于進(jìn)程的并發(fā)服務(wù)器
構(gòu)造并發(fā)最簡單的就是使用進(jìn)程,像fork函數(shù)。例如,一個并發(fā)服務(wù)器,在父進(jìn)程中接受客戶端連接請求,然后創(chuàng)建一個新的子進(jìn)程來為每個新客戶端提供服務(wù)。為了了解這是如何工作的,假設(shè)我們有兩個客戶端和一個服務(wù)器,服務(wù)器正在監(jiān)聽一個監(jiān)聽描述符(比如描述符3)上的連接請求。下面顯示了服務(wù)器是如何接受這兩個客戶端的請求的。
進(jìn)程并發(fā)示例
關(guān)于進(jìn)程的優(yōu)劣,對于在父、子進(jìn)程間共享狀態(tài)信息,進(jìn)程有一個非常清晰的模型:共享文件表,但是不共享用戶地址空間。進(jìn)程有獨立的地址控件愛你既是優(yōu)點又是缺點。由于獨立的地址空間,所以進(jìn)程不會覆蓋另一個進(jìn)程的虛擬存儲器。但是另一方面進(jìn)程間通信就比較麻煩,至少開銷很高。
(2)基于I/O多路復(fù)用的并發(fā)編程
比如一個服務(wù)器,它有兩個I/O事件:1)網(wǎng)絡(luò)客戶端發(fā)起連接請求,2)用戶在鍵盤上鍵入命令行。我們先等待那個事件呢?沒有那個選擇是理想的。如果accept中等待連接,那么無法響應(yīng)輸入命令。如果在read中等待一個輸入命令,我們就不能響應(yīng)任何連接請求(這個前提是一個進(jìn)程)。
針對這種困境的一個解決辦法就是I/O多路復(fù)用技術(shù)。基本思想是:使用select函數(shù),要求內(nèi)核掛起進(jìn)程,只有在一個或者多個I/O事件發(fā)生后,才將控制返給應(yīng)用程序。
I/O多路復(fù)用的優(yōu)劣:由于I/O多路復(fù)用是在單一進(jìn)程的上下文中的,因此每個邏輯流程都能訪問該進(jìn)程的全部地址空間,所以開銷比多進(jìn)程低得多;缺點是編程復(fù)雜度高。
(3)基于線程的并發(fā)編程
每個線程都有自己的線程上下文,包括一個線程ID、棧、棧指針、程序計數(shù)器、通用目的寄存器和條件碼。所有的運行在一個進(jìn)程里的線程共享該進(jìn)程的整個虛擬地址空間。由于線程運行在單一進(jìn)程中,因此共享這個進(jìn)程虛擬地址空間的整個內(nèi)容,包括它的代碼、數(shù)據(jù)、堆、共享庫和打開的文件。所以我認(rèn)為不存在線程間通信,線程間只有鎖的概念。
線程執(zhí)行的模型。線程和進(jìn)程的執(zhí)行模型有些相似。每個進(jìn)程的生明周期都是一個線程,我們稱之為主線程。但是大家要有意識:線程是對等的,主線程跟其他線程的區(qū)別就是它先執(zhí)行。
一般來說,線程的代碼和本地數(shù)據(jù)被封裝在一個線程例程中(就是一個函數(shù))。該函數(shù)通常只有一個指針參數(shù)和一個指針返回值。
在Unix中線程可以是joinable(可結(jié)合)或者detached(分離)的。joinable可以被其他線程殺死,detached線程不能被殺死,它的存儲器資源有系統(tǒng)自動釋放。
線程存儲器模型,每個線程都有它自己的獨立的線程上下文,包括線程ID、棧、棧指針、程序計數(shù)器、條件碼和通用目的寄存器。每個線程和其他線程共享剩下的部分,包括整個用戶虛擬地址空間,它是由代碼段、數(shù)據(jù)段、堆以及所有的共享庫代碼和數(shù)據(jù)區(qū)域組成。不同線程的棧是對其他線程不設(shè)防的,也就是說:如果一個線程以某種方式得到一個指向其他線程的指針,那么它可以讀取這個線程棧的任何部分。
什么樣的變量多線程可以共享,什么樣的不可以共享?
有三種變量:全局變量、本地自動變量(局部變量)和本地靜態(tài)變量,其中本地自動變量每個線程的本地棧中都存有一份,不共享。而全局變量和靜態(tài)變量可以共享。
————————————————
版權(quán)聲明:本文為CSDN博主「小敏紙」的原創(chuàng)文章,遵循 CC 4.0 BY-SA 版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/lanxuezaipiao/article/details/41704011
總結(jié)
以上是生活随笔為你收集整理的深入理解计算机系统9个重点笔记的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 10道C++输出易错笔试题收集(敢进来挑
- 下一篇: Java I/O系统学习系列一:File