Prática horrível: Uso de MAX_PATH

Eis algumas coisas que considero muito erradas quando alguém tenta criar alguns tipos corriqueiros de rotinas: usar a pilha inescrupulosamente, e achar que o tamanho máximo de um pathname é de 260 caracteres. Ambas costumam levar a problemas sérios… Eis um exemplo:

void faz_algo(void)
{
  char fname[MAX_PATH+1];
  char *p;

  getcwd(fname, MAX_PATH);
  p = fname + strlen(fname) - 1;
  if (*p == '//') *p = '\0';
  strcat(fname, "/o-nome-maluco-de-algum-arquivo.txt");
  ...
}

Aqui, além do buffer apontado por fname ser alocado na pilha, gastando mais que 260 bytes, já que a pilha tem que estar, necessariamente, alinhada e ainda existe a red-zone, esse buffer tem tamanho máximo que parece bom… O que acontece se o diretório corrente for algo assim: /home/frederico/files/br/com/xpto/alpha/beta/theta/caralhos-me-levem/abobora/? Well… Ainda é provável que isso caiba num buffer de 260 caracteres, mas saiba que em certos ambientes o path máximo pode ter de 32 KiB! É o caso, por exemplo, do Windows (veja aqui).

A ideia do esquema acima pressupõe que tenhamos espaço suficiente e evita alocação dinâmica e, de bandeja, livra-se do buffer quando a função terminar. É interessante, mas não é assim que as linguagens que possuem o tipo primitivo para strings funciona. Eis uma maneira de fazer, com alocação, que não fica tão estranha assim:

char *fname, *cwd;

// get_current_directory() é uma extensão da glibc que
// aloca espaço da string pra você!
cwd = get_current_directory();

// asprintf() aloca espaço suficiente para todos os
// caracteres da string resultante!
asprintf(&fname,
         "%s/o-nome-maluco-de-algum-arquivo.txt",
         cwd);

// Não precisamos mais do bloco alocado em cwd.
free(cwd);
...
// Livra-se do buffer apontado por fname...
free(fname);

Ficou mais complicado? Que nada! O único detalhe é que você terá que se livrar dos buffers manualmente.

Os mais atentos verão que não faço nenhuma checagem de falha. Isso porque a alocação de pedaços pequenos de memória quase nunca falham. E free não liga se o ponteiro contém NULL… Mas, mesmo que isso seja importante pra você, alguns simples if‘s resolvem tudo.

Para lidar com strings dinâmicamente você pode, até mesmo, usar realocação, como, por exemplo:

char *_str_concat(char **pp, const char * const *s)
{
  char *tmp;
  size_t size;

  size = strlen(s);
  if (*pp) size += strlen(*pp);

  if (tmp = realloc(*pp, size+1))
  {
    *pp = tmp;
    strcat(*pp, s);
  }
  return *pp;
}

Neste caso, se o ponteiro para sua string for NULL, realloc alocará o espaço desejado (do mesmo tamanho da string apontada por s), caso contrário, os dois tamanhos são adicionados, o espaço da string original é aumentado e depois a concatenação é feita. O uso da dupla indireção pode te confundir, mas o uso é bem simples:

char *s = NULL;

_str_concat(&s, "Fred");
_str_concat(&s, " é ");
_str_concat(&s, "bom nisso!");
...
// Não se esqueça de livrar-se da string
free(s);

No fim das contas, o ponteiro s apontará para um buffer dinamicamente alocado contendo apenas a string “Fred é bom nisso\0“.

Você pode até mesmo garantir que sua string seja declarada corretamente usando uma macro:

#define DECLARE_STRING(name) char *name = NULL

Para usar, é claro, basta fazer DECLARE_STRING(s);, ao invés de escrever char *s = NULL;. E no caso do free, você pode fazer:

// Chame da mesma forma que chamou _str_concat.
void DELETE_STRING(char **pp)
{ if (*pp) free(*pp); *pp = NULL; }

Uma operação de assinalamento numa string já alocada não poderia ser mais simples:

char *_str_assign(char **pp, char *s)
{
  DELETE_STRING(pp);
  _str_concat(pp, s);
}

