Les routines de copie de mémoire peuvent être beaucoup plus compliquées et plus rapides qu'une simple copie de mémoire via des pointeurs tels que:
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++;
}
Améliorations
La première amélioration que l'on peut faire est d'aligner l'un des pointeurs sur une limite de mot (par mot, je veux dire une taille entière native, généralement 32 bits / 4 octets, mais peut être de 64 bits / 8 octets sur les architectures plus récentes) et utiliser un mouvement de taille de mot / copier les instructions. Cela nécessite d'utiliser une copie d'octet en octet jusqu'à ce qu'un pointeur soit aligné.
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--;
}
}
}
Différentes architectures fonctionneront différemment selon que la source ou le pointeur de destination est correctement aligné. Par exemple, sur un processeur XScale, j'ai obtenu de meilleures performances en alignant le pointeur de destination plutôt que le pointeur source.
Pour améliorer encore les performances, un certain déroulement de boucle peut être effectué, de sorte que davantage de registres du processeur soient chargés de données et cela signifie que les instructions de chargement / stockage peuvent être entrelacées et avoir leur latence masquée par des instructions supplémentaires (telles que le comptage de boucles, etc.). L'avantage que cela apporte varie un peu selon le processeur, car les latences des instructions de chargement / stockage peuvent être très différentes.
À ce stade, le code finit par être écrit en Assembly plutôt qu'en C (ou C ++), car vous devez placer manuellement les instructions de chargement et de stockage pour tirer le meilleur parti du masquage de la latence et du débit.
En général, toute une ligne de cache de données doit être copiée en une itération de la boucle déroulée.
Ce qui m'amène à la prochaine amélioration, l'ajout de la pré-extraction. Ce sont des instructions spéciales qui indiquent au système de cache du processeur de charger des parties spécifiques de la mémoire dans son cache. Puisqu'il y a un délai entre l'émission de l'instruction et le remplissage de la ligne d'antémémoire, les instructions doivent être placées de telle manière que les données soient disponibles au moment où elles doivent être copiées, et pas plus tôt / plus tard.
Cela signifie mettre des instructions de prélecture au début de la fonction ainsi qu'à l'intérieur de la boucle de copie principale. Avec les instructions de prélecture au milieu de la boucle de copie, des données qui seront copiées en plusieurs itérations.
Je ne me souviens pas, mais il peut également être avantageux de pré-extraire les adresses de destination ainsi que celles de la source.
Les facteurs
Les principaux facteurs qui affectent la vitesse de copie de la mémoire sont:
- La latence entre le processeur, ses caches et la mémoire principale.
- La taille et la structure des lignes de cache du processeur.
- Les instructions de déplacement / copie de la mémoire du processeur (latence, débit, taille du registre, etc.).
Donc, si vous voulez écrire une routine de gestion de la mémoire efficace et rapide, vous devrez en savoir beaucoup sur le processeur et l'architecture pour lesquels vous écrivez. Qu'il suffise de dire qu'à moins que vous n'écriviez sur une plate-forme intégrée, il serait beaucoup plus facile d'utiliser simplement les routines de copie mémoire intégrées.