terça-feira, 23 de fevereiro de 2010

Memset e suas pegadinhas

Em C a forma mais comum de iniciar um array, o que, normalmente, implica em atribuir o valor 0 a todos os índices de um array é usando a função memset da biblioteca default do C. Alguns programadores que gostam de reinventar a roda, como eu, podem acabar criando um loop para iterar nos itens do array e fazer a atribuição na mão, só que isso é lento. Embora seja a forma mais natural e, teoricamente, a que executa em melhor tempo, dentro das restrições da linguagem C pura, é um jeito forçado de implementar uma rotina como essa. É comum que as funções da biblioteca default sejam codificadas em assembly e além disso, pelo menos nos cpus da intel, existem instruções que fazem a copia de um valor diversas vezes na memória iniciando em um endereço base qualquer, e isso acontece muito mais rápido do que usando um loop em C. Em muitos casos cpus não gostam de loops, mas deixa isso para um outro post, por agora fixe sua mente no seguinte: Por mais que você se ache fodão, você não é melhor que a galera que codifica a biblioteca padrão, seja da GNU, da Microsoft, da Intel ou outra implementação de grande porte existente. Então usar memset é o jeito mais rápido e padronizado de realizar esta tarefa.

Só que nem sempre desejamos iniciar o array com o valor 0, e se você quiser que todos itens armazenem o valor 5? Aqui vem a pegadinha, alguém poderia surgir com a idéia: Simples, uso memset! Ok, pode funcionar, caso você esteja utilizando um array de char, que acaba sendo um byte na maioria dos casos. Mas se seu array for um array de inteiros, aí a coisa muda de figura. Vejamos o que o manpage do Linux tem a dizer sobre o memset(Tomei a liberdade de traduzir de forma que faça mais sentido em português):

NOME

memset - Preenche uma região de memória com um valor(byte) constante.

SINOPSE


#include < string.h >

void *memset(void *s, int c, size_t n);


DESCRIÇÃO

A função memset() preenche os primeiros n bytes da área de memória apontada por s com o valor constante do byte c.

Lembrete: Embora o parametro c seja um inteiro somente o primeiro byte, o de menor ordem, será utilizado e esse byte será convertido para um byte sem sinal, unsigned byte. Então mesmo que você tente passar o valor -1 ele será visto como 255 internamente à memset, logicamente o resultado real vai depender do tipo do container que você estiver utilizando.

Ok, e daí? Daí que a função memset opera copiando bytes e um inteiro (int) ocupa, normalmente, quatro bytes na memória. Então o que acontece quando você tenta copiar um valor diferente de zero para um array de inteiro é o seguinte:

Imagine que as caixinhas abaixo são os bytes na memória que correspondem a um inteiro (Estou assumindo um cpu little endian - se não sabe do que se trata espere um post futuro ou procure no google!) Imagine também que o valor zero está armazenado no inteiro, os números sobre as caixinhas indicam o endereço base começando em 0:

0[00000000]1[00000000]2[00000000]3[00000000]

Este é um int na memória com o valor 0, um inteiro com o valor 42 seria armazenado da seguinte forma, em binário:

0[00101010]1[00000000]2[00000000]3[00000000]

Quando você usa memset em um array de char, tudo bem, o código funciona pois, um char na maioria dos casos ocupa um byte na memória ou seja um único quadrinho:

0

[00000000]

Em um array de 5 elementos:

char cArray[5];

memset(cArray,5,sizeof(char)*5);

Tudo funciona como esperado:

endereço: 0

[00000101] indice: 0

endereço: 1

[00000101] indice: 1

endereço: 2

[00000101] indice: 2

endereço: 3

[00000101] indice: 3

endereço: 4

[00000101] indice: 4

Cada elemento contem o valor 5 como esperado, agora quando você tentar executar um código como:

int iArray[5];

memset(iArray,5,sizeof(int)*5);

Eis o que vai acontecer:

0[00000101]1[00000101]2[00000101]3[00000101] indice: 0

4[00000101]5[00000101]6[00000101]7[00000101] indice: 1

8[00000101]9[00000101]10[00000101]11[00000101] indice: 2

12[00000101]13[00000101]14[00000101]15[00000101] indice: 3

16[00000101]17[00000101]18[00000101]19[00000101] indice: 4

Cada índice vai conter o valor: "00000101000001010000010100000101" cuja representação em decimal é 84215045, um bocado longe do que era desejado. Para reforçar o assunto vamos utilizar outro valor e ver o que acontece. Imagine o valor 255 que é o maior valor que um byte sem sinal pode representar, lembrando que o byte, sendo composto por oito bits, pode representar 256 valores distintos, um intervalo de 0 a 255 normalmente. O valor 255 em hexa 0xFF pode ser armazenado com folga em um int, mesmo em um int com sinal. O valor 255 não pode ser representado em um char com sinal já que um signed char utiliza o primeiro bit para indicar sinal. Até aqui tudo bem, qualquer programador com meio neurônio sabe disso, mas seria razoável alguém cometer o erro do memset contando com o fato de que um int suporta um byte com sobra. A mesma operação usando o valor 255 teria como resultado o seguinte:

