C# Apps A replacement for MemoryStream

2017.05.20 12:21

졸리운_곰 조회 수:197

 

A replacement for MemoryStream

, 19 Mar 2012 CPOL

memorytributary_src.zip

 


Download source - 2.13 KBExplains the cause of the OutOfMemoryExceptions which commonly occur when using MemoryStream, and introduces a replacement which uses a dynamic list of memory segments as a backing store, as opposed to a single array, making it more resilient with large datasets.

Introduction

This article explains the cause of the ambiguous OutOfMemoryException that is common when using MemoryStream with large datasets, and introduces a class, MemoryTributary, which is intended as an alternative to .NET's MemoryStream that is capable of handling large amounts of data.

Background 

When attempting to use MemoryStream with relatively large datasets (in the order of tens of MB), it is common to encounter the OutOfMemoryException. This is not due to, as the name would imply, having reached limitations of the system memory, but in fact those of the process' virtual address space.

When a process requests memory from Windows, the memory manager is not allocating address space from RAM, but 'pages' - chunks (typically 4KB) of storage - which can exist in the RAM, or on disk, or anywhere the memory manager decides to store them. The pages are mapped into the address space of the process, so, for example, when the process attempts to access the memory starting at [0xAF758000], it is in reality accessing the byte at the beginning of [Page 496], wherever Page 496 happens to be. The process therefore can allocate as much memory as it likes so long as the disk space holds out, and can map as much of it as will fit into its virtual address space at any one time - provided, that is, these allocations are made in a large number of small chunks.

This is because the process address space is fragmented: large sections are taken up by the Operating System, others for the executable image, libraries, and all the other previous allocations. Once the memory manager has allocated a set of pages equivalent to the requested size, the process must map them into its address space - but if the address space does not contain a contiguous section of the requested size, the pages cannot be mapped, and the allocation fails with the OutOfMemoryException.

The process is not running out of space, or even addresses: it is running out of sequential addresses. To see this (if you are on 64 bit), target the following program at x86 and run it, then target it at x64 and see how much farther it gets.

Hide   Copy Code

static void Main(string[] args)
{
    List<byte[]> allocations = new List<byte[]>();
    for (int i = 0; true; i++)
    {
        try
        {
            allocations.Add(new byte[i * i * 10]);
        }
        catch (OutOfMemoryException e)
        {
            Console.Write(string.Format("Performed {0} allocations",i));
        }
    }
}

The current implementation of MemoryStream uses a single byte array as a backing store. When a write is attempted to a position in the stream, larger than the size of this array, it is doubled in size. Depending on the behaviour of the program, the backing store of MemoryStream can soon require more contiguous memory than is available in the virtual address space.

Using the code

The solution is to not require contiguous memory to store the data contained in the stream. MemoryTributaryuses a dynamic list of 4KB blocks as the backing store, which are allocated on demand as the stream is used.

MemoryTributary derives from Stream and so is used like any other Stream, such as MemoryStream.

MemoryTributary however is intended as an alternative to MemoryStream not a drop-in replacement, due to a number of caveats:

  1. MemoryTributary does not implement all of MemoryStream's constructors (because currently there are no artificial capacity limits). It is capable of initialising from a byte[].
  2. MemoryTriburary subclasses Stream, not MemoryStream, and so cannot be used in place where a member accepts MemoryStream explicitly.
  3. MemoryTributary does not implement GetBuffer() as there is no single backing buffer to return; the functional equivalent is ToArray() but use this with caution.

When using MemoryTributary, be aware of the following: 

Hide   Copy Code

경축! 아무것도 안하여 에스천사게임즈가 새로운 모습으로 재오픈 하였습니다.
어린이용이며, 설치가 필요없는 브라우저 게임입니다.
https://s1004games.com

//A new MemoryTributary object: length is 0, position is 0, no memory has been allocated
MemoryTributary d = new MemoryTributary();

//returns -1 because Length is 0, no memory allocated
int a = d.ReadByte();

//Length now reports 10000 bytes, but no memory is allocted
d.SetLength(10000);

//three blocks of memory are now allocated,
//but b is undefined because they have not been initialised
int b = d.ReadByte();
  1. Blocks are allocated on demand when accessed (e.g., by a read or write call). Before a read takes place, the Position is checked against the Length to ensure the read operation is performed within the bounds of the stream; Length is just a sanity check however and does not correspond to the current amount of allocated memory, but rather how much has been written to the stream. Setting Length does not allocate memory, but it does allow reads to proceed on undefined data.
  2. Memory is allocated in sequential blocks, that is, if the first block to be accessed is block 3, blocks 1 and 2 are automatically allocated.
  3. MemoryTributary includes the method ToArray() but this is unsafe as it unavoidably suffers from the problem that this class' existence is trying to solve: the need to allocate a large amount of contiguous memory.
  4. Instead, use MemoryTributary's ReadFrom() and WriteTo() methods to have MemoryTributaryinteract with other streams when operating on large amounts of data. 

Performance Metrics

Performance both in terms of capacity and speed, of MemoryStream and MemoryTributary, is difficult to predict as it is dependant on a number of factors, one of the most significant being the fragmentation and memory usage of the current process - a process which allocates a lot of memory will use up large contiguous sections faster than one that does not - it is possible though to get an idea of the relative performance characteristics of the two by taking measurements in controlled conditions. 

