É melhor usar std :: memcpy () ou std :: copy () em termos de desempenho?


163

É melhor usar memcpycomo mostrado abaixo ou é melhor usar std::copy()em termos de desempenho? Por quê?

char *bits = NULL;
...

bits = new (std::nothrow) char[((int *) copyMe->bits)[0]];
if (bits == NULL)
{
    cout << "ERROR Not enough memory.\n";
    exit(1);
}

memcpy (bits, copyMe->bits, ((int *) copyMe->bits)[0]);

Observe que charpode ser assinado ou não, dependendo da implementação. Se o número de bytes puder ser> = 128, use unsigned charpara suas matrizes de bytes. (O (int *)elenco seria mais seguro, como (unsigned int *), também.)
Dan Breslau

13
Por que você não está usando std::vector<char>? Ou desde que você diz bits, std::bitset?
GManNickG 16/01

2
Na verdade, você poderia me explicar o que (int*) copyMe->bits[0]faz?
User3728501

4
não tenho certeza por que algo que parece uma bagunça com tão pouco contexto vital fornecido estava em +81, mas ei. @ user3728501 meu palpite é que o início do buffer determina into tamanho, mas isso parece uma receita para um desastre definido pela implementação, como tantas outras coisas aqui.
underscore_d

2
De fato, esse (int *)elenco é apenas um comportamento indefinido puro, não definido pela implementação. Tentar fazer punções de tipo por meio de um elenco viola regras estritas de alias e, portanto, é totalmente indefinido pelo Padrão. (Além disso, em C ++, embora não seja C, você também não pode digitar trocadilhos por meio de um union.) Praticamente a única exceção é se você estiver convertendo para uma variante de char*, mas a permissão não é simétrica.
underscore_d

Respostas:


207

Vou contra a sabedoria geral aqui que std::copyterá uma perda de desempenho leve, quase imperceptível. Acabei de fazer um teste e achei que era falso: notei uma diferença de desempenho. No entanto, o vencedor foi std::copy.

Eu escrevi uma implementação C ++ SHA-2. No meu teste, fiz o hash de 5 strings usando todas as quatro versões do SHA-2 (224, 256, 384, 512) e faço loop 300 vezes. Eu medo os tempos usando o Boost.timer. Esse contador de 300 laços é suficiente para estabilizar completamente meus resultados. Eu executei o teste 5 vezes cada, alternando entre a memcpyversão e a std::copyversão. Meu código aproveita a captura de dados no maior número possível de partes (muitas outras implementações operam com char/ char *, enquanto eu opero com T/ T *(onde Té o maior tipo na implementação do usuário que tem o comportamento correto de estouro)), para um acesso rápido à memória no O maior número de tipos possíveis é essencial para o desempenho do meu algoritmo. Estes são os meus resultados:

Tempo (em segundos) para concluir a execução dos testes SHA-2

std::copy   memcpy  % increase
6.11        6.29    2.86%
6.09        6.28    3.03%
6.10        6.29    3.02%
6.08        6.27    3.03%
6.08        6.27    3.03%

Aumento médio total na velocidade de std :: copy over memcpy: 2.99%

Meu compilador é o gcc 4.6.3 no Fedora 16 x86_64. Meus sinalizadores de otimização são -Ofast -march=native -funsafe-loop-optimizations.

Código para minhas implementações SHA-2.

Decidi também executar um teste na minha implementação MD5. Os resultados foram muito menos estáveis, então decidi fazer 10 corridas. No entanto, após minhas primeiras tentativas, obtive resultados que variaram bastante de uma corrida para a outra, então acho que havia algum tipo de atividade do SO em andamento. Eu decidi começar de novo.

Mesmas configurações e sinalizadores do compilador. Existe apenas uma versão do MD5 e é mais rápida que o SHA-2, então eu fiz 3000 loops em um conjunto semelhante de 5 sequências de teste.

Estes são os meus 10 resultados finais:

Tempo (em segundos) para concluir a execução dos testes MD5