0[11111111]1[11111111]2[11111111]3[11111111] indice: 0

4[11111111]5[11111111]6[11111111]7[11111111] indice: 1

8[11111111]9[11111111]10[11111111]11[11111111] indice: 2

12[11111111]13[11111111]14[11111111]15[11111111] indice: 3

16[11111111]17[11111111]18[11111111]19[11111111] indice: 4

Cada índice contem o valor “11111111111111111111111111111111” que em decimal é “4294967295” ou em hexa 0xFFFFFFFF, agora se nosso array tiver sido declarado como int, o que por default faz dele um signed int, teremos o valor -1 armazenado em cada um dos 5 elementos do array, o que com certeza é uma surpresa nada agradável para um programador olhando um dump de memória ou tentando debugar o código, afinal se eu copiei o valor 255 em um inteiro como é que posso ter -1? E não para por ai, para arrays do tipo float a coisa é ainda pior, floats em C são codificados de acordo com o padrão IEEE 754, e o único valor que tem uma correspondência binária entre valores inteiros e valores de ponto flutuante é o valor/número zero, todos os outros diferem, e muito, em sua representação. Sendo assim o único uso de memset com floats será para atribuir o valor zero, qualquer outra tentativa resultará em um valor errado e dificílimo de ser entendido. Mesmo sabendo o padrão binário armazenado na região de memória é complicado definir o valor codificado como ponto flutuante no olhometro, ao contrário de valores codificados como inteiro. Por exemplo, o valor decimal 457 é armazenado como inteiro no seguinte padrão “111001001” se somar um, ele passa a valer, logicamente, 458 e a representação binária é “111001010”, ambos facilmente legíveis, basta uma operação mental ou canetal de troca de base para saber o valor. O maior problema que poderia ser encontrado seria eventuais diferenças entre maquinas big endian e little endian. Agora, usando armazenamento em variáveis de ponto flutuante o valor 457 é codificado em binário como “1000011111001001000000000000000” já o valor 458 é representado como “1000011111001010000000000000000” note que embora seja possível encontrar o valor aí, não é nada human-readable. Logo evite codificar valores de ponto flutuante na mão ;) É claro que você pode codificar na unha o valor desejado, mas não existe nenhuma situação razoável onde este truque faça sentido. Então, para finalizar, fique atento aos atalhos que algumas funções da biblioteca padrão, como a memset, parecem oferecer pois, você pode acabar mandando seu míssil teleguiado pro lugar errado devido a um erro de calculo astronômico causado por valores espúrios que podem resultar de um erro tão simples quanto esse. Abaixo um código que demonstra exatamente o comportamento que foi descrito no artigo:


#include < stdio.h >
#include < stdlib.h >
#include < string.h >

int main()
{
int iArray[5];
int i;

printf("Valores no array(lixo):\n");
for (i = 0; i < 5; ++i)
{
printf("%d ", iArray[i]);
}
printf("\n");

memset(iArray,0,sizeof(int)*5);

printf("Valores apos o memset(com 0): \n");
for (i = 0; i < 5; ++i)
{
printf("%d ", iArray[i]);
}
printf("\n");

memset(iArray,5,sizeof(int)*5);

printf("Valores apos o memset usando o valor 5: \n");

for (i = 0; i < 5 ++i)
{
printf("%d ", iArray[i]);
}
printf("\n");

return 0;
}


A primeira linha da saida depende do ambiente uma vez que depende do que estiver
na memória no momento da execução, porem, no geral, a saida será parecida com:


187-24-217-3:~ killocanmhc$ ./teste
Valores no array(lixo):
0 0 0 0 -1881139893
Valores apos o memset(com 0):
0 0 0 0 0
Valores apos o memset usando o valor 5:
84215045 84215045 84215045 84215045 84215045


That's all folks!

5 comentários:

Eduardo Uberlândia MG disse...

Boa rapaz!

Blog do Rodolfo disse...

Ai cara estou precisando fazer um trabalho sobre arquivo em C e umas das perguntas é apagar o conteúdo de arquivo C (Obs: Nao é a apagar o arquivo e sim só o conteúdo)

Unknown disse...

Certo. Mas e se eu quisesse armazenar um valor (x+1) em um vetor de n elementos - sendo que x inicia com 1?

Unknown disse...

Que tenso, vou tomar mais cuidado ao usar memset, vlw xD
Muito bom o blog.

Unknown disse...

Excelente artigo. Deu pra entender perfeitamente sem dificuldades tudo.