Perché memmove è più veloce di memcpy?


89

Sto studiando gli hotspot delle prestazioni in un'applicazione che trascorre il 50% del suo tempo in memmove (3). L'applicazione inserisce milioni di interi a 4 byte in array ordinati e utilizza memmove per spostare i dati "a destra" per fare spazio al valore inserito.

La mia aspettativa era che la copia della memoria fosse estremamente veloce e sono rimasto sorpreso dal fatto che così tanto tempo sia stato speso in memmove. Ma poi ho avuto l'idea che memmove sia lento perché sta spostando regioni sovrapposte, che devono essere implementate in un ciclo stretto, invece di copiare grandi pagine di memoria. Ho scritto un piccolo microbenchmark per scoprire se c'era una differenza di prestazioni tra memcpy e memmove, aspettandomi che memcpy vincesse a mani basse.

Ho eseguito il mio benchmark su due macchine (core i5, core i7) e ho visto che memmove è effettivamente più veloce di memcpy, sul vecchio core i7 addirittura quasi il doppio! Ora cerco spiegazioni.

Ecco il mio punto di riferimento. Copia 100 mb con memcpy, quindi si sposta di circa 100 mb con memmove; origine e destinazione si sovrappongono. Vengono provate varie "distanze" per origine e destinazione. Ogni test viene eseguito 10 volte, viene stampato il tempo medio.

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

Ecco i risultati sul 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). Il numero tra parentesi è la distanza (dimensione del gap) tra la sorgente e la destinazione:

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 è implementato come un codice assembler ottimizzato per SSE, copiando da dietro a davanti. Utilizza il prefetch hardware per caricare i dati nella cache e copia 128 byte nei registri XMM, quindi li memorizza nella destinazione.

( memcpy-ssse3-back.S , righe 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)

Perché memmove è più veloce di memcpy? Mi aspetto che memcpy copi le pagine di memoria, il che dovrebbe essere molto più veloce del loop. Nel peggiore dei casi, mi aspetto che memcpy sia veloce quanto memmove.

PS: so che non posso sostituire memmove con memcpy nel mio codice. So che il codice di esempio mescola C e C ++. Questa domanda è davvero solo per scopi accademici.

AGGIORNAMENTO 1

Ho eseguito alcune varianti dei test, in base alle varie risposte.

  1. Quando si esegue memcpy due volte, la seconda esecuzione è più veloce della prima.
  2. Quando si "tocca" il buffer di destinazione di memcpy ( memset(b2, 0, BUFFERSIZE...)), anche la prima esecuzione di memcpy è più veloce.
  3. memcpy è ancora un po 'più lento di memmove.

Ecco i risultati:

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

La mia conclusione: sulla base di un commento di @Oliver Charlesworth, il sistema operativo deve impegnare la memoria fisica non appena si accede al buffer di destinazione memcpy per la prima volta (se qualcuno sa come "provarlo", aggiungi una risposta! ). Inoltre, come ha detto @Mats Petersson, memmove è più adatto alla cache di memcpy.

Grazie per tutte le ottime risposte e commenti!


1
Hai guardato il codice memmove, hai guardato anche il codice memcpy?
Oliver Charlesworth

8
La mia aspettativa era che la copia della memoria fosse estremamente veloce , solo quando la memoria è nella cache L1. Quando i dati non si adattano alle cache, le prestazioni di copia diminuiscono.
Maxim Egorushkin

1
A proposito, hai copiato solo un ramo di memmove. Questo ramo non può gestire lo spostamento quando l'origine si sovrappone alla destinazione e la destinazione è a indirizzi inferiori.
Maxim Egorushkin

2
Non ho avuto il tempo di accedere a una macchina Linux, quindi non posso ancora testare questa teoria. Ma un'altra possibile spiegazione è l' eccessivo impegno ; il tuo memcpyciclo è la prima volta che b2si accede al contenuto di , quindi il sistema operativo deve impegnare la memoria fisica per esso mentre procede.
Oliver Charlesworth

2
PS: se questo è un collo di bottiglia, riconsidererei l'approccio. Che ne dici di inserire i valori in un elenco o in una struttura ad albero (ad es. Albero binario) e poi leggerli in un array alla fine. I nodi in un tale approccio sarebbero un ottimo candidato per l'allocazione del pool. Vengono aggiunti solo fino alla fine quando vengono rilasciati in massa. Ciò è particolarmente vero se sai di quanti ne avrai bisogno all'inizio. Le librerie boost hanno un allocatore di pool.
Persixty

Risposte:


56

Le tue memmovechiamate spostano la memoria da 2 a 128 byte, mentre l' memcpyorigine e la destinazione sono completamente diverse. In qualche modo questo spiega la differenza di prestazioni: se copi nello stesso posto, vedrai memcpyfinire forse un po 'più velocemente, ad esempio su 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

Quasi nulla, però - nessuna prova che riscrivere una pagina di memoria già difettosa abbia un grande impatto, e certamente non stiamo vedendo un dimezzamento del tempo ... ma mostra che non c'è niente di sbagliato a rendere memcpyinutilmente più lento rispetto alle mele -per-mele.


Mi sarei aspettato che le cache della CPU non causassero la differenza perché i miei buffer sono molto più grandi delle cache.
cruppstahl

2
Ma ognuno richiede lo stesso numero totale di accessi alla memoria principale, giusto? (Cioè 100 MB di lettura e 100 MB di scrittura). Il modello di cache non lo aggira. Quindi l'unico modo in cui uno potrebbe essere più lento dell'altro è se alcune cose devono essere lette / scritte dalla / alla memoria più di una volta.
Oliver Charlesworth

