Esse post é o segundo da série sobre o estudo do Programming Pearls. O primeiro post, onde falei sobre as colunas 1 a 5, pode ser encontrado aqui. Hoje vou falar sobre as colunas 6 a 10, que englobam o assunto de desempenho dos programas.
Coluna 6: Algorithm Design Techniques
Na primeira coluna sobre o assunto, o livro descreve um caso real onde a otimização do programa diminuiu o tempo de execução do programa em 400 vezes. Essa otimização foi feita em diversos níveis:
Algoritmos e estruturas de dados — Investir um tempo para desenvolver algoritmos e escolher as estruturas de dados corretas pode reduzir a complexidade. Em geral é onde se consegue a maior redução no tempo.
Ajuste de algoritmos — Dependendo do problema, o uso de heurísticas para evitar processamento desnecessário pode ser interessante. A complexidade não mudará, mas na prática podemos ganhar um pouco de tempo. (Hey, minha área de pesquisa está aqui!)
Ajuste de código — Quando não se consegue mais melhorias com os níveis anteriores, pode-se apelar para truques de implementação. Aqui vale tudo: escrever o código em assembly, fazer uso de particularidades do sistema, paralelizar código, etc. O problema é que se perde em legibilidade de código e é um caminho sem volta.
Se seu código otimizado ainda deverá sofrer manutenção, pode ficar bastante difícil programar. Já dizia o mestre:
We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil
Donald Knuth
Ou seja, só desça para esse nível se seu código estiver funcional e não for sofrer maiores alterações.
Hardware — Após passar por todos os níveis, se quiser ganhar mais eficiência, uma ideia é investir em equipamentos mais sofisticados. Sistemas multi-core, clusters, GPU’s estão na moda. Não é preciso dizer que essa é a opção mais cara de se implementar.
Coluna 7: Back of the Envelope
Nas primeiras aulas de desenho técnico, lembro-me que o professor não deixava usar régua e compasso. A justificativa era que precisávamos aprender a improvisar rascunhos em situações em que não tivéssemos tais instrumentos à mão. Acredito que seja esse o espírito da coluna 7: estimar valores e custos.
Algumas regras para certos tipos de estimativas são apresentadas nessa coluna.
[1] Quantos segundos aproximadamente tem um ano? Faça a conta 3600*24*30*12, ou use o mnemônico: “pi segundos é um nano centenário” , para concluir que 1 ano ~= 3,14*10e7 s
[2] Removendo os 9 — Essa regra parte da propriedade de que o resto da divisão de um número N por 9, é igual ao resto da divisão da soma dos dígitos de N. Por indução, podemos repetir o argumento até que a soma contenha um dígito apenas, ficando fácil determinar o resto da divisão de N por 9.
Exemplo: N = 23718. A soma dos dígitos dá 21. A soma dos dígitos de 21 dá 3. Logo, 23718 % 9 = 3.
Mas a regra consiste em usar essa propriedade para determinar se uma soma está errada. Lembrando que o módulo da soma deve ser igual à soma dos módulos dos termos somados, vejamos o exemplo extraído do livro: 3142 + 2718 + 1123 = 6973. Temos que 3142 % 9 = 1, 2718 % 9 = 0 e 1123 % 9 = 7. A soma dá 8, mas 6973 % 9 = 7, então a soma deve estar errada.
[3] Regra do 72 — Essa regra vem de contabilidade e diz que se J é o rendimento de uma aplicação e T é o tempo em que o dinheiro ficou investido, então quando J*T = 72, o montante dobra. Por exemplo, se investimos R$1000 com rendimento de 6% ao ano, a regra diz que T = 12. Fazendo a conta para juros compostos, teremos 1000*(1 + .06)^12 = 2012.20.
[4] Lei de Little — diz “O número médio de objetos em um sistema é o produto do fluxo médio em que os objetos deixam o sistema e o tempo médio em que os objetos permanecem no sistema.”
Curiosamente eu já tinha usado essa regra para fazer uma estimativa sem saber que ela existia. Lembrando de um episódio do Chaves em que o prof. Girafales dizia “Cada vez que você respira, morre um chinês”, fiquei curioso para saber quantas pessoas morrem por segundo na China.
Sem ter acesso a qualquer referência, decidi fazer a estimativa. Eu tinha uma noção da população (número médio de pessoas no sistema) e chutei 1.5bi. Também chutei que a expectativa de vida lá é 70 anos (tempo que permanecem no sistema). Supus que a distribuição de pessoas por idade era uniforme, então pessoas com “0 anos”, devem corresponder a cerca de 1.5bi/70 =~ 20.000.000 por ano, o que dá 0.7 de recém-nascidos por segundo. Como a população da China está aumentando, o número de mortes deve ser menor do que nascimentos, mas como não tinha como usar essa informação, supus que eram iguais.
Não lembro qual foi minha estimativa para o tempo que levamos para respirar, mas aparentemente a média é 5 segundos. Isso siginifica que cada vez que o Chaves respira morrem 3.5 chineses. Não bateu exatamente com a afirmação do professor, mas como a estimativa ficou na mesma ordem de grandeza parece razoável.
Coluna 8: Algorithm Design Techniques
Nessa coluna o autor apresenta o clássico problema da subsequência de soma máxima. Dado um vetor de n elementos, por exemplo números inteiros, deseja-se encontrar i < j tal que a soma dos elementos de i a j, inclusive, seja máxima.
É mostrada uma abordagem inicial O(n^3), que vai reduzindo para O(n^2), O(n log n) por divisão e conquista, até chegar no famoso algoritmo baseado em programação dinâmica O(n).
Coluna 9: Code Tuning
People who play with bits should expect to get bitten
Jurg Nievergelt
Aqui são apresentados alguns truques para otimizar código. Os exemplos citados incluem:
— Evitar operações de divisão e módulo quando há uma alternativa simples;
— Substituir funções por macros (ou usar o modificador inline do C++);
Porém, usar macros pode ser perigoso. Considere o código abaixo:
#define max(a, b) ((a) > (b) ? (a) : (b)) int arrmax(int *x, int n){ if (n == 1) return x[0]; else return max(x[n-1], arrmax(x, n-1)); }
Qual a complexidade desse código? E se substituirmos a macro por uma função?
— Usar loop unrolling, embora eu não tenha conseguido implementar um código onde tal estratégia tenha diminuído o tempo de execução.
Coluna 10: Squeezing Space
Com a disponibilidade de memória na casa dos giga bytes, não faz mais tanto sentido escovar código para reduzir consumo de memória, exceto para sistemas embarcados. Nessa coluna o autor descreve algumas técnicas para a redução do consumo de memória, tais como
— Estruturas de dados esparsos – como matrizes esparsas
— Compressão de dados – usando informação sobre o intervalo de valores que seu dado possa assumir, dá para escolher um tipo de dado mais adequado. Por exemplo, usar char
ao invés de int
.
— Garbage Collection – algumas linguagens como Java têm essa funcionalidade embutida. Em linguagens que não oferecem esse mecanismo como C++, existe a alternativa de se usar smart pointers.
Deverá estar ligado para publicar um comentário.