Todas as outras funções de string.h continuam funcionando do mesmo jeito e, ao contrário da crença “popular”, alocações de pequenos blocos são realmente rápidas. Você ainda pode criar rotinas de housekeeping, assim:

void alguma_funcao(void)
{
  jmp_buf jb;
  DECLARE_STRING(s);

  if (setjmp(jb))
  {
    // Coloque aqui seus DELETE_STRING()...
    DELETE_STRING(&s);
    return;
  }

  ...

  // Antes de sair chame longjmp()! :)
  // Não esqueça de fazer isso tb se tiver algum "return"
  // no código, fora daquele bloco de setjmp(), ai em cima...
  longjmp(jb, 1);
}

É claro, você pode criar MACROS para simplificar a coisa, tipo: DECLARE_HOUSEKEEPING, que declarará jb, HOUSEKEEPING que declarará o if (setjmp(jb)) e EXIT_FUNCTION, que chamará o longjmp(jb, 1):

// Coloque essas 3 num header!
#define DECLARE_HOUSEKEEPING jmp_buf _jb
#define HOUSEKEEPING if (setjmp(_jb))
#define EXIT_FUNCTION longjmp(_jb,1)

void alguma_funcao(void)
{
  DECLARE_HOUSEKEEPING;
  DECLARE_STRING(s);

  HOUSEKEEPING
  {
    // Coloque aqui seus DELETE_STRING()...
    DELETE_STRING(&s);
    return;
  }

  ...

  // Antes de sair chame EXIT_FUNCTION :)
  // Não esqueça de fazer isso tb se tiver algum "return"
  // no código, fora daquele bloco de setjmp(), ai em cima...
  EXIT_FUNCTION;
}

Ficou mais fácil de ler?

Anúncios

Teimosia…

Tem gente teimosa que insiste em fazer as coisas da forma errada… Neste artigo eu informei a meus leitores, explicitamente, que a string que deve ser passada para a função setlocale segue os padrões ISO 639 e ISO 3166. Alguns acreditam que isso só vale para ambientes Unix e que no Windows a coisa é “mais fácil”… A insistência, é claro, é pela passagem de strings como “Portuguese” ou “Portugues”, como em:

// Isto está ERRADO!!!
setlocale(LC_ALL, "Portuguese");

A função setlocale(), como descrita na MSDN Library (aqui), diz claramente: “The set of locale names supported by setlocale are described in Locale Names, Languages, and Country/Region Strings.” e o tópico em questão nos diz: “The locale name form—for example, en-US for English (United States) or bs-Cyrl-BA for Bosnian (Cyrillic, Bosnia and Herzegovina)—is preferred“.

Mais um exemplo para comprovar que, mesmo o Windows, usa o padrão para locale, é usar uma função da Win32 API para obter o locale padrão do sistema:

#include <stdio.h>
#include <wchar.h>
#include <windows.h>

void main(void)
{
  wchar_t locale[LOCALE_NAME_MAX_LENGTH+1];

  // Usando Win32 API para obter o locale padrão!
  // GetSystemDefaultLocaleName() exige um ponteiro
  // para wide string.
  GetSystemDefaultLocaleName(locale, 
                             LOCALE_NAME_MAX_LENGTH);
  wprintf(L"%ls\n", locale);
}

Ao compilar e executar o programinha acima, usando o MinGW64, obtemos:

$ x86_64-w64-mingw32-gcc -O2 -o locale.exe locale.c
locales.c: In function ‘main’:
locales.c:8:3: warning: implicit declaration of function ‘GetSystemDefaultLocaleName’ [-Wimplicit-function-declaration]
   GetSystemDefaultLocaleName(locale, LOCALE_NAME_MAX_LENGTH);
   ^
... Copiando o locale.exe para o Windows e executando:
C:\Work> locale
pt_BR

Yep… tem esse aviso ai (provavelmente estou esquecendo de definir algum símbolo para compilar código para versão do Windows superior ao Vista), mas o linker não reclamou, então ele conseguiu resolver a referência corretamente…

Note que a própria Win32 API prefere o padrão ISO!

Mais uma vez: Nem sempre é bom desenvolver diretamente em assembly!!!

