quinta-feira, 23 de fevereiro de 2012

C: Formas de armazenamento das variáveis

O C tem algumas formas de como uma variável será alocada. (Aqui se excluem as formas de alocação dinâmica, que deverá ser outro capítulo.) Estas formas são: auto, register, static e external.

register

É um pedido ao compilador, se for possível, colocar as variáveis nos registradores do processador, e se isto acontecer, não terá acesso à memória para pegar o conteúdo desda variável. O resultado é brutalmente rápido.

A desvantagem é que só tipos básicos podem ser register (e às vezes nem todos), e a quantidade de registradores limita quantas variáveis podem ser register. Os compiladores não costumam avisar que acabaram os registradores, mas eles são livres para realocar os registradores, nas etapas de otimização, conforme as variáveis são usadas. Se uma variável parou de ser usada em uma função, e outra que ainda não foi usada, vai passar a ser usada, ambas podem vir a usar o mesmo registrador.

Nota: Só serão usados realmente registradores conforme a quantidade de registradores e o julgamento do compilador e do otimizador, mas a colocação do pedido de register já pode ser encarado, no mínimo, como uma dica ao compilador. Antigamente a aceitação era mais literal, quando os otimizadores de código não eram tão bons.

Uma outra vantagem da variável register é que ela (se realmente o compilador conseguir colocá-la como register) não ocupa espaço em memória principal, já que ficará dentro do processador.

Quando uma variável register não é inicializada, ela pode conter qualquer coisa, portanto cuidado como a usa. E, se em sua criação foi pedido um valor inicial, este valor é atribuído ao registrador que representa esta variável antes do seu primeiro uso. Portanto existirá uma inicialização a cada vez que a função é chamada.

Na falta de registradores, ou outras impossibilidades, as variáveis normalmente se tornam auto.

auto

A maior parte das variáveis são criadas como auto, por exemplo:

main( argc )
int argc;
{
    float f ;
}

Tanto o parâmetro da função, argc, quanto a variável f são automáticas. Estas variáveis são tipicamente alocadas na pilha do processador. Quando não se menciona uma forma de armazenamento para a variável que está sendo definida, normalmente é assumido como auto.

É um pouco complicado explicar sem demonstração com código em assembler, mas vamos lá (Este é um dos motivos que programadores tem que saber assembler, mesmo que nunca usem.).

O processador tem um mecanismo de pilha, que usa a memória de cima para baixo (Exceto o IBM 360, e tem uma "alfinetada" num manual do Digital PDP 11/70 justamente sobre isto. rs). Quando certas instruções são executadas, dados são colocados nesta pilha, e o Stack Pointer é decrementado (A pilha cresce de cima para baixo.) (Alguns processadores tem a instrução push, entre outras, para isto.), e tem outras instruções que retiram dados da pilha, incrementando o Stack Pointer (Instrução pop, por exemplo.). Assim parâmetros de uma função, como o argc do exemplo acima, são passados através da pilha.

Outras instruções que lidam com a pilha são a call e ret, que fazem a chamada e o retorno de uma função. Na chamada, o endereço do ponto onde o programa tem que retornar depois de executada esta função, chamado de "Endereço de Retorno", é colocado na pilha, e o Frame Pointer é ajustado. Na ret o endereço de retorno é pego da pilha, desempilhando-o, e o Frame Pointer é reajustado para o contexto anterior. Existem dois modos de fazer isto: Ou a instrução call também empilha o Frame Pointer, colocando um novo valor nele, de acordo com o novo contexto, e a ret desfaz isto; ou ele é empilhado por uma outra instrução logo na entrada da função e lhe é atribuído um novo valor condizente com o novo contexto, e imediatamente antes do retorno ele é desempilhado o valor do contexto anterior.