std::copy   memcpy      % difference
5.52        5.56        +0.72%
5.56        5.55        -0.18%
5.57        5.53        -0.72%
5.57        5.52        -0.91%
5.56        5.57        +0.18%
5.56        5.57        +0.18%
5.56        5.53        -0.54%
5.53        5.57        +0.72%
5.59        5.57        -0.36%
5.57        5.56        -0.18%

Redução média total na velocidade de std :: copy over memcpy: 0.11%

Código para minha implementação MD5

Esses resultados sugerem que há alguma otimização que std :: copy usada nos meus testes SHA-2 que std::copynão pôde ser usada nos meus testes MD5. Nos testes SHA-2, ambas as matrizes foram criadas na mesma função que chamou std::copy/ memcpy. Nos meus testes MD5, uma das matrizes foi passada para a função como um parâmetro de função.

Fiz um pouco mais de teste para ver o que eu poderia fazer para std::copyacelerar mais rapidamente. A resposta acabou sendo simples: ative a otimização do tempo do link. Estes são meus resultados com o LTO ativado (opção -flto no gcc):

Tempo (em segundos) para concluir a execução dos testes MD5 com -flto

std::copy   memcpy      % difference
5.54        5.57        +0.54%
5.50        5.53        +0.54%
5.54        5.58        +0.72%
5.50        5.57        +1.26%
5.54        5.58        +0.72%
5.54        5.57        +0.54%
5.54        5.56        +0.36%
5.54        5.58        +0.72%
5.51        5.58        +1.25%
5.54        5.57        +0.54%

Aumento médio total na velocidade de std :: copy over memcpy: 0,72%

Em resumo, não parece haver uma penalidade de desempenho pelo uso std::copy. De fato, parece haver um ganho de desempenho.

Explicação dos resultados

Então, por que pode std::copydar um impulso no desempenho?

Primeiro, eu não esperaria que fosse mais lento para qualquer implementação, desde que a otimização do inlining estivesse ativada. Todos os compiladores se alinham agressivamente; é possivelmente a otimização mais importante porque permite muitas outras otimizações. std::copyÉ possível (e suspeito que todas as implementações do mundo real) detectar que os argumentos são trivialmente copiáveis ​​e que a memória é organizada em seqüência. Isso significa que, na pior das hipóteses, quando memcpyé legal, não std::copydeve ter desempenho pior. A implementação trivial std::copydisso adia memcpydeve atender aos critérios do seu compilador de "sempre alinhar isso ao otimizar velocidade ou tamanho".

No entanto, std::copytambém mantém mais informações. Quando você liga std::copy, a função mantém os tipos intactos. memcpyopera void *, que descarta quase todas as informações úteis. Por exemplo, se eu passar uma matriz de std::uint64_t, o compilador ou o implementador da biblioteca poderá tirar proveito do alinhamento de 64 bits com std::copy, mas pode ser mais difícil fazê-lo memcpy. Muitas implementações de algoritmos como esse funcionam primeiro trabalhando na parte não alinhada no início do intervalo, depois na parte alinhada e depois na parte não alinhada no final. Se tudo estiver garantido para estar alinhado, o código se tornará mais simples e rápido, e mais fácil para o preditor de ramificação do seu processador se corrigir.

Otimização prematura?

std::copyestá em uma posição interessante. Espero que nunca seja mais lento memcpye às vezes mais rápido com qualquer compilador de otimização moderno. Além disso, tudo o que você puder memcpy, você pode std::copy. memcpynão permite nenhuma sobreposição nos buffers, enquanto os std::copysuportes se sobrepõem em uma direção (com std::copy_backwardpara a outra direção de sobreposição). memcpysó funciona em ponteiros, std::copyfunciona em qualquer iteradores ( std::map, std::vector, std::deque, ou meu próprio tipo personalizado). Em outras palavras, você deve usar apenas std::copyquando precisar copiar blocos de dados.


35
Quero enfatizar que isso não significa que std::copyseja 2,99% ou 0,72% ou -0,11% mais rápido que memcpyesses tempos para o programa inteiro ser executado. No entanto, geralmente considero que referências no código real são mais úteis do que referências no código falso. Todo o meu programa teve essa mudança na velocidade de execução. Os efeitos reais de apenas os dois esquemas de cópia terão maiores diferenças do que as mostradas aqui quando tomadas isoladamente, mas isso mostra que eles podem ter diferenças mensuráveis ​​no código real.
David Stone

