Array.Copy vs Buffer.BlockCopy

124

Array.Copy e Buffer.BlockCopy fazem a mesma coisa, mas BlockCopytêm como objetivo a cópia rápida da matriz primitiva no nível de bytes, enquanto Copyé a implementação de uso geral. Minha pergunta é - em que circunstâncias você deve usar BlockCopy? Você deve usá-lo a qualquer momento quando estiver copiando matrizes de tipo primitivo ou deve usá-lo apenas se estiver codificando para desempenho? Existe algo inerentemente perigoso sobre o uso Buffer.BlockCopyexcessivo Array.Copy?

thecoop
fonte
3
Não esqueça Marshal.Copy:-). Bem, use Array.Copypara tipos de referência, tipos de valor complexos e, se o tipo não for alterado, Buffer.BlockCopypara "conversão" entre tipos de valor, matrizes de bytes e magia de bytes. F.ex. a combinação com StructLayouté bastante poderosa se você souber o que está fazendo. Quanto ao desempenho, parece que uma chamada não gerenciada para memcpy/ cpblké a mais rápida para isso - consulte code4k.blogspot.nl/2010/10/… .
Atlaste
1
Eu fiz alguns testes de benchmark com byte[]. Não houve diferença na versão Release. Às vezes Array.Copy, às vezes Buffer.BlockCopy(um pouco) mais rápido.
Bitterblue
Nova resposta abrangente postada abaixo. Observe que, nos casos com tamanhos de buffer pequenos, a cópia explícita do loop geralmente é melhor.
Special Sauce
Eu não acho que eles sempre fazem a mesma coisa - você não pode usar Array.Copy para copiar um array de inteiros para uma matriz de bytes, por exemplo
mcmillab
Array.Copyé uma versão especializada - por exemplo, ele pode copiar apenas as mesmas matrizes de classificação.
Astrowalker 11/10/19

Respostas:

59

Como os parâmetros Buffer.BlockCopysão baseados em bytes e não em índice, é mais provável que você estrague seu código do que se você usar Array.Copy, então eu usaria apenas Buffer.BlockCopyem uma seção crítica do desempenho do meu código.

MusiGenesis
fonte
9
Totalmente de acordo. Há muito espaço para erro com o Buffer.BlockCopy. Mantenha-o simples e não tente espremer nenhum suco do seu programa até saber onde está o suco (criação de perfil).
Stephen
5
E se você estiver lidando com um byte []? Existem outras dicas com o BlockCopy?
Thecoop 09/09/09
4
@ thecoop: se você está lidando com um byte [], provavelmente é bom usar o BlockCopy, a menos que a definição de "byte" seja alterada posteriormente para algo diferente de um byte, o que provavelmente teria um efeito bastante negativo em outras partes do seu código de qualquer maneira. :) A única outra pegada em potencial é que o BlockCopy apenas executa bytes diretos, por isso não leva em consideração o endianness, mas isso só entra em jogo em uma máquina que não seja Windows, e somente se você estragou o código o primeiro lugar. Além disso, pode haver alguma diferença estranha se você estiver usando mono.
MusiGenesis
6
Nos meus próprios testes, o Array.Copy () é muito semelhante no desempenho ao Buffer.BlockCopy (). O Buffer.BlockCopy é consistentemente <10% mais rápido para mim ao lidar com matrizes de 640 bytes de elemento (que é do tipo que mais me interessa). Mas você deve fazer seu próprio teste com seus próprios dados, pois presumivelmente variará dependendo dos dados, tipos de dados, tamanhos de matriz e assim por diante. Devo observar que ambos os métodos são aproximadamente 3x mais rápidos que o uso de Array.Clone () e talvez 20x mais rápidos do que copiá-lo em um loop for.
Ken Smith
3
@ KevinMiller: uh, UInt16são dois bytes por elemento. Se você passar essa matriz para o BlockCopy junto com o número de elementos na matriz, é claro que apenas metade da matriz será copiada. Para que isso funcione corretamente, você precisará passar o número de elementos vezes o tamanho de cada elemento (2) como parâmetro de comprimento. msdn.microsoft.com/en-us/library/… e procure INT_SIZEnos exemplos.
MusiGenesis 7/08
129

Prelúdio

