成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

談一談Windows中的堆

系統 Windows
如果在Windows中編程應該了解一些Windows的內存管理,而堆(Heap)也屬于內存管理的一部分。這篇文章對你理解Windows內存分配的基本原理和調試堆內存問題或許會有所幫助。

[[413851]]

本文轉載自微信公眾號「一個程序員的修煉之路」,作者河邊一枝柳。轉載本文請聯系一個程序員的修煉之路公眾號。

如果在Windows中編程應該了解一些Windows的內存管理,而堆(Heap)也屬于內存管理的一部分。這篇文章對你理解Windows內存分配的基本原理和調試堆內存問題或許會有所幫助。

Windows Heap概述

下圖參考<<Windows高級調試>>所畫,并做了一些小小的修改??梢钥闯鰜沓绦蛑袑Χ训闹苯硬僮髦饕腥N:

  1. 進程默認堆。每個進程啟動的時候系統會創建一個默認堆。比如LocalAlloc或者GlobalAlloc也是從進程默認堆上分配內存。你也可以使用GetProcessHeap獲取進程默認堆的句柄,然后根據用這個句柄去調用HeapAlloc達到在系統默認堆上分配內存的效果。
  2. C++編程中常用的是malloc和new去申請內存,這些由CRT庫提供方法。而根據查看在VS2010之前(包含),CRT庫會使用HeapCreate去創建一個堆,供CRT庫自己使用。在VS2015以后CRT庫的實現,并不會再去創建一個單獨的堆,而使用進程默認堆。 (VS2013的CRT源碼我并未查看,有興趣的可以看看VS2013默認的CRT庫采用的是進程默認堆還是新建的堆)。
  3. 自建堆。這個泛指程序通過HeapCreate去創建的堆,然后利用HeapAlloc等API去操作堆,比如申請空間。

那么堆管理器是通過調用虛擬管理器的一些方法進行堆管理的實現,比如VirtualAlloc之類的函數。同樣應用程序也可以直接使用VirtualAlloc之類的函數對內存進行使用。

說到這里不免有些生澀,我們就寫一個示例代碼來看看一個進程的堆情況。

  1. #include <windows.h> 
  2. #include <iostream> 
  3. #include <intsafe.h> 
  4.  
  5. using namespace std; 
  6. const char* GetHeapTypeString(HANDLE pHandle) 
  7.   ULONG ulHeapInfo; 
  8.   HeapQueryInformation(pHandle, 
  9.     HeapCompatibilityInformation, 
  10.     &ulHeapInfo, 
  11.     sizeof(ulHeapInfo), 
  12.     NULL); 
  13.   switch (ulHeapInfo) 
  14.   { 
  15.   case 0: 
  16.     return "Standard"
  17.   case 1: 
  18.     return "Look Aside List"
  19.   case 2: 
  20.     return "Low Fragmentation"
  21.   } 
  22.   return "Unknow type"
  23.  
  24. void PrintAllHeaps() 
  25.  
  26.   DWORD dwNumHeap = GetProcessHeaps(0, NULL); 
  27.   if (dwNumHeap == 0) 
  28.   { 
  29.     cout << "No Heap!" << endl; 
  30.     return
  31.   } 
  32.  
  33.   PHANDLE pHeaps; 
  34.   SIZE_T  uBytes; 
  35.   HRESULT Result = SIZETMult(dwNumHeap, sizeof(*pHeaps), &uBytes); 
  36.   if (Result != S_OK) { 
  37.     return
  38.   } 
  39.  
  40.   pHeaps = (PHANDLE)malloc(uBytes); 
  41.   dwNumHeap = GetProcessHeaps(dwNumHeap, pHeaps); 
  42.   cout << "Process has heaps: " << dwNumHeap << endl; 
  43.   for (int i = 0; i < dwNumHeap; ++i) 
  44.   { 
  45.     cout << "Heap Address: " << pHeaps[i] 
  46.       << ", Heap Type: " << GetHeapTypeString(pHeaps[i]) << endl; 
  47.   } 
  48.  
  49.   return
  50.  
  51. int main() 
  52.   cout << "========================" << endl; 
  53.   PrintAllHeaps(); 
  54.   cout << "========================" << endl; 
  55.  
  56.   HANDLE hDefaultHeap = GetProcessHeap(); 
  57.   cout << "Default Heap: " << hDefaultHeap 
  58.     << ", Heap Type: " << GetHeapTypeString(hDefaultHeap) << endl; 
  59.  
  60.   return 0; 

