Módulo IV
Técnicas de Otimização I
Otimizações Gerais usando uma Linguagem Compilada
Computação de Constantes no Laço
LoopInvar(int size,int step,int ratio)
{
int gradient;
for ( i = 0; i < size/step; i += step )
{
gradient = step*ratio;
sort[i] *= gradient;
}
Este cálculo é uma
return;
}
O compilador
não consegue
otimizar esta
operação, assim
temos um valor
constante sendo
calculado
sempre e sem
necessidade
constante, mas o
compilador o trata como
uma equação qualquer,
processando-a a cada
iteração no laço
Computação de Constantes em Laços
LoopInvar(int size,int step,int ratio)
{
int gradient;
int limit;
gradient = step * ratio;
limit = size/step;
for ( i = 0; i < limit; i += step )
{
sort[i] *= gradient;
}
return;
}
Uso de variáveis
temporárias pode
ajudar o
compilador na
otimização do
código
Computação de Constantes em Laços: Desvios (Branches)
InvarBranch(int size, int mode, int offset){
for (int i = 0; i < size; i += 1)
if (mode & DIV_OFST)
data[i] /= offset;
else
data[i] += offset;
return;
}
Desvios no código não
combinam com
desempenho, então
por que fazer um
desvio a cada passo
dentro do laço?
O valor de mode pode mudar a
cada chamada dessa função, mas
não dentro do laço. Uma vez
dentro do laço, toda iteração é
igual, não precisamos do desvio
Computação de Constantes em Laços: Desvios (Branches)
InvarBranch(int size, int mode, int offset) {
if (mode & DIV_OFST)
for (i = 0; i < size; i += 1)
data[i] /= offset;
else
for (i = 0; i < size; i += 1)
data[i] += offset;
return;
}
Mais código,
menos intuitivo
e mais rápido!
Laços com Contagem: Decrementando em um Laço
CountDown(int size) {
int i;
for (i = 0; i < size; i++)
array[i] = 0;
return;
}
É mais simples para
o processador
comparar a variável i
com zero!
Laços com Contagem: Um Solução
CountDown(int size) {
int i;
for (i = size-1; i >= 0; i--)
array[i] = 0;
return;
}
A comparação com
zero é mais eficiente,
no decremento ao
passar por zero o
indicador de zero do
processador é ativado,
nenhuma comparação
é necessária com outro
valor para resolver
esta condição
Agrupando Variáveis
int ClusterVar( int offset, int ratio, int size) {
int i, gradient, delta;
gradient = offset * ratio;
gradient é calculado
pelos valores de offset
e ratio
for (i = 0; i < size; i++)
array[i] = 0;
delta = offset – ratio;
return delta + gradiente;
}
delta também é
calculado pelos
valores de offset e
ratio
Agrupando Variáveis de maneira Eficiente
int ClusterVar(int offset, int ratio, int size){
int i, gradient, delta;
for ( i = 0; i < size; i++)
array[i] = 0;
gradient = offset * ratio;
delta = offset – ratio;
return delta + gradiente;
}
A utilização agrupada
das variáveis facilita a
tarefa de otimização
ao compilador
Interpretando e Simplificando uma Função
int IsEven(int value) {
int result;
result = value / 2;
if (result * 2 == value)
return 1;
else
return 0;
}
O objetivo desta
função é determinar
se o parâmetro é par.
Se for retorna 1.
Uma Solução mais Simples
int IsEven(int value) {
return !(value & 0x1);
}
Toda representação
binária de um número
par termina com zero e
esta função utiliza esse
fato
Simplificando ainda mais
__inline int IsEven(int value)
{
return (~value)&0x1;
}
Agora a função é tão
simples que devemos
considerar o uso de
inline ou macro
Inlining
 Otimização muito oportuna para eliminar a sobrecarga