Sim… de fato, a máxima “Não existe melhor otimizador do que o está entre suas orelhas” continua sendo verdadeira, só que essa massa de carne nem sempre realiza o melhor trabalho. Eis um caso onde uma rotina simples deveria ser mais rápida se feita em ASM:

uint16_t cksum(void *data, size_t length)
{
  uint32_t sum;
  uint16_t *p = data;
  _Bool rem;

  sum = 0;
  rem = length & 1;
  length >>= 1;

  while (length--)
    sum += *p++;

  if (rem)
    sum += *(uint8_t *)p;

  while (sum >> 16)
    sum = (sum & 0xffff) + (sum >> 16);

  return ~sum;
}

Essa é a implementação do cálculo de CHECKSUM de 16 bits, de acordo com a RFC 1071 (aqui). O código é bem simples e deveria gerar uma listagem em assembly tão simples quanto, mas quando você compila com a máxima otimização consegue ums listagem enorme que, inclusive, na plataforma x86-64, usa até SSE!

A minha implementação da rotina acima, em puro assembly e “otimizada”, para a plataforma x86-64, seria essa:

  bits 64

  section .text

  global cksum2

; uint16_t cksum2(void *data, size_t length);
  align 16
cksum2:
  xor   eax,eax   ; sum = 0;
  xor   ecx,ecx   ; RCX será usado como índice...
  shr   rsi,1     ; # de words!
  jnc   .even     ; Se CF=0, RSI era par!

  ; Ao invés de adicionar o byte extra no final,
  ; optei pelo início.
  movzx dx,byte [rdi]
  add   ax,dx
  inc   rdi
  jmp   .even

  ; Loops geralmente são críticos. Note que optei
  ; por colocar o teste no final, deixando o salto
  ; conticional para trás para não ferir o
  ; algoritmo estático do "branch prediction".
  ; Também, o ponto de entrada alinhado aos 16 bytes
  ; ajuda com possíveis efeitos negativos com o 
  ; cache L1i...
  align 16
.loop:
  add   ax,[rdi+rcx*2]
  adc   ax,0
  dec   rsi        ; length--;
  inc   rcx        ; próxima word...
.even:
  test  rsi,rsi    ; length == 0?
  jnz   .loop      ; não! continua no loop.
  
  not   ax         ; sum = ~sum;
  ret

Compare essa minha rotina com a gerada pelo compilador:

cksum:
  mov   r11,rsi
  shr   rsi
  push  rbp
  and   r11d,1
  test  rsi,rsi
  push  rbx
  je    .L2
  mov   rdx,rdi
  lea   r10,[rsi-1]
  and   edx,15
  shr   rdx
  neg   rdx
  and   edx,7
  cmp   rdx,rsi
  cmova rdx,rsi
  cmp   rsi,10
  ja    .L74
  mov   rdx,rsi
.L3:
  cmp   rdx,1
  lea   rcx,[rdi+2]
  movzx eax,WORD PTR [rdi]
  lea   r8,[rsi-2]
  je    .L5
  movzx r8d,WORD PTR [rdi+2]
  lea   rcx,[rdi+4]
  add   eax,r8d
  cmp   rdx,2
  lea   r8,[rsi-3]
  je    .L5
  movzx r8d,WORD PTR [rdi+4]
  lea   rcx,[rdi+6]
  add   eax,r8d
  cmp   rdx,3
  lea   r8,[rsi-4]
  je    .L5
  movzx r8d,WORD PTR [rdi+6]
  lea   rcx,[rdi+8]
  add   eax,r8d
  cmp   rdx,4
  lea   r8,[rsi-5]
  je    .L5
  movzx r8d,WORD PTR [rdi+8]
  lea   rcx,[rdi+10]
  add   eax,r8d
  cmp   rdx,5
  lea   r8,[rsi-6]
  je    .L5
  movzx r8d,WORD PTR [rdi+10]
  lea   rcx,[rdi+12]
  add   eax,r8d
  cmp   rdx,6
  lea   r8,[rsi-7]
  je    .L5
  movzx r8d,WORD PTR [rdi+12]
  lea   rcx,[rdi+14]
  add   eax,r8d
  cmp   rdx,7
  lea   r8,[rsi-8]
  je    .L5
  movzx r8d,WORD PTR [rdi+14]
  lea   rcx,[rdi+16]
  add   eax,r8d
  cmp   rdx,8
  lea   r8,[rsi-9]
  je    .L5
  movzx r8d,WORD PTR [rdi+16]
  lea   rcx,[rdi+18]
  add   eax,r8d
  cmp   rdx,10
  lea   r8,[rsi-10]
  jne   .L5
  movzx r8d,WORD PTR [rdi+18]
  lea   rcx,[rdi+20]
  add   eax,r8d
  lea   r8,[rsi-11]
