簡介
這篇文章闡述了當使用MemoryStream處理大型數據集時經常觸發的模棱兩可的OutofMemoryException異常,並且介紹了一個類——MemoryTributary,他可以用來替代.NET內置的MemoryStream,並且能夠支持大型數據的處理。
背景
當試圖使用MemoryStream處理較大數據(in the order of tens of MB)時,它通常會引發OutofMemoryException異常。這是不是因為,正如其命名的那樣,超出了系統內存的限制了呢?但實際上那都是進程的虛擬地址空間。
當進程從Windows申請內存的時候,內存管理器並沒有從RAM中分配地址空間,但是“頁面”——存儲塊(通常是4KB)可以存在於RAM、磁盤或者任何內存管理器能夠決定存儲它們的地方。頁面映射到進程的地址空間,因此,假如當進程試圖從[0xAF758000]開始訪問內存時,它實際上是在訪問[第496頁]開始的地方,無論是否恰好是[第496頁]。可以為進程分配任何大小的足夠的內存,只要磁盤空間充足,並可以將其中的一大部分作為虛擬地址空間,這適用於任何時候。在這個分配過程中會產生大量的小碎片(這里是指存儲塊)。
這是因為進程地址空間是支離破碎的:大部分都被操作系統占用,諸如應用程序映像、庫以及其他預分配的空間等。一旦內存管理器分配了所請求的相當於頁面大小的內存,就必須在進程內映射到(虛擬)地址空間。而當剩余的地址空間中沒有足夠的連續空間時,頁面就無法被映射,從而導致內存分配失敗,也就引發了OutofMemoryException異常。
這個過程並沒有耗盡空間和地址,它運行在連續的地址上。要看到這個(如果你在用64位系統),在x86目標平台下編譯如下程序並運行它,然后再編譯為x64目標平台,看看兩者的區別。
1 static void Main(string[] args)
2 {
3 List<byte[]> allocations = new List<byte[]>();
4
5 for (int i = 0; true; i++)
6 {
7 try
8 {
9 allocations.Add(new byte[i * i * 10]);
10 }
11 catch (OutOfMemoryException e)
12 {
13 Console.Write(string.Format("Performed {0} allocations",i));
14 }
15 }
16 }
.NET當前實現的MemoryStream使用了一個字節數組作為后備存儲。當要寫入的內容超出數組長度的時候,容量就會自動擴展一倍(老陳注:這是.NET數組自身的內存分配機制)。根據程序這種行為,基於這種后備存儲機制的MemoryStream會很快就需要比可用虛擬地址空間更多的內存。
代碼用例
這個解決方案並不是申請更多更大的連續內存去存儲流中包含的數據。MemoryTributary內部使用了4KB的動態集合來作為后備存儲,實現按需分配。
MemoryTributary從Stream派生,因此可以像使用其他Stream一樣的使用它,類似於MemoryStream。
MemoryTributary不過是想作為MemoryStream的一個替代方案,但不是可以在任何情況下都可以替代MemoryStream的,以下是一些注意事項:
- MemoryTributary沒有完全實現MemoryStream所有的構造函數(因為MemoryTributary的容量是沒有人為限制的)。MemoryTributary的初始容量從一個byte[]開始。
- MemoryTributary是Stream的子類,而不是MemoryStream,所以不能用在僅接受MemoryStream成員的地方。
MemoryTriburary
沒有后備存儲實現GetBuffer(),類似功能的方法是ToArray(),但是要慎用!
使用MemoryTributary要注意以下幾點:
- 塊是在訪問時按需分配的(例如在讀取或寫入時)。在讀取之前,重新檢查Position,以確保讀取操作是在流范圍內進行的。
1 // 創建 MemoryTributary 的新實例:此時Length為0,Position為0,沒有內存被分配
2 MemoryTributary d = new MemoryTributary();
3
4 // 因為Length為0所以返回-1,沒有內存被分配
5 int a = d.ReadByte();
6
7 // Length現在設定為10000字節,但是沒有內存被分配!
8 d.SetLength(10000);
9
10 // 現在有3個內存塊被分配了,
11 // 但是b是未定義的,因為它們還沒有完成初始化
12 int b = d.ReadByte(); - 內存是分配在連續的塊上的,也就是說,如果第一個塊是塊3的話,那么塊1和塊2也會被自動分配掉;
- MemoryTributary包含了一個ToArray()方法,但這是不安全的;
- 作為替代,MemoryTributary使用ReadFrom()和WriteTo()這兩個方法對大數據進行操作。
性能指標
在容量和速度上,MemoryStream和MemoryTributary都是很難預料的,因為這依賴於很多因素。一個很重要的因素是當前進程的內存碎片——一個進程分配了大量內存將會導致大量內存碎片的發生。
容量
流 | 平均崩潰臨界值(MB) |
---|---|
MemoryStream | 488 |
MemoryTributary | 1272 |
速度(訪問時間)
流測試執行時間比對 (ms) | ||||
讀寫容量 |
MemoryStream | MemoryTributary (4KB Block) |
MemoryTributary (64KB Block) |
MemoryTributary (1MB Block) |
10 | 10 | 13 | 11 | 7 |
3 | 5 | 3 | 3 | |
3 | 6 | 3 | 3 | |
3 | 5 | 3 | 3 | |
4 | 5 | 3 | 3 | |
3 | 6 | 3 | 3 | |
100 | 100 | 148 | 123 | 52 |
34 | 54 | 42 | 35 | |
34 | 48 | 35 | 34 | |
35 | 47 | 36 | 35 | |
34 | 48 | 36 | 35 | |
35 | 51 | 35 | 35 | |
500 | 516 | 390 | 290 | 237 |
167 | 222 | 184 | 170 | |
168 | 186 | 154 | 167 | |
167 | 187 | 151 | 168 | |
167 | 186 | 151 | 168 | |
167 | 185 | 153 | 168 | |
1,000 | 1185 | 1585 | 1299 | 485 |
347 | 547 | 431 | 344 | |
343 | 463 | 350 | 345 | |
338 | 462 | 350 | 345 | |
3377 | 461 | 349 | 345 | |
339 | 465 | 351 | 343 |
代碼下載
請到原文地址下載:http://www.codeproject.com/Articles/348590/A-replacement-for-MemoryStream