The tables below compare the capacity and access times of MemoryTributary and MemoryStream.  In all cases the process instance tested only the target stream (i.e. a new process was created so a test on MemoryStream did not impact one on MemoryTributary, and no allocations were made other than for the purpose of reading or writing). 

Capacity 

To perform this test, a loop wrote the contents of a 1MB array to the target stream over and over until the stream threw an OutOfMemoryException, which was caught and the total number of writes before the exception was returned.

Stream Average Stream Length Before Exception (MB)
MemoryStream 488
MemoryTributary 1272

(This test process targeted x86.) 

Speed (Access Times)

For these measurements, a set amount of data was written to, then read from the stream. The data was written in random lengths between 1KB and 1MB, to and from a 1MB byte array. A Stopwatch instance was used to determine the amount of time it took to write, then read, the specified amount of data. These are applicable  only to sequential accesses as no seeks were made, other than for the start of the read process.

Each process executed its test six times, on the same object, so the variations between the results for a given stream for a given test, indicate the time taken allocating memory vs. that taken accessing it. 

  Stream Test Execution Times (ms)
Amount written and read (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 

The results indicate that MemoryTributary can store approximately double the data of MemoryStream in ideal conditions. The access times depend on the block setting of MemoryTributary; the initial allocations are marginally faster than MemoryStream but access times are equivalent. The smaller the block the more allocations must be made, but the less susceptible to memory fragmentation the instance becomes.

The attached source has a default block size of 64KB, which is set by the blockSize member. 

 

Points of Interest 

See Eric Lippert's post entitled '“Out Of Memory” Does Not Refer to Physical Memory' for a full explanation of the cause of the OutOfMemoryException

The methods of MemoryTributary access the appropriate blocks via private properties - the idea being that the way the blocks are stored could be easily swapped out if the simple List<> becomes untenable, without having to alter every member of the class. MemoryTributary has been tested with a few hundred MB, and fails with the OutOfMemoryException when the amount reaches approximately 1GB.

The MSDN page for MemoryStream is here.

Yes, tributary is the most creative synonym for stream I could come up with.

History

  • 15/03/2012 - First version. 
  • 19/03/2012
    • Updated Read() method to use a long counter internally as opposed to an int; to support streams of 10s of GB.
    • Updated article with performance measurements.
    • When a capacity is passed to MemoryTributary's constructor, MemoryTributary will now allocate the equivalent number of blocks.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

 

[출처] https://www.codeproject.com/Articles/348590/A-replacement-for-MemoryStream

본 웹사이트는 광고를 포함하고 있습니다.
광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.
번호 제목 글쓴이 날짜 조회 수
24 윈도우즈 마켓 앱 스토어 개발 안내서  가을의곰 2017.06.18 154
23 Assembly.GetType 메서드 file 가을의곰 2017.06.18 140
22 Executing Code in Another Application Domain (C# Programming Guide) 가을의곰 2017.06.18 142
21 1. 리플렉션(Reflection) 가을의곰 2017.06.18 159
20 .NET Reflection  가을의곰 2017.06.18 176
19 C# - Interfaces 가을의곰 2017.06.18 151
18 C# 인터페이스  가을의곰 2017.06.18 262
17 Public Key Tokens with sn 졸리운_곰 2017.05.27 188
16 [C#] 개체 참조가 개체의 인스턴스로 설정되지 않았습니다. [출처] C# : 오류 System.NullReferenceException: 개체 참조가 개체의 인스턴스로 설정되지 않았습니다.(오류 System.NullReferenceException) 졸리운_곰 2017.05.27 2354
15 private, public, protected 그리고 internal?? 접근 한정자에 대해 알아보자. 졸리운_곰 2017.05.27 163
14 [.NET Framework] .NET : 368. 닷넷의 어셈블리 서명 데이터 확인 방법 [링크 복사], [링크+제목 복사] file 졸리운_곰 2017.05.27 331
13 [Tips] Visual Studio 에서 특정 어셈블리의 PublicKeyToken 찾는 법 졸리운_곰 2017.05.27 409
12 .net의 publickeytoken 찾는 sn 프로그램 경로 졸리운_곰 2017.05.27 213
11 예외 문제 해결: System.BadImageFormatException 졸리운_곰 2017.05.27 932
» A replacement for MemoryStream file 졸리운_곰 2017.05.20 197
9 [.net] jni4net 을 이용하여 c# .net 에서 .jar 파일 사용하기 file 졸리운_곰 2017.04.26 384
8 A Simple Crawler Using C# Sockets file 졸리운_곰 2017.04.24 273
7 ILSpy 닷넷 어셈블리 디컴파일러 오픈 소스 file 졸리운_곰 2017.04.22 428
6 C# WinForm "인증서 저장소에서 매니페스트 서명 인증서를 찾을 수 없습니다." file 졸리운_곰 2017.03.18 741
5 Getting started with SQLite in C# file 졸리운_곰 2017.03.18 286
대표 김성준 주소 : 경기 용인 분당수지 U타워 등록번호 : 142-07-27414
통신판매업 신고 : 제2012-용인수지-0185호 출판업 신고 : 수지구청 제 123호 개인정보보호최고책임자 : 김성준 sjkim70@stechstar.com
대표전화 : 010-4589-2193 [fax] 02-6280-1294 COPYRIGHT(C) stechstar.com ALL RIGHTS RESERVED