É o Frame Pointer que referencia todas as variáveis locais automáticas, auto, da função, sejam parâmetros ou variáveis locais. Todas estas variáveis são achadas com uma conta usando o Frame Pointer. Seria o valor do Frame Pointer mais tanto (tipicamente para parâmetros), ou menos tanto (tipicamente para variáveis). Mesmo ele sendo um registrador dentro do processador (tal como o Stack Pointer), ainda tem uma a ser feita uma conta, cuja uma das partes tem que ser pega da memória, para descobrir o endereço de memória onde está a variável, para enfim, lidar com a variável. Ou seja, é a forma menos eficiente de armazenamento de variáveis que existe.

Quando não se define uma forma de armazenamento da variável, o compilador assume que é auto. Ela acomoda qualquer tipo de variável, sejam tipos básicos, a tipos mais complexos como struct, union, arrays etc. Muitos compiladores, pelo menos quando não se menciona explicitamente que a variável é auto, tomam a liberdade de otimizá-las, alocando em muitos casos como register.

Uma vantagem é que a variável só existe dentro de um contexto, e quando termina este contexto, todas as variáveis deixam de existir. Digamos que a função a() chame a função b() que chama a c(). Se estamos executando a função c(), todas as variáveis auto da função a() e b() existem, mas em outro contexto, portanto estão ocupando memória, tal como as variáveis da função c(), mas só podemos usar diretamente as variáveis e parâmetros da c() por que o Frame Pointer referencia somente a eles. Quando a função c() retornar, o Frame Pointer será restaurado para o contexto da função b(), então não existem mais as variáveis auto da função c(), desalocando o espaço delas na memória, mas continuam existindo as variáveis da função a() e b(), tal como existia antes. Se a função b() retornar, suas variáveis e parâmetros serão desalocados, só existindo o que existe na a().

As variáveis são alocadas e desalocadas durante a execução do programa, como explicado acima, portanto, se foi pedida a inicialização da variável com algum valor na criação dela, este valor é atribuído a cada vez que ela é criada.

Cuidado que neste tipo de alocação a variável pode vir suja, com qualquer lixo que contenha na memória de onde foram alocadas. Se for necessário, fique atento à inicialização.

static

Esta forma de alocação de variável é feita durante a compilação, e não durante a execução, como as duas formas acima. E ela pode ser feita de duas formas diferentes, transparentes ao usuário. Para melhor entender, tem que ser entendido como a imagem de um processo é montada em memória (leia Processo x Programa Executável).

Uma variável static, se inicializada com um valor diferente de zero, existirá no Programa Executável, ocupará espaço no arquivo, e será carregada na área data da imagem do Processo, como explicado no artigo mencionado no parágrafo acima. Se não inicializada (e, em alguns compiladores, inicializada com zero, creio eu) ela ficará na área de BSS, o que implica que não ocupará espaço no arquivo do programa executável, mas ocupará um espaço de memória que será zerado.

Uma das implicações, é que, ou a variável é inicializada com zero, ou com o valor que o usuário pediu, que se for zero dá na mesma. Estas variáveis nunca começam com um valor realmente aleatório, com um lixo da memória, tal como a auto, ou com um lixo que existia no registrador, como no caso da register.

Uma variável static existirá, e ocupará espaço na memória, não importando se a função na qual ela foi definida esteja executando ou não. Ela sempre (A não ser no caso de invasão de memória, que costuma se desastroso.) terá o valor deixado nela pela última vez, mesmo que tenha sido na chamada anterior da função. Ela sempre terá o seu espaço reservado na memória.

A inicialização de uma variável static só ocorrerá uma vez, durante a compilação. Não importando quantas vezes a função seja chamada, ela nunca reinicializará a variável. Isto pode ser útil se um algoritmo depender de algo de que tenha acontecido numa chamada anterior.

Variáveis static são bem mais rápidas que as auto, mas bem mais lentas que as register. O compilador Aztec C tinha uma opção para transformar todas as variáveis das funções, que não fossem definidas explicitamente como auto, em static, para melhorar o desempenho.

Abaixo um exemplo que testa a inicialização da variável e a preservação do valor entre uma chamada e outra:

#include        <stdio.h>

/* Isto é explicado adiante. */
static int funcao( int i )
{
        static  int     st = 10 ;
        auto    int     t       ;

        t = st ;

        st = i ;

        return t ;
}