.L5:
  cmp   rsi,rdx
  je    .L6
.L4:
  mov   rbx,rsi
  sub   r10,rdx
  sub   rbx,rdx
  lea   r9,[rbx-8]
  shr   r9,3
  add   r9,1
  cmp   r10,6
  lea   rbp,[0+r9*8]
  jbe   .L7
  pxor  xmm0,xmm0
  lea   r10,[rdi+rdx*2]
  xor   edx,edx
  pxor  xmm2,xmm2
.L8:
  movdqa  xmm1,XMMWORD PTR [r10]
  add   rdx,1
  add   r10,16
  cmp   r9,rdx
  movdqa  xmm3,xmm1
  punpckhwd xmm1,xmm2
  punpcklwd xmm3,xmm2
  paddd xmm0,xmm3
  paddd xmm0,xmm1
  ja    .L8
  movdqa  xmm1,xmm0
  sub   r8,rbp
  lea   rcx,[rcx+rbp*2]
  psrldq  xmm1,8
  paddd xmm0,xmm1
  movdqa  xmm1,xmm0
  psrldq  xmm1,4
  paddd xmm0,xmm1
  movd  edx,xmm0
  add   eax,edx
  cmp   rbx,rbp
  je    .L6
.L7:
  movzx edx,WORD PTR [rcx]
  add   eax,edx
  test  r8,r8
  je    .L6
  movzx edx,WORD PTR [rcx+2]
  add   eax,edx
  cmp   r8,1
  je    .L6
  movzx edx,WORD PTR [rcx+4]
  add   eax,edx
  cmp   r8,2
  je    .L6
  movzx edx,WORD PTR [rcx+6]
  add   eax,edx
  cmp   r8,3
  je    .L6
  movzx edx,WORD PTR [rcx+8]
  add   eax,edx
  cmp   r8,4
  je    .L6
  movzx edx,WORD PTR [rcx+10]
  add   eax,edx
  cmp   r8,5
  je    .L6
  movzx edx,WORD PTR [rcx+12]
  add   eax,edx
.L6:
  test  r11,r11
  lea   rdi,[rdi+rsi*2]
  je    .L71
.L17:
  movzx edx,BYTE PTR [rdi]
  add   eax,edx
  mov   edx,eax
  shr   edx,16
  test  edx,edx
  je    .L75
.L49:
  movzx eax,ax
  add   eax,edx
.L71:
  mov   edx,eax
  shr   edx,16
  test  edx,edx
  jne   .L49
.L75:
  not   eax
.L67:
  pop   rbx
  pop   rbp
  ret
.L74:
  test  rdx,rdx
  jne   .L3
  mov   r8,r10
  mov   rcx,rdi
  xor   eax,eax
  jmp   .L4
.L2:
  test  r11,r11
  je    .L76
  xor   eax,eax
  jmp   .L17
.L76:
  mov   eax,-1
  jmp   .L67

UAU! Obviamente a minha rotina é mais rápida que esse caminhão de instruções, certo? (aliás, repare nas instruções entre os labels .L8 e .L7! hehe)

Infelizmente os testes mostram que não! Comparando o cálculo do checksum de um buffer de 2 KiB (menos 1 byte para termos um buffer de tamanho impar, ou seja, o pior caso), a minha rotina é 5 vezes mais lenta que a gerada pelo compilador! Num de minhas máquinas de teste obtive, para cksum(), 3432 ciclos e para cksum2(), 16872!

É interessante notar que se mudarmos o código em C para efetuar a adição do byte isolado, se houver um, a performance vai ser semelhante à obtida em cksum2():

