O título deste post iria ser "
Coisas que te ensinamos errado", mas o fato é que não foi ensinado necessariamente errado, só não foi da melhor forma. Em função da heterogeneidade da turma, da limitação de tempo e da preocupação com que os alunos consigam resolver os problemas propostos, mesmo que não da melhor forma, os professores (eu, pelo menos) muitas vezes não codificam seus exemplos como eles próprios fariam ou recomendariam.
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
Para os que só acreditam vendo, segue o
assembly x86 gerado pelo GCC 4.7.1 32 bits, compilando com flag de otimização
-O2. Algumas diretivas e instruções não relacionadas foram omitidas para facilitar a visualização. Se você não está afim de encarar código assembly, então jmp solucao.
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.
Antes de alguém aí pensar "
Então é melhor eu mesmo definir o array global e dispensar a cópia" eu já adianto: O melhor, neste caso é deixá-lo local mas estático (
static). Neste caso você mantém a visibilidade local, o que muito bom, mas o array é alocado estaticamente no segmento de dados, já com seus valores. Ou seja, quando o
loader do SO carregar o binário para a memória, já estará pronto.
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.
Mas vale a pena?
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.