Marshaling Arrays of Structures
.NET에서 매니지드 코드와 언매니지드 코드 간에 구조체(Struct) 배열을 주고받을 때, 마샬링은 중요한 역할을 합니다. 특히, 구조체 배열을 마샬링할 때는 메모리 배치, 성능, 데이터 일관성 등이 주요 고려사항이 됩니다. 이 과정에서 잘못된 마샬링으로 인해 성능 저하나 메모리 오류가 발생할 수 있기 때문에, 적절한 마샬링 전략을 사용하는 것이 필수적입니다. 이 글에서는 구조체 배열을 매니지드 코드와 언매니지드 코드 사이에서 마샬링하는 방법을 다양한 예제와 함께 설명하며, 성능 최적화와 메모리 관리를 위한 기법도 함께 다룹니다.
구조체 배열 마샬링의 개요
.NET에서 구조체는 **값 타입(Value Type)**으로, 메모리에 직접 저장됩니다. 따라서 구조체 배열을 마샬링할 때는 배열의 각 요소가 메모리에 어떻게 배치되는지를 고려해야 합니다. 특히 구조체가 고정된 크기의 데이터 타입(예: int, float, double)으로만 이루어진 경우와, 가변 크기의 데이터 타입(예: 문자열, 배열)을 포함한 경우의 처리 방식이 다릅니다. 구조체 배열을 마샬링할 때 주로 사용하는 두 가지 주요 전략은 다음과 같습니다:
- 고정 크기 구조체 배열 마샬링
- 가변 크기 구조체 배열 마샬링
고정 크기 구조체 배열 마샬링
구조체가 고정 크기 데이터를 포함할 때는 메모리 배치가 간단하며, 배열을 메모리 상에 일렬로 배치하여 바로 언매니지드 코드로 전달할 수 있습니다. 이를 통해 성능을 크게 향상시킬 수 있습니다.
예제 1: 고정 크기 구조체 배열 마샬링
using System;
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct Point
{
public int X;
public int Y;
}
class Program
{
[DllImport("NativeLib.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void ProcessPoints([MarshalAs(UnmanagedType.LPArray, SizeConst = 3)] Point[] points);
static void Main()
{
Point[] points = new Point[]
{
new Point { X = 1, Y = 2 },
new Point { X = 3, Y = 4 },
new Point { X = 5, Y = 6 }
};
ProcessPoints(points); // 구조체 배열을 언매니지드 코드로 전달
}
}
이 예제에서는 Point
구조체 배열을 언매니지드 코드로 마샬링합니다. [MarshalAs(UnmanagedType.LPArray, SizeConst = 3)]
특성을 사용하여 배열의 크기를 지정하고, 배열을 메모리 상에서 연속적으로 배치해 전달합니다. 구조체가 고정된 크기를 가지므로 마샬링 작업이 단순하고 성능이 좋습니다.
구조체 배열의 Pinned 메모리 사용
대형 구조체 배열을 반복적으로 전달할 경우, 메모리 복사 없이 데이터를 직접 참조하는 Pinned 메모리를 사용할 수 있습니다. 이를 통해 성능을 최적화하고, 배열이 가비지 컬렉션 중 이동되지 않도록 할 수 있습니다.
예제 2: 고정된 구조체 배열 마샬링
using System;
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct Point
{
public int X;
public int Y;
}
class Program
{
[DllImport("NativeLib.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void ProcessPoints(IntPtr points, int length);
static void Main()
{
Point[] points = new Point[]
{
new Point { X = 1, Y = 2 },
new Point { X = 3, Y = 4 },
new Point { X = 5, Y = 6 }
};
GCHandle handle = GCHandle.Alloc(points, GCHandleType.Pinned);
try
{
IntPtr pointer = handle.AddrOfPinnedObject();
ProcessPoints(pointer, points.Length); // Pinned 메모리로 전달
}
finally
{
handle.Free(); // 메모리 해제
}
}
}
이 예제에서는 구조체 배열을 Pinned 메모리로 고정한 후, 메모리 복사 없이 언매니지드 코드로 전달합니다. GCHandle
을 사용하여 메모리를 고정하면, 성능 저하 없이 대형 구조체 배열을 안전하게 처리할 수 있습니다.
가변 크기 구조체 배열 마샬링
구조체가 문자열이나 배열과 같은 가변 크기 필드를 포함할 경우, 메모리 배치와 마샬링이 더 복잡해집니다. 이러한 구조체 배열을 마샬링할 때는 각각의 구조체가 별도로 메모리에 배치되며, 추가적인 마샬링 작업이 필요합니다.
예제 3: 가변 크기 구조체 배열 마샬링
using System;
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct Person
{
public int Age;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 50)]
public string Name;
}
class Program
{
[DllImport("NativeLib.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void ProcessPeople([MarshalAs(UnmanagedType.LPArray, SizeConst = 2)] Person[] people);
static void Main()
{
Person[] people = new Person[]
{
new Person { Age = 25, Name = "Alice" },
new Person { Age = 30, Name = "Bob" }
};
ProcessPeople(people); // 가변 크기 필드를 포함한 구조체 배열 마샬링
}
}
이 예제에서는 Person
구조체가 문자열 필드를 포함하고 있으며, 이를 언매니지드 코드로 마샬링하는 과정을 보여줍니다. **MarshalAs(UnmanagedType.ByValTStr)
**를 사용하여 고정된 크기의 문자열을 처리할 수 있습니다. 그러나 문자열 필드를 마샬링할 때는 메모리 복사와 변환 작업이 추가로 발생하므로 성능에 주의해야 합니다.
가변 길이 배열 마샬링
구조체 배열의 크기가 동적으로 결정되는 경우, 배열의 크기를 함께 전달하여 메모리 참조 오류를 방지해야 합니다. 이때 배열의 길이를 명확히 전달하고, 배열이 메모리 상에서 연속적으로 배치되도록 해야 합니다.
예제 4: 가변 길이 배열 마샬링
using System;
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct Point
{
public int X;
public int Y;
}
class Program
{
[DllImport("NativeLib.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void ProcessPoints(IntPtr points, int length);
static void Main()
{
Point[] points = new Point[]
{
new Point { X = 1, Y = 2 },
new Point { X = 3, Y = 4 },
new Point { X = 5, Y = 6 }
};
GCHandle handle = GCHandle.Alloc(points, GCHandleType.Pinned);
try
{
IntPtr pointer = handle.AddrOfPinnedObject();
ProcessPoints(pointer, points.Length); // 가변 길이 배열 마샬링
}
finally
{
handle.Free(); // 메모리 해제
}
}
}
이 예제에서는 구조체 배열의 크기를 함께 전달하여, 가변 길이 배열을 안전하게 마샬링하는 방법을 보여줍니다. 메모리 해제를 명확히 처리하여 메모리 누수를 방지해야 합니다.
성능 최적화와 주의 사항
1. 메모리 고정 및 복사 최소화
대형 구조체 배열을 반복적으로 마샬링할 때는 Pinned 메모리를 사용하여 메모리 복사를 최소화하고 성능을 최적화해야 합니다. 또한, 메모리 해제를 명확히 처리하여 메모리 누수를 방지하는 것이 중요합니다.
2. 배열 크기 명시
구조체 배열을 마샬링할 때는 항상 배열의 크기를 명확히 전달해야 합니다. 특히, 가변 길이 배열을 처리할 때 배열의 크기를 전달하지 않으면 메모리 참조 오류나 데이터 손실이 발생할 수 있습니다.
3. Zero-Copy 방식 적용
구조체 배열을 자주 주고받는 경우 Zero-Copy 방식으로 메모리를 참조하여 성능
을 향상시킬 수 있습니다. 이를 위해 Span<T>
와 같은 구조를 사용하여 메모리 복사 없이 데이터를 처리하는 것이 효과적입니다.
결론
구조체 배열을 매니지드와 언매니지드 코드 간에 마샬링할 때는 메모리 배치와 성능을 고려한 최적화가 필요합니다. 고정 크기 구조체 배열은 비교적 간단하게 마샬링할 수 있지만, 가변 크기 필드를 포함한 구조체 배열은 더 복잡한 마샬링 작업이 요구됩니다. Pinned 메모리와 Zero-Copy 방식 같은 최적화 기법을 사용하여 성능을 향상시킬 수 있으며, 배열의 크기를 명확히 전달하여 안전한 메모리 관리를 보장해야 합니다.