da chamada e do retorno de uma função
int IsEven(int value)
int IsEven(int value)
{
int result;
Simplificando
{
return (~value)&0x1;
}
result = value / 2;
if ( result * 2 == value )
{
return 1;
}
else
{
return 0;
}
}
Usando __inline na função
__inline int IsEven(int value)
{
return (~value)&0x1;
}
Ponteiros: Indexação
BasePtr(int *ptr, int *cmask, int section, int limit) {
int i;
for (i = 0; i < limit; i++) {
*(ptr + section * secsize + i) &= *cmask;
cmask++;
}
return;
}
Uma parte do cálculo
deste ponteiro não
varia dentro deste
laço, logo é possível
implementações mais
eficientes
Ponteiros: Indexação
BasePtr(int
Poderíamos ir
além dessa
otimização?
*ptr, int *cmask, int section, int limit)
{
int *auxptr, i;
auxptr = ptr + section * secsize;
for (i = 0; i < limit; i++) {
*(auxptr + i ) &= *cmask;
cmask++;
}
return;
}
Calculando a parte do
ponteiro que é
independente de
qualquer valor do laço,
resta uma parte de
cálculo bem mais
simples para ser
executada dentro dele.
Ponteiros: Mais otimizações
int BasePtr(int *ptr,int *cmask,
int section,int limit)
{
int i;
Ajustamos nossos
ponteiros para o último
elemento, assim podemos
contar decrementando
int *auxptr;
auxptr = ptr + section * secsize;
auxptr += limit – 1;
cmask += limit – 1;
Fazer o laço decrementar
até zero tira vantagem do
conjunto de instruções do
processador ARM
for ( i = limit - 1; i >=; 0 i-- )
*(auxptr--) &= *(cmask--);
return;
}
Pós-decremento dos
ponteiros torna mais fácil
para o compilador o uso de
instruções de load/store
com pós-decremento.
Redução de Desvios (Branches)
Void MultBranch(void) {
//iniciação de mask e shiftbits
if (mode & TRANSPNT ) {
Iniciação de
shiftbits e
mask
mask = 0x0110;
shiftbits = 4;
} else {
Iniciação
de limit
mask = 0x1100;
shiftbits = 8;
}
//ajustando limite
limit = 5;
if (mode & TRANSPNT) limit = 9;
...
Existe
alguma
maneira de
reduzirmos
os ifs e os
desvio?
Redução de Desvios (Branches)
void MultBranch(void) {
//iniciação de mask, shiftbits e limit
if (mode & TRANSPNT ) {
limit = 5;
mask = 0x0110;
shiftbits = 4;
Iniciação de shiftbits e
mask, além de alterar o
valor de limit, tudo em
um só if.
} else {
limit = 9;
mask = 0x1100;
shiftbits = 8;
}
...
Evitar
desvios em
um código é
uma prática
saudável!
Localidade de Referência de Memória
int Image[128][128];
void ImageFilter(void) {
Estamos percorrendo a
matriz imagem pela
coluna, embora ela
seja armazenada na
memória em função de
suas linhas
int i, j, k, sum;
// Percorre as colunas
for ( i = 0; i < 128; i++ ) {
sum = 0;
for ( j = 0; j < 128; j++ ) {
sum = 0;
for ( k = 0; k < 8; k++ ) {
sum += Image[j][i] * filter[j];
}
Image[j][i] = sum;
}
}
}
// Percorre as linhas
for ( i = 0; i < 128; i++ )
Pensando como os dados são
armazenados na cache vemos
que essa não é uma boa maneira
de se trabalhar com um array...
Localidade de Referência de Memória
TransmuteArray(Image,0);
for ( i = 0; i < 128; i++ ) {
sum = 0;
for ( j = 0; j < 128; j++ ) {
sum = 0;
for ( k = 0; k < 8; k++ )
Reordene a matriz
antes de processá-la
de maneira a transpor
as linhas pelas
colunas. Depois reprocesse novamente a
matriz
sum += Image[i][j] * filter[j];
Image[i][j] = sum;
}
}
TransmuteArray(Image,1);
Essa é uma maneira
muito mais eficiente de
se percorrer a matriz
Uso Eficiente de Bibliotecas
void OutputMethod(HANDLE JavaSource, HANDLE JavaBin)
{
unsigned char BCode;
int EndMethod;
EndMethod = 0;
A rotina write é
chamada para todos os
bytecodes pela JIT
while( !EndMethod )
{
EndMethod =
GenByteCode(JavaSource, &BCode);
write(JavaBin,&BCode,1);
}
}
A função write realiza
várias verificações no
handle do arquivo e no
seu estado (e faz isso a
cada chamada da
função) Ela pode
armazenar ou não as
escritas internamente
Uso Eficiente de Bibliotecas - Otimização
#define BSIZE 128
EndMethod = GenByteCode(JavaSource,
void OutputMethod(
HANDLE JavaSource,
HANDLE JavaBin)
&BCode);
*pBuff++ = BCode;
if (BuffLeft-- <= 0 )
{
{
write(JavaBin,buffer,BSIZE);
unsigned char *pBuff;
BuffLeft = BSIZE;
unsigned char buffer[BSIZE];
pBuff = buffer;
int BuffLeft;
int EndMethod;
/* write out bytecodes till
we reach the end of method */
EndMethod = 0;
}
} // end of while…
if ( BuffLeft < BSIZE )
{
write(JavaBin,buffer,
pBuff = buffer;
BuffLeft = BSIZE;
while( !EndMethod )
{
BSIZE – BuffLeft);
}
} // end of function OutputMethod
Uso Eficiente de Bibliotecas - Otimização
Agora a função write é
usada muito mais
eficientemente
EndMethod = GenByteCode(JavaSource,
&BCode);
*pBuff++ = BCode;
if (BuffLeft-- <= 0 )
{
Observe esse ponteiro
pós-incrementado: Uma
única instrução assembly
STR é capaz de
armazenar e atualizar o
ponteiro
write(JavaBin,buffer,BSIZE);
BuffLeft = BSIZE;
pBuff = buffer;
}
} // end of while…
if ( BuffLeft < BSIZE )
{
write(JavaBin,buffer,
Veja que a variável
BuffLeft é decrementada
até Zero
BSIZE – BuffLeft);
}
} // end of function OutputMethod
Uso Eficiente de Bibliotecas
 Use cada função com mais eficiência, pois fazer