Estou entrando na festa tarde, mas com 32 mil visualizações, vale a pena fazer isso direito. A maioria do código de marca de microbench nas respostas postadas até agora sofre de uma ou mais falhas técnicas graves, incluindo não mover alocações de memória para fora dos loops de teste (que introduz artefatos graves de GC), não testar fluxos de execução variáveis ​​versus determinísticos, aquecimento do JIT, e não rastrear a variabilidade intra-teste. Além disso, a maioria das respostas não testou os efeitos de tamanhos variados de buffer e tipos primitivos variados (em relação aos sistemas de 32 ou 64 bits). Para abordar essa questão de maneira mais abrangente, vinculei-a a uma estrutura de microbenchmarking personalizada que desenvolvi que reduz a maior parte das "dicas" comuns na medida do possível. Os testes foram executados no modo de versão .NET 4.0 em máquinas de 32 bits e de 64 bits. A média dos resultados foi de mais de 20 execuções de teste, nas quais cada execução teve 1 milhão de tentativas por método. Os tipos primitivos testados forambyte(1 byte), int(4 bytes) e double(8 bytes). Três métodos foram testados: Array.Copy(), Buffer.BlockCopy(), e simples de atribuição num ciclo per-índice. Os dados são muito volumosos para serem postados aqui, então vou resumir os pontos importantes.

The Takeaways

  • Se o comprimento do tampão é de cerca de 75-100 ou menos, uma rotina exemplar circuito explícito é geralmente mais rápida (cerca de 5%) do que qualquer um Array.Copy()ou Buffer.BlockCopy()para todos os 3 tipos de primitivas testadas em ambas as máquinas de 32 bits e de 64 bits. Além disso, a rotina de cópia de loop explícita tem uma variação notavelmente menor no desempenho em comparação com as duas alternativas. O bom desempenho é quase certamente devido à localidade de referência explorada pelo cache de memória CPU L1 / L2 / L3 em conjunto com nenhuma sobrecarga de chamada de método.
    • Somente para doublebuffers em máquinas de 32 bits : A rotina de cópia de loop explícita é melhor que as duas alternativas para todos os tamanhos de buffer testados até 100k. A melhoria é 3-5% melhor que os outros métodos. Isso ocorre porque o desempenho Array.Copy()e Buffer.BlockCopy()fica totalmente degradado ao passar a largura nativa de 32 bits. Portanto, presumo que o mesmo efeito se aplicaria aos longbuffers também.
  • Para tamanhos de buffer que excedem ~ 100, a cópia explícita do loop rapidamente se torna muito mais lenta que os outros 2 métodos (com a única exceção específica mencionada). A diferença é mais notável em byte[]que a cópia explícita de loop pode se tornar 7x ou mais lenta em tamanhos de buffer grandes.
  • Em geral, para todos os três tipos primitivos testados e em todos os tamanhos de buffer, Array.Copy()e Buffer.BlockCopy()executados quase de forma idêntica. Em média, Array.Copy()parece ter uma margem muito pequena de cerca de 2% ou menos do tempo gasto (mas é típico de 0,2% a 0,5% melhor), embora Buffer.BlockCopy()ocasionalmente o superasse. Por razões desconhecidas, Buffer.BlockCopy()apresenta uma variabilidade intra-teste notavelmente maior que Array.Copy(). Esse efeito não pôde ser eliminado, apesar de eu tentar várias mitigações e não ter uma teoria operacional sobre o porquê.
  • Por Array.Copy()ser um método "mais inteligente", mais geral e muito mais seguro, além de ser um pouco mais rápido e com menor variabilidade em média, deve ser preferido Buffer.BlockCopy()em quase todos os casos comuns. O único caso de uso em Buffer.BlockCopy()que será significativamente melhor é quando os tipos de valores da matriz de origem e de destino são diferentes (como apontado na resposta de Ken Smith). Embora esse cenário não seja comum, ele Array.Copy()pode ter um desempenho muito ruim aqui devido à conversão contínua do tipo de valor "seguro", em comparação à conversão direta de Buffer.BlockCopy().
  • Evidência adicional externa do StackOverflow, que Array.Copy()é mais rápida do que Buffer.BlockCopy()a cópia de matriz do mesmo tipo, pode ser encontrada aqui .