2
Quero discordar de suas descobertas, mas resultados são resultados: /. No entanto, uma pergunta (eu sei que foi há muito tempo e você não se lembra de pesquisas, então apenas comente da maneira que pensa), provavelmente você não procurou o código de montagem;
ST3

2
Na minha opinião memcpye std::copytem implementações diferentes, portanto, em alguns casos, o compilador otimiza o código circundante e o código de cópia de memória real como uma parte integrante do código. Em outras palavras, às vezes um é melhor que outro e até em outras palavras, decidir qual usar é uma otimização prematura ou até estúpida, porque em todas as situações você precisa fazer novas pesquisas e, além do mais, programas geralmente estão sendo desenvolvidos; algumas pequenas alterações na vantagem da função sobre outras podem ser perdidas.
ST3

3
@ ST3: Eu imagino que, na pior das hipóteses, std::copyé uma função em linha trivial que apenas chama memcpyquando é legal. O alinhamento básico eliminaria qualquer diferença negativa de desempenho. Vou atualizar o post com uma explicação de por que std :: copy pode ser mais rápido.
David Stone

7
Análise muito informativa. Re Redução média total na velocidade de std :: copy over memcpy: 0,11% , enquanto o número estiver correto, os resultados não serão estatisticamente significativos. Um intervalo de confiança de 95% para a diferença de médias é (-0,013s, 0,025), que inclui zero. Como você apontou, houve variação de outras fontes e, com seus dados, você provavelmente diria que o desempenho é o mesmo. Para referência, os outros dois resultados são estatisticamente significativos - as chances de você ver uma diferença em tempos tão extremos por acaso são de cerca de 1 em 100 milhões (primeiro) e 1 em 20.000 (último).
TooTone

78

Todos os compiladores que eu conheço substituirão um simples std::copypor um memcpyquando for apropriado, ou melhor ainda, vetorizar a cópia para que ela seja ainda mais rápida que a memcpy.

De qualquer forma: perfil e descubra você mesmo. Compiladores diferentes farão coisas diferentes, e é bem possível que não faça exatamente o que você pede.

Veja esta apresentação sobre otimizações do compilador (pdf).

Aqui está o que o GCC faz para um simples std::copytipo de POD.

#include <algorithm>

struct foo
{
  int x, y;    
};

void bar(foo* a, foo* b, size_t n)
{
  std::copy(a, a + n, b);
}

Aqui está a desmontagem (apenas com -Ootimização), mostrando a chamada para memmove:

bar(foo*, foo*, unsigned long):
    salq    $3, %rdx
    sarq    $3, %rdx
    testq   %rdx, %rdx
    je  .L5
    subq    $8, %rsp
    movq    %rsi, %rax
    salq    $3, %rdx
    movq    %rdi, %rsi
    movq    %rax, %rdi
    call    memmove
    addq    $8, %rsp
.L5:
    rep
    ret

Se você alterar a assinatura da função para

void bar(foo* __restrict a, foo* __restrict b, size_t n)

então memmovetorna-se um memcpypara uma ligeira melhoria de desempenho. Observe que memcpyele próprio será fortemente vetorizado.


1
Como posso criar perfis. Qual ferramenta usar (no Windows e Linux)?
user576670

5
@ Konrad, você está correto. Mas memmovenão deve ser mais rápido. Em vez disso, deve ser mais lento, porque deve levar em consideração a possibilidade de os dois intervalos de dados se sobreporem. Eu acho que std::copypermite a sobreposição de dados, e por isso tem que ligar memmove.
Charles Salvia

2
@ Konrad: Se o memmove fosse sempre mais rápido que o memcpy, ele chamaria memmove. O que std :: copy realmente pode enviar (se houver) é definido pela implementação, portanto, não é útil mencionar detalhes sem mencionar a implementação.
precisa saber é o seguinte

1
Embora um programa simples para reproduzir esse comportamento, compilado com -O3 no GCC, mostre-me a memcpy. Isso me leva a acreditar que o GCC verifica se há sobreposição de memória.
jweyrich

