C# 12 - 인라인 배열(Inline Arrays)
현재(2023-09-14) 기능 명세표를 보면 C# 12의 기능이 모두 8개로 확정(및 closed 상태)되었고, Visual Studio 2022 17.8.0 Preview 1.0 환경에서 "ref readonly parameters"를 제외한 7개의 문법을 모두 실습할 수 있습니다.
7개의 구문 중 4개는 알아봤고, 이번엔 남은 3개 중에서 "인라인 배열(Inline Arrays)"에 대해 알아보겠습니다.
인라인 배열은, struct에 대해 InlineArray 특성을 지정하는 경우 C# 언어에서 새롭게 취급하는 배열 타입입니다.
[System.Runtime.CompilerServices.InlineArray(5)]
public struct Buffer
{
private int _element0; // public 접근을 허용하지만 실용적이지 않음
// 필드는 단 한 개만 정의할 수 있음
}
C# 컴파일러는 위와 같은 정의를 내부 필드의 타입 기준으로 InlineArray 특성에 전달된 수(예제에서는 5)만큼 연속적인 공간에 메모리가 할당된 것으로 처리합니다.
즉, 위의 예제에서는 int 타입 4바이트 * 5 = 20바이트가 연속적으로 메모리에 할당이 되는 식입니다. 이후, 사용 방법도 배열과 완전히 동일하게 취급할 수 있습니다.
namespace ConsoleApp1;
internal class Program
{
static void Main(string[] args)
{
{
Buffer b = new Buffer(); // 새롭게 정의한 배열 타입
for (int i = 0; i < 5; i ++)
{
b[i] = i; // indexer 구문으로 개별 요소 접근
Console.WriteLine(b[i]);
}
}
}
}
[System.Runtime.CompilerServices.InlineArray(5)]
public struct Buffer
{
private int _element0;
}
Inline Array 타입은 Length 속성이 없어 위의 코드에서 5개의 요소를 열거하기 위해 하드 코딩으로 5를 지정했는데, 약간 복잡하지만 다음과 같이 구할 수는 있습니다.
int len = Unsafe.SizeOf<Buffer>() / Unsafe.SizeOf<int>();
for (int i = 0; i < len; i ++)
{
// ...
}
하지만, Span과도 연동이 되므로 이를 이용하는 것이 더 편리합니다.
Buffer b = new Buffer();
Span<int> s = b; // Span을 경유해,
for (int i = 0; i < s.Length; i ++) // s.Length를 사용해 열거
{
// ...
}
이렇게 만든 인라인 배열 타입을 메서드 내에서 쓰면 당연히 Stack 영역에 할당합니다. 그런 의미에서 봤을 때 기존의 stackalloc과 유사하다고도 볼 수 있습니다.
// InlineArray 사용
Buffer b = new Buffer(); // 스택에 sizeof(int) * 5 크기만큼의 연속된 공간을 할당
// stackalloc 사용
int* ptr = stackalloc int[5]; // 스택에 sizeof(int) * 5 크기만큼의 연속된 공간을 할당
하지만 stackalloc은 unsafe 문맥을 요구하는 반면 InlineArray는 managed(safe) 환경에서 사용할 수 있습니다.
또한,
C# 7.3에 추가된 fixed의 기능과 유사하다고도 볼 수 있는데요,
unsafe struct CppStructType
{
public fixed int fields[5]; // sizeof(int) * 5 크기만큼의 연속된 공간을 할당
}
[System.Runtime.CompilerServices.InlineArray(5)] // sizeof(int) * 5 크기만큼의 연속된 공간을 할당
public struct Buffer
{
private int _element0;
}
이것 역시 마찬가지로 fixed는 unsafe 문맥을 필요로 하지만 InlineArray는 managed(safe) 환경에서 사용할 수 있습니다.
결국, '음지'에서 천대받던 ^^ "고정 크기 (스택) 배열"을 '양지'로 끌어낸 기능이 바로 InlineArray입니다. 따라서, 이것의 도입으로 인해 1) C/C++과의 Interop을 좀 더 편하게 할 수 있게 되었고, 2) GC의 개입을 줄이기 위해 스택에 좀 더 간편하게 배열을 생성할 수 있게 되었습니다.
한 가지 유의하셔야 할 것은, 당연히 메서드 또는 다른 struct 타입 내에서 사용하는 경우 스택에 할당되기 때문에 자칫 배열 크기를 너무 크게 잡으면 Stack overflow가 발생하므로 주의해야 합니다.
namespace ConsoleApp1;
internal class Program
{
static unsafe void Main(string[] args)
{
Buffer b = new Buffer();
Console.WriteLine($"b: {b[0]}");
}
}
[System.Runtime.CompilerServices.InlineArray(1500000)]
public struct Buffer
{
public byte _element0;
}
/* 출력 결과
Stack overflow.
at System.Byte.TryFormat(System.Span`1<Char>, Int32 ByRef, System.ReadOnlySpan`1<Char>, System.IFormatProvider)
at System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendFormatted[[System.Byte, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](Byte)
at ConsoleApp1.Program.Main(System.String[])
*/
반면, class 내에 정의한다면 Heap에 연속적인 배열 공간을 잡게 되므로 크기 제약이 완화됩니다.
namespace ConsoleApp1;
internal class Program
{
static unsafe void Main(string[] args)
{
MyClass m = new MyClass();
Console.WriteLine(m.Buf[4500000 - 1]); // 정상 실행
}
}
public class MyClass
{
public Buffer Buf;
}
[System.Runtime.CompilerServices.InlineArray(4500000)]
public struct Buffer
{
private int _element0;
}
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]