Molho Especial
fonte
Como um aparte, também acontece que em torno de um comprimento de matriz de 100 é quando .NET é Array.Clear()primeiro começa a bater uma compensação atribuição de loop explícita de uma matriz (configuração para false, 0, ou null). Isso é consistente com minhas descobertas semelhantes acima. Esses benchmarks separados foram descobertos on-line aqui: manski.net/2012/12/net-array-clear-vs-arrayx-0-performance
Special Sauce
Quando você diz o tamanho do buffer; você quer dizer em bytes ou contagem de elementos?
dmarra 8/07
Na minha resposta acima, "tamanho do buffer" e "tamanho do buffer" geralmente se referem à contagem de elementos.
Special Sauce
Eu tenho um exemplo em que preciso copiar frequentemente cerca de 8 bytes de dados em uma leitura de buffer de uma origem compensada por 5 bytes. Eu achei a cópia explícita do loop muito mais rápida que o Buffer.BlockCopy ou 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 No entanto, se o tamanho da cópia for> ~ 20 bytes, o loop explícito será significativamente mais lento.
Tod Cunningham
@TodCunningham, 8 bytes de dados? Você quer dizer equivalente longo? Crie e copie um único elemento (rápido demais) ou simplesmente desenrole esse loop manualmente.
astrowalker 7/04
67

Outro exemplo de quando faz sentido usar Buffer.BlockCopy()é quando você recebe uma matriz de primitivas (digamos, shorts) e precisa convertê-lo em uma matriz de bytes (digamos, para transmissão em uma rede). Uso esse método frequentemente ao lidar com áudio do Silverlight AudioSink. Ele fornece a amostra como uma short[]matriz, mas você precisa convertê-la em uma byte[]matriz ao criar o pacote ao qual você envia Socket.SendAsync(). Você pode usar BitConvertere percorrer a matriz um por um, mas é muito mais rápido (cerca de 20x nos meus testes) apenas para fazer isso:

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

E o mesmo truque também funciona ao contrário:

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

Isso é o mais próximo que você obtém do C # seguro do (void *)tipo de gerenciamento de memória tão comum em C e C ++.

Ken Smith
fonte
6
Essa é uma ideia legal - você já teve problemas com endianness?
Phillip
Sim, acho que você pode encontrar esse problema, dependendo do seu cenário. Meus próprios cenários costumam ser: (a) preciso alternar entre matrizes de bytes e matrizes curtas na mesma máquina; ou (b) sei que estou enviando meus dados para máquinas da mesma endianness, e que eu controlo o lado remoto. Mas se você estivesse usando um protocolo para o qual a máquina remota esperava que os dados fossem enviados em ordem de rede em vez de ordem de host, sim, essa abordagem causaria problemas.
Ken Smith
Ken também tem um artigo sobre BlockCopy em seu blog: blog.wouldbetheologian.com/2011/11/…
Drew Noakes
4
Observe que, desde o .Net Core 2.1, você pode fazer isso sem copiar. MemoryMarshal.AsBytes<T>ou MemoryMarshal.Cast<TFrom, TTo>permita que você interprete sua sequência de um primitivo como um sequeno de outro primitivo.
Timo
16

Com base nos meus testes, o desempenho não é um motivo para preferir Buffer.BlockCopy sobre Array.Copy. Do meu teste Array.Copy é realmente mais rápido que 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));

Saída de exemplo:

bufferLength: 396011520
BlockCopy: 00:00:02.0441855
ArrayCopy: 00:00:01.8876299
BlockCopy (average): 00:00:00.1020000
ArrayCopy (average): 00:00:00.0940000
Kevin
fonte
1
Desculpe por esta resposta ser mais um comentário, mas foi muito longo para um comentário. Como o consenso parecia ser que o Buffer.BlockCopy era melhor para o desempenho, achei que todos deveriam estar cientes de que eu não era capaz de confirmar esse consenso com o teste.
22411 Kevin
10
Eu acho que há um problema com sua metodologia de teste. Na maioria das vezes, a diferença de tempo que você está notando é o resultado do aplicativo girando, armazenando em cache, executando o JIT, esse tipo de coisa. Experimente com um buffer menor, mas alguns milhares de vezes; e repita o teste inteiro em um loop meia dúzia de vezes e preste atenção apenas na última execução. Meu próprio teste tem Buffer.BlockCopy () sendo executado talvez 5% mais rápido que Array.Copy () para matrizes de 640 bytes. Não muito mais rápido, mas um pouco.
Ken Smith
2
Eu medi o mesmo para um problema específico, não pude ver nenhuma diferença de desempenho entre Array.Copy () e Buffer.BlockCopy () . Se alguma coisa, a BlockCopy introduziu o inseguro que realmente matou meu aplicativo em uma instância.
gatopeich
1
Assim como adicionar Array.Copy suporta muito tempo para a posição de origem, de modo que a divisão em matrizes de bytes grandes não gera uma exceção fora do intervalo.
Alxwest
2
Com base nos testes que eu acabei de fazer ( bitbucket.org/breki74/tutis/commits/… ), eu diria que não há diferença prática de desempenho entre os dois métodos quando você está lidando com matrizes de bytes.
Igor Brejc 22/03
4

