Primeiro caso
O que há de ruim na função abaixo? A maioria dos meus alunos devem ter visto este exemplo, ou semelhante:
1 2 3 4 5 6 7 8 9 10 | // Retorna a quantidade de dias que um mes tem int diasmes(int mes, int ano) { int dias[]= {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; if((ano%4==0 && ano%100!=0) || ano%400==0) // É bissexto dias[1] = 29; // Fevereiro tem 29 dias return dias[mes-1]; } |
Obs: Como o assunto se aplica tanto ao C como C++ não estou usando nada específico do C++11, como a sintaxe uniforme de inicialização.
Quem é da velha guarda, e como eu aprendeu a programar C com o compilador cc do Unix sabe que o problema está na linha 4. O cc que usei era um compilador pré-ANSI e daria erro de compilação com este código. Qual o problema? Inicialização de arrays automaticamente alocados.
Quando se define e inicializa uma variável automática (local não static) como n abaixo, ela será alocada em registrador ou na pilha a cada vez que a função for executada. Após isto, também a cada execução, o valor será copiado para este armazenamento.
1 2 3 | void fn(){ int n=0; // ... |
Nada errado com uma variável de tipo fundamental como esta aí. Mas e com um array, como aquele do exemplo anterior? Arrays não podem ser armazenados em registrador. Irão ficar na memória, no segmento de pilha (stack). Mas o problema não é este, pois alocar espaço na pilha para um vetor usa uma única operação que subtrai o tamanho do vetor do Stack Pointer. Mas como ele é inicializado? Normalmente o compilador cria um vetor idêntico estático, e a cada entrada na função copia os dados daquele array para a área alocada.
Então, quando você escreve aquela função acima, é como se tivesse feito isto:
1 2 3 4 5 6 7 8 9 10 11 12 13 | int __s129ax334[]= {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; int diasmes(int mes, int ano) { int dias[12], i; for(i=0; i<12; ++i) dias[i] = __s129ax334[i]; if((ano%4==0 && ano%100!=0) || ano%400==0) // É bissexto dias[1] = 29; // Fevereiro tem 29 dias return dias[mes-1]; } |
Veja o for nas linhas 6 e 7. E pense que esta função pode estar sendo chamada N vezes durante a execução do programa. Está aí o problema!
A prova
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | .data LC0: .long 31 .long 28 .long 31 .long 30 .long 31 .long 30 .long 31 .long 31 .long 30 .long 31 .long 30 .long 31 .text _diasmes: ; ... sub esp, 48 ; Aloca espaco na pilha (12*4 bytes) mov edi, esp ; Endereço destino (dias[]) mov esi, OFFSET FLAT:LC0 ; Endereço origem (LC0) mov ecx, 12 ; Quantidade de palavras a serem copiadas rep movsd ; Repete 12 copias de inteiros |
Lá está o array estático LC0 no segmento de dados (linhas 2 a 14), a alocação do espaço na pilha (linha 19) e a cópia do array (linhas 20 a 23) com a instrução movsd que copia uma sequencia de inteiros do endereço apontado por esi (source index) para o endereço apontado por edi (destination index). O laço está implícito no prefixo rep, que repete a próxima instrução a quantidade de vezes especificada no ecx.
Solução
Aproveito para mais uma otimização secundária: como o maior valor do array é 31, não existe necessidade de ser int, que normalmente teria quatro bytes por valor. Pode muito bem ser um unsigned char, que ocupará apenas um byte por valor, na maioria das arquiteturas, e suporta um intervalo de valores de 0 a 255.
1 2 3 4 5 6 7 8 9 | int diasmes(int mes, int ano) { static unsigned char dias[]= {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; dias[1] = ((ano%4==0 && ano%100!=0) || ano%400==0)? 29: 28; return dias[mes-1]; } |
Outras situações equivalentes
Agora lembre que uma string C também é um array (de char), então aqui temos novamente a mesma questão. Se for uma string C++ também terá um objeto sendo construído e a sequencia de caracteres copiados a cada execução.
1 2 | char vogais[]="aeiou"; string nome="fulano"; |
Obs: Aí já aparece para outro episódio desta série que é a inicialização de uma std::string com o sinal de atribuição (=).
Quando não usar esta otimização?
Você pode ter problemas com isto quando estiver mudando os valores do vetor no corpo da função ou em outra função chamada que recebe o array por referência não constante e você quer descartar estas alteração a cada execução, garantindo que os valores originais sempre sejam encontrados, pois os valores estáticos são preservados entre chamadas da função. No exemplo acima, a única modificação é no segundo elemento do array, correspondente ao mês de fevereiro, então vale a pena. Melhor uma atribuição do que 12. Mas perceba que precisei (re)atribuir 28 para os anos não bissextos.
Se o array local está sendo inicializado e não é alterado, além de static defina-o constante (const).
Perceba que o problema está na inicialização, não na alocação. Então se o array não estiver sendo inicializado provavelmente é melhor que não seja estático.
Aqui nem é o caso de "tanto esforço por tão pouco ganho" pois o esforço é tão pequeno que deve valer a pena. Quanto mais a função for executada, maior o ganho de desempenho.
Eu tenho a opinião de que, com o atual desempenho e disponibilidade de memória nos computadores, podemos abdicar de algumas estratégias que levariam a um programa mais eficiente se isto resultar em código mais legível e manutenível. Não acho que este seja o caso.
Perceba que o problema está na inicialização, não na alocação. Então se o array não estiver sendo inicializado provavelmente é melhor que não seja estático.
Mas vale a pena?
Eu tenho a opinião de que, com o atual desempenho e disponibilidade de memória nos computadores, podemos abdicar de algumas estratégias que levariam a um programa mais eficiente se isto resultar em código mais legível e manutenível. Não acho que este seja o caso.
Por favor, corrija-me se estiver errado professor :-) mas apenas uma pequena adição: pré C++11, se a variável estática for um objeto e a aplicação for multi-threaded, a inicialização do objeto não é thread-safe e pode acontecer mais de uma vez por diferentes threads. Já na nova versão, temos a seguinte passagem no standard
ResponderExcluir"If control enters the declaration concurrently while the object is being initialized, the concurrent execution waits for completion of the initialization."
o que garante sincronia.
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2660.htm
http://stackoverflow.com/questions/8102125/is-local-static-variable-initialization-thread-safe-in-c11
Correto, Natan! No caso de um tipo fundamental ou POD inicializado com expressão constante, como no exemplo, a alocação e a inicialização podem ambas serem feitas em tempo de compilação (estaticamente). Em outros casos a alocação é estatica mas a inicialização é em tempo de execução (dinâmica) e ocorre na primeira vez que a função é chamada. No caso de um objeto precisa ser executado o construtor, mas mesmo no caso de tipos fundamentais inicializados com expressões não-constantes, leva a uma race condition e não era thread-safe nas versões anteriores. Agora o compilador deve colocar a inicialização numa sessão crítica.
ExcluirMas a ordem das inicializações ainda pode dar dor de cabeça em casos de dependências e compilação separada. É o chamado "static initialization order fiasco". Em geral não é uma boa inicializar dinamicamente variáveis locais estáticas.