uint16_t cksum3(void *data, size_t length)
{
  uint32_t sum;
  uint16_t *p = data;

  sum = 0;
  if (length & 1)
    sum += *(uint8_t *)p++;
  length >>= 1;

  while (length--)
    sum += *p++;

  while (sum >> 16)
    sum = (sum & 0xffff) + (sum >> 16);

  return ~sum;
}

E, também, se mudar o meu código para efetuar a soma do byte adicional no final, se houver um, como na rotina original, a performance continuará tão ruim quanto:

  align 16
cksum4:
  xor   eax,eax
  xor   ecx,ecx
  mov   edx,esi
  and   edx,1
  shr   rsi,1
  jmp   .even

  align 16
.loop:
  add   ax,[rdi+rcx*2]
  adc   ax,0
  dec   rsi
  inc   rcx
.even:
  jnz   .loop
  test  edx,edx
  jz    .exit
  add   ax,[rdi+rcx*2]
  adc   ax,0
.exit:
  not   ax
  ret

Ou seja, o compilador está tomando conta dos possíveis efeitos no cache e desenrolando os loops para que atinja a melhor performance possível, mesmo que, para isso, gere um código bem maluco. Coisa que apenas com muita experiência eu ou você poderíamos fazer…

Novamente, sempre meça a performance de seus códigos e não pense que só porque está em “assembly” é que você conseguirá a melhor performance!

Arredondamento. Qual é o “correto”?

O último artigo gerou alguma controvérsia… Alguns leitores perguntam: “Qual é o jeito certo?” e não se satisfazem com a resposta de que ambos os métodos, de truncamento e de arredondamento para baixo, estão certos, dependendo da aplicação. Aqui quero provocá-los mostrando algo similar…

Já mostrei antes que operações em ponto flutuante dependem de algum método de arredondamento bem definido. O padrão IEEE 754 define quatro deles:

  • No sentido de +\infty;
  • No sentido de -\infty;
  • No sentido de 0;
  • O valor “mais próximo”;

Ao calcular f=1.0/10.0, o valor 0.1, que não pode ser representado numa variável do tipo ponto flutuante, já que, em binário, será uma dízima periódica:

\displaystyle (0.1)_{10} = (0.0110011001100110011001100...)_{2}

Portanto, precisa ser arredondado. Se estivermos lidando com o tipo float esse valor será (1.1001100110011001100110?)_{2}\cdot 2^{-2}, onde ‘?’ é o algarismo impreciso… Ou seja, se ele for zero, termos um valor levemente inferior a 0.1, se ele for 1, teremos um valor levemente superior a 0.1. Qual deles devemos escolher?

Por default, o padrão IEEE 754 escolhe o método do “mais próximo”. Repare:

/* test.c */
#include <stdio.h>

extern float _mydiv(float, float);

void main(void)
{
  float f;
  unsigned int d = 0x3dcccccc;
  float *g = (float *)&d;
  unsigned int *p = (unsigned int *)&f;

  f = _mydiv(1.0f, 10.0f);

  printf("O valor '%.2f' é codificado como '0x%08X'.\n\n", 
    f, *p);

  printf("Mas, na verdade:\n"
         "0x%08X -> %.18f\n"
         "0x%08X -> %.18f\n",
    *p, f,
    d, *g);
}

O motivo de criar a função _mydiv é para evitar que o compilador otimize a operação de divisão entre duas constantes e não faça divisão alguma. Neste caso, o arredondamento será feito pelo compilador, não pelo nosso programa. A função _mydiv é, simplesmente:

/* mydiv.c */
float _mydiv(float a, float b) 
{ return a/b; }

Compilando e linkando tudo e depois executando, temos:

$ gcc -o test test.c mydiv.c
$ ./test
O valor '0.10' é codificado como '0x3DCCCCCD'.

Mas, na verdade:
0x3DCCCCCD -> 0.100000001490116119
0x3DCCCCCC -> 0.099999994039535522

É fácil perceber que o primeiro caso está mais próximo de 0.1 do que o segundo e é este que o processador escolherá (como isso é feito não interessa agora!). Mas, isso não significa que os outros métodos de arredondamento não sejam “corretos”. De fato, se mandarmos o processador arredondar de outra maneira ele o fará, Eis um exemplo:

