Usar int nem sempre é uma boa idéia…

Geralmente vemos fragmentos de código como este:

extern int x[]; /* definido em algum outro lugar. */

int i, sum;

sum=0;
for (i = 0; i < N; i++)
  sum += x[i];

O código deste fragmento no modo i386 é bem direto:

; esi = &x[0]
; edx = i
; eax = sum
  ...
  xor eax,eax
  xor edx,edx
  jmp .L2
.L1:
  add  eax,[esi]
  add  esi,4
  inc  edx
.L2:
  cmp  edx,N
  jle  .L1
  ...

Considere trocar o tipo da variável de indução do loop (a variável i) para size_t e haverá uma mudança no código… Ao invés de usar o salto condicional JLE, o compilador usará JBE. Já no modo x86-64 haverão duas mudanças e uma diferença: O salto condicional e também o tamanho da variável. Ao invés de usar edx o compilador usará rdx. A diferença que falei é que RSI será usado como ponteiro e não ESI.

Parece óbvio que usar int ao invés de size_t apresenta uma grande vantagem no modo x86-64. Só que temos um problema… Em certos códigos o compilador resolve estender um int para um long long int e precisa fazer isso estendendo o sinal, através da instrução MOVSX. Por exemplo, se ao invés de usar apenas RSI, tenhamos um endereçamento do tipo RSI+RDX, assim:

; rsi = &x[0]
; edx = i
; eax = sum
  ...
  xor eax,eax
  xor edx,edx
  jmp .L2
.L1:
  movsx rdx,edx
  add  eax,[rsi+rdx]
  add  edx,4
.L2:
  cmp  edx,N*4
  jle  .L1
  ...

Aqui o compilador viu que era necessário estender EDX para usá-lo como índice na instrução ADD EAX,[RSI+RDX]. E, me parece obvio, que isso poderá gerar um grande problema… Não neste caso em especial, mas em outros, onde a variável i não é inicializada tão explicitamente. Ao estender EDX para RDX, com sinal, o endereço efetivo [RSI+RDX] poderá sair da fronteira dos segmentos definidos para seu processo, causando um Segmentation Fault… Mas, porque diabos o compilador fez isso?!

Ele fez, justamente, porque você usou o tipo int. Esse tipo é sinalizado e, portanto, sua estensão de 32 para 64 bits tem que ser, obrigatoriamente, sinalizada! O tipo size_t, por outro lado, tem duas vantagens:

  1. Não tem sinal;
  2. Tem o mesmo tamanho de um ponteiro.

A ausência de sinal significa, obviamente, que a variável de indução poderá conter valores no intervalo iniciado por 0 até um valor máximo de 2^{64}-1… Mas, também significa que se ela for usada como índice no endereçamento o sinal não atrapalhará! E o compilador não precisará estender o registrador porque, no modo x86-64, o tipo size_t tem exatamente 64 bits de tamanho.

Tudo certo agora, não é?… Infelizmente não…

Existem casos em que a extensão do valor considerando o sinal é interessante, mas com um pequeno ajuste… O modo x86-64 espera que um ponteiro tenha o sinal estendido se o bit 51 for igual a 1! Um ponteiro, no modo x86-64, é um valor de 64 bits usado como offset num espaço de endereçamento linear de 52 bits de tamanho. Ou seja, neste modo temos, em teoria, disponíveis cerca de 4 PiB (Pebi Bytes, ou “Peta” bytes – isso é 4 seguindo de 15 zeros) de memória. Um espaço de endereçamento de 64 bits possibilitaria até quase 2 EiB (“Exi” bytes, ou “Exa” bytes – 2 seguindo de 19 zeros). Acontece que a instrução MOVSX não estende o sinal do operando fonte a partir de qualquer bit, mas sim do bit mais significativo (MOVSX RDX,EDX estende o sinal do bit 31 de EDX). Não teremos escolha, se tivermos que obedecer o endereçamento canônico, esperado no modo x86-64, a não ser criarmos uma rotina para esse tipo de extensão:

void *cannonize_ptr(void *ptr)
{
#ifdef __x86_64
  size_t mask = ~0ULL << 51;
  size_t p = (size_t)ptr;

  /* Se qualquer bit acima de 50 estiver setado,
     estende o sinal (bit 51). */
  if (p & mask)
    p |= mask;
  return (void *)p;
#else
  return ptr;
#endif
}

A boa notícia é que, provavelmente, o compilador fará isso para você, se for necessário e, mesmo que ele não o faça, é pouco provável que você encontre um caso onde isso seja um problema real, especialmente no userspace… Sua aplicação é limitada quanto ao espaço de endereçamento que o sistema operacional oferece. O mapa da memória virtual disponibilizado para cada processo assume o uso de endereços lineares fixos… Os processos “compartilham” endereços lineares a parir de 0x400000. Do ponto de vista do kernel esse não é o endereço físico real e, do ponto de vista do processo, qualquer tentativa de acessar algo em um endereço linear não disponibilizado pelo kernel vai gerar um “Segmentation Fault” de qualquer maneira…

Mesmo que isso não fosse o caso, um endereço linear não costuma passar de alguns “gibibytes”, já que mesmo grandes servidores não têm memória configurada superior a uns 256 GiB, por exemplo… Bem longe dos 4 PiB que 52 bits de endereçamento físico permitem… E, mesmo assim, estamos bastante seguros se todo o espaço de endereçamento virtual for limitado a 4 PiB…

O que quero dizer é que você estará mais seguro se usar os tipos certos para as finalidades específicas. O tipo definido para conter “tamanhos” é size_t, definido em stddef.h. Se for usar um “tamanho”, evite usar int, mesmo que isso te dê um aumentozinho na quantidade de ciclos de clock gastos (o uso de registradores de 32 bits gera instruções menores, diminuindo a “pressão” no cache L1i). Além do tipo size_t, se precisar usar um “tamanho” que possa ser negativo, use o tipo ssize_t.

Anúncios