menos chamadas implica:
 Menor sobrecarga da chamada da função
 Menor sobrecarga de verificação de erros
 Menos acessos a dispositivos físicos
 Compreenda as limitações dos dispositivos
 Eficiência na escrita em Flash
 Limitação da Biblioteca
Desenrolamento de Laço
void LoopUnRoll(int limit,int translate)
int i;
for ( i = 0; i < limit, i++ ) {
{
De 2 a 3 instruções a
cada iteração do laço do
for
array[i] += translate;
}
De 3 a 4 instruções para
executar o corpo do laço
}
A sobrecarga para
percorrer o laço prejudica
o desempenho!
Implementação do Desenrolamento de Laço
void LoopUnRoll(int limit,int translate)
int i;
{
De 2 a 3 instruções a cada
iteração do laço do for
for ( i = 0; i < limit, i += 4 ) {
array[i] += translate;
array[i + 1] += translate;
array[i + 2] += translate;
array[i + 3] += translate;
De 12 a 16 instruções para
executar o corpo do laço
}
}
E a escrita consegutiva para dados contíguos pode
aproveitar mais uma característica do processador
XScale: Write Coalescing
Usando Comentários no Código como Técnica de Medição
 Se você gostaria de saber o quanto uma linha
ou pedaço de código contribuem para o
desempenho de execução de uma função...
 Considere comentar a linha ou bloco de código a ser
analisado.
 Tenha certeza que o código ao redor não é
demasiadamente sem importância a ponto do
otimizador do compilador simplesmente descartá-lo.
 Substitua algo muito simples que o compilador não