/* test.c */
#include <stdio.h>
#include <fenv.h>

extern float _mydiv(float, float);

void main(void)
{
  float f;
  int oldround;

  oldround = fegetround();
  fesetround(FE_TOWARDZERO);
  f = _mydiv(1.0f, 10.0f);
  fesetround(oldround);

  printf("%.18f\n", f);
}

Ao compilar e executar esse código (será preciso especificar a libm com -lm, no gcc) você verá que a divisão de 1 por 10 será arredondada para o MENOR valor, ou melhor, em direção ao zero. Esse será o valor mais distante do verdadeiro, neste caso. Troque 1.0f por -1.0f e veja o que acontece…

Depois, modifique o método para FE_DOWNWARD (arredondamenteo no sentido de -\infty) e teste os dois casos (com 1.0f e -1.0f)… Note que o truncamento citado no artigo anterior é equivalente a FE_TOWARDZERO…

O que quero dizer aqui é que, tanto na programação quanto na matemática, às vezes existe mais de um jeito “correto” de fazer alguma coisa. Depende apenas da aplicação.

Pré processador e constantes

Um amigo me mostrou um problema interessante que, confesso, levei algum tempo para resolver. O fragmento de código abaixo simplesmente não compila, causando um erro de “invalid suffix ‘…’ on integer or constant“:

/* fragmento de keys.h */
...
#define OFFSET 0x1000
...
#define ALT_1  (0xf8+OFFSET)
#define ALT_2  (0xf9+OFFSET)
#define ALT_3  (0xfa+OFFSET)
#define ALT_4  (0xfb+OFFSET)
#define ALT_5  (0xfc+OFFSET)
#define ALT_6  (0xfd+OFFSET)
#define ALT_7  (0xfe+OFFSET)
#define ALT_8  (0xff+OFFSET)
#define ALT_9  (0x80+OFFSET)
#define ALT_0  (0x81+OFFSET)
...
/* fragmento de main.c */
...
int altconvert[] = {
  ALT_A,ALT_B,ALT_C,ALT_D,ALT_E,ALT_F,ALT_G,ALT_H,
  ALT_I,ALT_J,ALT_K,ALT_L,ALT_M,ALT_N,ALT_O,ALT_P,
  ALT_Q,ALT_R,ALT_S,ALT_T,ALT_U,ALT_V,ALT_W,ALT_X,
  ALT_Y,ALT_Z,ALT_0,ALT_1,ALT_2,ALT_3,ALT_4,ALT_5,
  ALT_6,ALT_7,ALT_8,ALT_9
};

O erro aparece na linha que define o símbolo ALT_7 para o pré processador:

keys.h:97:21: error: invalid suffix "+OFFSET" on integer constant
 #define ALT_7 (0xfe+OFFSET)
                ^
main.c:9:11: note: in expansion of macro ‘ALT_7’
  ALT_6,ALT_7,ALT_8,ALT_9
        ^

Meu amigo ficou se perguntando isso não seria um bug do compilador… E a pergunta que fiquei me fazendo é: Por que diabos as definições de ALT_1 até ALT-6 e de ALT_8 e ALT_9 não deram o mesmo problema?

A diferença entre ALT_6, ALT_7 e ALT_8 é apenas uma: ALT_7 é um valor em hexadecimal terminada em ‘e’ e as demais não… Qual é mesmo a maneira de declarar constantes que tenham um ‘e’? Ahhhhh… ponto flutuante:

float c = 1e+10;

Acontece que os valores associados a ponto flutuante têm que ser expressos em decimal. Não podem ser hexadecimais… A não ser que estejamos falando do padrão C99 e, neste caso, o caractere ‘e’ teria que ser trocado por ‘p’:

float c = 0xfp+10;

Note que o expoente continua tendo que ser decimal…

No caso apresentado, o símbolo ALT_7 é definido como a string ‘0xfe+0x1000’. O ‘e’, seguido do valor inteiro, indica uma constante em ponto flutuante, mas hexadecimal… dai o erro. Que faz parte da especificação da linguagem! Não se trata de um bug!

