Share via


MemoryOwner<T>

이는 MemoryOwner<T> 구현하는 IMemoryOwner<T>버퍼 형식, 포함된 길이 속성 및 일련의 성능 지향 API입니다. 그것은 본질적으로 몇 가지 추가 도우미 유틸리티와 함께, 유형 주위에 ArrayPool<T> 경량 래퍼입니다.

플랫폼 API:MemoryOwner<T>, AllocationMode

작동 방식

MemoryOwner<T>에는 다음과 같은 기본 기능이 있습니다.

  • API 및 API에서 반환 MemoryPool<T> 된 인스턴스에서 반환되는 ArrayPool<T> 배열의 IMemoryOwner<T> 기본 문제 중 하나는 사용자가 지정한 크기가 최소 크기로만 사용된다는 것입니다. 반환된 버퍼의 실제 크기는 실제로 더 클 수 있습니다. MemoryOwner<T> 는 원래 요청된 크기도 저장하여 이를 해결하므로 Memory<T> 검색된 인스턴스는 Span<T> 수동으로 조각화할 필요가 없습니다.
  • 사용할 IMemoryOwner<T>때 기본 버퍼에 대한 가져오기 Span<T> 를 위해서는 먼저 인스턴스를 Memory<T> 가져오고 다음 Span<T>을 수행해야 합니다. 이것은 Memory<T> 중간이 실제로 전혀 필요하지 않을 수 있기 때문에 상당히 비싸고 종종 불필요합니다. MemoryOwner<T>대신 풀에서 임대되는 내부 T[] 배열을 직접 래핑하므로 매우 가벼운 추가 Span 속성이 있습니다.
  • 풀에서 임대된 버퍼는 기본적으로 지워지지 않습니다. 즉, 이전에 풀로 반환될 때 지워지지 않은 경우 가비지 데이터가 포함될 수 있습니다. 일반적으로 사용자는 이러한 임대 버퍼를 수동으로 지워야 하며, 이는 특히 자주 수행될 때 자세한 정보가 될 수 있습니다. MemoryOwner<T> 에는 API를 통해 보다 유연한 접근 방식이 있습니다 Allocate(int, AllocationMode) . 이 메서드는 정확히 요청된 크기의 새 인스턴스를 할당할 뿐만 아니라 사용할 할당 모드를 지정하는 데도 사용할 수 있습니다( 동일한 ArrayPool<T>할당 모드 또는 임대된 버퍼를 자동으로 지우는 할당 모드).
  • 버퍼를 실제로 필요한 것보다 더 큰 크기로 임대한 다음 나중에 크기를 조정할 수 있는 경우가 있습니다. 이렇게 하려면 일반적으로 사용자가 새 버퍼를 임대하고 이전 버퍼에서 관심 영역을 복사해야 합니다. 대신 MemoryOwner<T> 지정된 관심 영역을 래핑하는 새 인스턴스를 반환하는 API를 노출 Slice(int, int) 합니다. 이렇게 하면 새 버퍼를 임대하고 항목을 완전히 복사하는 것을 건너뛸 수 있습니다.

구문

버퍼를 임대하고 인스턴스를 검색하는 방법의 예는 Memory<T> 다음과 같습니다.

// Be sure to include this using at the top of the file:
using Microsoft.Toolkit.HighPerformance.Buffers;

using (MemoryOwner<int> buffer = MemoryOwner<int>.Allocate(42))
{
    // Both memory and span have exactly 42 items
    Memory<int> memory = buffer.Memory;
    Span<int> span = buffer.Span;

    // Writing to the span modifies the underlying buffer
    span[0] = 42;
}

이 예제에서는 블록을 사용하여 using 버퍼를 선언 MemoryOwner<T> 했습니다. 기본 배열이 블록 끝에 있는 풀에 자동으로 반환되므로 특히 유용합니다. 대신 인스턴스의 MemoryOwner<T> 수명을 직접 제어할 수 없는 경우 개체가 가비지 수집기에서 완료될 때 버퍼가 풀로 반환됩니다. 두 경우 모두 임대 버퍼는 항상 공유 풀에 올바르게 반환됩니다.

언제 사용해야 하나요?

