닷넷 - 배열 크기의 한계
아래와 같은 질문이 있군요.
8GB 이상의 byte 배열 생성
; https://social.msdn.microsoft.com/Forums/ko-KR/fd578b7f-d3c8-4f00-9708-2407a03653f2/8gb-byte-?forum=visualcsharpko
위의 글에 대한 답변으로 예전에 썼던 .NET GC Heap을 2GB 넘게 사용하는 방법을 무심코 써먹었는데요. ^^;
.NET 4.5의 2GB 힙 한계 극복
; https://www.sysnet.pe.kr/2/0/1403
아쉽게도 이것은 다른 문제입니다. 정리하면, GC Heap을 2GB 넘게 사용하는 것은 gcAllowVeryLargeObjects 옵션을 이용해 가능하지만 그렇다고 해서 배열의 요소 수가 2
32을 넘지는 못합니다. 이에 대해서는 gcAllowVeryLargeObjects 문서에 자세하게 나와 있습니다.
<gcAllowVeryLargeObjects> Element
; https://learn.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/runtime/gcallowverylargeobjects-element
- The maximum number of elements in an array is UInt32.MaxValue.
- The maximum index in any single dimension is 2,147,483,591 (0x7FFFFFC7) for byte arrays and arrays of single-byte structures, and 2,146,435,071 (0X7FEFFFFF) for other types.
- The maximum size for strings and other non-array objects is unchanged.
위의 의미를 해석해 보면!
우선, 단일 차원의 크기는 Int32.MaxValue에서 56을 뺀 2,147,483,591(0x7FFFFFC7)이 최대 값입니다.
byte[] t1 = new byte[Int32.MaxValue - 56]; // Int32.MaxValue - 56 == 2,147,483,591
또한, 다중 차원으로 하는 경우 전체 요소의 수는 UInt32.MaxValue까지 가능합니다. 따라서, 다음과 같이 배열 할당은 가능하지만,
byte[,] t = new byte[Int32.MaxValue - 56, 2]; // 2,147,483,591 * 2 <= UInt32.MaxValue (4,294,967,295)
byte[,] t = new byte[65537, 65535]; // 65,537 * 65,535 <= UInt32.MaxValue (4294967295)
다음과 같은 배열 할당은 불가능합니다.
byte[,] t = new byte[Int32.MaxValue, 1];
byte[,] t = new byte[Int32.MaxValue - 56, 3]; // 2,147,483,591 * 3 > UInt32.MaxValue (4294967295)
byte[,] t = new byte[65537, 65536]; // 65,537 * 65,536 > UInt32.MaxValue (4294967295)
마지막으로 단일 문자열의 크기는 2GB를 넘을 수 없습니다. 즉, 문자 하나의 크기가 2바이트이므로 Int32.MaxValue / 2 == 1,073,741,823개의 글자만 담을 수 있는데 실제로 테스트해 보면 32를 뺀 1,073,741,791까지만 가능합니다. 즉, 다음은 허용 가능하지만,
int len = 1073741823 - 32;
string s1 = new string('c', len);
그 이상을 할당하면,
int len = 1073741823 - 31;
string s1 = new string('c', len);
OOM(System.OutOfMemoryException) 예외가 발생합니다.
("other non-array objects"라는 것은 단일 클래스로 내부의 primitive 멤버 만으로 2GB를 넘는 경우일 텐데 이런 상황은 거의 없을 것이므로 넘어갑니다. ^^)
참고로, byte 타입의 경우 Marshal.AllocCoTaskMem / AllocHGlobal을 사용하면 Int32.MaxValue의 모든 범위를 접근 가능한 배열을 얻을 수 있습니다.
int len = Int32.MaxValue;
IntPtr pBuf = Marshal.AllocCoTaskMem(len);
byte* ptr = (byte*)pBuf.ToPointer();
int i = 0;
for (i = 0; i < len; i++)
{
*(ptr + i) = 10;
}
Console.WriteLine(*(ptr + len - 1));
Console.WriteLine();
Marshal.FreeCoTaskMem(pBuf);
AllocCoTaskMem 메서드가 받아들이는 인자의 타입이 int이므로 어쩔 수 없이 2GB 한계는 갖게 되지만 CLR 오버헤드로 인한 56바이트의 제약은 벗어날 수 있습니다.
자... 그럼 재미있는 거 하나를 더 해볼까요? ^^
Marshal.AllocCoTaskMem 메서드는 사실 내부적으로 CoTaskMemAlloc Win32 API를 호출하는 것뿐입니다.
CoTaskMemAlloc function
; https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cotaskmemalloc
그리고, 이 함수의 입력 인자 타입은 SIZE_T인데 64비트에서는 __int64로 정의된 64비트 타입입니다. 오호~~~ 그렇다면 닷넷에서 직접 CoTaskMemAlloc을 불러 배열의 한계를 넘는 것이 가능할 수 있습니다. 실제로 아래와 같이 UInt32.MaxValue의 크기를 갖는 메모리를 할당 후 바이트 배열로 취급하는 것이 가능합니다.
[DllImport("ole32.dll")]
static extern IntPtr CoTaskMemAlloc(long len);
[DllImport("ole32.dll")]
static extern void CoTaskMemFree(IntPtr pBuf);
private unsafe static void Alloc2Test()
{
long len = UInt32.MaxValue;
IntPtr pBuf = CoTaskMemAlloc(len);
byte* ptr = (byte*)pBuf.ToPointer();
long i = 0;
for (i = 0; i < len; i++)
{
*(ptr + i) = 10;
}
Console.WriteLine(*(ptr + len - 1));
Console.WriteLine();
CoTaskMemFree(pBuf);
}
그럼, 이제 답이 나왔군요. "8GB 이상의 byte 배열 생성"은 Win32 API의 힘을 빌린다면 닷넷에서도 가능합니다. ^^
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]