Existem duas soluções possíveis:

  1. Colocar os valores inteiros na definição dos símbolos entre colchetes;
  2. Colocar um espaço entre os valores e o operador + (ou -).

É bom lembrar que o pré processador faz apenas substituições léxicas. Quando definimos um macro, como em:

#define advance_ptr(p) { (p)++; }

O pré compilador não efetua a operação… em qualquer parte do código onde advance_ptr for encontrado ele apenas substituirá p pelo parâmetro passado no macro… É o compilador que avaliará a operação ++, mas apenas depois que o pré compilador terminar suas substituições!

Portanto, a dica aqui é simples: Não assuma que o pré compilador “compila” alguma coisa ou avalia expressões… Nope, ele nunca fará isso!… Apenas “substituirá”. E “compilador” só tem um… a ênfase no nome deve ser no “pré”…

GCC 6.1.1 (unstable?) versus clang 3.8

Um leitor me fez a gentileza de compilar o código do artigo anterior (este aqui) usando o GCC 6.1.1 e o clang 3.8. O argumento, é claro, é um velho conhecido de todo mundo: “Ahhh, mas você está usando versões velhas!”. Isso porque usei o GCC 4.9.2 e o clang 3.5. Bem, comparem o código gerado por esses novíssimos e, supostamente, melhores compiladores, na mesma ordem do artigo anterior, primeiro o gerado pelo GCC 6.1.1 (que ainda está em fase de regressões e acertos) e o clang 3.8 (supostamente stable):

;------ GCC 6.1.1 -----------
  align 4
strlcpy:
  movzx ecx,byte [rsi]
  mov   rax,rdi
  test  cl,cl
  jz    .strlcpy_exit
  test  rdx,rdx
  jz    .strlcpy_exit

  add   rdx,rsi
  mov   r8,rdi
  jmp   .L1

  align 4
.loop:
  cmp   rdx,rsi
  je    .strlcpy_exit2
.L1:
  add   r8,1
  add   rsi,1
  mov   byte [r8-1],cl
  movzx ecx,byte [rsi]
  test  cl,cl
  jne   .loop

.strlcpy_exit2:
  mov   byte [r8],0
  ; --- O GCC 4.9 coloca o .strlcpy_exit aqui!
  ret

.strlcpy_exit:  
  mov   r8,rax
  jmp   .strlcpy_exit2

O GCC 6.1.1 conseguiu piorar, no finalzinho, uma rotina que parecia quase ideal. Note que a partir do label .strlcpy_exit o código é inútil, já que RDI já está em RAX!

O clang 3.8 faz um trabalho ainda pior que o seu irmão mais velho. Vejam:

;------ clang 3.8 -----------
  align 16
strlcpy:
  test  rdx,rdx
  mov   rax,rdi       ; Copia RDI para RAX
  jz    .strlcpy_exit ; Se size == 0, sai.

  mov   r8b,[rsi]     ; R8B = *src;
  test  r8b,r8b       
  mov   rax,rdi       ; Copia RDI para RAX (2ª vez).
  jz    .strlcpy_exit ; Se *src == '\0', sai.

  mov   ecx,1
  sub   rcx,rdx       ; RCX = 1 - RDX
  inc   rsi           ; src++;
  mov   rax,rdi       ; Copia RDI para RAX (3ª vez).
  
  align 16  
.loop:
  mov   [rax],r8b     ; *dest = *src;
  inc   rax           ; src++ (src temporário em RAX);
  test  rcx,rcx       
  jz    .strlcpy_exit ; RCX == 0? sai.

  mov   r8b,[rsi]     ; R8B = *src;
  inc   rcx           
  inc   rsi           ; src++;
  test  r8b,r8b
  jne   .loop         ; Se R8B != 0, loop.

.strlcpy_exit:
  mov   [rax],0       ; *dest = '\0';
  mov   rax,rdi       ; Copia RDI para RAX (4ª vez!).
  ret

O código não mudou muito desde a versão 3.5, exceto pelo fato de que, agora, a nova versão faz mais duas cópias desnecessárias de RDI para RAX!!!

Sinto muito, mas essas versões mais novas não me impressionaram nem um pouco… Bem… talvez o GCC tenha impressionado um pouquinho: Afinal, para uma versão não finalizada ele fez até um bom trabalho (como o GCC 4.9 faria)…