這是一個在Win10上運行的64位程序輸出的結果: 這個進程我們并沒有在main中顯示的創建Heap,我們都知道進程在啟動的時候初始化會創建相關的資源,其中也包含了堆。這個進程共創建了四個堆??梢钥闯鰜淼谝粋€堆就是進程的默認堆,并且是采用的 Low Fragmentation的分配策略的堆。

堆的內存分配策略

堆主要有前端分配器和后端分配器,我所理解的前端分配器就是類似于緩存一樣,便于快速的查詢所需要的內存塊,當前端分配器搞不定的時候,就交給后端分配器。

前端分配器主要分為, 而Windows Vista之后進程默認堆均采用低碎片前端分配器。

  • 旁視列表 (Look Aside List)
  • 低碎片 (Low Fragmentation)

以下的場景均采用32位的程序進行的描述。

前端分配器之旁視列表

旁視列表 (Look Aside List, LAL)是一種老的前端分配器,在Windows XP中使用。

這是一個連續的數組大小為128,每個元素對應一個鏈表,因為其存儲的是整個Heap塊的大小,那就包含了用戶申請的大小+堆塊元數據,而這里元數據大小為8字節, 而最小分配粒度為8字節(32位程序),那么最小的堆塊的大小則為16個字節。從數據1~127,每個鏈表鎖存儲的堆塊大小按照8字節粒度增加。

那么當用戶申請一個比如10字節大小的的內存,則在LAL中查找的堆塊大小為18字節=10字節+元數據8字節,則在表中找到的剛好匹配的堆塊大小為24字節的節點,并將其從鏈表中刪除。

而當用戶釋放內存的時候,也會優先查看前端處理器是否處理,如果處理則將內存插入到相應的鏈表中。

前端分配器之低碎片

先說說內存碎片我這里簡要概述下: 如下圖所示假設一段大的連續的內存被分割為若干個8字節的內存塊,然后這個時候釋放了圖中綠色部分的內存塊,那么此時總共空出了40字節的內存,但想去申請一個16字節的內存塊,卻無法申請到一個連續的16字節內存塊,從而分配內存失敗,這就是內存碎片。

所謂的低碎片前端分配器,是將LAL類似的數組中的粒度重新進行了劃分:

數據Index 堆塊遞增粒度 堆塊字節范圍
0~31 8 8~256
32~47 16 272~512
112-127 512 8704~16384
 

可以看到同樣的數組的大小,將其按照不同的粒度劃分,相比較LAL分配的大小粒度逐步增大,到了最后的112-127區間粒度已經增大到了512字節,最大支持的16384。粒度更大的分配有利于緩解內存碎片,提高內存的使用效率。Windows Vista之后進程默認堆均采用低碎片前端分配器。

后端分配器

其實講到前面這部分可能還有一些人云里霧里。那么我們的內存到底是怎么劃分出來的呢?這就是后端分配器要做的事情了??纯春蠖朔峙淦魇侨绾喂芾磉@些內存的。

