一種MemoryStream的替代方案


簡介

這篇文章闡述了當使用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的,以下是一些注意事項:

  1. MemoryTributary沒有完全實現MemoryStream所有的構造函數(因為MemoryTributary的容量是沒有人為限制的)。MemoryTributary的初始容量從一個byte[]開始。
  2. MemoryTributary是Stream的子類,而不是MemoryStream,所以不能用在僅接受MemoryStream成員的地方。
  3. MemoryTriburary沒有后備存儲實現GetBuffer(),類似功能的方法是ToArray(),但是要慎用!

使用MemoryTributary要注意以下幾點:

  1. 塊是在訪問時按需分配的(例如在讀取或寫入時)。在讀取之前,重新檢查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();
  2. 內存是分配在連續的塊上的,也就是說,如果第一個塊是塊3的話,那么塊1和塊2也會被自動分配掉;
  3. MemoryTributary包含了一個ToArray()方法,但這是不安全的;
  4. 作為替代,MemoryTributary使用ReadFrom()和WriteTo()這兩個方法對大數據進行操作。

性能指標

在容量和速度上,MemoryStream和MemoryTributary都是很難預料的,因為這依賴於很多因素。一個很重要的因素是當前進程的內存碎片——一個進程分配了大量內存將會導致大量內存碎片的發生。

容量

平均崩潰臨界值(MB)
MemoryStream 488
MemoryTributary 1272

速度(訪問時間)

流測試執行時間比對 (ms)

讀寫容量
(MB)

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM