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!

segunda-feira, 22 de fevereiro de 2010

new/delete new[]/delete[]

Não pretendo esgotar o assunto new/delete, pois não é esse o propósito do meu blog. Pretendo demonstrar somente a diferença entre a versão para array e a para objetos e mostrar como o código se comporta em cada caso, em um próximo post pretendo falar sobre sobrecarga dos operadores new/delete, sobre construtores para tipos primitivos, e sobre como atacar código que faz um mau uso do new/delete. Dito isso, e levando em conta uma plataforma Intel 32bits, vamos lá! É regra que os programadores novatos em C++ decoram a forma de utilizar delete e delete[] como se fosse um motto: “Usarei delete quando usar new e delete[] para new[]”. Também é fato que poucos sabem explicar o que motiva o design da linguagem a separar o significado de destruir um único objeto de destruir um array e, principalmente, como eles funcionam! Quanto à separação, tudo remota a uma questão histórica; antigamente, bem nos primórdios do c++, o operador delete para arrays utilizava um argumento que indicava quantos itens deveriam ser liberados, como no exemplo:

p = new AwesomeObj[iSize];

delete[iSize] p;

Não é necessário comentar o tanto que isso potencializa a tendência ao desastre. E vale lembrar que essa tendência de causar desastres é uma característica intrínseca do c++ hehehe. Exigir que o código mantenha uma contagem sobre quantos elementos foram alocados é uma tática muito perigosa e força o design do programa a carregar esse fardo, sem contar que, no caso de a memória poder ser liberada em funções de callback ou fora do escopo da thread corrente, informação adicional teria de ser passada de um lado para o outro criando um overhead, muitas vezes, inaceitável. Em revisões mais novas da linguagem o operador foi modificado não necessitando mais de um indicativo de quantos elementos devem ser destruídos, no entanto a distinção entre destruir um único objeto com delete e um array com delete[] foi mantida.

Para entender melhor o funcionamento de cada um, caso você não saiba exatamente o que tem feito nesses seus dias de programação hehehehe, é necessário saber um tiquinho como C/C++ organiza a memória, mas não se assuste só um tiquinho hehehe. Permita-me antes falar um pouco sobre um fato: C++, ao contrário de C, pode lidar com classes que possuem, pelo menos, dois métodos especiais, o construtor e o destrutor. Quando alocamos memória em C com malloc, por exemplo, tudo o que estamos fazendo é requisitar um pedaço de memória e nada alem disso. Já new além de alocar uma quantidade de memória que possa conter nosso objeto também chama o construtor do mesmo, e isso vale para delete, antes de liberar a memória, como a função free faz em C, o operador delete chama o destrutor do objeto. Esse comportamento vale também para a versão array, ou seja, new[]/delete[], em código isso se traduz em:

char* p1 = new char[100];

char* p2 = (char *) malloc(100);

// p1 e p2 apontam para 100 bytes

// (considerando um char de um byte) alocados na memória.

// nesse caso as duas chamadas são equivalentes em sentido.

delete [] p1;

free(p2);

// Ambos liberam os 100 bytes alocados,

// mais uma vez, em sentido, as chamadas são equivalentes.

Agora vem a diferença, considere uma classe qualquer como, por exemplo, uma chamada AwesomeObj:

class AwesomeObj

{

public:

AwesomeObj() { printf("Construtor\n"); }

~AwesomeObj() { printf("Destrutor\n"); }

};

AwesomeObj *t1 = new AwesomeObj[100];

AwesomeObj *t2 = (AwesomeObj*) malloc( sizeof(AwesomeObj) * 100 );

// Agora t1 e t2 ambos apontam para 100 AwesomeObj na memória.

// Entretanto a primeira chamada, com new[], vai alocar a memoria para os 100

// AwesomeObj e vai chamar o construtor de cada um deles, já a versão com malloc não!

delete [] t1;

free(t2);

// Aqui o mesmo, a versão com delete[] vai chamar o destrutor de cada um dos 100

// AwesomeObj antes de liberar a memória, já o free não vai.

