“瑜珈山夜話” ----記憶體分配(三) (轉)

amyz發表於2007-11-16
“瑜珈山夜話” ----記憶體分配(三) (轉)[@more@]

  器的結構層次
  我想大家都很清楚,在的儲存中,有各種各樣的儲存器,對他們的訪問頻率和訪問方式直接影響到我們的,一般來說,可以分為5個等級:暫存器、一級快取、二級快取、主存、儲存器。下面我們就把他們的特性大體的說一下:
  1、暫存器,是所有儲存器裡面延遲時間最短、頻寬最大、開銷最少的,毫無疑問,這是目前速度最快的儲存器,但是代價比較昂貴,所以暫存器的個數有限,我們的程式應該儘量充分利用暫存器,C/C++裡面不是有一個關鍵字:register麼?對於那些頻繁使用,有可能成為瓶頸的變數放入暫存器,可能會在效率上有些提高(畢竟程式的瓶頸在一個變數的情況很少出現,一般都在演算法上進行改進),但是對於個別的,效能可能會有數量級上的提升。不過,和inline一樣的是,register也只是一個提示符,提示不需要為變數分配,但是決定權在編譯器,有的編譯器(當然時說那些比較差的了)會完全忽略register指示符。
  2、一級快取,又稱為內快取,速度也是非常的快,其延遲也與暫存器處在同一個數量級,但是容量也很有限。目前,很多上面都有2級晶片內快取,我們可以把他們看作是一個晶片內快取,不影響我們的討論。
  根據1999年的資料,我們知道暫存器延遲2ns,一級快取也是2ns,,看起來似乎2者具有相同的效能,實則不然。通常情況下,一個暫存器組可以在一個時鐘週期內讀2個運算元並且寫一個運算元,一共處理3個運算元,對於一級快取來說2個時鐘週期才能夠處理一個運算元,相比之下,暫存器組的頻寬大約是一級快取的6倍。當然,對於某些特殊的可能不符合這個規律,但是無論如何,暫存器的頻寬都要比一級快取的大。
  3、二級快取,又稱為晶片外快取,速度比一級快取要慢上1-2倍。
  4、主存,也就是我們平常所說的記憶體,是一種半導體動態隨機訪問儲存器,常見的有DRAM、SDRAM、RAMBUS等,就目前來說,記憶體的容量已經變得很大了,常見的是256M、512M。
  計算機的主存訪問有一個非常有規律的特性,就是人們所說的“6-1-1-1-2-1-1-1特性”,什麼意思呢?就是說,最初訪問記憶體需要6個匯流排週期,隨後的3次訪問,每次只需一個匯流排週期,接下來的1次訪問需要2個匯流排週期,隨後的3次訪問,又是隻需要一個匯流排週期。這就是所謂的“6-1-1-1-2-1-1-1特性”
  5、磁碟儲存器,最常用的就是我們所說的,速度和上面的相比,差了好幾個數量級,訪問硬碟的動作屬於I/O訪問,對於效能影響很大,所以儘量避免。當然,有時候非要訪問不可,那就要採用一些有效的策略,例如:開個緩衝池。對於大型的資料訪問,其效能會和的虛擬記憶體機制緊密聯絡,也與結構緊密相關。

  虛擬記憶體的秘密
  虛擬記憶體機制是一種很不錯的機制,表面上看來,他把有限的記憶體轉化為無限的記憶體空間,特別是現代的,往往具有把永久資料對映到系統的虛擬儲存來訪問這些永久資料的能力,系統的增強,也加劇了我們編寫的程式大量依賴於虛擬記憶體機制。
  雖然,在我們訪問硬碟檔案時,資料在記憶體中的駐留有系統控制,系統在硬碟上開闢一段空間作為虛擬記憶體,用這塊虛擬記憶體來動態的管理執行時的檔案。但是,不要忘了,由於硬碟的訪問,使它不得不使用記憶體管理程式碼,結果他的開銷變得和系統的虛擬換頁系統一樣的昂貴。
  根據硬碟廠商提供的資料,磁碟訪問平均需要12--20ms,典型的磁碟訪問至少包括2次上下文轉換以及低階裝置介面的,這就需要數千條指令,數百萬個時鐘週期的延遲。所以如果我們的程式對於效能要求比較高的話,在使用虛擬記憶體的時候要考慮一下。

  記憶體分配策略
  關於記憶體分配,通常有2種比較常用的分配策略:可變大小分配策略、固定尺寸分配策略。

  可變大小分配策略,關鍵就在用他們的通用性上,透過他們,可以向系統申請任意大小的記憶體空間,顯然,這樣的分配方式很靈活,應用也很廣泛,但是他們也有自己致命的缺陷,不過,對於我們來說,影響最大的大約在2個方面:
  1、為了滿足使用者要求和系統的要求,不得不做一些額外的工作,效率自然就會有所下降;
  2、在程式執行期間,可能會有頻繁的記憶體分配和釋放動作,利用我們已有的資料結構和作業系統的知識,這樣就會在記憶體中形成大量的、不連續的、不能夠直接使用的記憶體碎片,在很多情況下,這對於我們的程式都是致命的。如果我們能夠每隔一段時間就重新啟動系統自然就沒有問題,但是有的程式不能夠中斷,就算是能夠中斷,讓使用者每隔一段時間去重起系統也是不現實的(誰還敢用你做的東東?)

  固定尺寸分配策略,這個策略的關鍵就在於固定,也就是說,當我們申請記憶體時,系統總是為我們返回一個固定大小(通常是2的指數倍)的記憶體空間,而不管我們實際需要記憶體的大小。和上面我們所說的通用分配策略相比,顯得比較呆板,但是速度更快,不會產生太多的細小碎片。

  一般情況下,可變大小分配策略和固定尺寸分配策略經常共同合作,例如,分配器會有一個分界線M,當申請的記憶體大小小於M的時候,就採用固定尺寸分配,當申請的大小大於M的時候就採用可變大小的分配。其實,在SGI STL裡面就是採用的這種混合策略,它採用的分界線是128B,如果申請的記憶體大小超過了128B,就移交第一級器處理,如果小於128B,則採用記憶體池策略,維護一個16個free-lists的小額區塊。

  記憶體服務層次
  記憶體分配有很多種策略,那麼我們怎麼知道是誰負責記憶體分配的呢?
  記憶體的分配服務和儲存結構一樣,也是分層次的:
  第一層,作業系統核心提供最基本服務,這是記憶體分配策略的基礎,也是和硬體系統聯絡最緊密的,所以說不同的平臺這些服務的特點也是不一樣的。
  第二層,編譯器的預設執行庫提供自己的分配服務,new/malloc提供的就是基於本地的分配服務,具體的服務方式要依賴於不同的編譯器,編譯器的服務可以只對本地分配服務做一層簡單的包裝,沒有考慮任何效率上的強化,例如,new就是對malloc的一層淺包裝。編譯器的服務也可以對本地服務進行過載,使用去合理的方式去分配記憶體。
  第三層,標準容器提供的記憶體分配服務,和預設的執行庫提供的服務一樣,他也可以簡單的利用編譯器的服務,例如,SGI中的標準配置器allocator,雖然為了符合STL標準,SGI定義了這個配置器,但是在實際應用中,SGI卻把它拋棄了。也可以對器進行過載,實現自己的策略和措施。,例如,SGI中使用的具有此配置能力的SGI空劍配置器。
  最外面的一層,使用者自定義的容器和分配器提供的服務,我們可以對容器的分配器實施自己喜歡的方案,也可以對new/delete過載,讓他做我們喜歡的事情。

  記憶體分配的開銷
  記憶體的開銷主要來自兩部分:維護開銷、對齊開銷。
  1、維護開銷
  在可變大小的分配策略下,在分配的時候,會採用一定的策略去維護分配和釋放記憶體空間的大小,例如,在VC6下面,就會在分配的記憶體塊其實位置放一個Cookie,,當進行delete的時候,指標前移4個位元組,讀出記憶體大小size,然後釋放size+4的空間,我們可以用下面的小程式進行簡單的測試:
    #include
    using namespace std;
    class A
    {
    public:
       A() {cout<       int i;
        ~A() {cout<     };
     int main()
     {
     A* pA=new A[5];
      int* p=(int*)pA;
     *(p-1)=1;
     delete []pA;
     return 0;
     }
    對於固定大小分配策略,因為已經知道記憶體塊的確定大小,自然就不需要這方面的開銷。
  2、對齊開銷
    很多的平臺都要求資料的對齊,在資料的間隙或尾端進行填充,我們可以利用sizeof進行測試:
     struct A
  {
  char c1;
  int  i;
  char c2;
  };
  在我們進行如下運算的時候,我們可能會發現sizeof(A)=12,但是char只是佔用1個位元組,int佔用4個位元組,加起來也不過6個位元組,怎麼會多了一倍呢?
    這就是對齊現象在起作用,實際佔用的空間是這3個變數都佔用4個位元組,在每一個char型的末尾都會填充3個位元組的0。那你把char c2和 int i位置,看看結果是多少?怎麼解釋呢?
    初看起來,只是一種浪費,為什麼會有這個特點呢?目的很簡單,就是要使bus運輸量達到最大。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752019/viewspace-982757/,如需轉載,請註明出處,否則將追究法律責任。

相關文章