differenze di prestazioni di memcpy tra processi a 32 e 64 bit
Domanda
Abbiamo macchine Core2 (Dell T5400) con XP64.
Osserviamo che quando si eseguono processi a 32 bit, le prestazioni di Memcpy sono nell'ordine di 1,2 GByte/s;Tuttavia, Memcpy in un processo a 64 bit raggiunge circa 2,2 GByte/s (o 2,4 GByte/s con la memcpy del compilatore Intel CRT).Mentre la reazione iniziale potrebbe essere quella di spiegarlo semplicemente a causa dei registri più ampi disponibili in codice a 64 bit, osserviamo che il nostro codice di assemblaggio SSE simile a memcpy (che dovrebbe utilizzare negozi di carico largo a 128 bit indipendentemente da 32 /64 bitness del processo) mostra limiti superiori simili sulla larghezza di banda della copia che raggiunge.
La mia domanda è: a cosa serve questa differenza?I processi a 32 bit devono saltare attraverso alcuni cerchi Wow64 extra per arrivare al RAM?È qualcosa a che fare con TLB o prefetcher o ...Che cosa ?
Grazie per qualsiasi intuizione.
Anche cresciuto Forum Intel.
Soluzione
Naturalmente, devi davvero guardare le istruzioni della macchina reale che vengono eseguite all'interno del ciclo più interno di memcpy, entrando nel codice macchina con un debugger. Qualsiasi altra cosa è solo una speculazione.
La mia domanda è che probabilmente non ha nulla a che fare con i 32 bit contro i 64 bit in sé; la mia ipotesi è che la routine di libreria più veloce sia stata scritta utilizzando gli archivi non temporali SSE.
Se il circuito interno contiene una variazione delle istruzioni convenzionali per l'archiviazione del carico, quindi la memoria di destinazione deve essere letta nella cache della macchina, modificata e riscritta. Dato che quella lettura non è assolutamente necessaria - i bit letti vengono sovrascritti immediatamente - è possibile risparmiare metà della larghezza di banda della memoria usando & Quot; non temporali & Quot; scrivere istruzioni che bypassano le cache. In questo modo, la memoria di destinazione viene appena scritta facendo un viaggio di sola andata nella memoria anziché un viaggio di andata e ritorno.
Non conosco la libreria CRT del compilatore Intel, quindi questa è solo una supposizione. Non c'è un motivo particolare per cui il libCRT a 32 bit non può fare la stessa cosa, ma l'accelerazione che citi è nel campo di gioco di quello che mi aspetterei semplicemente convertendo le istruzioni di movdqa in movnt ...
Poiché memcpy non sta eseguendo calcoli, è sempre vincolato dalla velocità con cui è possibile leggere e scrivere memoria.
Altri suggerimenti
Penso che quanto segue possa spiegarlo:
Per copiare i dati dalla memoria a un registro e di nuovo in memoria, lo fai
mov eax, [address]
mov [address2], eax
Questo sposta 32 bit (4 byte) dall'indirizzo all'indirizzo2.Lo stesso vale con 64 bit in modalità 64 bit
mov rax, [address]
mov [address2], rax
Questo sposta 64 bit, 2 byte, dall'indirizzo all'indirizzo2."mov" stesso, indipendentemente dal fatto che sia a 64 o 32 bit, ha una latenza di 0,5 e un throughput di 0,5 secondo le specifiche Intel.La latenza è il numero di cicli di clock necessari all'istruzione per viaggiare attraverso la pipeline, mentre il throughput è il tempo che la CPU deve attendere prima di accettare nuovamente la stessa istruzione.Come puoi vedere, può eseguire due movimenti per ciclo di clock, tuttavia, deve attendere mezzo ciclo di clock tra due movimenti, quindi può effettivamente eseguire solo un movimento per ciclo di clock (o sbaglio qui e interpreto male i termini?Vedere PDF qui per dettagli).
Naturalmente a mov reg, mem
può essere più lungo di 0,5 cicli, a seconda se i dati si trovano nella cache di 1° o 2° livello o non sono affatto nella cache e devono essere estratti dalla memoria.Tuttavia, il tempo di latenza di cui sopra ignora questo fatto (come afferma il PDF che ho collegato sopra), presuppone che tutti i dati necessari per il movimento siano già presenti (altrimenti la latenza aumenterà in base al tempo necessario per recuperare i dati ovunque si trovino) in questo momento - potrebbero trattarsi di diversi cicli di clock ed è completamente indipendente dal comando eseguito, afferma il PDF a pagina 482/C-30).
Ciò che è interessante è che il filmato sia a 32 o 64 bit non ha alcun ruolo.Ciò significa che, a meno che la larghezza di banda della memoria non diventi un fattore limitante, i mov a 64 bit sono altrettanto veloci dei mov a 32 bit e poiché è necessaria solo la metà dei mov per spostare la stessa quantità di dati da A a B quando si utilizza 64 bit, il throughput può (in teoria) essere il doppio (il fatto che non lo sia probabilmente perché la memoria non è illimitata).
Ok, ora pensi che quando usi i registri SSE più grandi, dovresti ottenere un throughput più veloce, giusto?Per quanto ne so, i registri xmm non sono 256, ma larghi 128 bit, a proposito (riferimento a Wikipedia).Tuttavia, hai considerato la latenza e il throughput?I dati che desideri spostare sono allineati a 128 bit oppure no.A seconda di ciò, puoi spostarlo utilizzando
movdqa xmm1, [address]
movdqa [address2], xmm1
o se non allineato
movdqu xmm1, [address]
movdqu [address2], xmm1
Bene, movdqa/movdqu ha una latenza di 1 e un throughput di 1.Quindi le istruzioni impiegano il doppio del tempo per essere eseguite e il tempo di attesa dopo le istruzioni è doppio rispetto a un normale movimento.
E un'altra cosa di cui non abbiamo nemmeno preso in considerazione è il fatto che la CPU in realtà suddivide le istruzioni in micro-operazioni e può eseguirle in parallelo.Ora inizia a diventare davvero complicato...anche troppo complicato per me.
Ad ogni modo, so per esperienza che il caricamento dei dati da/verso i registri xmm è molto più lento del caricamento dei dati da/verso i registri normali, quindi la tua idea di accelerare il trasferimento utilizzando i registri xmm è stata condannata fin dal primo secondo.In realtà sono sorpreso che alla fine il memmove SSE non sia molto più lento di quello normale.
Finalmente sono arrivato al fondo di questo (e la risposta di Die in Sente è stata sulla linea giusta, grazie)
Nel seguito, dst e src sono 512 MByte std :: vector. Sto usando il compilatore Intel 10.1.029 e CRT.
Su 64 bit entrambi
memcpy(&dst[0],&src[0],dst.size())
e
memcpy(&dst[0],&src[0],N)
dove N è stato precedentemente dichiarato const size_t N=512*(1<<20);
chiama
__intel_fast_memcpy
la maggior parte dei quali è costituita da:
000000014004ED80 lea rcx,[rcx+40h]
000000014004ED84 lea rdx,[rdx+40h]
000000014004ED88 lea r8,[r8-40h]
000000014004ED8C prefetchnta [rdx+180h]
000000014004ED93 movdqu xmm0,xmmword ptr [rdx-40h]
000000014004ED98 movdqu xmm1,xmmword ptr [rdx-30h]
000000014004ED9D cmp r8,40h
000000014004EDA1 movntdq xmmword ptr [rcx-40h],xmm0
000000014004EDA6 movntdq xmmword ptr [rcx-30h],xmm1
000000014004EDAB movdqu xmm2,xmmword ptr [rdx-20h]
000000014004EDB0 movdqu xmm3,xmmword ptr [rdx-10h]
000000014004EDB5 movntdq xmmword ptr [rcx-20h],xmm2
000000014004EDBA movntdq xmmword ptr [rcx-10h],xmm3
000000014004EDBF jge 000000014004ED80
e funziona a ~ 2200 MByte / s.
Ma a 32 bit
_mm_stream_ps
chiamate
dst.size()
la maggior parte delle quali è costituita da
004447A0 sub ecx,80h
004447A6 movdqa xmm0,xmmword ptr [esi]
004447AA movdqa xmm1,xmmword ptr [esi+10h]
004447AF movdqa xmmword ptr [edx],xmm0
004447B3 movdqa xmmword ptr [edx+10h],xmm1
004447B8 movdqa xmm2,xmmword ptr [esi+20h]
004447BD movdqa xmm3,xmmword ptr [esi+30h]
004447C2 movdqa xmmword ptr [edx+20h],xmm2
004447C7 movdqa xmmword ptr [edx+30h],xmm3
004447CC movdqa xmm4,xmmword ptr [esi+40h]
004447D1 movdqa xmm5,xmmword ptr [esi+50h]
004447D6 movdqa xmmword ptr [edx+40h],xmm4
004447DB movdqa xmmword ptr [edx+50h],xmm5
004447E0 movdqa xmm6,xmmword ptr [esi+60h]
004447E5 movdqa xmm7,xmmword ptr [esi+70h]
004447EA add esi,80h
004447F0 movdqa xmmword ptr [edx+60h],xmm6
004447F5 movdqa xmmword ptr [edx+70h],xmm7
004447FA add edx,80h
00444800 cmp ecx,80h
00444806 jge 004447A0
e funziona solo a ~ 1350 MByte / s.
TUTTAVIA
memcpy(&dst[0],&src[0],N)
dove N è stato precedentemente dichiarato movnt
viene compilato (su 32 bit) in una chiamata diretta a un
__intel_VEC_memcpy
la maggior parte delle quali è costituita da
0043FF40 movdqa xmm0,xmmword ptr [esi]
0043FF44 movdqa xmm1,xmmword ptr [esi+10h]
0043FF49 movdqa xmm2,xmmword ptr [esi+20h]
0043FF4E movdqa xmm3,xmmword ptr [esi+30h]
0043FF53 movntdq xmmword ptr [edi],xmm0
0043FF57 movntdq xmmword ptr [edi+10h],xmm1
0043FF5C movntdq xmmword ptr [edi+20h],xmm2
0043FF61 movntdq xmmword ptr [edi+30h],xmm3
0043FF66 movdqa xmm4,xmmword ptr [esi+40h]
0043FF6B movdqa xmm5,xmmword ptr [esi+50h]
0043FF70 movdqa xmm6,xmmword ptr [esi+60h]
0043FF75 movdqa xmm7,xmmword ptr [esi+70h]
0043FF7A movntdq xmmword ptr [edi+40h],xmm4
0043FF7F movntdq xmmword ptr [edi+50h],xmm5
0043FF84 movntdq xmmword ptr [edi+60h],xmm6
0043FF89 movntdq xmmword ptr [edi+70h],xmm7
0043FF8E lea esi,[esi+80h]
0043FF94 lea edi,[edi+80h]
0043FF9A dec ecx
0043FF9B jne ___intel_VEC_memcpy+244h (43FF40h)
e funziona a ~ 2100MByte / s (e dimostrare che 32 bit non è in qualche modo limitato dalla larghezza di banda).
Ritiro la mia affermazione che il mio codice SSE simile a memcpy soffre di a
simile ~ 1300 MByte / limite nelle build a 32 bit; Ora non ho problemi
ottenere > 2 GByte / s su 32 o 64 bit; il trucco (come suggeriscono i risultati precedenti)
consiste nell'utilizzare negozi non temporali (" streaming ") (ad esempio CPUID
intrinseco).
Sembra un po 'strano che il 32 bit " <=> " memcpy alla fine non lo fa chiama il più veloce " <=> " versione (se passi a memcpy c'è di più incredibile quantità di <=> logica euristica e di controllo, ad es. confronto dei numeri di byte da copiare con dimensioni della cache ecc. prima che vadano ovunque vicino a dati reali) ma almeno ora capisco il comportamento osservato (ed è non correlato a SysWow64 o H / W).
La mia ipotesi immediata è che i processi a 64 bit utilizzano la dimensione di memoria nativa a 64 bit del processore, il che ottimizza l'uso del bus di memoria.
Grazie per il feedback positivo!credo che posso in parte spiegare cosa sta succedendo qui.
Usare gli archivi non temporali per memcpy è sicuramente il digiuno Se stai solo cronometrando la chiamata memcpy.
D'altra parte, se stai confrontando un'applicazione, gli archivi movdqa hanno il vantaggio di lasciare la memoria di destinazione nella cache.O almeno la parte che sta nella cache.
Pertanto, se stai progettando una libreria runtime e se puoi presumere che l'applicazione che ha chiamato memcpy utilizzerà il buffer di destinazione immediatamente dopo la chiamata memcpy, allora ti consigliamo di fornire la versione movdqa.Ciò ottimizza efficacemente il viaggio dalla memoria alla CPU che seguirebbe la versione movntdq e tutte le istruzioni successive alla chiamata verranno eseguite più velocemente.
D'altra parte, se il buffer di destinazione è grande rispetto alla cache del processore, l'ottimizzazione non funziona e la versione movntdq fornirebbe benchmark delle applicazioni più rapidi.
Quindi l'idea memcpy avrebbe più versioni sotto il cofano.Quando il buffer di destinazione è piccolo rispetto alla cache del processore, utilizza movdqa, altrimenti, se il buffer di destinazione è grande rispetto alla cache del processore, utilizza movntdq.Sembra che questo sia ciò che sta accadendo nella libreria a 32 bit.
Naturalmente, nulla di tutto ciò ha a che fare con le differenze tra 32 bit e 64 bit.
La mia congettura è che la libreria a 64 bit non sia così matura.Gli sviluppatori non sono ancora riusciti a fornire entrambe le routine in quella versione della libreria.
Non ho un riferimento davanti a me, quindi non sono assolutamente positivo sui tempi / istruzioni, ma posso ancora dare la teoria. Se stai eseguendo uno spostamento della memoria in modalità a 32 bit, eseguirai qualcosa di simile a un & Quot; rep movsd & Quot; che sposta un singolo valore di 32 bit ad ogni ciclo di clock. In modalità 64-bit, puoi fare un & Quot; rep movsq & Quot; che esegue un singolo spostamento di 64 bit ad ogni ciclo di clock. Quell'istruzione non è disponibile per il codice a 32 bit, quindi faresti 2 x rep movsd (a 1 ciclo un pezzo) per metà della velocità di esecuzione.
MOLTO semplificato, ignorando tutti i problemi di larghezza di banda / allineamento della memoria, ecc., ma è qui che inizia tutto ...
Ecco un esempio di una routine memcpy orientata specificamente per l'architettura a 64 bit.
void uint8copy(void *dest, void *src, size_t n){
uint64_t * ss = (uint64_t)src;
uint64_t * dd = (uint64_t)dest;
n = n * sizeof(uint8_t)/sizeof(uint64_t);
while(n--)
*dd++ = *ss++;
}//end uint8copy()
L'articolo completo è qui: http://www.godlikemouse.com/2008/03/04/ ottimizzando-memcpy-routine /