Por que memmove é mais rápido que memcpy?

89

Estou investigando pontos de acesso de desempenho em um aplicativo que gasta 50% do tempo no memmove (3). O aplicativo insere milhões de inteiros de 4 bytes em matrizes classificadas e usa memmove para deslocar os dados "para a direita" a fim de liberar espaço para o valor inserido.

Minha expectativa era que copiar a memória fosse extremamente rápido, e fiquei surpreso ao ver que tanto tempo é gasto no memmove. Mas então eu tive a ideia de que memmove é lento porque está movendo regiões sobrepostas, que devem ser implementadas em um loop fechado, em vez de copiar grandes páginas de memória. Eu escrevi um pequeno microbenchmark para descobrir se havia uma diferença de desempenho entre memcpy e memmove, esperando que memcpy vencesse.

Eu executei meu benchmark em duas máquinas (core i5, core i7) e vi que memmove é realmente mais rápido que memcpy, no antigo core i7 quase duas vezes mais rápido! Agora estou procurando explicações.

Aqui está minha referência. Ele copia 100 MB com memcpy e move cerca de 100 MB com memmove; origem e destino estão sobrepostos. Várias "distâncias" para origem e destino são tentadas. Cada teste é executado 10 vezes, o tempo médio é impresso.

https://gist.github.com/cruppstahl/78a57cdf937bca3d062c

