Jump to content

Dois detalhes que constam do padrão da linguagem C e você não percebeu


fredericopissarra

Recommended Posts

Posted

Escrevo muito sobre "escovação de bits" e, de tempos em tempos, topo com alguma pergunta interessante. Dessas perguntas vieram esses dois detalhes que o estudante assume como verdade:

O resultado de uma expressão booleana é sempre 0 ou 1

Tá certo que qualquer valor diferente de zero pode ser interpretado com verdadeiro, mas estou falando do resultado de uma expressão. Esse fato é citado na especificação ISO 9989, pelo menos de 1999 em diante (acredito que é assim desde a versão ANSI) e cria opotunidades interessantes. Por exemplo: Suponha que você queira fazer uma função que será usada para decidir se um valor a é maior que, menor que ou igual a um valor b. Pode-se fazer algo assim:

int compare( int a, int b )
{
  if ( a > b ) 
    return 1;
  else if ( a < b ) 
    return -1;
  return 0;
}

A rotina acima, otimizada, e na arquitetura x86-64 fica assim:

compare:
  xor  eax,eax
  cmp  edi,esi
  mov  edx,1
  setl al
  neg  eax
  cmp edi,esi
  cmovg eax,edx
  ret

No entanto, (a > b) e (a < b) são expressões booleanas e seus resultados só podem ser 0 ou 1... A mesma rotina pode ser escrita, sem nenhuma penalidade, assim:

int compare( int a, int b ) { return ( a > b ) - ( a < b ); }

Sabendo da regra do 0 ou 1, interpretar a função acima é simples! Veja agora o código gerado:

compare:
  xor  eax,eax
  cmp  edi,esi
  setl dl
  setg al
  movzx edx,dl
  sub eax,edx
  ret

Dá pra ver que a segunda rotina tem menos operações aritméticas e que ela é menor que a anterior e, portanto, ligeiramente mais rápida? Talvez isso não te convença, mas mudemos um pouco a rotina para usar ponteiros, como na rotina necessária para uso da função da stdlib.h, qsort():

int compare1 ( int *a, int *b )
{ return ( *a > *b ) ? 1 : ( *a < *b ) ? -1 : 0; }

int compare2 ( int *a, int *b )
{ return ( *a > *b ) - ( *a < *b ); }

Eis ambos os códigos, lado a lado:

compare1:            compare2:         
  mov   edx,[rsi]      mov   edx,[rsi]
  cmp   [rdi],edx      mov   ecx,[rdi]
  mov   eax,1          xor   eax,eax
  jg   .L1             cmp   ecx,edx
  setl  al             setl  dl
  movzx eax,al         setg  al
  neg   eax            movzx edx,dl
.L1:                   sub   eax,edx
  ret                  ret

Agora temos um salto condicional "para frente", que viola o algoritmo de branch prediction estático. A rotina de compare2() não tem nenhum salto... Isso, por si só, torna a rotina compare2() de 30% a quase 70% mais rápida que compare1() -- medido!

Esse mesmo tipo de macete pode ser feito para eliminar a necessidade de algum if, como em:

if ( x > 0 ) 
  y++;

// perfeitamente substituível por:
y += (x > 0);

O código gerado não será muito diferente, mas em certos casos fica melhor...

Constantes, entre ' e ', não são do tipo char!

Você pode estar acostumado a fazer algo assim para inicializar uma variável do tipo char:

char ch = 'a';

De fato, se o tipo char tiver 8 bits de tamanho o código (de 8 bits) do caracter 'a' será colocado na variável a como você queria. Mas, de acordo com a especificação ISO 9989, uma constante entre ' é sempre do tipo int, ou seja, nos x86 ela sempre tem 32 bits de tamanho... O que acontece no fragmento acima é que o int 'a' é convertido para o tipo char antes de ser colocado dentro de a.

A especificação ainda nos diz que uma construção como abaixo é perfeitamente válida:

int x = 'abcd';

Contanto que o que vá entre ' tenha exatamente o tamanho de um int, o compilador só avisará, mas a construção é legal.

Por que o compilador avisa? Criar uma constante desse tipo, multibyte, não é portável. O que está entre ' vai seguir o padrão little endian ou big endian? A ordem vai ser respeitada? No caso, se vocẽ imprimir x, o que obterá?

$ gcc -xc -include stdio.h - <<EOF
void main( void ) { int x = 'abcd'; printf( "%#x\n", x ); }
EOF
<stdin>: In function ‘main’:
<stdin>:1:29: warning: multi-character character constant [-Wmultichar]

$ ./a.out
0x61626364

Eis o aviso (warning) e eis o resultado no GCC. Ele segue a ordem little endian, colocando o último caracter na frente, ou seja, a "string" é invertida. Assim, se eu quiser que x contenha os caracteres para a string "fred", tenho que fazer

int x = 'derf';

Mas, de novo, isso não é portável e deve ser evitado.

Mas, existe outro motivo pelo uso do int... Uma das primeiras funções para leitura de um stream (arquivo) foi fgetc(), que pode devolver um "caracter" ou EOF... Porque EOF é um símbolo com valor especial, ele não pode ter 8 bits de tamanho. Assim, o valor -1, inteiro, foi escolhido para representar a condição de "fim de arquivo". E um "caracter" passou, por convenção, a ser um int. O mesmo funciona com o retorno de fscanf()... retorno de EOF (-1) ou a quantidade de itens convertidos (>= 0).

Há, ainda, mais um motivo... Considere o charset UTF-8... caracteres como 'ç' possuem 2 bytes de tamanho: "\xc3\xa7". Embora a codificação de um caracter, usando UTF-8, possa ter de 1 até 5 bytes, faz todo sentido usar 4 bytes para uma constante entre 's, para a maioria dos caracteres.

Archived

This topic is now archived and is closed to further replies.

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...