할당 패러다임 전환
이전 C# 프로그래밍에서 문자열을 자르거나 배열의 일부를 복사할 때마다 새로운 객체가 힙에 생성되었다. 이는 곧 가비지 컬렉터의 일거리가 늘어남을 의미했으며, 최신 .NET은 이를 해결하기 위해 슬라이싱(Slicing) 개념을 도입했다.
Span<T>와 ReadOnlySpan<T>
Span<T>는 메모리의 연속적인 영역을 가리키며, 힙 할당 없이 관리되는 메모리, 비관리 메모리, 스택 메모리를 모두 추상화하여 안전하게 다룰 수 있는 구조체이다.
-
Ref Struct 위력: Span<
T>는 ref struct 키워드로 선언되어 있다. 이는 무조건 스택에만 존재해야 함을 의미하며, 힙으로 올라갈 수 없다. 따라서 힙에 생성되지 않으므로 가비지 컬렉터가 스캔하거나 수집할 필요가 전혀 없다. -
예시: 대용량 로그 파일 파싱(Parsing) 시, 문자열을 수천 개로 쪼개는 대신 원본 문자열의 특정 위치만 Span으로 가리키면 할당량이 0바이트가 된다.
Memory<T>
Span<T>는 스택에만 살아야 하므로, yield return이나 await 키워드 뒤에는 사용할 수 없다. 이 제약을 해결하는 것이 Memory<T>이다.
-
역할: Span과 비슷하지만 힙에 저장될 수 있다. 비동기 메소드 사이에서 메모리의 일부분을 전달할 때 사용한다.
-
흐름: Memory<
T>를 전달하다가, 실제 데이터를 조작해야 하는 짧은 순간에만 .Span 속성을 통해 Span으로 변환하여 처리한다.
POH
과거에는 외부 라이브러리와 데이터를 주고받을 때 객체가 움직이지 못하게 고정해야 했다. 하지만 0~2세대 사이에 고정된 객체가 있으면 가비지 컬렉터가 메모리를 압착할 때 파편화를 유발했다.
-
해결 방법: .NEY 5 이후 고정될 객체들만 모아두는 별도의 힙인 POH(Pinned Object Heap)를 신설했다.
-
이점: 고정된 객체가 일반 세대(0~2세대)에 섞여 있지 않으므로, 가비지 컬렉터는 일반 객체들을 아무 방해 없이 압착하여 메모리 효율을 극대화할 수 있다.
ValueTask<T>
비동기 프로그래밍에서는 Task<>T>는 클래스이므로 호출할 때마다 힙에 할당된다. 결과가 이미 나와 있는 경우조차 Task 객체를 만드는 것은 낭비이다.
-
ValueTask: 구조체로 구현된 비동기 반환 타입이다.
-
최적화: 비동기 작업이 동기적으로 끝나는 경우 힙 할당이 발생하지 않는다. 고성능 네트워크 라이브러리에서 사용된다.
ReadOnlySequence<T>
네트워크 패킷처럼 데이터가 여러 조각으로 나뉘어 들어올 때, 이를 하나로 합치려면 할당이 발생한다. ReadOnlySequence<T>는 흩어져 있는 메모리 블록들을 하나의 연속된 데이터처럼 읽게 해주는 다리 역할을 하여 할당 없는 파싱을 가능하게 한다.
NativeMemory
.NET 6 이후 System.Runtime.InteropServices.NativeMemory를 통해 C++ 언어처럼 직접 메모리를 할당하고 해제할 수 있는 방법을 제공한다. 가비지 컬렉터의 관리를 아예 받지 않고 임계 영역을 설계할 때 사용한다.