Udostępnij za pośrednictwem


Właściciel<pamięci T>

Jest MemoryOwner<T> to typ buforu implementowania IMemoryOwner<T>właściwości , osadzonej długości i serii interfejsów API zorientowanych na wydajność. Jest to zasadniczo uproszczona otoka wokół ArrayPool<T> typu, z dodatkowymi narzędziami pomocnika.

Interfejsy API platformy: MemoryOwner<T>, AllocationMode

Jak to działa

MemoryOwner<T> ma następujące główne funkcje:

  • Jednym z głównych problemów tablic zwracanych przez ArrayPool<T> interfejsy API i IMemoryOwner<T> wystąpień zwracanych przez MemoryPool<T> interfejsy API jest to, że rozmiar określony przez użytkownika jest używany tylko jako minimalny rozmiar: rzeczywisty rozmiar zwracanych może być rzeczywiście większy. MemoryOwner<T> Rozwiązuje to problem, przechowując również oryginalny żądany rozmiar, tak aby Memory<T> Span<T> wystąpienia pobrane z niego nigdy nie musiały być ręcznie dzielone.
  • W przypadku używania metody IMemoryOwner<T>uzyskiwanie elementu Span<T> dla buforu bazowego wymaga najpierw uzyskania Memory<T> wystąpienia, a następnie .Span<T> Jest to dość kosztowne i często niepotrzebne, ponieważ pośredni Memory<T> może w ogóle nie być potrzebne. MemoryOwner<T> Zamiast tego ma dodatkową Span właściwość, która jest niezwykle uproszczona, ponieważ bezpośrednio opakowuje tablicę wewnętrzną T[] wynajmowaną z puli.
  • wynajęte z puli nie są domyślnie czyszczone, co oznacza, że jeśli nie zostały wyczyszczone podczas poprzedniego powrotu do puli, mogą zawierać dane śmieci. Zwykle użytkownicy muszą ręcznie wyczyścić te wynajęte, co może być pełne szczególnie w przypadku częstego wykonywania. MemoryOwner<T> ma bardziej elastyczne podejście do tego za pośrednictwem interfejsu Allocate(int, AllocationMode) API. Ta metoda nie tylko przydziela nowe wystąpienie dokładnie żądanego rozmiaru, ale może również służyć do określenia trybu alokacji, który ma być używany: taki sam jak ArrayPool<T>, lub taki, który automatycznie czyści wynajęty bufor.
  • Istnieją przypadki, w których bufor może zostać wynajęty o większym rozmiarze niż to, co jest rzeczywiście potrzebne, a następnie zmieniony rozmiar później. Zwykle wymagałoby to od użytkowników wynajęcia nowego buforu i skopiowania regionu zainteresowania ze starego buforu. Zamiast tego uwidacznia interfejs API, MemoryOwner<T> który po prostu zwraca nowe wystąpienie opakowujące Slice(int, int) określony obszar zainteresowania. Pozwala to pominąć wynajem nowego buforu i całkowicie skopiować elementy.

Składnia

Oto przykład sposobu wynajmu buforu i pobierania Memory<T> wystąpienia:

// 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;
}

W tym przykładzie użyto using bloku do zadeklarowania buforu MemoryOwner<T> : jest to szczególnie przydatne, ponieważ macierz bazowa zostanie automatycznie zwrócona do puli na końcu bloku. Jeśli zamiast tego nie mamy bezpośredniej kontroli nad okresem istnienia MemoryOwner<T> wystąpienia, bufor zostanie po prostu zwrócony do puli, gdy obiekt zostanie sfinalizowany przez moduł odśmiecenia pamięci. W obu przypadkach wynajęte będą zawsze prawidłowo zwracane do udostępnionej puli.

Kiedy należy go użyć?

MemoryOwner<T> może być używany jako typ buforu ogólnego przeznaczenia, który ma zaletę zminimalizowania liczby alokacji wykonywanych w czasie, ponieważ wewnętrznie używa tych samych tablic z puli udostępnionej. Typowym przypadkiem użycia jest zastąpienie new T[] alokacji tablicy, zwłaszcza w przypadku wykonywania powtarzających się operacji, które wymagają tymczasowego buforu do pracy lub które w rezultacie generują bufor.

Załóżmy, że mamy zestaw danych składający się z serii plików binarnych i musimy odczytać wszystkie te pliki i przetworzyć je w jakiś sposób. Aby poprawnie oddzielić nasz kod, możemy napisać metodę, która po prostu odczytuje jeden plik binarny, który może wyglądać następująco:

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;
}

Zwróć uwagę na to new byte[] wyrażenie. Jeśli odczytamy dużą liczbę plików, w końcu przydzielimy wiele nowych tablic, co będzie wywierać dużą presję na moduł odśmiecania pamięci. Możemy chcieć refaktoryzować ten kod przy użyciu wynajętych z puli, w następujący sposób:

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);
}

Korzystając z tego podejścia, są teraz wynajmowane z puli, co oznacza, że w większości przypadków możemy pominąć alokację. Ponadto, ponieważ wynajęte nie są domyślnie wyczyszczone, możemy również zaoszczędzić czas potrzebny na wypełnienie ich zerami, co daje nam kolejną małą poprawę wydajności. W powyższym przykładzie załadowanie 1000 plików spowoduje przeniesienie całkowitego rozmiaru alokacji z około 1 MB w dół do zaledwie 1024 bajtów — wystarczyby przydzielić tylko jeden bufor, a następnie użyć go automatycznie.

Istnieją dwa główne problemy z powyższym kodem:

  • ArrayPool<T> może zwracać o rozmiarze większym niż żądany. Aby obejść ten problem, musimy zwrócić krotkę, która wskazuje również rzeczywisty rozmiar używany do naszego wynajętego buforu.
  • Po prostu zwracając tablicę, musimy być bardzo ostrożni, aby prawidłowo śledzić jego okres istnienia i przywrócić ją do odpowiedniej puli. Możemy obejść ten problem, używając MemoryPool<T> zamiast tego i zwracając IMemoryOwner<T> wystąpienie, ale nadal mamy problem z wynajętymi o większym rozmiarze niż to, czego potrzebujemy. IMemoryOwner<T> Ponadto ma pewne obciążenie podczas pobierania Span<T> elementu do pracy, ze względu na to, że jest to interfejs, i fakt, że zawsze musimy najpierw uzyskać Memory<T> wystąpienie, a następnie Span<T>.

Aby rozwiązać oba te problemy, możemy ponownie refaktoryzować ten kod przy użyciu polecenia 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;
}

Zwrócone IMemoryOwner<byte> wystąpienie zajmie się dysponowaniem buforu bazowego i zwróceniem go do puli podczas IDisposable.Dispose wywoływanej metody. Możemy go użyć, aby uzyskać Memory<T> wystąpienie lub Span<T> w celu interakcji z załadowanymi danymi, a następnie usunąć wystąpienie, gdy nie jest już potrzebne. Ponadto wszystkie MemoryOwner<T> właściwości (na przykład MemoryOwner<T>.Span) szanują początkowy żądany rozmiar, którego użyliśmy, więc nie musimy już ręcznie śledzić obowiązującego rozmiaru w wynajętym buforze.

Przykłady

Więcej przykładów można znaleźć w testach jednostkowych.