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.

Cuidado com o que você pensa que sabe…

Recentemente topei com um exemplo que deveria ser absolutamente simples e já estar “no sangue” de qualquer estudante do ensino básico (note bem: básico!). Por exemplo, o resto da divisão de 2 por 3 (2\mod 3) é 2, certo? Parece lógico extrapolar e dizermos que -2\mod 3=-2. Surpresa! A resposta pode estar errada, dependendo de qual definição de resto você usa! O problema está na definição de resto. Usando o método do truncamento do quociente, temos:

\displaystyle x\mod y=x - y\cdot trunc\left(\frac{x}{y}\right)

Mas, também existe a definição de Knuth:

\displaystyle x\mod y=x - y\left\lfloor\frac{x}{y}\right\rfloor

Ao usarmos essa definição teremos:

\displaystyle \begin{matrix}  -2\mod 3 &= (-2)-3\left\lfloor-\frac{2}{3}\right\rfloor \\  &= (-2)-3(-1) \\  &= -2+3 = 1 \\  \end{matrix}

Isso parece estranho até você lembrar da regrinha sobre o resto: Ele precisa ser sempre menor que o divisor. Mas qualquer valor negativo é menor que 3, certo? Portanto a restrição de que o quociente seja calculado através do operador floor, faz todo sentido…

Ainda, é interessante notar que, de acordo com a definição acima, o resto tem valor absoluto diferente nas expressões abaixo. Ele segue o sinal do divisor:

\displaystyle \begin{matrix}  -2\mod -3 = (-2)-(-3)\left\lfloor\frac{2}{3}\right\rfloor = -2 \\  2\mod -3 = 2-(-3)\left\lfloor-\frac{2}{3}\right\rfloor = -1 \\  -2\mod 3 = (-2)-3\left\lfloor-\frac{2}{3}\right\rfloor = 1 \\  2\mod 3 = 2-3\left\lfloor\frac{2}{3}\right\rfloor = 2 \\  \end{matrix}

Uma interpretação geométrica para o método de Knuth pode ser a de que a operação mod restringe o co-domínio da função para valores no intervalo entre 0 e o valor mais próximo (porém, absolutamente menor) que o do divisor, formando um “loop”. No caso de divisores positivos, o loop ficaria assim:

loop

Partindo de zero, uma vez que o dividendo (numerador) tem sinal contrário ao divisor (denominador), o ponteiro do “relógio” tem que ser “girado” duas casas no sentido “anti-horário” e obtemos o valor 1, como mostrado acima. No caso de um divisor negativo, a graduação é feita no sentido anti-horário usando 0, -1 e -2 e, se o dividendo tiver sinal contrário (positivo) o ponteiro girará no sentido horário, obtendo -1 (como pode ser visto na lista acima)… No caso de a=-2 e b=-3, a operação a\mod b graduará o loop no sentido anti-horário com 0, -1 e -2, já que o divisor é negativo, e o ponteiro será girado no mesmo sentido, já que o dividendo (numerador) também é negativo e, por isso, obtemos -2.

Essa é uma forma interessante de se entender o chamado complemento 2, usado na aritmética binária com sinais… Considere um conjunto com 16 valores possíveis (ou, 4 bits). O valor -2 deverá ser representado pelo valor positivo 14, já que -2\mod 16=14, segundo a definição. Isso também fica simples de representar geometricamente, graduando o loop no sentido horário e deslocando o ponteiro do “relógio” duas posições no sentido anti-horário:

loop2

Para maior quantidade de bits, mais graduações teremos no loop…

O problema é que a maioria das linguagens de programação lidam com o resto através do truncamento do quociente. Eis dois exemplos, o primeiro em Java:

/* test.java */
class test {
  public static void main(String[] args)
  { 
    int x = -2;
    int y = 3;
    int r = x % y;
    System.out.println(r); 
  }
}

Compilando e executando, obtemos:

$ javac test.java
$ java test
-2

A mesma coisa ocorre em C, C++ e Pascal (embora o Pascal padrão sempre resulte num resto positivo!), por exemplo… Quanto aos processadores, os compatíveis com a arquitetura Intel x86 possuem a instrução IDIV, que é definida como (quando a divisão é feita com valores de 32 bits):

Signed divide EDX:EAX by r/m32, with result stored in EAX ← Quotient, EDX ← Remainder

Assim, para obter o resto de uma divisão inteira temos que colocar o numerador no par de registradores EDX:EAX, o denominador em um outro lugar, executar IDIV e obter o resto em EDX. A função, para ser usada em C fica assim:

; mymod.asm
bits 64
section .text
global mymod
; Retorna A mod B.
; Entrada: EDI = A; ESI = B
; Saída: EAX = resto
mymod:
  mov  eax,edi
  cdq          ; estende o sinal de 'A' em EDX.
  idiv esi
  mov  eax,edx ; queremos só o resto.
  ret

Usarei a função da seguinte maneira:

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

extern int mymod(int, int);

void main(void)
{ printf("%d\n", mymod(-2,3)); }

Compilando tudo, linkando e executando, temos:

$ nasm -felf64 mymod.asm -o mymod.o
$ gcc -c -o test.o test.c
$ gcc -o test test.o mymod.o
$ ./test
-2

Ou seja, o processador também calcula o resto de maneira “tradicional”, truncando…

O outro método é o de Euclides e pode ser equacionado assim:

\displaystyle x\mod y = x - \left|y\right|\left\lfloor\frac{x}{\left|y\right|}\right\rfloor

Que, é claro, é bem mais complicado e tem como resultado o mesmo valor obtido pelo método de Knuth, exceto que o sinal do resto não é sempre o mesmo do divisor…

ATENÇÃO: Os 3 métodos são “corretos”, depende somente como uma divisão é definida!!

O alerta aqui é que, se você está adaptando uma equação que use aritmética modular (mod) na implementação que usa valores negativos, pode ser que tenha problemas com os resultados se não estiver certo sobre o método que o compilador/linguagem está lidando.

Como falei antes, a maioria das linguagens usa o método do truncamento do quociente, mas nem todas. Uma exceção interessante: Python calcula o resto usando o método de Knuth

$ python
Python 2.7.12 (default, Nov 19 2016, 06:48:10) 
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> -2 % 3
1
>>>^D
$

A mesma coisa acontece com a aplicação gnome-calculator

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, é…

‘continue’ não faz o que você imagina em loops ‘do…while’

Por causa de construções do tipo:

while (i < 10)
{
  if (!foo(i))
    continue;
  bar(&i);
}

Onde, se a função foo() retornar zero o código salta para o início do loop. Isso funciona do mesmo jeito num loop com for, mas e num do…while?

do {
  if (!foo(i))
    continue;
  bar(&i);
} while (i < 10);

Será que, enquanto foo() retornar zero, o loop será executado de novo? A resposta é: depende do valor de i!

O que continue faz é reavaliar a condição do loop (i < 10) e, se for satisfeita, o loop continua. Funciona da mesma forma que no while

Cuidado com continue!