Preenchendo um buffer com zeros…

Um amigo me perguntou se o código da função bzero() – que é POSIX – é o mesmo que:

void bzero(void *p, size_t size)
{
  char *pb = p;

  while (size--) *pb++ = 0;
}

A resposta é um sonoro e definitivo NÂO! A função bzero() tenta preencher um buffer com zeros usando, primeiro, os “maiores” tipos (QWORDs, por exemplo) e continuando com os menores (WORDs e BYTEs). A lógica é que bzero() escreve de 8 em 8 bytes, em primeiro lugar, até que menos que 8 bytes estejam disponíveis no buffer… Depois, tenta escrever de 4 em 4 e, por fim, 2 bytes (WORD) e o byte final… É um recurso inteligente, mas não é o mais veloz possível. Mesmo gritando o NÂO, devo chamar a atenção que a função acima, se compilada com a chave -O3, é subsituida por uma chamada à função intrinseca memset(), desta maneira:

void bzero(void *p, size_t size)
{
  memset(p, 0, size);
}

Tornando o loop, em certos casos, tão performático quanto chamar bzero(). Na arquitetura x86_64, a chamada à função memset() pode ser substituida pelo compilador por uma sequência de instruções usando, inclusive, SSE, ou ser mantida como uma chamada à função… Depende do que o compilador decidir o que seja mais veloz. Mas, eis um fato interessante: Desde o lançamento do Pentium, as instruções de string do processador foram condenadas como sendo “lentas”… Isso não é mais verdade! Instruções como “REP STOSB”, agora, tentam usar a mesma tática usada pela função bzero() que citei acima, mas usando, inclusive, uma variação temporária dos registradores YMM (se seu processador suporta AVX) e XMM (SSE). Com isso, a simples instrução “REP STOSB” pode chegar a ser até quase duas vezes mais rápida do que cópias discretas. Para exemplificar, eis alguns códigos de exemplo:

; Compilar com 'nasm -f elf64 bzero.asm -o bzero.o'
bits 64
section .text

; extern void bzero_asm(void *, size_t);
global bzero_asm:function
bzero_asm:
  push rcx
  xor rax,rax
  mov rcx,rsi
  rep stosb
  pop rcx
  ret

E o código em C:

#include <stdlib.h>
#include <x86intrin.h>

/* É interessante compilar essa função separadamente,
   usando a chave -O2, ao invés de -O3, para evitar a substituição
   por 'memset()'. */
void bzero_loop(void *p, size_t size)
{
  char *pb = p;

  while (size--) *pb++ = 0;
}

/* Tentando usar a tática do bzero(), mas usando SSE e
   o tipo long long (64 bits). */
void bzero_sse(void *p, size_t size)
{
  __m128i *psse = p;
  long long *pll;
  long *pl;
  short *ps;

  size_t sztmp;

  sztmp = size % sizeof(__m128i);
  while (sztmp--) 
  { 
    *psse++ = _mm_setzero_si128(); 
    size -= sizeof(__m128i); 
  }

  sztmp = size % sizeof(long long);
  pll = (long long *)psse;
  while (sztmp--) 
  { 
    *pll++ = 0; 
    size -= sizeof(long long); 
  }

  sztmp = size % sizeof(long);
  pl = (long *)pll;
  while (sztmp--) 
  { 
    *pl++ = 0; 
    size -= sizeof(long); 
  }

  sztmp = size % sizeof(short);
  ps = (short *)pl;
  if (sztmp) 
  { 
    *ps++ = 0; 
    size -= sizeof(short); 
  }

  if (size)
    *((char *)ps) = 0;
}

Medindo o tempo de 1000 chamadas a essas 3 funções. preenchendo um buffer de 1000 bytes, e das funções bzero() e memset(), com o mesmo critério, obtive:

bzero_loop: 3871 ciclos.
bzero_asm: 177 ciclos.
bzero_sse: 265 ciclos.
bzero: 228 ciclos.
memset: 228 ciclos.

Ou seja, a função bzero_sse(), que não é lá das mais otimizadas (eu poderia fazer melhor com um pouco mais de tempo), mesmo usando SSE, só é ligeiramente melhor que memset() e bzero() – só gasta pouco menos que 40 ciclos de máquina do que as outras duas… O loop, óbviamente, é o meis lento de todas as outras funções e, surpreendentemente, um simples REP STOSB é a função mais rápida de todas! Note que praticamente não há diferença entre bzero() e memset(). Os testes foram feitos num Core i7 (Família 6, modelo 60, step 3 – Arquitetura Haswell).

Anúncios

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s