先說說堆在內存中的展現形式,一個堆主要由若干個Segment(段)組成,每個Segment都是一段連續的空間,然后用雙向鏈表串起來。而一般情況下,一開始只有一個Segment,然后在這個Segment上申請空間,叫做Heap Entry(堆塊)。但是這個Segment可能會被用完,那就新開辟一個Segment,而且一般新的Segement大小是原先的2倍,如果內存不足則不斷的將申請空間減半。這里有個要注意的就是當劃分了一個新的Segment后比如其空間為1GBytes,那么其真實的使用的物理內存肯定不會是1GBytes,因為此時內存還沒有被應用程序申請,這個時候實際上這個Segment只是Reserve了這段虛擬地址空間,而當真正應用程序申請內存的時候,才會一小部分一小部分的Commit,這個時候才會用到真正的物理存儲空間。

而應用程序申請的內存在Segment上叫做Entry(塊),他們是連續的,可以看到一個塊一般具有:

  • 前置的元數據: 這里主要存儲有當前塊的大小,前一個塊的大小,當前塊的狀態等。
  • 用戶數據區: 這段內存才是用戶申請并且使用的內存。當然這塊數據可能比你申請的內存要大一些,因為32位下面最小的分配粒度是8字節。這也是為什么有時候程序有時候溢出了幾個字符,好像也沒有導致程序異常或者崩潰的原因。
  • 后置的元數據: 這個一般用于調試所用。一般發布的時候不會占用這塊空間。

那么哪些塊是可以直接使用的呢?這就涉及到這些塊元數據中的狀態,可以表明這個塊是否被占用,如果是空閑狀態則可以使用。

后端分配器,不會傻傻的去遍歷所有的塊的狀態來決定是否可以分配吧?這個時候就用到了后端分配器的策略。

這個表有點類似于LAL, 只是注意看下這個index為0的多了一個list,從小到大排列,可變大小的從大于1016字節的小于524272字節的將在這個鏈表里面存儲。超過524272字節將直接通過VirtualAlloc之類的API直接獲取內存。

假設此時前端堆管理器需要尋找一個32字節的堆塊, 后端管理器將如何操作?

這個時候請求到了后端分配器,后端分配器假設也沒有在這個表中查找到32字節的空閑塊,那么將先查找64字節的空閑塊,如果找到,則將其從列表中移除,然后將其分割為兩個16字節的塊, 一個設置為占用狀態返回給應用程序,一個設置為空閑狀態插入響應的鏈表中。

那如果還沒有找到呢?那么這個時候堆管理器會從Segment中提交(Commit)更多的內存去使用,創建新的塊, 如果當前Segment空間也不夠了,那就創建新的Segement

有細心的同學可能說,那前端分配器和后端分配器差不多嗎,這里面有個很重要的就是,前端分配器鏈表中的塊是屬于占用狀態的, 而后端分配器鏈表中的塊是屬于空閑狀態的。

假設釋放內存,該如何操作?

首先要看前端分配器是否處理這個釋放的塊,比如加入到相應的鏈表中去,如果不處理,那么后端分配器將會查看相鄰的塊是否也是空閑的,如果是空閑狀態,將會采用塊合并成一個大的塊,并對相應的后端分配器鏈表進行操作。

當然了當你釋放的內存足夠多的時候,其實堆管理器也不會長期霸占著物理存儲器的空間,也會在適當的情況下調用Decommit操作來減少物理存儲器的使用。

Windbg查看進程中的堆

進程堆信息查看

進程堆的信息是放在PEB(進程環境塊)中,可以通過查看PEB相關的信息, 可以看到當前進程包含有3個堆,并且堆的數組地址為0x77756660

  1. 0:000> dt _PEB @$peb 
  2.    ...... 
  3.    +0x088 NumberOfHeaps    : 3 
  4.    ...... 
  5.    +0x090 ProcessHeaps     : 0x77756660  -> 0x00fa0000 Void 
  6.  
  7.    ...... 