MemoryOwner<T> 는 공유 풀에서 동일한 배열을 내부적으로 재사용하므로 시간이 지남에 따라 수행되는 할당 수를 최소화할 수 있는 범용 버퍼 형식으로 사용할 수 있습니다. 일반적인 사용 사례는 특히 임시 버퍼가 작동해야 하거나 결과적으로 버퍼를 생성하는 반복 작업을 수행하는 경우 배열 할당을 대체하는 new T[] 것입니다.

일련의 이진 파일로 구성된 데이터 세트가 있고 이러한 모든 파일을 읽고 어떤 식으로든 처리해야 한다고 가정해 보겠습니다. 코드를 제대로 분리하기 위해 다음과 같이 보일 수 있는 하나의 이진 파일을 읽는 메서드를 작성하게 될 수 있습니다.

public static byte[] GetBytesFromFile(string path)
{
    using Stream stream = File.OpenRead(path);

    byte[] buffer = new byte[(int)stream.Length];

    stream.Read(buffer, 0, buffer.Length);

    return buffer;
}

해당 식에 유의 new byte[] 하세요. 많은 수의 파일을 읽는 경우 많은 새 배열을 할당하게 되므로 가비지 수집기에서 많은 부담이 가중됩니다. 다음과 같이 풀에서 임대된 버퍼를 사용하여 이 코드를 리팩터링할 수 있습니다.

public static (byte[] Buffer, int Length) GetBytesFromFile(string path)
{
    using Stream stream = File.OpenRead(path);

    byte[] buffer = ArrayPool<T>.Shared.Rent((int)stream.Length);

    stream.Read(buffer, 0, (int)stream.Length);

    return (buffer, (int)stream.Length);
}

이 방법을 사용하면 버퍼가 풀에서 임대되므로 대부분의 경우 할당을 건너뛸 수 있습니다. 또한 임대 버퍼는 기본적으로 지워지지 않으므로 0으로 채우는 데 필요한 시간을 절약하여 또 다른 작은 성능 향상을 제공합니다. 위의 예제에서 1,000개의 파일을 로드하면 총 할당 크기가 약 1MB에서 1024바이트까지 감소합니다. 단일 버퍼만 효과적으로 할당된 다음 자동으로 재사용됩니다.

위의 코드에는 두 가지 기본 문제가 있습니다.

  • ArrayPool<T> 는 요청된 버퍼보다 크기가 큰 버퍼를 반환할 수 있습니다. 이 문제를 해결하려면 실제 사용된 크기를 임대 버퍼에 나타내는 튜플을 반환해야 합니다.
  • 배열을 반환하기만 하면 수명을 적절하게 추적하고 적절한 풀로 반환하는 데 매우 주의해야 합니다. 대신 사용하고 MemoryPool<T> 인스턴스를 반환하여 IMemoryOwner<T> 이 문제를 해결할 수 있지만 여전히 임대 버퍼의 크기가 필요한 것보다 큰 문제가 있습니다. IMemoryOwner<T> 또한 작업할 항목을 검색 Span<T> 할 때 인터페이스이기 때문에 약간의 오버헤드가 있으며, 항상 먼저 인스턴스를 가져온 다음 인스턴스를 Memory<T> 가져와야 한다는 사실도 있습니다Span<T>.

이러한 문제를 모두 해결하기 위해 다음을 사용하여 MemoryOwner<T>이 코드를 다시 리팩터링할 수 있습니다.

public static MemoryOwner<byte> GetBytesFromFile(string path)
{
    using Stream stream = File.OpenRead(path);

    MemoryOwner<byte> buffer = MemoryOwner<byte>.Allocate((int)stream.Length);

    stream.Read(buffer.Span);

    return buffer;
}

반환된 IMemoryOwner<byte> 인스턴스는 기본 버퍼를 삭제하고 메서드가 호출될 때 IDisposable.Dispose 풀에 반환하는 작업을 처리합니다. 로드된 데이터와 상호 작용할 인스턴스를 Span<T> 가져오는 Memory<T> 데 사용한 다음, 더 이상 필요하지 않을 때 인스턴스를 삭제할 수 있습니다. 또한 모든 MemoryOwner<T> 속성(예: MemoryOwner<T>.Span)은 사용한 초기 요청 크기를 준수하므로 더 이상 임대 버퍼 내에서 유효 크기를 수동으로 추적할 필요가 없습니다.

예제

단위 테스트에서 더 많은 예제를 찾을 수 있습니다.