Não confie numa biblioteca… nunca!

Alguns assumem que a biblioteca que acompanha o compilador é a mais otimizada possível. Não é! Não pode ser! O motivo é simples: Ela tem que ser genérica. Eis um exemplo do que estou falando:

A glibc codifica da melhor maneira possível suas funções. É de se pressupor que a função strcpy, por exemplo, seja melhor que a função tradicional, que é definida assim:

char *strcpy(char *dest, const char *src)
{
  char *p = dest;
  while ((*p++ = *src++) != '\0')
    ;

  return dest;
}

Na arquitetura x86-64 o código gerado é bem direto:

strcpy:
  add   rsi,1
  movzx eax,byte [rsi-1]
  add   rdi,1
  test  al,al
  mov   byte [rdi-1],al
  jne   strcpy
  ret

A glibc, por outro lado, usa uma abordagem interessante:

char *strcpy(char *dest, const char *src)
{
  char c;
  char *s = (char *) src;

  // A função usa o ponteiro s como base para a cópia.
  // Note que off é o deslocamento de dest em relação a s.
  // É subtraido 1 aqui porque o ponteiro é incrementado 
  //   logo abaixo.
  const ptrdiff_t off = dest - s - 1;

  do
  {
    c = *s++;
    s[off] = c;
  }
  while (c);

  return dest;
}

E o código gerado é esse:

strcpy:
  mov rcx,rdi
  mov rax,rdi
  sub rcx,rsi
.loop:
  add rsi,1
  movzx eax,byte [rsi-1]
  test dl,dl
  mov byte [rsi-1+rcx],dl
  jne .loop
  ret

Ora, o loop tem uma instrução a menos! É de se supor que ele seja ligeiramente mais rápido! No entanto, o código tradicional mostrou-se cerca de 20% mais rápido em minhas medições. A diferença está, ao que parece, no cálculo do endereço linear feito no armazenamento de DL. Eis uma das medições ao copiar uma string com 511 ‘a’s:

strcpy da glibc: 872 ciclos
strcpy tradicional: 688 ciclos

Outras funções podem sofrer do mesmo “problema”, mas isso não significa que refazê-las em assembly seja a solução. De fato, a função strlen, do jeito que está implementada na glibc, é bem melhor que uma possível reescrita (como a função mystrlen no meu livro “Linguagem Assembly para i386 e x86-64”). Ao invés de comparar um byte de cada vez, a glibc compara de 8 em 8 (no x86-64) ou de 4 em 4 (no i386). Nesse sentido, usar o artifício da instrução prefixada REPNZ SCASB não nos ajuda muito (aliás, só atrapalha… Isso é quase 4 vezes mais lento!). Só refrescando a memória, a função mystrlen foi escrita assim, no livro, como exemplo:

mystrlen:
  mov rsi,rdi
  mov rcx,-1
  xor eax,eax
  repne scasb
  not rcx
  dec rcx
  mov rax,rcx
  ret
Anúncios