然后我們查看對應的三個堆的地址,分別為0xfa0000, 0x14b0000和0x2e10000, 而第一個一般為進程的默認堆00fa0000。

  1. 0:006> dd 0x77756660 
  2. 77756660  00fa0000 014b0000 02e10000 00000000 
  3. 77756670  00000000 00000000 00000000 00000000 
  4. 77756680  00000000 00000000 00000000 00000000 
  5. 77756690  00000000 00000000 00000000 00000000 
  6. 777566a0  00000000 00000000 00000000 00000000 
  7. 777566b0  00000000 00000000 00000000 00000000 
  8. 777566c0  ffffffff ffffffff 00000000 00000000 
  9. 777566d0  00000000 020007d0 00000000 00000000 

其實上述步驟Windbg提供了一個方法可以直接查看概要信息了, 可以看到系統默認堆00fa0000為LFH堆,并且已經Reserve了空間為1128K, Commit的內存為552K。

  1. 0:000> !heap -s 
  2. ...... 
  3. LFH Key                   : 0x8302caa1 
  4. Termination on corruption : ENABLED 
  5.   Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast  
  6.                     (k)     (k)    (k)     (k) length      blocks cont. heap  
  7. ----------------------------------------------------------------------------- 
  8. 00fa0000 00000002    1128    552   1020    178    21     1    1      0   LFH 
  9. 014b0000 00001002      60     12     60      1     2     1    0      0       
  10. 02e10000 00001002    1188     92   1080      4     4     2    0      0   LFH 
  11. ----------------------------------------------------------------------------- 

