Técnicas para desenvolvimento e aceleração de códigos científicos


Prof. Edson Borin
Instituto de Computação
Universidade Estadual de Campinas
Raul Baldin
Faculdade de Engenharia Civil, Arquitetura e Urbanismo
Universidade Estadual de Campinas

Atividade Prática: Otimizações do Compilador

Nesta atividade, exercitaremos a aplicação de otimizações com o compilador.

Conceitos Básicos

Compiladores são ferramentas que auxiliam na tradução de código escrito em linguagens de programação para código em linguagem de máquina. Além de traduzir o código, os compiladores aplicam uma série de otimizações para melhorar o desempenho do mesmo. O conjunto de otimizações aplicados pelo compilador pode variar de acordo com o compilador, a versão do compilador ou mesmo com as opções de compilação. De fato, o usuário pode, em muitos casos, escolher o conjunto de otimizações que deve ser aplicado no código durante a tradução.

Nesta atividade, utilizaremos o compilador para aplicar diferentes conjuntos de otimizações no código e verificar o desempenho do código resultante.

Atividades

Cada atividade possui um arquivo com código fonte para executar o algoritmo, medir o tempo de execução e reportar o tempo e o desempenho do acesso à memória, em Gigabytes por segundo (GB/s). O algoritmo é executado múltiplas vezes e os tempos médio, menor e maior são reportados.

Compilação com diferentes níveis de otimização

O conjunto de otimizações e a ordem em que as otimizações são aplicadas podem afetar significativamente o desempenho do código resultante. Para que o usuário do compilador não tenha que escolher e habilitar manualmente as otimizações mais promissoras durante a compilação, os desenvolvedores de compiladores geralmente disponibilizam conjuntos pré-selecionados de otimizações. Estes conjuntos são geralmente habilitados com as flags: -O1, -O2, -Os e -O3, onde o -O3 habilita o conjunto mais agressivo (maior número de otimizações) e -O1 habilita o menor número de otimizações. A flag -O0 não habilita otimizações e gera um código sub-ótimo, equivalente à compilação sem flags de otimização.

Nesta atividade, vamos comparar o desempenho de uma aplicação quando compilada com diferentes opções de otimização.

Ao inspecionar o código do programa, você pode observar que:

Responda às seguintes perguntas:

Desempenho com o reuso de dados nas caches

Execute o experimento novamente, mas desta vez, modifique o código para que a rotina clean_caches() não seja invocada entre as iterações do kernel. O objetivo é manter os dados na cache entre as iterações. Para que isso seja possível, os três vetores (ma, mb e mc) devem caber na cache. Para tanto, você deve ajustar o tamanho da definição ARRAY_SZ de forma que seus vetores caibam na cache.
Sugestão: Se seu computador tiver uma memória cache de 4MB (4*1024*1024 bytes), você pode dimensionar o vetor da seguinte forma:

ARRAY_SZ=((4*1024*1024) / (3*sizeof(DATATYPE)))
sizeof(DATATYPE) é o tamanho do tipo de dados utilizado no programa (4 bytes para float e 8 para double).

Responda às seguintes perguntas:

Desafio

Inspecione o código em linguagem de montagem gerado com as flags -O2 e -O3.
Para gerar o código em linguagem de montagem com as flags -O3, execute:

gcc -O3 kernel10-array_sum.c -S -o kernel10-array_sum.s
Procure pela rotina array_sum_naive e identifique o laço principal da rotina.
Quantas iterações cada laço executa?
Quantas instruções são executadas por iteração?
Qual o número total de instruções executados no laço principal em cada uma das versões?