possa otimizar.
Usando Comentários no Código como Técnica de Medição
Sx = 0;
for ( col = 0; col < numCols; col++) {
Exemplo:
for ( row = 3; row < (numRows – 3);row++) {
ploc1 = in + (row*numCols + col);
ploc2 = ploc1 + 1;
Comente a linha que
contém sqrt
Substitua esta linha
com algo que evite que
o compilador altere
drasticamente o código
compilado.
Kr = ((*(ploc1-adj3) ...
Ks = ((*(ploc2 – adj3)....
}
if (Kr < 0 ) Kr = -Kr;
if (Ks < 0 ) Ks = -Ks;
// Sx += (S16Word)sqrt( Kr +Ks );
Sx += Kr + Ks;
}
Módulo IV
Técnicas de Otimização II
O que é PreLoad?
 PreLoad coloca o dado a ser utilizado
processador dentro da memória cache.
pelo
 Suporte na extensão ARM .v5 DSP do compilador
Microsoft na versão 4.0
 Dado um endereço, o PreLoad inicia a carga dos 32
bytes da memória que inclui o endereço para a
memória cache.
O que é PreLoad?
 Os 5 bits inferiores de endereço são ignorados, desde
que as linhas da cache são alinhadas em 32 bytes.
 Não há o conceito de largura. Não se pode carregar o
conteúdo de um valor de tamanho maior que 32 bytes
usando PreLoad, pois ele ultrapassa o tamanho da
linha de cache (32 bytes).
 Leitura sem atribuição para um registrador de destino,
não ocupa o pend buffer.
 Endereços inválidos de memória não geram exceções,
são ignorados.
Quando o PreLoad é eficiente?
 Quando o processamento de dados contíguos são
repetido dentro de um laço
 Quando o endereço de elementos em uma iteração
posterior são facilmente determinados
 O dado provavelmente não está na cache
 É muito grande em bytes para caber inteiro na cache
 O dado não foi recentemente gerado ou preparado em
outro estágio
Por que usar PreLoad?
 Buscar um dado na memória é uma operação cara
em qualquer tipo de processador
 Dados presentes na memória cache são
manipulados mais rapidamente
 PreLoad busca o dado e o coloca na cache. Se bem
usado, pode economizar tempo do processador na
utilização de um dado da memória
PreLoad irá ajudar?
 Utilize o VTune
Taxa alta de
Cache Miss
Cache Miss
= 28,76%
 Procure por funções onde há uma taxa alta de “cache miss”
Alterações no Código para Incrementar a
Eficiência de PreLoad
 Otimize o uso da Cache
 Os dados devem preferencialmente estar próximos na
memória
 Use dados em posições contíguas de memória
 Para listas encadeadas, onde todos os elementos
estão “embaralhados”
 Use um ponteiro especial que aponta para ‘n’
elementos a frente
Alterações no Código para Incrementar a
Eficiência de PreLoad
 Para matrizes de estruturas onde somente alguns
membros são usados em algum laço
 Faça duas matrizes de estruturas, com os membros mais
usados em uma estrutura e o resto em outra matriz
 Permite que as estruturas com os dados usados mais
freqüentemente fiquem contidas em uma única linha da
memória cache
Ajustando o PreLoad
 Quantos dados devem ser colocados na cache por
“antecipação de uso”?
 Tente usar o método empírico
 Depende da complexidade do código executado no laço
entre sucessivas execuções do PreLoad
 O PreLoad deve carregar os dados na cache com 40 a 80
clocks de instrução antes de usá-los
 Faça do valor de antecipação do PreLoad uma constante
que possa ser alterada no seu código
Eficiência máxima do PreLoad
 Melhor se houve apenas um PreLoad por linha de cache
 (Tempo do processamento por item de dado) * (Número
de itens por linha de cache) deve ser aproximadamente
tempo de acesso externo
 Senão, faça PreLoad
32 conjuntos
no número apropriado
de linhas de cache
Via 0
Linha de cache
 Não exceda 4 linhas
Via 1
Linha de cache
com PreLoad de uma
vez
Ender.
Dados
Via 31
Linha de cache
Eficiência máxima do PreLoad
 Considere que outros dados também receberão acesso
(leitura e escrita à memória). Lembre-se do fill buffer.
 A taxa de alimentação deve ser igual a taxa de consumo
 Se o tempo de processamento por uma linha de cache é
menor do que o tempo entre PreLoads, então é
necessário fazer o PreLoad de um endereço mais a
frente
Riscos ao Usar o PreLoad
 A instrução de PreLoad consome um ciclo de clock, se ele
não melhorar o desempenho, ele poderá piorá-lo
 Evite algoritmos que demandem muito custo
computacional para descobrir qual endereço que o
PreLoad usará
 Não exagere no uso de PreLoad colocando-o em todo o
código, pois ele também usa um buffer de preenchimento
 O processador ficará em espera se o buffer de preenchimento
estiver cheio
Exemplo de PreLoad
Inclua o Protótipo
void __PreLoad(void *ptr);
void UsePreLoad(int array[], int limit) {
int i;
PreLoad chamado para
for (i = 0; i < limit; i++) {
carregar na cache
/*Carrega elemento antecipadamente
elementos do array com 10
10 iterações a frente*/
iterações de antecedência
__PreLoad(&(array[i+10]));
/* Código de processamento do array[i] aqui */
}
return;
}
PreLoad será chamado a cada iteração
do laço mesmo que o elemento do array
já esteja na linha da cache.
Exemplo de PreLoad: Melhoria
void __PreLoad(void *ptr);
void UsePreLoad(int array[], int limit){
int i;
for (i = 0; i < limit; i +=8) {
O laço é estendido para
poder processar todos
/*Carrega elemento antecipadamente
os elementos do vetor
10 iterações a frente*/
que estão presentes na
__PreLoad(&(array[i+10]));
/* código para array[i]*/
linha de cache
/* código para array[i+1]*/
carregada da memória
/* código para array[i+2]*/
/*
/*
/*
/*
/*
código
código
código
código
código
}
return;
}
para
para
para
para
para
array[i+3]*/
array[i+4]*/
array[i+5]*/
array[i+6]*/
array[i+7]*/
Isso reduz o número de
chamadas que são feitas ao
PreLoad com o dado já
presente na linha de cache
Use PreLoad cuidadosamente
 O bom senso é necessário para balancear
 quantos itens estão presentes na linha de cache,
 qual o tempo para processar os elementos na linha de cache
 determinar o futuro uso de memória que PreLoad precisa
carregar
 qual o tempo necessário para a carga da cache
 Se o PreLoad for chamado a cada iteração do laço
carregando linhas da cache não necessárias, junto aos
acessos normais do processamento, poderá resultar em
esperas devido a falta de entradas vazias no buffer de
preenchimento
Módulo IV
Laboratório
Download

O que é PreLoad?