Porque os tipos size_t e ssize_t são importantes?

Já me perguntaram por que diabos esses tipos existem… Bem, a espeficação ISO os definem com sendo inteiros com o mesmo tamanho de um ponteiro. Isso simplifica o código gerado pelo compilador em alguns casos. Vejamos um exemplo:

#include <stdlib.h>

extern int x[];

int f(int i) { return x[i]; }
int g(size_t i) { return x[i]; }

O compilador tende a converter o índice i para o mesmo tamanho de um ponteiro, já que o símbolo x é, na verdade, o ponteiro para o primeiro elemento do array. E, já que size_t tem o mesmo tamanho de um ponteiro, a conversão não é necessária:

f:
  movsx rdi,edi     ; Converte 'int' para 'size_t'.
  mov   eax,[rdi*4] ; O índice é multiplicado por 4
                    ; porque um 'int' tem 4 bytes.
  ret

g:
  mov   eax,[rdi*4] ; O índice é multiplicado por 4
                    ; porque um 'int' tem 4 bytes.
  ret

Usar o tipo int como índice de arrays é prática corriqueira mas cria códigos menos que ótimos, a não ser que o tamanho de um int seja o mesmo que o tamanho de um ssize_t (Note o ‘s’ antes de ‘size_t’. O tipo ssize_t tem o mesmo tamanho que size_t, mas tem sinal!).

O uso de ssize_t pode, até mesmo, criar código bem mais performático por causa da ausência da conversão de tipos. Por exemplo:

; void f(int count)
; { while (count--) x[count] = 0; }
f:
  test  edi,edi
  je    .L1
  lea   eax,[rdi-1]   ; **
  movsx rdi,edi
  xor   esi,esi
  sal   rdi,2         ; !!
  lea   rdx,[4+rax*4] ; **
  sub   rdi,rdx       ; !!
  add   rdi,x         ; !!
  jmp   memset
.L1:
  ret

; void g(size_t count)
; { while (count--) x[count] = 0; }
g:
  test  rdi,rdi
  je    .L1
  lea   rdx,[rdi*4]   ; **
  xor   esi,esi
  mov   edi,x         ; !!
  jmp   memset
.L1:
  ret

As rotinas são exatamente as mesmas, a única diferença é o uso do índice… As instruções marcadas com ‘!!’ são as que o compilador usou para calcular o endereço efetivo do array x e as marcadas com ‘**’ são usadas para calcular o tamanho do bloco de memória que será preenchido por memset.

Viu só como o compilador lutou bravamente para converter int para size_t no código de f(), mas acabou com um código complicado?

Vale dizer que isso não tem nada a ver com size_t ser um tipo não sinalizado e int ter sinal. Se você substituir size_t por ssize_t obterá exatamente o mesmo código para a função g(). Isso tem haver com ponteiros!

Anúncios