Qual dos dois você prefere (2)?

Pros amadores, defensores do C++, eis a rotininha basica mais usada para introduzir uma linguagem ao estudante: Hello, world!. Dessa vez, ambas escritas em C e C++ e seus equivalentes em assembly. Me diz qual você acha que é melhor, ok?

;------ C code -----
; #include <stdio.h>
; void main(void) { puts("Hello"); }
;-------------------
  bits 64

  section .rodata
hellostr: db  'Hello',0

  section .text

  extern puts

  global main
main:
  mov   rdi,hellostr
  jmp   puts

Agora veja o código em C++… Note que os nomes das funções são tão grandes que a formatação para evitar a barra de scroll na parte de baixo da listagem não é possível…

;------- C++ code -----
; #include <iostream>
; int main() { std::cout << "Hello\n"; }
;----------------------
  bits 64

  section .rodata
hellostr: db  'Hello',0x0a,0

  section .text

  ; MACACOS ME MORDAM!
  ; Precisa mesmo de 7 símbolos externos para
  ; imprimir uma stringzinha?!
  extern _ZSt4cout
  extern _ZSlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc 
  extern _ZStL8_ioinit
  extern _ZNSt8ios_base4InitC1Ev
  extern _ZNSt8ios_base4InitD1Ev
  extern __dso_handle
  extern __cxa_atexit

  global main
main:
  sub   rsp,8
  mov   rsi,hellostr
  mov   rdi,_ZSt4cout
  call  _ZSlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
  xor   eax,eax
  add   rsp,8
  ret

_GLOBAL__sub_I_main:
  sub   rsp,8
  mov   rdi,_ZStL8_ioinit
  call  _ZNSt8ios_base4InitC1Ev
  mov   rdx,__dso_handle
  mov   rsi,_ZStL8__ioinit
  mov   rdi,_ZNSt8ios_base4InitD1Ev
  add   rsp,8
  jmp   __cxa_atexit

Credo! E olha que usei a otimização máxima… E quanto a usar a otimização mínima?! Usando a chave -O0, obtemos algo assim para as duas funções:

; código em C:
main:
  push rbp
  mov  rbp,rsp
  mov  rdi,hellostr
  call puts
  pop  rbp
  ret

Agora, segure-se na cadeira ao ver o código criado pelo C++:

; Código em C++:
main:
  push rbp
  mov  rbp,rsp
  mov  rsi,hellostr
  mov  rdi,_ZSt4cout
  call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
  mov  eax,0    
  pop  rbp
  ret

_Z41__static_initialization_and_destruction_0ii:
  push rbp
  mov  rbp,rsp
  sub  rsp,16
  mov  [rbp-4],edi
  mov  [rbp-8],esi
  mov  dword [rbp-4],1
  jne  .L3
  cmp  dword [rbp-8],0xffff
  jne  .L3
  mov  rdi,_ZStL8__ioinit
  call _ZNSt8ios_base4InitC1Ev
  mov  rdx,__dso_handle
  mov  rsi,_ZStL8__ioinit
  mov  rdi,_ZNSt8ios_base4InitD1Ev
  call __cxa_atexit
.L3:
  leave
  ret

_GLOBAL__sub_I_main:
  push rbp
  mov  rbp,rsp
  mov  esi,0xffff
  mov  edi,1
  call _Z41__static_initialization_and_destruction_0ii
  pop  rbp
  ret

Caramba! Os mesmos 7 símbolos e mais uma função e ainda com uns 2 saltos condicionais!

Agora me diz: Os códigos finais, gerados pelo compilador C++ são ou não uma sopa de letrinhas? Se eu te der um código em assembly, gerado pelo g++, você seria capaz de entendê-lo? Olhando para esses códigos ai em cima, nas variantes C, a chamada a puts é bem evidente (Opa! Então estamos imprimindo uma string usando stdout!), mas não podemos dizer o mesmo dos códigos compilados C++…

Afinal, o que _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc significa? E essas funções estáticas adicionais? Com um pouco de experiência você saberá que essas funções adicionais são relacionadas ao destrutor da classe basic_ostream, mas que é confuso pacas, é…