Mostrando postagens com marcador C++. Mostrar todas as postagens
Mostrando postagens com marcador C++. Mostrar todas as postagens

terça-feira, 30 de dezembro de 2014

Apostila de Linguagem C

Atendendo a inúmeros pedidos estou disponibilizando uma versão PDF da minha apostila de C (não é C++). Este material foi utilizado por muitos anos nas minhas disciplinas de Programação no Curso de Ciência da Computação da Universidade de Passo Fundo.

Como, desde 2009, substituímos a linguagem inicial de programação por C++ não tenho investido tempo neste documento. Recentemente efetuei apenas uma breve atualização para o C99. De qualquer forma espero que seja útil para todos que procuram um material básico sobre a linguagem, seja para consulta própria ou mesmo para utilizar como recurso didático em outros cursos/instituições.




Obs: As condições da licença de uso estão apresentadas na segunda página do documento.

quarta-feira, 17 de dezembro de 2014

Faça como eu faço, não como eu digo (parte 1)

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.

Solução


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.