Array.Copy vs Buffer.BlockCopy


124

Array.Copy e Buffer.BlockCopy fanno entrambi la stessa cosa, ma BlockCopysono finalizzati alla copia veloce di array primitivi a livello di byte, mentre Copyè l'implementazione generica. La mia domanda è: in quali circostanze dovresti usare BlockCopy? Dovresti usarlo in qualsiasi momento quando copi array di tipi primitivi o dovresti usarlo solo se stai codificando per le prestazioni? C'è qualcosa di intrinsecamente pericoloso nell'usare Buffer.BlockCopyover Array.Copy?


3
Non dimenticare Marshal.Copy:-). Bene, usalo Array.Copyper i tipi di riferimento, i tipi di valore complessi e se il tipo non cambia, Buffer.BlockCopyper la "conversione" tra tipi di valore, array di byte e byte magic. F.ex. la combinazione con StructLayoutè abbastanza potente se sai cosa stai facendo. Per quanto riguarda le prestazioni, sembra che una chiamata non gestita a memcpy/ cpblksia la più veloce per questo - vedere code4k.blogspot.nl/2010/10/… .
atlante

1
Ho fatto alcuni test di benchmark con byte[]. Non c'era differenza nella versione di rilascio. A volte Array.Copy, a volte Buffer.BlockCopy(leggermente) più veloce.
Bitterblue

Nuova risposta completa appena pubblicata di seguito. Si noti che nei casi con dimensioni di buffer ridotte, la copia esplicita del ciclo è solitamente la migliore.
Salsa speciale

Non penso che facciano sempre la stessa cosa: non puoi usare Array.Copy per copiare un array di Ints in un array di Byte, ad esempio
mcmillab

Array.Copyè piuttosto una versione specializzata, ad esempio può copiare solo gli stessi array di rango.
Astrowalker

Risposte:


59

Poiché i parametri Buffer.BlockCopysono basati su byte piuttosto che su indice, è più probabile che si rovini il codice rispetto a se lo si utilizza Array.Copy, quindi lo userei solo Buffer.BlockCopyin una sezione critica per le prestazioni del mio codice.


9
Sono completamente d'accordo. C'è troppo spazio per errori con Buffer.BlockCopy. Mantienilo semplice e non cercare di spremere alcun succo dal tuo programma finché non sai dove si trova il succo (profilazione).
Stephen

5
E se hai a che fare con un byte []? Ci sono altri trucchi con BlockCopy?
thecoop

4
@thecoop: se hai a che fare con un byte [] allora probabilmente va bene usare BlockCopy, a meno che la definizione di "byte" non venga successivamente modificata in qualcosa di diverso da un byte, il che probabilmente avrebbe un effetto piuttosto negativo su altre parti di il tuo codice comunque. :) L'unico altro potenziale problema è che BlockCopy fa solo byte diretti, quindi non tiene conto dell'endianness, ma questo entrerebbe in gioco solo su una macchina non Windows e solo se avessi sbagliato il codice il primo posto. Inoltre, potrebbe esserci qualche strana differenza se stai usando mono.
MusiGenesis

