Condividi tramite


MemoryOwner<T>

MemoryOwner<T> è un tipo di buffer che implementa IMemoryOwner<T>, una proprietà length incorporata e una serie di API orientate alle prestazioni. Si tratta essenzialmente di un wrapper leggero intorno al ArrayPool<T> tipo, con alcune utilità helper aggiuntive.

API della piattaforma:MemoryOwner<T>, AllocationMode

Funzionamento

MemoryOwner<T> presenta le funzionalità principali seguenti:

  • Uno dei problemi principali delle matrici restituite dalle ArrayPool<T> API e delle IMemoryOwner<T> istanze restituite dalle MemoryPool<T> API è che le dimensioni specificate dall'utente vengono usate solo come dimensioni minime : le dimensioni effettive dei buffer restituiti potrebbero effettivamente essere maggiori. MemoryOwner<T> risolve questo problema archiviando anche le dimensioni richieste originali, in modo che Memory<T> e Span<T> le istanze recuperate da esso non dovranno mai essere sezionate manualmente.
  • Quando si usa IMemoryOwner<T>, ottenere un Span<T> per il buffer sottostante richiede innanzitutto di ottenere un'istanza Memory<T> di e quindi un oggetto Span<T>. Questo è piuttosto costoso, e spesso non necessario, in quanto l'intermedio Memory<T> potrebbe effettivamente non essere necessario affatto. MemoryOwner<T> ha invece una proprietà aggiuntiva Span estremamente leggera, poiché esegue direttamente il wrapping della matrice interna T[] in affitto dal pool.
  • I buffer affittati dal pool non vengono cancellati per impostazione predefinita, il che significa che se non sono stati cancellati quando vengono restituiti in precedenza al pool, potrebbero contenere dati di Garbage. In genere, gli utenti devono cancellare manualmente questi buffer in affitto, che possono essere verbosi soprattutto quando vengono eseguiti frequentemente. MemoryOwner<T> ha un approccio più flessibile a questo, tramite l'API Allocate(int, AllocationMode) . Questo metodo non solo alloca una nuova istanza di esattamente le dimensioni richieste, ma può anche essere usata per specificare la modalità di allocazione da usare: quella ArrayPool<T>di o quella che cancella automaticamente il buffer affittato.
  • Ci sono casi in cui un buffer potrebbe essere affittato con una dimensione maggiore di quella effettivamente necessaria e quindi ridimensionato in seguito. In genere, gli utenti devono noleggiare un nuovo buffer e copiare l'area di interesse dal buffer precedente. Espone invece MemoryOwner<T> un'API Slice(int, int) che restituisce semplicemente una nuova istanza che esegue il wrapping dell'area di interesse specificata. In questo modo è possibile ignorare l'affitto di un nuovo buffer e copiare completamente gli elementi.

Sintassi

Ecco un esempio di come noleggiare un buffer e recuperare un'istanza 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;
}

In questo esempio è stato usato un using blocco per dichiarare il MemoryOwner<T> buffer: ciò è particolarmente utile perché la matrice sottostante verrà restituita automaticamente al pool alla fine del blocco. Se invece non si ha il controllo diretto sulla durata di un'istanza MemoryOwner<T> , il buffer verrà semplicemente restituito al pool quando l'oggetto viene finalizzato dal Garbage Collector. In entrambi i casi, i buffer in affitto verranno sempre restituiti correttamente al pool condiviso.

Quando usare questa opzione?

MemoryOwner<T> può essere usato come tipo di buffer per utilizzo generico, che ha il vantaggio di ridurre al minimo il numero di allocazioni eseguite nel tempo, perché riutilizza internamente le stesse matrici da un pool condiviso. Un caso d'uso comune consiste nel sostituire new T[] le allocazioni di matrici, soprattutto quando si eseguono operazioni ripetute che richiedono un buffer temporaneo per funzionare o che producono un buffer come risultato.

Si supponga di avere un set di dati costituito da una serie di file binari e che sia necessario leggere tutti questi file ed elaborarli in qualche modo. Per separare correttamente il codice, è possibile scrivere un metodo che legge semplicemente un file binario, che potrebbe essere simile al seguente:

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

Si noti che new byte[] l'espressione. Se si legge un numero elevato di file, si finisce per allocare un sacco di nuove matrici, che metteranno molta pressione sul Garbage Collector. È possibile eseguire il refactoring di questo codice usando buffer noleggiati da un pool, come indicato di seguito:

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

Usando questo approccio, i buffer vengono ora affittati da un pool, il che significa che nella maggior parte dei casi è possibile ignorare un'allocazione. Inoltre, poiché i buffer in affitto non vengono cancellati per impostazione predefinita, è anche possibile risparmiare il tempo necessario per riempirli con zeri, che ci dà un altro piccolo miglioramento delle prestazioni. Nell'esempio precedente, il caricamento di 1000 file porterebbe le dimensioni di allocazione totali da circa 1 MB a soli 1024 byte: solo un singolo buffer verrebbe allocato in modo efficace e quindi riutilizzato automaticamente.

Il codice precedente presenta due problemi principali:

  • ArrayPool<T> può restituire buffer con dimensioni maggiori di quelle richieste. Per risolvere questo problema, è necessario restituire una tupla che indica anche le dimensioni usate effettive nel buffer affittato.
  • Semplicemente restituendo una matrice, è necessario prestare particolare attenzione per tenere traccia della durata corretta e per restituirla al pool appropriato. È possibile risolvere questo problema usando MemoryPool<T> invece e restituendo un'istanza IMemoryOwner<T> , ma è ancora presente il problema dei buffer a noleggio con dimensioni maggiori rispetto a quelle necessarie. Inoltre, IMemoryOwner<T> ha un sovraccarico durante il recupero di un Span<T> oggetto per lavorare, a causa di un'interfaccia e del fatto che è sempre necessario ottenere un'istanza Memory<T> di e quindi un Span<T>oggetto .

Per risolvere entrambi questi problemi, è possibile effettuare di nuovo il refactoring di questo codice usando 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;
}

L'istanza restituita IMemoryOwner<byte> si occuperà di eliminare il buffer sottostante e di restituirlo al pool quando viene richiamato il relativo IDisposable.Dispose metodo. È possibile usarlo per ottenere un'istanza Memory<T> o Span<T> per interagire con i dati caricati e quindi eliminare l'istanza quando non è più necessaria. Inoltre, tutte le MemoryOwner<T> proprietà (ad esempio MemoryOwner<T>.Span) rispettano le dimensioni iniziali richieste usate, quindi non è più necessario tenere traccia manualmente delle dimensioni effettive all'interno del buffer affittato.

Esempi

Altri esempi sono disponibili negli unit test.