Então vem a pergunta e se eu usar delete para algo alocado com new[] ou usar delete[] para algo alocado com new? A resposta é: depende hehehe, o subsistema de memória e a implementação do seu compilador que vai ditar o que de fato é feito para alocar/desalocar um objeto, mas em linhas gerais quando você usa new o código irá alocar memória de acordo com o tamanho do tipo que você requisitou e chamar o construtor, se houver, do objeto recém alocado. No caso de utilizar new[] ele vai chamar o construtor de todos os itens que forem alocados, alem de manter uma sentinela que indica quantos objetos foram alocados. A localização exata dessa sentinela varia de compilador para compilador e de plataforma para plataforma. Para alocar memória o operador new/new[] vai chamar alguma função intermediaria entre o SO e a biblioteca default, chamada libc no caso do gcc, digamos que malloc seja utilizada para realizar a tarefa de alocar a memória. A função malloc, dependendo da implementação, irá alocar os X bytes requisitados + quatro, sendo que esses quatro bytes adicionais correspondem a uma palavra(WORD) em maquinas 32bits, essa palavra vai ficar no endereço &p – sizeof(unsigned int), ou, em miúdos, quatro bytes antes do inicio da área que é retornada para ser utilizada. Utilizando este valor o free sabe quantos bytes deve liberar. Daí que o operador delete[] irá realizar um trabalho semelhante a:

delete_array(obj * ptr)

{

size_t *tmpptr = ((size_t *)ptr) - 1;

int num_elements = *tmpptr;

for (int i=num_elements-1; i>=0; i--)

{

call_destructors(ptr[i]);

}

free(tmpptr);

}

Traduzindo, ele vai chamar o destrutor de todos objetos, e vai executar num_elements vezes, posteriormente vai chamar free para marcar a área alocada como livre novamente. Lembrando que não é garantido que somente um índice, o num_elements, seja mantido, alguma implementação pode manter duas listas separadas, uma para descrever quantos bytes foram alocados e outra para manter a contagem de quantos objetos estão em memória, facilitando a abstração do código. O delete vai operar de forma semelhante porem não vai procurar uma lista de objetos para serem removidos. Ele vai operar sobre um único objeto. Ai vem a pergunta, porque não usar delete[] quando o ponteiro tiver sido alocado com new? A resposta é simples e complicada ao mesmo tempo hehehe, quando new aloca um objeto ele não necessita marcar quantos objetos foram alocados, ou seja, não necessita criar uma sentinela de quantos objetos foram criados, dependendo da implementação somente a lista de bytes alocados mantida pelo subsistema do SO já baste para o caso new/delete. Isso é possível já que se trata de apenas de um objeto. Isso salva espaço no seu executável final e algumas instruções a menos podem significar algum ganho de velocidade, ok e daí? Daí que se você chamar delete[] é bem provável que ele vá procurar a sentinela que mantém a quantidade de objetos alocados e vai encontrar lixo. Uma vez que ele vai encarar esse lixo como um valor que corresponde à quantidade de objetos alocados ele pode sair chamando o destrutor infinitamente ou simplesmente quebrar o processo ou quem sabe fazer o seu vizinho te perseguir com uma serra elétrica, enfim o resultado é o que chamam de UB (undefined behaviour). O mesmo vale para o caso onde você aloca um array com new[] e chama delete, alguns dirão que ele vai liberar somente o primeiro item e liberar a memoria total, mas na verdade é UB, não é garantido que isso vá acontecer. Então, sabendo disso, mantenha o motto na cabeça só que agora sabendo ;)

That’s all folks!

E lá vamos nós!

Ok, resolvi, sei lá pq, que vou compartilhar um pouco das maluquices que aprendi na minha vida como programador, digo vida como programador pq minha vida é basicamente isso hauwhauwhauwhaw É quase certo que esse material será melhor assimilado pra quem saca c/c++, mas quando der na telha falo sobre java pascal assembly brainfuck e outras linguagens bacanas :) e vamos nós!!!!