Las rutinas de copia de memoria pueden ser mucho más complicadas y rápidas que una simple copia de memoria a través de punteros como:
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++;
}
Mejoras
La primera mejora que se puede hacer es alinear uno de los punteros en un límite de palabra (por palabra me refiero al tamaño entero nativo, generalmente 32 bits / 4 bytes, pero puede ser 64 bits / 8 bytes en arquitecturas más nuevas) y usar movimiento del tamaño de una palabra / copiar instrucciones. Esto requiere usar una copia de byte a byte hasta que se alinee un puntero.
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--;
}
}
}
Las diferentes arquitecturas se comportarán de manera diferente en función de si el puntero de origen o de destino está alineado correctamente. Por ejemplo, en un procesador XScale obtuve un mejor rendimiento alineando el puntero de destino en lugar del puntero de origen.
Para mejorar aún más el rendimiento, se puede realizar un desenrollado de bucles, de modo que más registros del procesador se carguen con datos y eso significa que las instrucciones de carga / almacenamiento se pueden intercalar y tener su latencia oculta por instrucciones adicionales (como el conteo de bucles, etc.). El beneficio que esto trae varía bastante según el procesador, ya que las latencias de instrucción de carga / almacenamiento pueden ser bastante diferentes.
En esta etapa, el código termina por escribirse en Ensamblador en lugar de C (o C ++), ya que debe colocar manualmente las instrucciones de carga y almacenamiento para obtener el máximo beneficio de la ocultación de latencia y el rendimiento.
Por lo general, se debe copiar una línea de datos de caché completa en una iteración del ciclo desenrollado.
Lo que me lleva a la siguiente mejora, la adición de búsqueda previa. Estas son instrucciones especiales que le dicen al sistema de caché del procesador que cargue partes específicas de la memoria en su caché. Dado que hay un retraso entre la emisión de la instrucción y el llenado de la línea de caché, las instrucciones deben colocarse de tal manera que los datos estén disponibles cuando se van a copiar, y no antes / después.
Esto significa poner instrucciones de captación previa al inicio de la función, así como dentro del bucle de copia principal. Con las instrucciones de captación previa en medio del ciclo de copia, se obtienen datos que se copiarán en varias iteraciones.
No lo recuerdo, pero también puede ser beneficioso obtener previamente las direcciones de destino y las de origen.
Factores
Los principales factores que afectan la rapidez con que se puede copiar la memoria son:
- La latencia entre el procesador, sus cachés y la memoria principal.
- El tamaño y la estructura de las líneas de caché del procesador.
- Las instrucciones de movimiento / copia de la memoria del procesador (latencia, rendimiento, tamaño de registro, etc.).
Por lo tanto, si desea escribir una rutina de manejo de memoria rápida y eficiente, necesitará saber bastante sobre el procesador y la arquitectura para los que está escribiendo. Es suficiente decir que, a menos que esté escribiendo en alguna plataforma integrada, sería mucho más fácil usar las rutinas de copia de memoria integradas.