Le routine di copia in memoria possono essere molto più complicate e veloci di una semplice copia in memoria tramite puntatori come:
void simple_memory_copy(void* dst, void* src, unsigned int bytes)
{
unsigned char* b_dst = (unsigned char*)dst;
unsigned char* b_src = (unsigned char*)src;
for (int i = 0; i < bytes; ++i)
*b_dst++ = *b_src++;
}
Miglioramenti
Il primo miglioramento che si può fare è allineare uno dei puntatori su un confine di parola (per parola intendo la dimensione intera nativa, di solito 32 bit / 4 byte, ma può essere 64 bit / 8 byte su architetture più recenti) e utilizzare lo spostamento di dimensioni di parola / copia le istruzioni. Ciò richiede l'utilizzo di una copia da byte a byte fino all'allineamento di un puntatore.
void aligned_memory_copy(void* dst, void* src, unsigned int bytes)
{
unsigned char* b_dst = (unsigned char*)dst;
unsigned char* b_src = (unsigned char*)src;
// Copy bytes to align source pointer
while ((b_src & 0x3) != 0)
{
*b_dst++ = *b_src++;
bytes--;
}
unsigned int* w_dst = (unsigned int*)b_dst;
unsigned int* w_src = (unsigned int*)b_src;
while (bytes >= 4)
{
*w_dst++ = *w_src++;
bytes -= 4;
}
// Copy trailing bytes
if (bytes > 0)
{
b_dst = (unsigned char*)w_dst;
b_src = (unsigned char*)w_src;
while (bytes > 0)
{
*b_dst++ = *b_src++;
bytes--;
}
}
}
Architetture diverse funzioneranno in modo diverso a seconda che il puntatore di origine o di destinazione sia allineato in modo appropriato. Ad esempio su un processore XScale ho ottenuto prestazioni migliori allineando il puntatore di destinazione anziché il puntatore di origine.
Per migliorare ulteriormente le prestazioni è possibile eseguire alcuni cicli di srotolamento, in modo che più registri del processore vengano caricati con i dati e ciò significa che le istruzioni di caricamento / memorizzazione possono essere intercalate e la loro latenza è nascosta da istruzioni aggiuntive (come il conteggio dei loop, ecc.). Il vantaggio che ciò comporta varia notevolmente a seconda del processore, poiché le latenze delle istruzioni di caricamento / memorizzazione possono essere molto diverse.
In questa fase il codice finisce per essere scritto in Assembly anziché in C (o C ++) poiché è necessario posizionare manualmente il caricamento e archiviare le istruzioni per ottenere il massimo vantaggio dall'occultamento della latenza e dalla velocità effettiva.
Generalmente un'intera linea di dati della cache dovrebbe essere copiata in un'iterazione del ciclo srotolato.
Il che mi porta al miglioramento successivo, aggiungendo il prelettura. Queste sono istruzioni speciali che indicano al sistema di cache del processore di caricare parti specifiche di memoria nella sua cache. Poiché c'è un ritardo tra l'emissione dell'istruzione e il riempimento della riga della cache, le istruzioni devono essere posizionate in modo tale che i dati siano disponibili proprio come devono essere copiati e non prima / poi.
Ciò significa inserire le istruzioni di precaricamento all'inizio della funzione e all'interno del ciclo di copia principale. Con le istruzioni di prefetch nel mezzo del ciclo di copia, si recuperano i dati che verranno copiati in diverse iterazioni.
Non ricordo, ma può anche essere utile precaricare gli indirizzi di destinazione oltre a quelli di origine.
Fattori
I principali fattori che influenzano la velocità di copia della memoria sono:
- La latenza tra il processore, le sue cache e la memoria principale.
- La dimensione e la struttura delle linee della cache del processore.
- Le istruzioni di spostamento / copia della memoria del processore (latenza, velocità effettiva, dimensione del registro, ecc.).
Quindi, se vuoi scrivere una routine di gestione della memoria efficiente e veloce, avrai bisogno di sapere abbastanza sul processore e sull'architettura per cui stai scrivendo. Basti dire che, a meno che tu non stia scrivendo su qualche piattaforma incorporata, sarebbe molto più facile usare semplicemente le routine di copia della memoria incorporate.