可以通過dt _HEAP 00fa0000命令去查看進程默認堆的信息,也可以通過Windbg直接提供的命令去查看, 可以看到其分配空間的最小粒度(Granularity)為8字節。并且只有一個Segment.

  1. 0:006> !heap -a 00fa0000 
  2. Index   Address  Name      Debugging options enabled 
  3.   1:   00fa0000  
  4.     Segment at 00fa0000 to 0109f000 (00089000 bytes committed
  5.     Flags:                00000002 
  6.     ForceFlags:           00000000 
  7.     Granularity:          8 bytes 
  8.     Segment Reserve:      00100000 
  9.     Segment Commit:       00002000 
  10.     DeCommit Block Thres: 00000800 
  11.     DeCommit Total Thres: 00002000 
  12.     Total Free Size:      0000597f 
  13.     Max. Allocation Size: 7ffdefff 
  14.     Lock Variable at:     00fa0248 
  15.     Next TagIndex:        0000 
  16.     Maximum TagIndex:     0000 
  17.     Tag Entries:          00000000 
  18.     PsuedoTag Entries:    00000000 
  19.     Virtual Alloc List:   00fa009c 
  20.         03321000: 00100000 [commited 101000, unused 1000] - busy (b), tail fill 
  21.     Uncommitted ranges:   00fa008c 
  22.             01029000: 00076000  (483328 bytes) 
  23.     FreeList[ 00 ] at 00fa00c0: 00ffcf40 . 00ff3290   
  24.         00ff3288: 00208 . 00010 [100] - free 
  25.         00fb1370: 00060 . 00010 [100] - free 
  26.         00fb10a0: 00020 . 00010 [100] - free 
  27.         00fa6c40: 00088 . 00010 [100] - free 
  28.         00fa8e98: 00010 . 00010 [100] - free 
  29.         00fafa78: 000d0 . 00018 [100] - free 
  30.         00faea20: 00138 . 00018 [100] - free 
  31.         00fafc38: 00030 . 00020 [100] - free 
  32.         00ff4570: 00128 . 00028 [100] - free 
  33.         00faeeb8: 00058 . 00028 [100] - free 
  34.         00faf0c8: 00060 . 00028 [100] - free 
  35.         00fad980: 00050 . 00028 [100] - free 
  36.         00fb83f0: 00050 . 00040 [100] - free 
  37.         00faed78: 00030 . 00080 [100] - free 
  38.         00feebd8: 000e8 . 00080 [100] - free 
  39.         00faeb80: 00050 . 000d0 [100] - free 
  40.         00ff0398: 00148 . 000d8 [100] - free 
  41.         00fafed0: 000b0 . 000f0 [100] - free 
  42.         00fb8130: 00210 . 00270 [100] - free 
  43.         00fef460: 00808 . 003c8 [100] - free 
  44.         00ffcf38: 003c8 . 2c0a8 [100] - free 
  45.  
  46.     Segment00 at 00fa0000: 
  47.         Flags:           00000000 
  48.         Base:            00fa0000 
  49.         First Entry:     00fa0498 
  50.         Last Entry:      0109f000 
  51.         Total Pages:     000000ff 
  52.         Total UnCommit:  00000076 
  53.         Largest UnCommit:00000000 
  54.         UnCommitted Ranges: (1) 
  55.  
  56.     Heap entries for Segment00 in Heap 00fa0000 
  57.          address: psize . size  flags   state (requested size
  58.         00fa0000: 00000 . 00498 [101] - busy (497) 
  59.         00fa0498: 00498 . 00108 [101] - busy (100) 
  60.         00fa05a0: 00108 . 000d8 [101] - busy (d0) 
  61.  
  62.         ...... 
  63.         01029000:      00076000      - uncommitted bytes. 

查看Segment

一般來說我們通過上述的命令已經可以基本查看到Segment在一個堆中的信息了。如果要針對一個Segment進行查看可以用如下方式:

  1. 0:006> dt _HEAP_SEGMENT 00fa0000 
  2. ntdll!_HEAP_SEGMENT 
  3.    +0x000 Entry            : _HEAP_ENTRY 
  4.    +0x008 SegmentSignature : 0xffeeffee 
  5.    +0x00c SegmentFlags     : 2 
  6.    +0x010 SegmentListEntry : _LIST_ENTRY [ 0xfa00a4 - 0xfa00a4 ] 
  7.    +0x018 Heap             : 0x00fa0000 _HEAP 
  8.    +0x01c BaseAddress      : 0x00fa0000 Void 
  9.    +0x020 NumberOfPages    : 0xff 
  10.    +0x024 FirstEntry       : 0x00fa0498 _HEAP_ENTRY 
  11.    +0x028 LastValidEntry   : 0x0109f000 _HEAP_ENTRY 
  12.    +0x02c NumberOfUnCommittedPages : 0x76 
  13.    +0x030 NumberOfUnCommittedRanges : 1 
  14.    +0x034 SegmentAllocatorBackTraceIndex : 0 
  15.    +0x036 Reserved         : 0 
  16.    +0x038 UCRSegmentList   : _LIST_ENTRY [ 0x1028ff0 - 0x1028ff0 ] 

查看申請的內存地址

其實在調試過程中一般最關注的是變量的地址關聯的內容信息。比如說我寫了個程序其申請的內存變量地址為0x00fb5440, 申請的大小為5字節。

首先可以通過如下命令查找到地址所在的位置為堆:

  1. 0:000> !address 0x00fb5440 
  2.  
  3. Building memory map: 00000000 
  4. Mapping file section regions... 
  5. Mapping module regions... 
  6. Mapping PEB regions... 
  7. Mapping TEB and stack regions... 
  8. Mapping heap regions... 
  9. Mapping page heap regions... 
  10. Mapping other regions... 
  11. Mapping stack trace database regions... 
  12. Mapping activation context regions... 
  13.  
  14. Usage:                  Heap 
  15. Base Address:           00fa0000 
  16. End Address:            01029000 
  17. Region Size:            00089000 ( 548.000 kB) 
  18. State:                  00001000          MEM_COMMIT 
  19. Protect:                00000004          PAGE_READWRITE 
  20. Type:                   00020000          MEM_PRIVATE 
  21. Allocation Base:        00fa0000 
  22. Allocation Protect:     00000004          PAGE_READWRITE 
  23. More info:              heap owning the address: !heap 0xfa0000 
  24. More info:              heap segment 
  25. More info:              heap entry containing the address: !heap -x 0xfb5440 

然后可以通過如下命令查看當前申請內存的詳細堆塊信息, 其處于被占用狀態(busy)??梢钥吹狡涠褖K的大小為0x10, 我們實際申請的內存為5字節,那么0x10(Size) - 0xb (Unused) = 5, 可以看出來Unused是包含了_HEAP_ENTRY塊元數據的大小的。而我們實際用戶可用的內存是8字節 (最小分配粒度),比我們申請的5字節多了三個字節,這也是為什么程序有時候溢出了幾個字符,并沒有導致程序崩潰或者異常的原因。

  1. 0:000> !heap -x 0xfb5440 
  2. Entry     User      Heap      Segment       Size  PrevSize  Unused    Flags 
  3. ----------------------------------------------------------------------------- 
  4. 00fb5438  00fb5440  00fa0000  00fad348        10      -            b  LFH;busy 

那么我們也可以直接查看Entry的結構:

  1. 0:000> dt _HEAP_ENTRY 00fb5438 
  2. ntdll!_HEAP_ENTRY 
  3.    +0x000 UnpackedEntry    : _HEAP_UNPACKED_ENTRY 
  4.    +0x000 Size             : 0xa026 
  5.    +0x002 Flags            : 0xdc '' 
  6.    +0x003 SmallTagIndex    : 0x83 '' 
  7.    +0x000 SubSegmentCode   : 0x83dca026 
  8.    +0x004 PreviousSize     : 0x1b00 
  9.    +0x006 SegmentOffset    : 0 '' 
  10.    +0x006 LFHFlags         : 0 '' 
  11.    +0x007 UnusedBytes      : 0x8b '' 
  12.    +0x000 ExtendedEntry    : _HEAP_EXTENDED_ENTRY 
  13.    +0x000 FunctionIndex    : 0xa026 
  14.    +0x002 ContextValue     : 0x83dc 
  15.    +0x000 InterceptorValue : 0x83dca026 
  16.    +0x004 UnusedBytesLength : 0x1b00 
  17.    +0x006 EntryOffset      : 0 '' 
  18.    +0x007 ExtendedBlockSignature : 0x8b '' 
  19.    +0x000 Code1            : 0x83dca026 
  20.    +0x004 Code2            : 0x1b00 
  21.    +0x006 Code3            : 0 '' 
  22.    +0x007 Code4            : 0x8b '' 
  23.    +0x004 Code234          : 0x8b001b00 
  24.    +0x000 AgregateCode     : 0x8b001b00`83dca026 

如果細心的同學可以能會發現以下兩個問題:

  1. 結構中Size的值是0xa026和之前命令中看到的大小0x10不一樣,這個是因為Windows對這些元數據做了編碼,需要用堆中的一個編碼數據做異或操作才能得到真實的值。具體方法筆者試過,在這里不在贅述,可以在參考文章中獲取方法。
  2. Size是2字節描述,那么最大可以描述的大小應該為0xffff,但是之前不是說最大的塊可以是0x7FFF0 (524272字節), 應該不夠存儲啊?這個也和第一個問題有關聯,在通過上述方法計算出的Size之后還需要乘以8, 才是真正的數據大小。

Windows 自建堆的使用建議

在<

保護組件

先看看書中原話:

假如你的應用程序需要保護兩個組件,一個是節點結構的鏈接表,一個是 B R A N C H結構的二進制樹。你有兩個源代碼文件,一個是 L n k L s t . c p p,它包含負責處理N O D E鏈接表的各個函數,另一個文件是 B i n Tr e e . c p p,它包含負責處理分支的二進制樹的各個函數。

現在假設鏈接表代碼中有一個錯誤,它使節點 1后面的8個字節不

小心被改寫了,從而導致分支 3中的數據被破壞。當B i n Tr e e . c p p文件中的代碼后來試圖遍歷二進制樹時,它將無法進行這項操作,因為它的內存已經被破壞。當然,這使你認為二進制樹代碼中存在一個錯誤,而實際上錯誤是在鏈接表代碼中。由于不同類型的對象混合放在單個堆棧中,因此跟蹤和確定錯誤將變得非常困難。

我個人認為在一個應用的工程中,也許不需要做到上述那么精細的劃分。但是你想一想,在一個大型工程中,會混合多個模塊。比如你是做產品的,那么產品會集成其他部門甚至是外部第三方的組件,那么這些組件同時在同一個進程,使用同一個堆的時候,那么難免會出現,A模塊的內存溢出問題,導致了B模塊的數據處理異常,從而讓你追蹤問題異常復雜,更坑的是,很可能讓B模塊的團隊背鍋了。而這些是切實存在的。 這里的建議更適合于讓一些關鍵模塊使用自己的堆,從而降低自己內存使用不當,覆蓋了其他組件使用的內存,從而導致異常,讓問題的追蹤可以集中在出錯的模塊中。當然這也不是絕對的,因為進程的組件都在同一個地址空間內,內存破壞也存在一種跳躍式內存訪問破壞,但是大多數時候內存溢出是連續的上溢較多,這樣做確實可以提高這種問題追蹤的效率。

更有效的內存管理

這個主要強調是,將同種類型大小的對象放在一個堆中,盡量避免不同大小內存對象摻雜在一起導致的內存碎片問題,從而帶來的堆管理效率下降。同一種對象,則可以避免內存碎片問題。當然了這些只是提供了一種思想,至于你的工程是否有必要采用這樣的做法,由工程師自己來做決定。

進行本地訪問

先來看看原文的描述:

每當系統必須在 R A M與系統的頁文件之間進行 R A M頁面的交換時,系統的運行性能就會受到很大的影響。如果經常訪問局限于一個小范圍地址的內存,那么系統就不太可能需要在 R A M與磁盤之間進行頁面的交換。

所以,在設計應用程序的時候,如果有些數據將被同時訪問,那么最好把它們分配在互相靠近的位置上。讓我們回到鏈接表和二進制樹的例子上來,遍歷鏈接表與遍歷二進制樹之間并無什么關系。如果將所有的節點放在一起(放在一個堆棧中),就可以使這些節點位于相鄰的頁面上。實際上,若干個節點很可能恰好放入單個物理內存頁面上。遍歷鏈接表將不需要 C P U為了訪問每個節點而引用若干不同的內存頁面。

這個思想其實就是一種Cache思想,RAM與磁盤上的page.sys存儲器(磁盤上的虛擬內存)進行頁交換會帶來一些時間成本。舉個極限的例子,你的RAM只有一個頁,你有兩個對象A和B,A存放在Page1上,而B存放在Page2上,當你訪問A對象的時候,必然要把Page1的內容加載到RAM中,那么這個時候B對象所在Page2肯定就在page.sys中,當你又訪問B對象的時候,這個時候就得把Page2從page.sys中加載到RAM中替換掉Page1.

理解了頁切換帶來的性能開銷后,其實這一段的思想就是將最可能連續訪問的對象放在一個堆中,那么他們在一個頁面的可能性也更大,提高了效率。

減少線程同步的開銷

這一個很好理解,一般情況下創建的自建堆是支持多線程的,那么多線程的內存分配必然會帶來同步的時間消耗,但是對于有些工程來說,只有一個線程,那么對于這一個線程的程序,在調用HeapCreate的時候設置HEAP_NO_SERIALIZE, 則這個堆只支持單線程,從而提高內存申請的效率。

迅速釋放堆棧

這種思想第一提高了內存釋放的效率,第二是盡可能的降低了內存泄露。記得之前看過一篇文章介紹過Arena感覺比較類似,在一個生命周期內的內存是從Arena申請,然后這個聲明周期結束后,不是直接釋放各個對象,而是直接銷毀這個Arena,提高了釋放效率,并且降低了內存泄露的可能。那么使用自建堆的原理和Arena是類似的,比如在一個任務處理之前創建一個堆,在任務處理過程中所申請的內存在這個堆上申請,然后釋放的時候,直接銷毀這個堆即可。

那對于對象的申請,C++中可以重載new和delete等操作符,來實現自定義的內存分配,并且可以將這個先封裝成一個基類,在這個過程中需要創建的對象均繼承于這個基類,復用new和delete。

總結和參考

我本以為這些是已經掌握的知識,但是寫文章的時間也超過了我預想的時間,在實踐中也也發現了一些自己曾經錯誤的理解。如果文中還有不當的地方,也希望讀者給與指正。

參考

《Windows核心編程》

《Windows高級調試》

Windows Heap Chunk Header Parsing and Size Calculation: https://stackoverflow.com/questions/28483473/windows-heap-chunk-header-parsing-and-size-calculation

Understanding the Low Fragmentation Heap: http://www.illmatics.com/Understanding_the_LFH.pdf

 

WINDOWS 10SEGMENT HEAP INTERNALS: https://www.blackhat.com/docs/us-16/materials/us-16-Yason-Windows-10-Segment-Heap-Internals-wp.pdf

 

責任編輯:武曉燕 來源: 一個程序員的修煉之路
相關推薦

2022-07-04 10:51:27

數據中臺數據倉庫

2021-02-19 09:19:11

消息隊列場景

2018-08-21 14:42:29

閃存存在問題

2022-02-14 22:22:30

單元測試Junit5

2014-07-17 10:11:53

Android LAPI谷歌

2018-01-11 09:51:34

2015-03-27 15:07:55

云計算IaaS平臺Docker

2017-11-21 14:32:05

容器持久存儲

2016-07-08 13:33:12

云計算

2021-05-11 08:48:23

React Hooks前端

2021-11-23 09:45:26

架構系統技術

2016-10-09 23:47:04

2021-03-15 22:42:25

NameNodeDataNode分布式

2011-07-28 09:22:56

Oracle WDPOracle數據庫

2019-01-30 10:59:48

IPv6Happy EyebaIPv4

2020-04-08 10:18:56

MySQL數據庫SQL

2018-08-28 06:42:06

邊緣計算SDNMEC

2019-11-12 08:40:03

RocketMQ架構

2020-06-19 15:32:56

HashMap面試代碼

2020-11-20 10:22:34

代碼規范設計
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 久草在线在线精品观看 | 狠狠躁天天躁夜夜躁婷婷老牛影视 | 一级少妇女片 | 91视频在线| 久久av网站| 天天夜夜人人 | 91免费视频观看 | 日韩在线观看 | 国产伊人久久久 | 国产在线不卡 | 欧美精品99| 黄色在线免费网站 | 日韩精品一区二区三区视频播放 | 久久国产一区 | 国产91在线观看 | aⅴ色国产 欧美 | 亚洲一区二区在线播放 | 久久91精品国产 | 国产精品一区二区欧美黑人喷潮水 | 久草久 | 欧美一级二级在线观看 | 久久精品欧美视频 | 国产精品久久 | 欧美久久久久久 | 91精品国产一区二区在线观看 | hsck成人网 | 一区二区三区欧美 | 亚洲高清在线免费观看 | 情侣黄网站免费看 | 免费看一区二区三区 | 男女网站免费观看 | 国产欧美一区二区三区在线看 | 久久曰视频 | 国产色爽| 97伦理影院 | 亚洲国产精品久久久久婷婷老年 | 日韩在线观看中文字幕 | 国产免费一区二区三区网站免费 | 欧美不卡在线 | 国产福利免费视频 | 中文字幕成人 |