Aqui estão os resultados no Core i5 (Linux 3.5.0-54-generic # 81 ~ precise1-Ubuntu SMP x86_64 GNU / Linux, gcc é 4.6.3 (Ubuntu / Linaro 4.6.3-1ubuntu5). O número entre colchetes é a distância (tamanho da lacuna) entre a origem e o destino:

memcpy        0.0140074
memmove (002) 0.0106168
memmove (004) 0.01065
memmove (008) 0.0107917
memmove (016) 0.0107319
memmove (032) 0.0106724
memmove (064) 0.0106821
memmove (128) 0.0110633

Memmove é implementado como um código assembler otimizado SSE, copiando de trás para frente. Ele usa pré-busca de hardware para carregar os dados no cache e copia 128 bytes para os registradores XMM e os armazena no destino.

( memcpy-ssse3-back.S , linhas 1650 ff)

L(gobble_ll_loop):
    prefetchnta -0x1c0(%rsi)
    prefetchnta -0x280(%rsi)
    prefetchnta -0x1c0(%rdi)
    prefetchnta -0x280(%rdi)
    sub $0x80, %rdx
    movdqu  -0x10(%rsi), %xmm1
    movdqu  -0x20(%rsi), %xmm2
    movdqu  -0x30(%rsi), %xmm3
    movdqu  -0x40(%rsi), %xmm4
    movdqu  -0x50(%rsi), %xmm5
    movdqu  -0x60(%rsi), %xmm6
    movdqu  -0x70(%rsi), %xmm7
    movdqu  -0x80(%rsi), %xmm8
    movdqa  %xmm1, -0x10(%rdi)
    movdqa  %xmm2, -0x20(%rdi)
    movdqa  %xmm3, -0x30(%rdi)
    movdqa  %xmm4, -0x40(%rdi)
    movdqa  %xmm5, -0x50(%rdi)
    movdqa  %xmm6, -0x60(%rdi)
    movdqa  %xmm7, -0x70(%rdi)
    movdqa  %xmm8, -0x80(%rdi)
    lea -0x80(%rsi), %rsi
    lea -0x80(%rdi), %rdi
    jae L(gobble_ll_loop)

Por que o memmove é mais rápido do que o memcpy? Eu esperaria que o memcpy copiasse páginas de memória, o que deve ser muito mais rápido do que o loop. Na pior das hipóteses, eu esperaria que memcpy fosse tão rápido quanto memmove.

PS: Eu sei que não posso substituir memmove por memcpy em meu código. Eu sei que o exemplo de código mistura C e C ++. Esta pergunta é realmente apenas para fins acadêmicos.

ATUALIZAÇÃO 1

Executei algumas variações dos testes, com base nas várias respostas.

  1. Ao executar o memcpy duas vezes, a segunda execução é mais rápida do que a primeira.
  2. Ao "tocar" no buffer de destino do memcpy ( memset(b2, 0, BUFFERSIZE...)), a primeira execução do memcpy também é mais rápida.
  3. memcpy ainda é um pouco mais lento que memmove.

Aqui estão os resultados:

memcpy        0.0118526
memcpy        0.0119105
memmove (002) 0.0108151
memmove (004) 0.0107122
memmove (008) 0.0107262
memmove (016) 0.0108555
memmove (032) 0.0107171
memmove (064) 0.0106437
memmove (128) 0.0106648

Minha conclusão: com base em um comentário de @Oliver Charlesworth, o sistema operacional precisa comprometer a memória física assim que o buffer de destino memcpy é acessado pela primeira vez (se alguém souber como "comprovar" isso, adicione uma resposta! ) Além disso, como @Mats Petersson disse, memmove é mais amigável para o cache do que memcpy.

Obrigado por todas as ótimas respostas e comentários!

cruppstahl
fonte
1
Você olhou para o código memmove, você também olhou para o código memcpy?
Oliver Charlesworth de
8
Minha expectativa era que a cópia de memória fosse extremamente rápida - apenas quando a memória estava no cache L1. Quando os dados não cabem em caches, seu desempenho de cópia diminui.
Maxim Egorushkin
1
BTW, você copiou apenas um branch de memmove. Esta ramificação não pode manipular a movimentação quando a origem sobrepõe o destino e o destino está em endereços inferiores.
Maxim Egorushkin
2
Não tive tempo de acessar uma máquina Linux, então ainda não posso testar essa teoria. Mas outra explicação possível é o comprometimento excessivo ; seu memcpyloop é a primeira vez que o conteúdo de b2é acessado, portanto, o sistema operacional precisa comprometer memória física para ele.
Oliver Charlesworth
2
PS: Se esse for um gargalo, reconsideraria a abordagem. Que tal colocar os valores em uma lista ou estrutura de árvore (por exemplo, árvore binária) e depois lê-los em um array no final. Os nós em tal abordagem seriam um excelente candidato para alocação de pool. Eles só são adicionados até o final, quando são lançados em massa. Isso é especialmente verdadeiro se você souber de quantas precisará no início. As bibliotecas de impulso têm um alocador de pool.
Persixty de

Respostas:

56

Suas memmovechamadas estão embaralhando a memória em 2 a 128 bytes, enquanto sua memcpyorigem e destino são completamente diferentes. De alguma forma, isso é responsável pela diferença de desempenho: se você copiar para o mesmo lugar, verá que memcpytermina possivelmente um pouco mais rápido, por exemplo, em ideone.com :

memmove (002) 0.0610362
memmove (004) 0.0554264
memmove (008) 0.0575859
memmove (016) 0.057326
memmove (032) 0.0583542
memmove (064) 0.0561934
memmove (128) 0.0549391
memcpy 0.0537919

Quase nada nele - nenhuma evidência de que escrever de volta para uma página já com falhas na memória tenha muito impacto e certamente não estamos vendo uma redução do tempo pela metade ... mas mostra que não há nada de errado em tornar memcpydesnecessariamente mais lento quando comparadas maçãs -para-maçãs.

Tony Delroy
fonte
Eu esperava que os caches da CPU não estivessem causando a diferença porque meus buffers são muito maiores do que os caches.
cruppstahl
2
Mas cada um requer o mesmo número total de acessos à memória principal, certo? (Ou seja, 100 MB de leitura e 100 MB de gravação). O padrão de cache não contorna isso. Portanto, a única maneira de um ser mais lento do que o outro é se alguma coisa tiver que ser lida / gravada da / para a memória mais de uma vez.
Oliver Charlesworth
2
@Tony D - Minha conclusão foi perguntar às pessoas que são mais espertas do que eu;)
cruppstahl
1
Além disso, o que acontece se você copiar para o mesmo lugar, mas fizer memcpyprimeiro de novo?
Oliver Charlesworth de
1
@OliverCharlesworth: o primeiro teste executado sempre tem um impacto significativo, mas fazendo dois testes memcpy: memcpy 0,0688002 0,0583162 | memmove 0,0577443 0,05862 0,0601029 ... consulte ideone.com/8EEAcA
Tony Delroy
24

Quando você está usando memcpy, as gravações precisam ir para o cache. Quando você usa memmovewhere quando está copiando um pequeno passo à frente, a memória que você está copiando já estará no cache (porque foi lida 2, 4, 16 ou 128 bytes "para trás"). Tente fazer um em memmoveque o destino tenha vários megabytes (> 4 * tamanho do cache) e suspeito (mas não posso me dar ao trabalho de testar) que você obterá resultados semelhantes.

Garanto que ALL é sobre manutenção de cache quando você faz operações de grande memória.

Mats Petersson
fonte
1 Acho que pelos motivos que você mencionou, um memmove com loop reverso é mais amigável para o cache do que memcpy. No entanto, descobri que, ao executar o teste memcpy duas vezes, a segunda execução é tão rápida quanto memmove. Por quê? Os buffers são tão grandes que uma segunda execução de memcpy deve ser tão ineficiente (em relação ao cache) quanto a primeira. Portanto, parece que existem fatores adicionais aqui que causam a penalidade de desempenho.
cruppstahl
3
Dadas as circunstâncias certas, um segundo memcpyserá notavelmente mais rápido simplesmente porque o TLB é pré-preenchido. Além disso, um segundo memcpynão terá que esvaziar o cache de coisas das quais você pode precisar "se livrar" (linhas de cache sujas são "ruins" para o desempenho de muitas maneiras. Para ter certeza, no entanto, você precisa execute algo como "perf" e experimente coisas como falhas de cache, falhas de TLB e assim por diante.
Mats Petersson
15

Historicamente, memmove e memcopy têm a mesma função. Eles funcionaram da mesma forma e tiveram a mesma implementação. Percebeu-se então que a memcopy não precisa ser (e frequentemente não era) definida para lidar com áreas sobrepostas de nenhuma maneira particular.

O resultado final é que memmove foi definido para lidar com regiões sobrepostas de uma maneira particular, mesmo que isso afete o desempenho. O Memcopy deve usar o melhor algoritmo disponível para regiões não sobrepostas. As implementações são normalmente quase idênticas.

O problema que você encontrou é que existem tantas variações do hardware x86 que é impossível dizer qual método de troca de memória será o mais rápido. E mesmo se você achar que tem um resultado em uma circunstância, algo tão simples como ter um 'avanço' diferente no layout da memória pode causar um desempenho de cache muito diferente.

Você pode avaliar o que está realmente fazendo ou ignorar o problema e confiar nos benchmarks feitos para a biblioteca C.

Edit: Oh, e uma última coisa; mudar muitos conteúdos da memória é MUITO lento. Eu acho que seu aplicativo seria executado mais rápido com algo como uma implementação B-Tree simples para lidar com seus inteiros. (Oh você está ok)

Edit2: Para resumir minha expansão nos comentários: O microbenchmark é o problema aqui, não é medir o que você pensa que é. As tarefas atribuídas a memcpy e memmove diferem significativamente umas das outras. Se a tarefa dada ao memcpy for repetida várias vezes com memmove ou memcpy, os resultados finais não dependerão de qual função de deslocamento de memória você usar, A MENOS que as regiões se sobreponham.

user3710044
fonte
Mas é disso que se trata - estou avaliando o que estou realmente fazendo. Esta questão é sobre como interpretar os resultados do benchmark, que contradiz o que você está reivindicando - que memcpy é mais rápido para regiões não sobrepostas.
cruppstahl
Meu aplicativo é uma b-tree! Sempre que inteiros são inseridos em um nó folha, memmove é chamado para criar espaço. Estou trabalhando em um mecanismo de banco de dados.
cruppstahl
1
Você está usando um micro benchmark e nem mesmo o memcopy e o memmove deslocam os mesmos dados. Os locais exatos na memória em que residem os dados que você está copiando fazem diferença para o cache e quantas viagens de ida e volta para a memória a CPU precisa fazer.
user3710044 de
Embora essa resposta esteja correta, ela não explica realmente por que é mais lento neste caso, mas essencialmente dizendo "é mais lento porque em alguns casos pode ser mais lento".
Oliver Charlesworth de
Estou dizendo que para as mesmas circunstâncias, incluindo o mesmo layout de memória para copiar / mover os benchmarks SERÁ o mesmo porque as implementações são as mesmas. O problema está no microbenchmark.
user3710044
2

"memcpy é mais eficiente do que memmove." No seu caso, você provavelmente não está fazendo exatamente a mesma coisa enquanto executa as duas funções.

Em geral, USE memmove somente se for necessário. USE-o quando houver uma chance muito razoável de que as regiões de origem e destino estejam sobrepostas.

Referência: https://www.youtube.com/watch?v=Yr1YnOVG-4g Dr. Jerry Cain, (Stanford Intro Systems Lecture - 7) Horário: 36:00

Ehsan
fonte