int main()
{
        printf( "%d\n", funcao( 11 ) );
        printf( "%d\n", funcao( 22 ) );
        printf( "%d\n", funcao( 33 ) );
        printf( "%d\n", funcao( 44 ) );
        printf( "%d\n", funcao( 55 ) );

        return 0 ;
}

O resultado impresso é:

10
11
22
33
44

Este teste mostra que o valor é preservado entre as chamadas, e que a inicialização acontece antes da função ser chamada, e uma única vez.

Variáveis globais, que são definidas fora das funções, são estáticas por definição. Sem um contexto de execução não é possível ter variáveis auto ou register, então só resta static. Neste contexto a palavra chave static ganha um novo significado, pois se não for usada, as variáveis já são estáticas, mas se usada, elas ainda serão estáticas, mas também serão locais a este fonte, e mesmo que um programa tenha vários fontes, que no processo de compilação gerem vários Objeto Realocáveis separados, os outros Objetos Realocáveis não poderão usar estas variáveis.

Em suma, palavra static, em seu uso fora de uma função, significa que a variável só pode ser vista dentro do mesmo Objeto Realocável. A palavra static pode ser usada na definição de uma função, como uma função estática? Sim, pois neste contexto ela dita que a função é local, e não tem como outro Objeto Realocável chamar esta função. Qual a vantagem disto? Dá a liberdade do otimizador do compilador pegar alguns atalhos, simplificar a passagem de parâmetros e o retorno de valores, e até mesmo não gerar o código da função, introduzindo-a no ponto onde ela é chamada. Fiz isto no exemplo acima.

external

Esta não é realmente uma forma de alocação. É só uma definição que avisa que a variável existe, que tipo ela é, mas não reserva espaço em memória para ela, pois este está reservado em outro local. É usado para variáveis globais, que são tipicamente definidas em outros módulos de um programa, ou biblioteca de funções.

Melhor explicando: Um programa é composto de 5 fontes, f1.c, f2.c, f3.c, f4.c e f5.c, que são compilados separadamente, gerando cada um deles um Objeto Realocável diferente, f1.o, f2.o, f3.o, f4.o e f5.o. Estes cinco realocáveis serão unidos, acrescentando o que eles precisam das bibliotecas, pelo programa de link edição. Se existir uma variável xxx, que é usada pelos módulos f1.c, f2.c, f4.c e f5.c, ela só poderá ser definida realmente uma vez, em um só lugar. Se for definida em mais de um lugar, serão várias variáveis xxx diferentes. Então devemos ter uma definição real para ela, alocando espaço, como variável global, sem usar a palavra static, e nos outros lugares ela tem que ser definida como extern, avisando que existe, mas que realmente será encontrada em outro lugar. Por exemplo, ela é definida no f2.c, e nos outros ela tem que ser extern.

No f2.c tem:

int xxx ;

E no f1.c, f4.c e no f5.c tem que ser

extern int xxx ;

Se não mencionado o tipo?

Se na definição de uma variável não definido o tipo especificamente, como mostrado abaixo, ele será int:

int     main()
{
        auto            a       ;
        register        b       ;
        static          c       ;

        printf( "%d %d %d\n",sizeof(a),sizeof(b),sizeof(c) );
}

Tal como acontece em outras situações na linguagem C.

Observação final

Sei que este texto é bem complexo, e o assunto é complexo para ser bem entendido, mas tentei ser o mais simples sem entrar com código em assembler.

Outra nota importante, é que este assunto tem que ser entendido para realmente saber usar bem as variáveis, e ele ajuda a explicar por que programadores tem que saber assembler, mesmo existindo muitos tolos preguiçosos, que falam que memória e processadores são baratos, dizendo ao contrário.

Acho muito difícil alguém realmente ser um bom programador sem ter uma noção de assembler, e de como as coisas em baixo nível funcionam.

Em caso de dúvidas, façam perguntas. O espaço para cometários serve para isto, entre outras coisas.

Nenhum comentário:

Postar um comentário