1
@ Konrad: o padrão std::copypermite sobreposição em uma direção, mas não na outra. O início da saída não pode estar dentro da faixa de entrada, mas o início da entrada pode estar dentro da faixa de saída. Isso é um pouco estranho, porque a ordem das atribuições é definida e uma chamada pode ser UB mesmo que o efeito dessas atribuições, nessa ordem, seja definido. Mas suponho que a restrição permita otimizações de vetorização.
Steve Jessop

24

Sempre use std::copyporque memcpyestá limitado apenas a estruturas de POD no estilo C, e o compilador provavelmente substituirá as chamadas std::copypor memcpyse os destinos forem de fato POD.

Além disso, std::copypode ser usado com muitos tipos de iteradores, não apenas com ponteiros. std::copyé mais flexível, sem perda de desempenho e é o vencedor.


Por que você deve copiar os iteradores?
Atmocreations

3
Você não está copiando os iteradores, mas o intervalo definido por dois iteradores. Por exemplo, std::copy(container.begin(), container.end(), destination);copiará o conteúdo de container(tudo entre begine end) no buffer indicado por destination. std::copynão requer travessuras como &*container.begin()ou &container.back() + 1.
David Stone

16

Em teoria, memcpypode ter uma vantagem de desempenho leve , imperceptível , infinitesimal , apenas porque não possui os mesmos requisitos que std::copy. Na página do manual de memcpy:

Para evitar estouros, o tamanho das matrizes apontadas pelos parâmetros de destino e de origem deve ser de pelo menos num bytes e não deve se sobrepor (para sobreposição de blocos de memória, o memmove é uma abordagem mais segura).

Em outras palavras, memcpypode ignorar a possibilidade de sobreposição de dados. (Passar matrizes sobrepostas para memcpyé um comportamento indefinido.) Portanto, memcpynão é necessário verificar explicitamente essa condição, std::copypois pode ser usado desde que o OutputIteratorparâmetro não esteja no intervalo de origem. Observe que isso não é o mesmo que dizer que o intervalo de origem e o destino não podem se sobrepor.

Portanto, como std::copypossui requisitos um pouco diferentes, em teoria, ele deve ser um pouco (com ênfase extrema em um pouco ) mais lento, pois provavelmente verificará a sobreposição de matrizes C ou delegará a cópia de matrizes C para memmove, que precisa executar o Verifica. Mas, na prática, você (e a maioria dos criadores de perfil) provavelmente nem detectará nenhuma diferença.

Obviamente, se você não estiver trabalhando com PODs , não poderá usá-lo de memcpyqualquer maneira.


7
Isto é verdade para std::copy<char>. Mas std::copy<int>pode assumir que suas entradas estão alinhadas int. Isso fará uma diferença muito maior, porque afeta todos os elementos. A sobreposição é uma verificação única.
MSalters

2
@MSalters, é verdade, mas a maioria das implementações que memcpyeu já vi verificar alinhamento e tentar copiar palavras em vez de byte a byte.
26412 Charles Salvia

1
std :: copy () também pode ignorar a sobreposição de memória. Se você deseja dar suporte à sobreposição de memória, é necessário escrever a lógica para chamar std :: reverse_copy () nas situações apropriadas.
Cygon 06/06/12

2
Existe um argumento oposto que pode ser feito: ao passar pela memcpyinterface, perde as informações de alinhamento. Portanto, memcpyprecisa fazer verificações de alinhamento no tempo de execução para lidar com inícios e fins desalinhados. Esses cheques podem ser baratos, mas não são gratuitos. Considerando que std::copypode evitar essas verificações e vetorizar. Além disso, o compilador pode provar que as matrizes de origem e destino não se sobrepõem e novamente se vetorizam sem que o usuário precise escolher entre memcpye memmove.
Maxim Egorushkin

11

Minha regra é simples. Se você estiver usando C ++, prefira bibliotecas C ++ e não C :)


40
C ++ foi explicitamente projetado para permitir o uso de bibliotecas C. Isso não foi um acidente. Geralmente é melhor usar std :: copy do que memcpy em C ++, mas isso não tem nada a ver com qual deles é C, e esse tipo de argumento geralmente é a abordagem errada.
precisa saber é o seguinte