2
@Tony D - La mia conclusione è stata di chiedere a persone che sono più intelligenti di me;)
cruppstahl

1
Inoltre, cosa succede se si copia nello stesso posto, ma lo si memcpyripete prima?
Oliver Charlesworth

1
@OliverCharlesworth: la prima esecuzione di test ha sempre un successo significativo, ma facendo due test memcpy: memcpy 0.0688002 0.0583162 | memmove 0.0577443 0.05862 0.0601029 ... vedi ideone.com/8EEAcA
Tony Delroy

24

Quando si utilizza memcpy, le scritture devono andare nella cache. Quando usi memmovedove quando stai copiando un piccolo passo avanti, la memoria su cui stai copiando sarà già nella cache (perché è stata letta 2, 4, 16 o 128 byte "indietro"). Prova a fare un punto in memmovecui la destinazione è di diversi megabyte (> 4 * dimensione della cache) e sospetto (ma non posso preoccuparmi di testare) che otterrai risultati simili.

Garantisco che TUTTO riguarda la manutenzione della cache quando si eseguono operazioni di memoria di grandi dimensioni.


+1 Penso che per i motivi che hai menzionato, un memmove in loop all'indietro sia più amichevole per la cache di memcpy. Tuttavia, ho scoperto che quando si esegue il test memcpy due volte, la seconda esecuzione è veloce quanto memmove. Perché? I buffer sono così grandi che una seconda esecuzione di memcpy dovrebbe essere inefficiente (dal punto di vista della cache) come la prima esecuzione. Quindi sembra che qui ci siano altri fattori che causano la riduzione delle prestazioni.
cruppstahl

3
Date le giuste circostanze, un secondo memcpysarà notevolmente più veloce semplicemente perché il TLB è precompilato. Inoltre, un secondo memcpynon dovrà svuotare la cache di cose di cui potresti aver bisogno di "sbarazzarti" (le righe della cache sporche sono "cattive" per le prestazioni in tanti modi. Per essere sicuri, tuttavia, dovresti eseguire qualcosa come "perf" e campionare cose come cache-miss, TLB miss e così via.
Mats Petersson,

15

Storicamente, memmove e memcopy hanno la stessa funzione. Funzionavano allo stesso modo e avevano la stessa implementazione. Si è quindi capito che memcopy non ha bisogno di essere (e spesso non lo era) definito per gestire le aree sovrapposte in un modo particolare.

Il risultato finale è che memmove è stato definito per gestire le regioni sovrapposte in un modo particolare anche se questo influisce sulle prestazioni. Si suppone che Memcopy utilizzi il miglior algoritmo disponibile per le regioni non sovrapposte. Le implementazioni sono normalmente quasi identiche.

Il problema in cui ti sei imbattuto è che ci sono così tante varianti dell'hardware x86 che è impossibile dire quale metodo di spostamento della memoria sarà il più veloce. E anche se pensi di avere un risultato in una circostanza, qualcosa di semplice come avere un "passo" diverso nel layout della memoria può causare prestazioni della cache molto diverse.

Puoi valutare ciò che stai effettivamente facendo o ignorare il problema e fare affidamento sui benchmark eseguiti per la libreria C.

Edit: Oh, e un'ultima cosa; spostare molti contenuti della memoria è MOLTO lento. Immagino che la tua applicazione funzionerebbe più velocemente con qualcosa come una semplice implementazione B-Tree per gestire i tuoi numeri interi. (Oh lo sei, okay)

Edit2: Per riassumere la mia espansione nei commenti: il microbenchmark è il problema qui, non misura ciò che pensi che sia. I compiti assegnati a memcpy e memmove differiscono in modo significativo l'uno dall'altro. Se l'attività assegnata a memcpy viene ripetuta più volte con memmove o memcpy, i risultati finali non dipenderanno dalla funzione di spostamento della memoria utilizzata A MENO CHE le regioni non si sovrappongano.


Ma è di questo che si tratta: sto valutando ciò che sto effettivamente facendo. Questa domanda riguarda l'interpretazione dei risultati del benchmark, che contraddicono ciò che stai affermando: che memcpy è più veloce per le regioni non sovrapposte.
cruppstahl

La mia applicazione è un b-tree! Ogni volta che gli interi vengono inseriti in un nodo foglia, memmove viene chiamato per creare spazio. Sto lavorando su un motore di database.
cruppstahl

1
Stai usando un micro benchmark e non stai nemmeno facendo in modo che memcopy e memmove spostino gli stessi dati. Le posizioni esatte nella memoria in cui risiedono i dati che stai copiando fanno la differenza per il caching e per quanti round trip in memoria la CPU deve fare.
user3710044

Sebbene questa risposta sia corretta, in realtà non spiega perché è più lento in questo caso, essenzialmente sta dicendo "è più lento perché in alcuni casi potrebbe essere più lento".
Oliver Charlesworth

Sto dicendo che per le stesse circostanze, incluso lo stesso layout di memoria per copiare / spostare i benchmark SARÀ lo stesso perché le implementazioni sono le stesse. Il problema è nel microbenchmark.
user3710044

2

"memcpy è più efficiente di memmove." Nel tuo caso, molto probabilmente non stai facendo la stessa identica cosa mentre esegui le due funzioni.

In generale, USA memmove solo se necessario. UTILIZZALO quando c'è una possibilità molto ragionevole che le regioni di origine e di destinazione si sovrappongano.

Riferimento: https://www.youtube.com/watch?v=Yr1YnOVG-4g Dr.Jerry Cain, (Stanford Intro Systems Lecture - 7) Ora: 36:00

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.