Tradições

Recentemente alguém me perguntou sobre um código simples onde continha algo assim:

const char *array[] = { "abc", "def", "ghi" };

const char *pegaString(char i) 
{ 
  if (i >= 0 && i < 3) 
    return array[i]; 
  else 
    return NULL; 
}

É um código perfeitamente válido, mas tem um problema: Tradicionalmente, o tipo preferido para índices de arrays é ‘int’, não ‘char’. Há um motivo…

O tipo char só tem 1 byte de tamanho e ponteiros têm de 4 a 8 bytes de tamanho. O tipo ‘int’ tem 4 bytes de tamanho e ajusta-se perfeitamente à artitmética de ponteiros. O tipo ‘char’ precisa ser convertido.

Num código compilado para 32 bits, ao obter o ponteiro do array via “array[i]”, o código acima provavelmente fará algo assim:

; Supondo que ESI contém o ponteiro base para 'array' 
; e CL contenha 'i':
movsx  ecx,cl        ; Converte de 8 para 32 bits.
mov    eax,[esi+ecx]

Usando ‘int’ essa conversão não acontece. Mas, no modo x86-64, não temos tanta sorte com ‘int’:

; Supondo que RSI contém o ponteiro base para 'array' 
; e ECX contenha 'i':
movsx  rcx,ecx       ; Converte de 8 para 32 bits.
mov    eax,[rsi+rcx]

Em ambos os casos a instrução MOVSX precisa ser usada para considerar o sinal da variável ‘i’. No caso do modo x86-64 e usando ‘i’ como tipo ‘int’ a mesma coisa tem que ser feita…

Acontece que a tradição não condiz com a especificação da linguagem! Índices de arrays deveriam ser do tipo ‘ssize_t’. Este tipo, definido no header “unistd.h” é, essencialmente, o mesmo tipo ‘size_t’, definido em “stdlib.h”, mas com sinal. E ambos têm o tamanho de um registrador (ou, de um ponteiro)…

Compare as três rotinas abaixo, no modo x86-64:

#include <unistd.h>

extern const char *arrayOfStrings[];

const char *getStringFromArrayByIndex1(char index) 
  { return arrayOfStrings[index]; }
const char *getStringFromArrayByIndex2(int index) 
  { return arrayOfStrings[index]; }
const char *getStringFromArrayByIndex3(ssize_t index) 
  { return arrayOfStrings[index]; }

GCC cria as três funções assim:

$ gcc -O3 -mtune=native -c test.c -o test.o
$ objdump -d -M intel test.o
test.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <getStringFromArrayByIndex1>:
  0:  48 8b 04 fd 00 00 00   mov    rax,QWORD PTR [rdi*8+0x0]
  7:  00 
  8:  c3                     ret
  9:  0f 1f 80 00 00 00 00   nop    DWORD PTR [rax+0x0]

0000000000000010 <getStringFromArrayByIndex2>:
  10: 48 63 ff               movsxd rdi,edi
  13: 48 8b 04 fd 00 00 00   mov    rax,QWORD PTR [rdi*8+0x0]
  1a: 00
  1b: c3                     ret
  1c: 0f 1f 40 00            nop    DWORD PTR [rax+0x0]

0000000000000020 <getStringFromArrayByIndex3>:
  20: 48 0f be ff            movsx  rdi,dil
  24: 48 8b 04 fd 00 00 00   mov    rax,QWORD PTR [rdi*8+0x0]
  2b: 00 
  2c: c3                     ret

Além de não desperdiçar 1 ciclo de máquina, que pode ser valioso em certas circunstâncias, a função getStringFromArrayByIndex1 só consome 9 bytes. O NOP esquisito, depois de RET, está lá apenas por questão de alinhamento… As funções nos códigos em C são alinhadas por parágrafo (de 16 em 16 bytes).

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