2
@FredNurk Normalmente, você deseja evitar uma área fraca de C em que C ++ forneça uma alternativa mais segura.
Phil1970

@ Phil1970 Não tenho certeza se C ++ é muito mais seguro nesse caso. Ainda precisamos passar iteradores válidos que não excedam etc. Eu acho que poder usar em std::end(c_arr)vez de c_arr + i_hope_this_is_the_right_number_of elementsé mais seguro? e talvez mais importante, mais claro. E isso seria o ponto Enfatizo, neste caso específico: std::copy()é mais idiomática, mais sustentável se os tipos de iteradores muda mais tarde, leva a sintaxe mais clara, etc.
underscore_d

1
@underscore_d std::copyé mais seguro porque copia corretamente os dados passados, caso não sejam do tipo POD. memcpyfelizmente copiará um std::stringobjeto para um novo byte byte de representação.
Jens

3

Apenas uma pequena adição: a diferença de velocidade entre memcpy()e std::copy()pode variar bastante, dependendo se as otimizações estão ativadas ou desativadas. Com o g ++ 6.2.0 e sem otimizações memcpy(), ganha claramente:

Benchmark             Time           CPU Iterations
---------------------------------------------------
bm_memcpy            17 ns         17 ns   40867738
bm_stdcopy           62 ns         62 ns   11176219
bm_stdcopy_n         72 ns         72 ns    9481749

Quando as otimizações estão ativadas ( -O3), tudo parece praticamente o mesmo novamente:

Benchmark             Time           CPU Iterations
---------------------------------------------------
bm_memcpy             3 ns          3 ns  274527617
bm_stdcopy            3 ns          3 ns  272663990
bm_stdcopy_n          3 ns          3 ns  274732792

Quanto maior a matriz, menos perceptível o efeito fica, mas mesmo assim N=1000 memcpy()é duas vezes mais rápido quando as otimizações não estão ativadas.

Código fonte (requer o Google Benchmark):

#include <string.h>
#include <algorithm>
#include <vector>
#include <benchmark/benchmark.h>

constexpr int N = 10;

void bm_memcpy(benchmark::State& state)
{
  std::vector<int> a(N);
  std::vector<int> r(N);

  while (state.KeepRunning())
  {
    memcpy(r.data(), a.data(), N * sizeof(int));
  }
}

void bm_stdcopy(benchmark::State& state)
{
  std::vector<int> a(N);
  std::vector<int> r(N);

  while (state.KeepRunning())
  {
    std::copy(a.begin(), a.end(), r.begin());
  }
}

void bm_stdcopy_n(benchmark::State& state)
{
  std::vector<int> a(N);
  std::vector<int> r(N);

  while (state.KeepRunning())
  {
    std::copy_n(a.begin(), N, r.begin());
  }
}

BENCHMARK(bm_memcpy);
BENCHMARK(bm_stdcopy);
BENCHMARK(bm_stdcopy_n);

BENCHMARK_MAIN()

/* EOF */

18
Medir o desempenho com otimizações desativadas é ... bem ... praticamente inútil ... Se você estiver interessado em desempenho, não será compilado sem otimizações.
Bolov #

3
@bolov Nem sempre. Um programa relativamente rápido em depuração é importante em alguns casos.
Acorn

2

Se você realmente precisa do desempenho máximo de cópia (o que talvez não seja), use nenhum deles .

Há um monte de que pode ser feito para cópia de memória otimizar - ainda mais se você estiver disposto a usar vários segmentos / núcleos para ele. Veja, por exemplo:

O que está faltando / abaixo do ideal nessa implementação memcpy?

a pergunta e algumas das respostas sugeriram implementações ou links para implementações.


4
modo pedante: com a ressalva usual de que " não use nenhum deles " significa se você provou ter uma situação / requisito altamente específico para o qual nenhuma das funções padrão fornecidas por sua implementação é rápida o suficiente ; caso contrário, minha preocupação usual é que as pessoas que não provaram isso se desviem da otimização prematura do código de cópia, em vez das partes geralmente mais úteis do programa.
Underscore_d

-2

