MemoryOwner<T>
El MemoryOwner<T>
es un tipo de búfer que implementa IMemoryOwner<T>
, una propiedad de longitud incrustada y una serie de API orientadas al rendimiento. Básicamente, es un contenedor ligero alrededor del tipo ArrayPool<T>
, con algunas utilidades auxiliares adicionales.
API de la plataforma:
MemoryOwner<T>
,AllocationMode
Funcionamiento
MemoryOwner<T>
tiene las siguientes características principales:
- Uno de los principales problemas de las matrices devueltas por las
ArrayPool<T>
API y de las instanciasIMemoryOwner<T>
devueltas por las APIMemoryPool<T>
es que el tamaño especificado por el usuario solo se usa como un tamaño mínimo: el tamaño real de los búferes devueltos podría ser realmente mayor.MemoryOwner<T>
resuelve esto almacenando también el tamaño solicitado original, de modo queMemory<T>
ySpan<T>
instancias recuperadas de ella nunca será necesario segmentar manualmente. - Al usar
IMemoryOwner<T>
, obtener unSpan<T>
para el búfer subyacente requiere primero obtener una instanciaMemory<T>
y, a continuación, unaSpan<T>
. Esto es bastante caro y, a menudo, innecesario, ya que elMemory<T>
intermedio podría no ser necesario en absoluto.MemoryOwner<T>
en su lugar tiene una propiedadSpan
adicional que es extremadamente ligera, ya que encapsula directamente la matrizT[]
interna que se alquila desde el grupo. - Los búferes alquilados desde el grupo no se borran de forma predeterminada, lo que significa que si no se borraron cuando se devolvieron anteriormente al grupo, podrían contener datos de elementos no utilizados. Normalmente, los usuarios deben borrar estos búferes alquilados manualmente, lo que puede ser detallado especialmente cuando se hace con frecuencia.
MemoryOwner<T>
tiene un enfoque más flexible para esto, a través de la APIAllocate(int, AllocationMode)
. Este método no solo asigna una nueva instancia de exactamente el tamaño solicitado, sino que también se puede usar para especificar el modo de asignación que se va a usar: el mismo queArrayPool<T>
, o uno que borra automáticamente el búfer alquilado. - Hay casos en los que se puede alquilar un búfer con un tamaño mayor que lo que realmente se necesita y, a continuación, cambiar el tamaño después. Normalmente, esto requeriría que los usuarios alquilaran un nuevo búfer y copiaran la región de interés del búfer anterior. En su lugar,
MemoryOwner<T>
expone unaSlice(int, int)
API que simplemente devuelve una nueva instancia que ajusta el área de interés especificada. Esto permite omitir el alquiler de un nuevo búfer y copiar completamente los artículos.
Sintaxis
Este es un ejemplo de cómo alquilar un búfer y recuperar una instancia 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;
}
En este ejemplo, hemos usado un using
bloque para declarar el búfer MemoryOwner<T>
: esto es especialmente útil, ya que la matriz subyacente se devolverá automáticamente al grupo al final del bloque. Si en su lugar no tenemos control directo sobre la duración de una instancia MemoryOwner<T>
, el búfer simplemente se devolverá al grupo cuando el recolector de elementos no utilizados finalice el objeto. En ambos casos, los búferes alquilados siempre se devolverán correctamente al grupo compartido.
¿Cuándo se debe usar?
MemoryOwner<T>
se puede usar como un tipo de búfer de uso general, que tiene la ventaja de minimizar el número de asignaciones realizadas con el tiempo, ya que reutiliza internamente las mismas matrices de un grupo compartido. Un caso de uso común consiste en reemplazar new T[]
asignaciones de matriz, especialmente cuando se realizan operaciones repetidas que requieren un búfer temporal en el que trabajar o que producen un búfer como resultado.
Supongamos que tenemos un conjunto de datos que consta de una serie de archivos binarios y que necesitamos leer todos estos archivos y procesarlos de alguna manera. Para separar correctamente nuestro código, es posible que terminemos escribiendo un método que simplemente lea un archivo binario, que podría tener este aspecto:
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;
}
Tenga en cuenta que new byte[]
expresión. Si leemos un gran número de archivos, terminaremos asignando una gran cantidad de matrices nuevas, lo que pondrá mucha presión sobre el recolector de elementos no utilizados. Es posible que desee refactorizar este código mediante búferes alquilados desde un grupo, de la siguiente manera:
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);
}
Con este enfoque, los búferes ahora se alquilan desde un grupo, lo que significa que en la mayoría de los casos podemos omitir una asignación. Además, dado que los búferes alquilados no se borran de forma predeterminada, también podemos ahorrar el tiempo necesario para rellenarlos con ceros, lo que nos da otra pequeña mejora del rendimiento. En el ejemplo anterior, la carga de 1000 archivos traería el tamaño total de asignación de aproximadamente 1 MB a solo 1024 bytes: solo se asignaría un único búfer y, a continuación, se reutilizaría automáticamente.
Hay dos problemas principales con el código anterior:
ArrayPool<T>
puede devolver búferes con un tamaño mayor que el solicitado. Para solucionar este problema, es necesario devolver una tupla que también indica el tamaño usado real en nuestro búfer alquilado.- Simplemente devolviendo una matriz, es necesario tener cuidado adicional para realizar un seguimiento correcto de su duración y devolverla al grupo adecuado. Podríamos solucionar este problema usando
MemoryPool<T>
en su lugar y devolviendo una instanciaIMemoryOwner<T>
, pero todavía tenemos el problema de los búferes alquilados que tienen un tamaño mayor que lo que necesitamos. Además,IMemoryOwner<T>
tiene cierta sobrecarga al recuperarSpan<T>
en el que trabajar, debido a que es una interfaz y el hecho de que siempre necesitamos obtener una instanciaMemory<T>
primero y, a continuación, unaSpan<T>
.
Para resolver estos dos problemas, podemos refactorizar este código de nuevo mediante 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;
}
La instancia IMemoryOwner<byte>
devuelta se encargará de eliminar el búfer subyacente y devolverla al grupo cuando se invoque su método IDisposable.Dispose
. Podemos usarlo para obtener una instanciaMemory<T>
o Span<T>
para interactuar con los datos cargados y, a continuación, eliminar la instancia cuando ya no la necesitemos. Además, todas las propiedades MemoryOwner<T>
(como MemoryOwner<T>.Span
) respetan el tamaño solicitado inicial que usamos, por lo que ya no es necesario realizar un seguimiento manual del tamaño efectivo dentro del búfer alquilado.
Ejemplos
Encontrará más ejemplos en las pruebas unitarias.
.NET Community Toolkit