6
Nei miei test, Array.Copy () è molto simile nelle prestazioni a Buffer.BlockCopy (). Buffer.BlockCopy è costantemente <10% più veloce per me quando si tratta di matrici di byte da 640 elementi (che è l'ordinamento che mi interessa di più). Ma dovresti fare i tuoi test con i tuoi dati, perché presumibilmente varierà a seconda dei dati, dei tipi di dati, delle dimensioni dell'array e così via. Dovrei notare che entrambi i metodi sono circa 3 volte più veloci rispetto all'utilizzo di Array.Clone (), e forse 20 volte più veloci rispetto alla copia in un ciclo for.
Ken Smith,

3
@KevinMiller: uh, UInt16sono due byte per elemento. Se si passa questo array a BlockCopy insieme al numero di elementi nell'array, ovviamente verrà copiata solo metà dell'array. Affinché funzioni correttamente, è necessario passare il numero di elementi moltiplicato per la dimensione di ciascun elemento (2) come parametro di lunghezza. msdn.microsoft.com/en-us/library/… e cerca INT_SIZEnegli esempi.
MusiGenesis

129

Preludio

Mi unisco alla festa tardi, ma con 32k visualizzazioni, vale la pena farlo bene. La maggior parte del codice di microbenchmarking nelle risposte pubblicate finora soffre di uno o più gravi difetti tecnici, incluso il mancato spostamento delle allocazioni di memoria fuori dai loop di test (che introduce gravi artefatti GC), il mancato test di flussi di esecuzione variabili rispetto a deterministici, riscaldamento JIT, e non tenere traccia della variabilità intra-test. Inoltre, la maggior parte delle risposte non ha testato gli effetti delle diverse dimensioni del buffer e dei diversi tipi primitivi (rispetto ai sistemi a 32 bit o 64 bit). Per rispondere a questa domanda in modo più completo, l'ho collegato a un framework di microbenchmarking personalizzato che ho sviluppato che riduce la maggior parte dei comuni "trucchi" per quanto possibile. I test sono stati eseguiti in modalità di rilascio .NET 4.0 sia su una macchina a 32 bit che su una macchina a 64 bit. I risultati sono stati calcolati in media su 20 sessioni di test, in cui ciascuna corsa aveva 1 milione di prove per metodo. I tipi primitivi testati eranobyte(1 byte), int(4 byte) e double(8 byte). Tre metodi sono stati testati: Array.Copy(), Buffer.BlockCopy()e semplice assegnazione per-index in un ciclo. I dati sono troppo voluminosi per essere pubblicati qui, quindi riassumerò i punti importanti.

I Takeaway

  • Se la lunghezza del buffer è di circa 75-100 o inferiore, una routine di copia del ciclo esplicita è solitamente più veloce (di circa il 5%) di uno Array.Copy()o Buffer.BlockCopy()di tutti e 3 i tipi primitivi testati su macchine a 32 e 64 bit. Inoltre, la routine di copia del ciclo esplicito ha una variabilità notevolmente inferiore nelle prestazioni rispetto alle due alternative. Le buone prestazioni sono quasi sicuramente dovute alla località di riferimento sfruttata dal caching della memoria CPU L1 / L2 / L3 in combinazione con nessun overhead di chiamata al metodo.
    • Solo per i doublebuffer su macchine a 32 bit : la routine di copia del ciclo esplicita è migliore di entrambe le alternative per tutte le dimensioni del buffer testate fino a 100k. Il miglioramento è del 3-5% migliore rispetto agli altri metodi. Questo perché le prestazioni di Array.Copy()e si Buffer.BlockCopy()riducono completamente al passaggio della larghezza nativa di 32 bit. Quindi presumo che lo stesso effetto si applicherebbe anche ai longbuffer.
  • Per dimensioni del buffer superiori a ~ 100, la copia esplicita del ciclo diventa rapidamente molto più lenta rispetto agli altri 2 metodi (con l'unica eccezione appena annotata). La differenza è più evidente con byte[], dove la copia esplicita del loop può diventare 7 volte o più lenta con buffer di grandi dimensioni.
  • In generale, per tutti e 3 i tipi primitivi testati e per tutte le dimensioni del buffer, Array.Copy()ed Buffer.BlockCopy()eseguiti in modo quasi identico. In media, Array.Copy()sembra avere un leggero margine di circa il 2% o meno del tempo impiegato (ma 0,2% - 0,5% migliore è tipico), anche Buffer.BlockCopy()se occasionalmente lo ha battuto. Per ragioni sconosciute, Buffer.BlockCopy()ha una variabilità intra-test notevolmente maggiore rispetto a Array.Copy(). Questo effetto non può essere eliminato nonostante io abbia provato più mitigazioni e non avessi una teoria utilizzabile sul perché.
  • Poiché Array.Copy()è un metodo "più intelligente", più generale e molto più sicuro, oltre ad essere leggermente più veloce e avere mediamente meno variabilità, dovrebbe essere preferito Buffer.BlockCopy()in quasi tutti i casi comuni. L'unico caso d'uso in cui Buffer.BlockCopy()sarà significativamente migliore è quando i tipi di valore dell'array di origine e di destinazione sono diversi (come sottolineato nella risposta di Ken Smith). Anche se questo scenario non è comune, Array.Copy()può funzionare molto male qui a causa del continuo casting "sicuro" del tipo di valore, rispetto al casting diretto di Buffer.BlockCopy().
  • Ulteriori prove dall'esterno di StackOverflow che Array.Copy()è più veloce rispetto Buffer.BlockCopy()alla copia di array dello stesso tipo possono essere trovate qui .

Per inciso, risulta anche che circa una lunghezza di matrice 100 è quando NET è Array.Clear()prima comincia a battere un esplicito assegnazione ciclo di compensazione di un array (impostazione false, 0o null). Ciò è coerente con le mie scoperte simili sopra. Questi benchmark separati sono stati scoperti online qui: manski.net/2012/12/net-array-clear-vs-arrayx-0-performance
Special Sauce

Quando dici dimensione del buffer; intendi in byte o conteggio elementi?
dmarra

Nella mia risposta precedente, sia la "lunghezza del buffer" che la "dimensione del buffer" si riferiscono generalmente al conteggio degli elementi.
Salsa speciale

Ho un esempio in cui ho bisogno di copiare frequentemente circa 8 byte di dati in una lettura del buffer da una sorgente con offset di 5 byte. Ho trovato che la copia del loop esplicita è significativamente più veloce quindi usa Buffer.BlockCopy o Array.Copy. Loop Results for 1000000 iterations 17.9515ms. Buffer.BlockCopy Results for 1000000 iterations 39.8937ms. Array.Copy Results for 1000000 iterations 45.9059ms Tuttavia, se la dimensione della copia> ~ 20 byte, il ciclo esplicito è notevolmente più lento.
Tod Cunningham

@TodCunningham, 8 byte di dati? Intendi equivalente lungo? O lancia e copia un singolo elemento (velocissimo) o semplicemente srotola quel ciclo manualmente.
Astrowalker

67

Un altro esempio di quando ha senso usare Buffer.BlockCopy()è quando ti viene fornito un array di primitive (ad esempio, short) e devi convertirlo in un array di byte (ad esempio, per la trasmissione su una rete). Uso spesso questo metodo quando si tratta di audio da Silverlight AudioSink. Fornisce il campione come un short[]array, ma è necessario convertirlo in un byte[]array quando si crea il pacchetto a cui si invia Socket.SendAsync(). Potresti usare BitConvertere iterare attraverso l'array uno per uno, ma è molto più veloce (circa 20 volte nei miei test) solo per fare questo:

Buffer.BlockCopy(shortSamples, 0, packetBytes, 0, shortSamples.Length * sizeof(short)).  

E lo stesso trucco funziona anche al contrario:

Buffer.BlockCopy(packetBytes, readPosition, shortSamples, 0, payloadLength);

Questo è quanto di più vicino si arriva in C # sicuro al (void *)tipo di gestione della memoria così comune in C e C ++.


6
È un'idea interessante: hai mai avuto problemi con l'endianness?
Phillip

Sì, penso che potresti incontrare quel problema, a seconda del tuo scenario. I miei scenari sono stati tipicamente (a) ho bisogno di passare avanti e indietro tra array di byte e array brevi sulla stessa macchina, oppure (b) mi capita di sapere che sto inviando i miei dati a macchine dello stesso endianness, e di cui controllo il lato remoto. Ma se stavi usando un protocollo per il quale la macchina remota si aspettava che i dati fossero inviati in ordine di rete piuttosto che in ordine host, sì, questo approccio ti darebbe problemi.
Ken Smith

Ken ha anche un articolo su BlockCopy sul suo blog: blog.wouldbetheologian.com/2011/11/…
Drew Noakes,

4
Nota che a partire da .Net Core 2.1 puoi farlo senza copiare. MemoryMarshal.AsBytes<T>o MemoryMarshal.Cast<TFrom, TTo>lascia che tu interpreti la sequenza di una primitiva come una sequenza di un'altra primitiva.
Timo

16

Sulla base dei miei test, le prestazioni non sono un motivo per preferire Buffer.BlockCopy su Array.Copy. Dal mio test Array.Copy è effettivamente più veloce di Buffer.BlockCopy.

var buffer = File.ReadAllBytes(...);

var length = buffer.Length;
var copy = new byte[length];

var stopwatch = new Stopwatch();

TimeSpan blockCopyTotal = TimeSpan.Zero, arrayCopyTotal = TimeSpan.Zero;

const int times = 20;

for (int i = 0; i < times; ++i)
{
    stopwatch.Start();
    Buffer.BlockCopy(buffer, 0, copy, 0, length);
    stopwatch.Stop();

    blockCopyTotal += stopwatch.Elapsed;

    stopwatch.Reset();

    stopwatch.Start();
    Array.Copy(buffer, 0, copy, 0, length);
    stopwatch.Stop();

    arrayCopyTotal += stopwatch.Elapsed;

    stopwatch.Reset();
}

Console.WriteLine("bufferLength: {0}", length);
Console.WriteLine("BlockCopy: {0}", blockCopyTotal);
Console.WriteLine("ArrayCopy: {0}", arrayCopyTotal);
Console.WriteLine("BlockCopy (average): {0}", TimeSpan.FromMilliseconds(blockCopyTotal.TotalMilliseconds / times));
Console.WriteLine("ArrayCopy (average): {0}", TimeSpan.FromMilliseconds(arrayCopyTotal.TotalMilliseconds / times));

Output di esempio:

bufferLength: 396011520
BlockCopy: 00:00:02.0441855
ArrayCopy: 00:00:01.8876299
BlockCopy (average): 00:00:00.1020000
ArrayCopy (average): 00:00:00.0940000

1
Mi dispiace che questa risposta sia più un commento, ma era troppo lungo per un commento. Poiché il consenso sembrava essere che Buffer.BlockCopy fosse migliore per le prestazioni, ho pensato che tutti dovessero essere consapevoli del fatto che non ero in grado di confermare tale consenso con i test.
Kevin

10
Penso che ci sia un problema con la tua metodologia di test. La maggior parte della differenza di tempo che stai notando è il risultato dell'applicazione che gira, che si memorizza nella cache, che esegue JIT, quel genere di cose. Provalo con un buffer più piccolo, ma qualche migliaio di volte; e poi ripetere l'intero test in un ciclo una mezza dozzina di volte e prestare attenzione solo all'ultima esecuzione. I miei test hanno Buffer.BlockCopy () in esecuzione forse il 5% più veloce di Array.Copy () per array da 640 byte. Non molto più veloce, ma un po '.
Ken Smith,

2
Ho misurato lo stesso per un problema specifico, non ho potuto vedere alcuna differenza di prestazioni tra Array.Copy () e Buffer.BlockCopy () . Semmai, BlockCopy ha introdotto unsafey che in realtà ha ucciso la mia app in un'istanza.
gatopeich

1
Proprio come aggiungere Array.Copy supporta long per la posizione di origine, quindi l'interruzione in array di grandi byte non genererà un'eccezione fuori intervallo.
Alxwest

2
Sulla base dei test che ho appena fatto ( bitbucket.org/breki74/tutis/commits/… ) direi che non c'è alcuna differenza pratica nelle prestazioni tra i due metodi quando hai a che fare con array di byte.
Igor Brejc

4

ArrayCopy è più intelligente di BlockCopy. Capisce come copiare gli elementi se l'origine e la destinazione sono lo stesso array.

Se popoliamo un array int con 0,1,2,3,4 e applichiamo:

Array.Copy (array, 0, array, 1, array.Length - 1);

finiamo con 0,0,1,2,3 come previsto.

Prova questo con BlockCopy e otteniamo: 0,0,2,3,4. Se assegno array[0]=-1dopo, diventa -1,0,2,3,4 come previsto, ma se la lunghezza dell'array è pari, come 6, otteniamo -1,256,2,3,4,5. Roba pericolosa. Non utilizzare BlockCopy se non per copiare un array di byte in un altro.

C'è un altro caso in cui puoi usare solo Array.Copy: se la dimensione dell'array è maggiore di 2 ^ 31. Array.Copy ha un overload con un longparametro size. BlockCopy non ce l'ha.


2
I risultati dei tuoi test con BlockCopy non sono inaspettati. È perché Block copy cerca di copiare blocchi di dati alla volta anziché un byte alla volta. Su un sistema a 32 bit copia 4 byte alla volta, su un sistema a 64 bit copia 8 byte alla volta.
Pharap

Quindi previsto comportamento indefinito.
binki

2

Per pesare su questo argomento, se non si sta attenti a come hanno creato questo benchmark, potrebbero essere facilmente fuorviati. Ho scritto un test molto semplice per illustrare questo. Nel mio test di seguito se cambio l'ordine dei miei test tra l'avvio Buffer.BlockCopy prima o Array.Copy quello che va per primo è quasi sempre il più lento (anche se è uno vicino). Ciò significa che per una serie di motivi che non andrò semplicemente a eseguire i test più volte, specialmente uno dopo l'altro, non darà risultati accurati.

Ho fatto ricorso a mantenere il test così com'è con 1000000 tentativi ciascuno per un array di 1000000 doppi sequenziali. Tuttavia, allora ignoro i primi 900000 cicli e faccio la media del resto. In tal caso il Buffer è superiore.

private static void BenchmarkArrayCopies()
        {
            long[] bufferRes = new long[1000000];
            long[] arrayCopyRes = new long[1000000];
            long[] manualCopyRes = new long[1000000];

            double[] src = Enumerable.Range(0, 1000000).Select(x => (double)x).ToArray();

            for (int i = 0; i < 1000000; i++)
            {
                bufferRes[i] = ArrayCopyTests.ArrayBufferBlockCopy(src).Ticks;
            }

            for (int i = 0; i < 1000000; i++)
            {
                arrayCopyRes[i] = ArrayCopyTests.ArrayCopy(src).Ticks;
            }

            for (int i = 0; i < 1000000; i++)
            {
                manualCopyRes[i] = ArrayCopyTests.ArrayManualCopy(src).Ticks;
            }

            Console.WriteLine("Loop Copy: {0}", manualCopyRes.Average());
            Console.WriteLine("Array.Copy Copy: {0}", arrayCopyRes.Average());
            Console.WriteLine("Buffer.BlockCopy Copy: {0}", bufferRes.Average());

            //more accurate results - average last 1000

            Console.WriteLine();
            Console.WriteLine("----More accurate comparisons----");

            Console.WriteLine("Loop Copy: {0}", manualCopyRes.Where((l, i) => i > 900000).ToList().Average());
            Console.WriteLine("Array.Copy Copy: {0}", arrayCopyRes.Where((l, i) => i > 900000).ToList().Average());
            Console.WriteLine("Buffer.BlockCopy Copy: {0}", bufferRes.Where((l, i) => i > 900000).ToList().Average());
            Console.ReadLine();
        }

public class ArrayCopyTests
    {
        private const int byteSize = sizeof(double);

        public static TimeSpan ArrayBufferBlockCopy(double[] original)
        {
            Stopwatch watch = new Stopwatch();
            double[] copy = new double[original.Length];
            watch.Start();
            Buffer.BlockCopy(original, 0 * byteSize, copy, 0 * byteSize, original.Length * byteSize);
            watch.Stop();
            return watch.Elapsed;
        }

        public static TimeSpan ArrayCopy(double[] original)
        {
            Stopwatch watch = new Stopwatch();
            double[] copy = new double[original.Length];
            watch.Start();
            Array.Copy(original, 0, copy, 0, original.Length);
            watch.Stop();
            return watch.Elapsed;
        }

        public static TimeSpan ArrayManualCopy(double[] original)
        {
            Stopwatch watch = new Stopwatch();
            double[] copy = new double[original.Length];
            watch.Start();
            for (int i = 0; i < original.Length; i++)
            {
                copy[i] = original[i];
            }
            watch.Stop();
            return watch.Elapsed;
        }
    }

https://github.com/chivandikwa/Random-Benchmarks


5
Non vedo alcun risultato temporale nella tua risposta. Includere l'output della console.
ToolmakerSteve

0

Voglio solo aggiungere il mio caso di test che mostra ancora una volta BlockCopy non ha alcun vantaggio "PERFORMANCE" su Array.Copy. Sembrano avere le stesse prestazioni in modalità di rilascio sulla mia macchina (entrambi impiegano circa 66 ms per copiare 50 milioni di numeri interi). In modalità debug, BlockCopy è solo leggermente più veloce.

    private static T[] CopyArray<T>(T[] a) where T:struct 
    {
        T[] res = new T[a.Length];
        int size = Marshal.SizeOf(typeof(T));
        DateTime time1 = DateTime.Now;
        Buffer.BlockCopy(a,0,res,0, size*a.Length);
        Console.WriteLine("Using Buffer blockcopy: {0}", (DateTime.Now - time1).Milliseconds);
        return res;
    }

    static void Main(string[] args)
    {
        int simulation_number = 50000000;
        int[] testarray1 = new int[simulation_number];

        int begin = 0;
        Random r = new Random();
        while (begin != simulation_number)
        {
            testarray1[begin++] = r.Next(0, 10000);
        }

        var copiedarray = CopyArray(testarray1);

        var testarray2 = new int[testarray1.Length];
        DateTime time2 = DateTime.Now;
        Array.Copy(testarray1, testarray2, testarray1.Length);
        Console.WriteLine("Using Array.Copy(): {0}", (DateTime.Now - time2).Milliseconds);
    }

3
Senza offesa, ma il risultato del tuo test non è davvero utile;) Prima di tutto "20ms più veloce" non ti dice nulla senza conoscere il tempo complessivo. Hai anche eseguito questi due test in modo molto diverso. Il caso BlockCopy ha una chiamata di metodo aggiuntiva e l'allocazione del tuo array di destinazione che non hai nel tuo caso Array.Copy. A causa delle fluttuazioni del multithreading (possibile cambio di attività, interruttore principale) è possibile ottenere facilmente risultati diversi ogni volta che si esegue il test.
Bunny83

@ Bunny83 grazie per il commento. Ho leggermente modificato la posizione del timer che dovrebbe dare un confronto più equo ora. E sono un po 'sorpreso che blockcopy non sia affatto più veloce di array.copy.
stt106
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.