A criação de perfil mostra essa afirmação: std::copy()é sempre tão rápida quanto memcpy()ou mais rápida é falsa.

Meu sistema:

HP-Compaq-dx7500-Microtower 3.13.0-24-genérico # 47-Ubuntu SMP Fri May 2 23:30:00 UTC 2014 x86_64 x86_64 x86_64 GNU / Linux.

gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2

O código (idioma: c ++):

    const uint32_t arr_size = (1080 * 720 * 3); //HD image in rgb24
    const uint32_t iterations = 100000;
    uint8_t arr1[arr_size];
    uint8_t arr2[arr_size];
    std::vector<uint8_t> v;

    main(){
        {
            DPROFILE;
            memcpy(arr1, arr2, sizeof(arr1));
            printf("memcpy()\n");
        }

        v.reserve(sizeof(arr1));
        {
            DPROFILE;
            std::copy(arr1, arr1 + sizeof(arr1), v.begin());
            printf("std::copy()\n");
        }

        {
            time_t t = time(NULL);
            for(uint32_t i = 0; i < iterations; ++i)
                memcpy(arr1, arr2, sizeof(arr1));
            printf("memcpy()    elapsed %d s\n", time(NULL) - t);
        }

        {
            time_t t = time(NULL);
            for(uint32_t i = 0; i < iterations; ++i)
                std::copy(arr1, arr1 + sizeof(arr1), v.begin());
            printf("std::copy() elapsed %d s\n", time(NULL) - t);
        }
    }

g ++ -O0 -o test_stdcopy test_stdcopy.cpp

memcpy () profile: main: 21: now: 1422969084: 04859 decorrido: 2650 us
std :: copy () profile: main: 27: now: 1422969084: 04862 decorrido: 2745 us
memcpy () decorrido 44 s std :: copy ( ) decorridos 45 s

g ++ -O3 -o test_stdcopy test_stdcopy.cpp

memcpy () profile: main: 21: now: 1422969601: 04939 decorrido: 2385 us
std :: copy () profile: main: 28: now: 1422969601: 04941 decorrido: 2690 us
memcpy () decorrido 27 s std :: copy ( ) decorridos 43 s

O Alerta Vermelho apontou que o código usa memcpy de matriz para matriz e std :: copy de matriz para vetor. Esse poderia ser um motivo para memcpy mais rápido.

Uma vez que existe

v.reserve (sizeof (arr1));

não deve haver diferença na cópia para vetor ou matriz.

O código é fixo para usar a matriz nos dois casos. memcpy ainda mais rápido:

{
    time_t t = time(NULL);
    for(uint32_t i = 0; i < iterations; ++i)
        memcpy(arr1, arr2, sizeof(arr1));
    printf("memcpy()    elapsed %ld s\n", time(NULL) - t);
}

{
    time_t t = time(NULL);
    for(uint32_t i = 0; i < iterations; ++i)
        std::copy(arr1, arr1 + sizeof(arr1), arr2);
    printf("std::copy() elapsed %ld s\n", time(NULL) - t);
}

memcpy()    elapsed 44 s
std::copy() elapsed 48 s 

1
errado, sua criação de perfil mostra que copiar em uma matriz é mais rápido do que copiar em um vetor. Fora do assunto.
Red Alert

Eu posso estar errado, mas no seu exemplo corrigido, com o memcpy, você não está copiando arr2 para arr1, enquanto que com std :: copy, você está copiando arr1 para arr2? ... O que você pode fazer é tornar várias alternativas experimentos (uma vez um lote de memcpy, uma vez um lote de std :: copy e, em seguida, novamente com memcopy, etc., várias vezes). Então, eu usaria clock () em vez de time (), porque quem sabe o que seu PC poderia estar fazendo além desse programa. Apenas meus dois centavos, embora ... :-)
paercebal

7
Então, mudar std::copyde um vetor para um array de alguma forma memcpylevou quase o dobro do tempo? Esses dados são altamente suspeitos. Compilei seu código usando gcc com -O3, e o assembly gerado é o mesmo para os dois loops. Portanto, qualquer diferença de tempo observada em sua máquina é apenas incidental.
Red Alert
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.