GCC: Que boa surpresa!

Bem… estou aqui me esgoelando para escrever um bom artigo sobre “Bit Block Transfers” e precisei efetuar rotações com grupos de bits. Em assembly é fácil: basta usar instruções como ROL e ROR, mas e em C?

Eu sei que os compiladores costumam usar um subconjunto das instruções disponíveis no processador e que, às vezes, isso leva a códigos menos que ótimos… Ainda, nos antigos Turbo C e Microsoft C 6.0, para MS-DOS, existiam as funções (não padronizadas) _rotl() e _rotr() para suprir a necessidade de realizar rotações de bits, coisa que não existe nos compiladores modernos (pelo menos eu não achei!).

Por padrão, C só disponibiliza os operadores << e >>, onde o shift para a direita, quando usado com tipos sinalizados, pode não funcionar como desejado (o comportamento é “dependente de implementação”, de acordo com as especificações da linguagem). Isso quer dizer que esse shift pode ser “lógico” ou “aritmético”, levando ou não em conta o sinal do valor sendo deslocado. Neste caso não há problemas: Basta sembre usar tipos não sinalizados… Mas, lembre-se, shift não é “rotação” e portanto, temos que usar um macete.

Fiz uma comparação das duas rotinas abaixo, usando o GCC 5.2:

unsigned int rotate_right(unsigned int data, int bits)
{
  return (data >> bits) |        // desloca para direita,
         (data << (32 - bits));  // zerand bits superiores e
                                 // desloca para esquerda,
                                 // zerando bits inferiores e
                                 // junta tudo com OR.
}

unsigned int rotate_right_asm(unsigned int data, int bits)
{
  __asm__ __volatile__ (
    "rorl %%cl,%0"
    : "+rm" (data) : "c" (bits)
  );

  return data;
}

A primeira, rotate_right() faz a rotação na marra! A segunda, obviamente, usa a instrução ROR. Bem… qual foi a surpresa? Essa aqui, ó:

$ cc -O2 -c -o test.o test.c
$ objdump -SM intel test.o
...
Disassembly of section .text:

0000000000000000 : 
   0:	89 f8                	mov    eax,edi
   2:	89 f1                	mov    ecx,esi
   4:	d3 c8                	ror    eax,cl
   6:	c3                   	ret    

0000000000000010 : 
  10:	89 f8                	mov    eax,edi
  12:	89 f1                	mov    ecx,esi
  14:	d3 c8                	ror    eax,cl
  16:	c3                   	ret    

O compilador é esperto o suficiente para perceber que eu queria fazer uma rotação de bits!! Viva o GCC!

Ahhh, sim… testei também com o clang 3.8. A rotina em C ficou semelhante a que foi mostrada acima, com uma coisa estranha (mostro abaixo), mas a rotina em assembly inline ficou pior!

$ clang -O2 -c -o test.o test.c
$ objdump -SM intel test.o
...
Disassembly of section .text:

0000000000000000 : 
   0:	40 88 f1             	mov    cl,sil
   3:	d3 cf                	ror    edi,cl
   5:	89 f8                	mov    eax,edi
   7:	c3                   	ret    

0000000000000010 : 
  10:	89 7c 24 fc          	mov    DWORD PTR [rsp-0x4],edi
  14:	89 f1                	mov    ecx,esi
  16:	d3 cf                	ror    edi,cl
  18:	89 7c 24 fc          	mov    DWORD PTR [rsp-0x4],edi
  1c:	89 f8                	mov    eax,edi
  1e:	c3                   	ret

Quanto a rotina em C, por que diabos o clang resolveu inicializar o CL ao invés de ECX? Mesmo que ROR só vá usar CL, copiar ESI para ECX é mais rápido do que fazer a mesma coisa necessitando de um prefixo REX na instrução! E, mesmo que não seja mais “rápido” (supondo que o prefixo REX não adicione 1 ciclo de clock extra), a função ficou maior que o necessário!

No caso da rotina escrita em assembly inline, por que diabos o clang está salvando EDI na pilha duas vezes? Mesmo porque o conteúdo de EDI não vai ser reaproveitado!!!

Mau, clang! Feio!

Anúncios