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 iIMemoryOwner<T>
wystąpień zwracanych przezMemoryPool<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 abyMemory<T>
Span<T>
wystąpienia pobrane z niego nigdy nie musiały być ręcznie dzielone. - W przypadku używania metody
IMemoryOwner<T>
uzyskiwanie elementuSpan<T>
dla buforu bazowego wymaga najpierw uzyskaniaMemory<T>
wystąpienia, a następnie .Span<T>
Jest to dość kosztowne i często niepotrzebne, ponieważ pośredniMemory<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 interfejsuAllocate(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 jakArrayPool<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ąceSlice(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ącIMemoryOwner<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 pobieraniaSpan<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ępnieSpan<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
.NET Community Toolkit