As rotinas de cópia de memória podem ser muito mais complicadas e rápidas do que uma simples cópia de memória por meio de indicadores 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++;
}
Melhorias
A primeira melhoria que se pode fazer é alinhar um dos ponteiros em um limite de palavra (por palavra, quero dizer tamanho inteiro nativo, geralmente 32 bits / 4 bytes, mas pode ser 64 bits / 8 bytes em arquiteturas mais recentes) e usar o movimento de tamanho de palavra / copiar instruções. Isso requer o uso de uma cópia de byte a byte até que um ponteiro esteja alinhado.
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--;
}
}
}
Diferentes arquiteturas terão um desempenho diferente com base no alinhamento apropriado da origem ou do ponteiro de destino. Por exemplo, em um processador XScale, obtive melhor desempenho alinhando o ponteiro de destino em vez do ponteiro de origem.
Para melhorar ainda mais o desempenho, pode ser feito algum desenrolamento de loop, de modo que mais registros do processador sejam carregados com dados e isso significa que as instruções de carga / armazenamento podem ser intercaladas e ter sua latência oculta por instruções adicionais (como contagem de loop, etc.). O benefício que isso traz varia um pouco de acordo com o processador, uma vez que as latências de instrução de carga / armazenamento podem ser bem diferentes.
Nesse estágio, o código acaba sendo escrito em Assembly em vez de C (ou C ++), pois você precisa colocar manualmente as instruções de carregamento e armazenamento para obter o máximo benefício de ocultação de latência e rendimento.
Geralmente, toda uma linha de cache de dados deve ser copiada em uma iteração do loop desenrolado.
O que me leva à próxima melhoria, adicionando pré-busca. Estas são instruções especiais que dizem ao sistema de cache do processador para carregar partes específicas da memória em seu cache. Como existe um atraso entre a emissão da instrução e o preenchimento da linha do cache, as instruções precisam ser colocadas de forma que os dados estejam disponíveis no momento exato em que devem ser copiados, e não antes ou depois.
Isso significa colocar as instruções de pré-busca no início da função, bem como dentro do loop de cópia principal. Com as instruções de pré-busca no meio do loop de cópia, busca dados que serão copiados em várias iterações.
Não me lembro, mas também pode ser útil buscar antecipadamente os endereços de destino e também os de origem.
Fatores
Os principais fatores que afetam a rapidez com que a memória pode ser copiada são:
- A latência entre o processador, seus caches e a memória principal.
- O tamanho e a estrutura das linhas de cache do processador.
- As instruções para mover / copiar a memória do processador (latência, taxa de transferência, tamanho do registro, etc.).
Portanto, se você quiser escrever uma rotina eficiente e rápida para lidar com a memória, precisará saber bastante sobre o processador e a arquitetura para os quais está escrevendo. Basta dizer que, a menos que você esteja escrevendo em alguma plataforma embarcada, seria muito mais fácil usar apenas as rotinas de cópia de memória integradas.