LargeAddressAware 옵션이 적용된 닷넷 32비트 프로세스의 가용 메모리 - 두 번째
지난 글에 쓴 내용을,
LargeAddressAware 옵션이 적용된 닷넷 32비트 프로세스의 가용 메모리
; https://www.sysnet.pe.kr/2/0/1441
조금 업데이트해야 할 필요가 있어 다시 작성합니다. ^^
현재 비주얼 스튜디오의 경우 .NET Framework 프로젝트에 대해서는 빌드 시 기본적으로
LargeAddressAware 플래그를 추가해 줍니다. (그래서 editbin.exe를 이용한 후처리 단계를 생략할 수 있습니다.)
반면, .NET Core/5+ 프로젝트는 그렇지 않습니다. 따라서 프로젝트의 PostBuildEvent 단계에 editbin을 이용한 설정 단계를 추가해야 합니다.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command=""C:\Program Files\Microsoft Visual Studio\2022\Enterprise\SDK\ScopeCppSDK\vc15\VC\bin\editbin.exe" /largeaddressaware "$(TargetDir)$(TargetName).exe"" />
</Target>
</Project>
끝입니다. ^^ 이제 다음과 같이 네이티브 메모리를 확보하는
Marshal.AllocCoTaskMem을 사용해,
// .NET 7 + Release + x86
using System.Runtime.InteropServices;
class Program
{
static void Main(string[] args)
{
int gb4 = 1024 * 1024;
for (int i = 0; i < gb4; i++)
{
Marshal.AllocCoTaskMem(4096); // 의도적으로 4KB씩 메모리 누수
}
}
}
실행하면 메모리가 3GB를 넘어 4GB 즈음에 OOM으로 비정상 종료하는 것을 볼 수 있습니다. 근데... 사실
작업 관리자에서 Commit size 메모리를 제대로 확인하기도 전에 너무 빨리 없어집니다.
그래서 이런 경우 windbg를 실행하고 Launch Executable(단축키: Ctrl + E)로 열어 실행을 하면, OOM 예외가 발생하는 즈음에 windbg가 대상 프로세스를 종료하지 못하게 잡아두기 때문에 그 시점에 작업 관리자의 메모리를 정확하게 확인할 수 있습니다.
PC마다 세세한 수치는 다를 수 있지만 제 경우에는 "3,989,884 KB"가 나왔습니다.
그런데, 네이티브 메모리가 아닌 GC 힙 메모리를 사용하는 예제는 어떨까요? 이를 위해 다음과 같이 코딩을 바꾸면 됩니다.
// .NET 7 + Release + x86
internal class Program
{
static void Main(string[] args)
{
int gb4 = 1024 * 1024;
List<byte[]> load = new List<byte[]>(gb4);
for (int i = 0; i < gb4; i++)
{
load.Add(new byte[4096]);
}
}
}
역시 이번에도 windbg를 이용해 위의 코드를 실행해 보면 이번에는 3,851,904 KB의 Commit size가 나옵니다. 그런데... 뭔가 수치가 이상하죠? ^^ 3,851,904 KB는 대략 3.67GB 정도가 되기 때문에 4KB씩 할당하는 과정치고는 4GB 근처도 못 가서 OOM이 나버린 것입니다.
이에 대한 이야기는 이미 "
작업 관리자에서의 "Commit size"가 가리키는 메모리의 의미" 글에서 했었습니다.
Commit size 외에도 주소 공간이 단순히 "예약된(reserved)" 공간도 있으므로 그것까지 합치면 4GB 가까운 수치가 나옵니다. 실제로 위에서 실습한 것을, "
Process Explorer"를 띄워
"Virtual Size"를 확인하면 4,007,496 KB가 나옵니다. 여전히 4,194,304 KB보다 186,808 KB가 부족하지만
뭔가 이유는 있을 것입니다. (혹시 여분의 공간에 대해 아시는 분은 덧글 부탁드립니다. ^^)
어쨌든 여기서 중요한 것은 3GB 메모리 벽을 넘느냐이기 때문에 그것을 확인했으니 목적은 달성했습니다. ^^
GC Heap 메모리의 크기는 보통
GC.GetTotalMemory를 사용해 구할 수 있지만, 이것은 당연히 그 외의 네이티브 메모리 영역은 포함하지 않습니다. 만약 순수하게 프로세스의 사용 메모리를 구하고 싶다면
System.Diagnostics.Process 타입이 제공하는 다양한 메모리 관련 멤버를 이용하면 되는데요, 이전에 설명한 Commit Size는
VirtualMemorySize64 속성을 이용해 구할 수 있습니다.
단, 이 값은 cache가 되기 때문에 다음과 같이 사용해서는 안 되고,
Process process = Process.GetCurrentProcess(); // 여기서 초기화
for (int i = 0; i < gb4; i++)
{
load.Add(new byte[4096]);
Console.WriteLine(process.VirtualMemorySize64.ToString("n0")); // 루프 내에서 속성 접근
}
구할 때마다 매번 Process 인스턴스를 가져오는 식이어야 합니다.
using System;
using System.Collections.Generic;
using System.Diagnostics;
internal class Program
{
static void Main(string[] args)
{
int gb4 = 1024 * 1024;
List<byte[]> load = new List<byte[]>(gb4);
for (int i = 0; i < gb4; i++)
{
load.Add(new byte[4096]);
Process process = Process.GetCurrentProcess(); // 루프 내에서 초기화 및 속성 접근
long kb = process.VirtualMemorySize64 / 1024;
Console.WriteLine($"{kb:n0}");
}
}
}
하지만, 위의 코드처럼은 실행하지 않는 것이 좋습니다. Process 관련 코드들의 부하가 있어 실행 시간이 더 늘어지기 때문인데 차라리 이럴 때는 4KB가 아닌 1MB 단위씩 증가하도록 만드는 것이 더 편합니다.
load.Add(new byte[1024 * 1024]);
/* 실행 시,
...[생략]...
3,812,616
3,812,568
3,812,588
3,812,616
3,812,604
3,812,580
*/
보는 바와 같이 출력 결과는 작업 관리자의 Commit size와 유사합니다.
VirtualMemorySize64의 도움말에 "Gets the amount of the virtual memory, in bytes,
allocated for the associated process."라고 나오는데, 아마도 여기서의 "allocated"에는 "reserved"는 포함하지 않는 듯합니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]