ArrayCopy é mais inteligente que BlockCopy. Ele descobre como copiar elementos se a origem e o destino forem da mesma matriz.

Se preenchermos uma matriz int com 0,1,2,3,4 e aplicamos:

Array.Copy (matriz, 0, matriz, 1, matriz.Comprimento - 1);

terminamos com 0,0,1,2,3 conforme o esperado.

Tente isso com o BlockCopy e obteremos: 0,0,2,3,4. Se eu atribuir array[0]=-1depois disso, ele se tornará -1,0,2,3,4 conforme o esperado, mas se o comprimento da matriz for par, como 6, obteremos -1,256,2,3,4,5. Coisas perigosas. Não use o BlockCopy além de copiar uma matriz de bytes para outra.

Há outro caso em que você só pode usar Array.Copy: se o tamanho da matriz for maior que 2 ^ 31. Array.Copy tem uma sobrecarga com um longparâmetro de tamanho. BlockCopy não tem isso.

user3523091
fonte
2
Os resultados dos seus testes com o BlockCopy não são inesperados. Isso ocorre porque a cópia em bloco tenta copiar partes de dados de cada vez, em vez de um byte de cada vez. Em um sistema de 32 bits, ele copia 4 bytes de cada vez; em um sistema de 64 bits, copia 8 bytes de cada vez.
Pharap
Comportamento indefinido tão esperado.
binki
2

Para refletir sobre esse argumento, se alguém não tomar cuidado com a forma como cria esse benchmark, pode ser facilmente enganado. Eu escrevi um teste muito simples para ilustrar isso. No meu teste abaixo, se eu trocar a ordem dos meus testes entre iniciar o Buffer.BlockCopy primeiro ou Array.Copy, o que for primeiro é quase sempre o mais lento (embora seja próximo). Isso significa que, por várias razões em que não vou simplesmente executar os testes várias vezes, um após o outro não fornecerá resultados precisos.

Eu tentei manter o teste como está com 1000000 tentativas cada uma para uma matriz de 1000000 duplas seqüenciais. No entanto, desconsiderei os primeiros 900000 ciclos e calculei a média do restante. Nesse caso, o buffer é superior.

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

Thulani Chivandikwa
fonte
5
Não vejo nenhum resultado de tempo na sua resposta. Por favor inclua a saída do console.
Home
0

Só quero adicionar meu caso de teste, que mostra novamente que o BlockCopy não possui o benefício 'DESEMPENHO' sobre o Array.Copy. Eles parecem ter o mesmo desempenho no modo de liberação na minha máquina (ambos levam cerca de 66ms para copiar 50 milhões de números inteiros). No modo de depuração, o BlockCopy é apenas um pouco mais rápido.

    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);
    }
stt106
fonte
3
Sem ofensa, mas o resultado do teste não é realmente útil;) Antes de tudo, "20ms mais rápido" não diz nada sem você saber o tempo todo. Você também realizou esses dois testes de uma maneira muito diferente. O caso BlockCopy possui uma chamada de método adicional e a alocação do seu array de destino que você não possui no seu caso Array.Copy. Devido a flutuações de multithreading (possível comutador de tarefas, comutador principal), você pode facilmente obter resultados diferentes sempre que executar o teste.
precisa saber é o seguinte
@ Bunny83 obrigado pelo comentário. Modifiquei levemente a localização do temporizador, o que deve oferecer uma comparação mais justa agora. E estou um pouco surpreso que o blockcopy não seja mais rápido que o array.copy.
stt106