Uma Análise de Vetorização Automática do Compilador GCC
-
Upload
jaguaraci-silva -
Category
Documents
-
view
147 -
download
41
description
Transcript of Uma Análise de Vetorização Automática do Compilador GCC
Uma Análise de Vetorização Automática do Compilador GCC
Jaguaraci Batista Silva
Instituto de Ciência e Tecnologia, Universidade Federal de São Paulo
Campus São José dos Campos, São Paulo-SP
Resumo: Existem três formas principais de se fazer uso de unidades de vetorização
nos processadores atuais: (i) pela programação em código de montagem, (ii) através
da programação de funções intrínsecas em linguagens de alto nível ou (iii) usar um
compilador que traduz automaticamente as operações escalares em vetoriais. Seja
através da programação em linguagem de montagem ou usando funções intrínsecas,
o programador deve possuir o controle completo dos detalhes da sua implementação,
que geralmente é específica para uma arquitetura. Por isso a perseguição da alta
performance, nestes casos, pode incorrer em baixa produtividade e portabilidade do
código. Esta trabalho apresenta uma análise de vetorização automática usando o
compilador GCC, com o objetivo de analisar quais códigos de programas em C/C++
podem ser vetorizados automaticamente e quais necessitam de esforços para
adicionar instruções intrísecas no código para alcançar os benefícios da vetorização.
Palavras-chaves: Performance, Algoritmo, Vetorização, GCC, SSE.
1 - Introdução
Em muitas aplicações é possível encontrar trechos simples de códigos envoltos por
loops onde a mesma operação é executada através de vetores de diferentes tipos de
elementos. A vetorização transforma estes laços em instruções que trabalham em
vários itens de dados simultaneamente. Tipicamente, a vetorização atinge tanto os
processadores como suas extensões de vetorização, cujo os processadores vetoriais
foram os principais componentes dos supercomputadores da década de 1980 e início
de 1990. As extensões de vetorização para microprocessadores de uso geral como
Intel MMX e AltiVec IBM, surgiu na década de 1990 para suporte a aplicações
multimídia e a tecnologia pode ser encontrada hoje facilmente, em consoles de
videogame e placas gráficas. Hoje, as extensões de vetorização não são utilizadas
apenas para os aplicativos multimídia e jogos de vídeo, mas também para computação
científica [1].
O speedup máximo que pode ser obtido por meio de extensões de vetorização dos
processadores pode ser dado em função da largura dos seus registros e unidades de
vetorização. A maioria das máquinas hoje possuem unidades de vetorização de 128
bits e as operações de vetorização em relação as sequenciais podem ser equivalentes
até 2, 4, 8 até 16 vezes mais rápida que a sua congénere escalar, dependendo do tipo
de dados. Assim, vetorização é uma das transformações do compilador que pode ter
um impacto significativo no desempenho das aplicações [1].
Existem três formas principais de se fazer uso de unidades de vetorização: (i) pela
programação em código de montagem, (ii) através da programação com funções
intrínsecas em linguagens de alto nível (e.g. C) ou (iii) usar um compilador que traduz
automaticamente as operações escalares em vetoriais. Seja através da programação
em linguagem de montagem ou usando funções intrínsecas, o programador é quem
deve possuir o controle completo dos detalhes da sua implementação, que geralmente
é específica para uma arquitetura. Por isso a perseguição da alta performance, nestes
casos, pode inferir em baixa produtividade e portabilidade do código. Por outro lado,
existem compiladores tais como GCC e ICC da Intel (ICC) que podem gerar
automaticamente o código vetorizado [1].
Este artigo apresenta uma análise do compilador GCC sobre quais laços de um
conjunto de testes podem ser automaticamente vetorizáveis, quais deles não foi
possível a vetorização e quais modificações foram necessárias para adicionar
instruções intrísecas de vetorização usando a extensão SSE3 (processadores Intel)
através da linguagem C. Após, foram realizadas medições de desempenho com todas
as versões dos códigos para medir: o tempo de execução, MFLOPS e CPE (Ciclos por
elementos do vetor). O estudo realizou ainda uma segunda análise em uma pequena
aplicação que implementa o método dos mínimos quadrados, contruindo uma versão
com e sem vetorização, analisando a vetorização automática dos compiladores e
adicionando funções intrinsecas para as extensões de vetorização do processador. A
análise também verificou o desempenho de todas as versões dos algoritmos utilizando
métricas de tempo de execução, MFLOPS e CPE.
2 – Análise da Vetorização de Código Usando o Compilador GCC
O experimento foi iniciado pela definição de duas etapas de análise: 1-cálculo de
vetores e 2-construção de uma aplicação, que obtém a partir de um arquivo, os dados
para o realizar o cálculo de mínimos quadrados. Essa última abordagem serve apenas
para ilustrar como seria uma modificação de código em um sistema legado para
alcançar os benefícios da vetorização.
2.1 Cálculo de Vetores
O cálculo de vetores tem o objetivo de analisar quais implementações de código
podem ser automaticamente vetorizadas e por outro lado, as que não possam ser,
indicar um modo de como podem ser modificadas para obterem o benefício da
vetorização. Neste estudo foram construídos 6 algorítmos, cujo código de
implementação utiliza a linguagem C:
A B C D E F
for(i=1;i<n; i++)
{
x[i]=a[i]+b[i];
r[i]=x[i-1]+1.0;
}
for(i=0;i<n;i++)
{
x[i]=a[i]+b[i];
r[i]=x[i+1]+1.0;
}
for(i=0;i<n;i++)
{
x[i]=a[i]+b[i];
a[i]=x[i+1]+1.0;
}
for(i=0;i<n;i++)
{
t=a[i]+b[i];
r[i]=t+1.0/t;
}
for (i=0; i<n; i++)
{
s += a[i];
}
for (i=1; i<n; i++)
{
a[i] = a[i-1] + b[i];
}
Tabela 1 – Algoritmos Implementados para Teste de Vetorização Automática.
Conforme a Tabela 1, é possível ver simultaneamente todos os algoritmos
implementados, onde a diferença entre eles está na operação de cálculo dos vetores.
A partir destas implementações, utilizou-se 3 modos de análise: (i) uma compilação do
código serial na sua forma original, (ii) compilação para possibilitar a vetorização
automática utilizando extensões SSE3 do processador e (iii) modificação do código
para possibilitar a vetorização intrísica.
B C F
for (i=0; i<n-1; i++) {
temp[i] = a[i] + b[i];
}
for (i=0; i<n-1; i++) {
r[i] = x[i+1] + 1.0;
}
for (i=0; i<n-1; i++) {
temp2[i] = r[i+1] + temp[i];
}
for (i=0; i<n-1; i++) {
r[i] = temp2[i];
}
for (i=0; i<n-1; i++) {
temp[i] = a[i] + b[i];
}
for (i=0; i<n-1; i++) {
a[i] = x[i+1] + 1.0;
}
for (i=0; i<n-1; i++) {
a[i] = temp[i];
}
for (i=1; i<n; i++) {
temp[i] = a[i-1] + b[i];
}
for (i=1; i<n; i++) {
a[i] = temp[i];
}
Tabela 2 – Algoritmos Modificados Para Suportar a Vetorização Automática.
A primeira tentativa de vetorização automática só foi possível com os algoritmos A e D,
sendo que o algorimo E foi necessário acrescentar a opção -funsafe-math-
optimizations do GCC, para que fosse possível vetorizá-lo automaticamente. Por
questão de dependência de dados, os outros algoritmos (Tabela 2) tiveram de ser
modificados, onde a estratégia de vetorização nesses casos foi baseada em [2].
B C F
long int i, nvecsse;
__m128 v1, v2;
nvecsse = n - (n%4);
for(i=0; i<n; i+=4) {
v1 = _mm_load_ps(a+i);
v2 = _mm_load_ps(b+i);
v2 = _mm_add_ps(v1, v2);
_mm_store_ps(x+i, v2);
}
long int i, nvecsse;
__m128 v1, v2;
nvecsse = n - (n%4);
for(i=0; i<n; i+=4) {
v1 = _mm_load_ps(a+i);
v2 = _mm_load_ps(b+i);
v2 = _mm_add_ps(v1, v2);
_mm_store_ps(x+i, v2);
}
long int i, nvecsse;
__m128 v1, v2, v3;
nvecsse = n - (n%4);
for(i=0; i<n; i+=4) {
v1 = _mm_load_ps(a+i);
v2 = _mm_load_ps(b+i);
v2 = _mm_add_ps(v1, v2);
_mm_store_ps(x+i, v2);
}
for (i=nvecsse; i<n; i++) {
r[i] = x[i] + 1.0;
}
for (i=nvecsse; i<n; i++) {
a[i] = x[i+1] + 1.0;
}
for (i=nvecsse; i<n; i++) {
a[i] = x[i-1] + b[i];
}
Tabela 3 – Algoritmos Modificados Para Vetorização Intrísica do Trecho não Vetorizado.
A segunda tentativa de vetorização utilizou a técnica de vetorização intrísica de código,
o que sugere criação de variáveis com acesso as extenções dos registradores
diretamente. Dessa forma, é possível forçar a vetorização dos trechos de código que
não puderam ser automaticamente vetorizáveis anteriormente (Tabela 1). O objetivo
desta modificação foi perceber se haveria influência na performance do trecho não
vetorizável do algoritmo passando por duas etapas de modificação (Tabela 2 e 3), o
que pode ser visto na seção de resultados (Figura 1).
2.2 Cálculo dos Mínimos Quadrados
A segunda parte do estudo exibe de forma pragmática como obter o benefício da
vetorização em um sistema legado, neste caso foi selecionado um trecho de uma
aplicação industrial que obtém os dados de um arquivo (primeira linha: quantidade de
elementos e demais linhas os elementos x e y) para leitura e cálculo da função usando
mínimos quadrados. O método dos mínimos quadrados consiste em um estimador que
minimiza a soma dos quadrados dos resíduos de uma regressão linear, de forma a maximizar o
grau de ajuste do modelo aos dados observados, procurando encontrar o melhor ajuste para
um conjunto de dados tentando minimizar a soma dos quadrados das diferenças entre o valor
estimado e os dados observados, onde tais diferenças são chamadas resíduos [3].
Programa Serial Modificado Para
Vetorização Automática
Modificado para Vetorização
Intrísica
for (i=0; i<n; i++) {
if (fscanf(infile, "%lf %lf",
&x[i], &y[i])!=-1)
printf("error reading
line\n");
SUMx = SUMx + x[i];
SUMy = SUMy + y[i];
SUMxy = SUMxy +
x[i]*y[i];
SUMxx = SUMxx +
x[i]*x[i];
}
for (i=0; i<n; i++) {
y_estimate = slope*x[i] +
y_intercept;
res = y[i] - y_estimate;
SUMres = SUMres +
res*res;
printf (" (%6.2lf %6.2lf)
%6.2lf %6.2lf\n",
for (i=0; i<n; i++) {
if (fscanf(infile, "%lf %lf",
&x[i], &y[i])!=-1)
printf("error reading
line\n");
}
for (i=0; i<n; i++) {
tempSUMx[i] += x[i];
tempSUMy[i] += y[i];
tempSUMxy[i] +=
x[i]*y[i];
tempx[i] = x[i];
tempSUMxx[i] +=
x[i]*tempx[i];
}
for (i=0; i<n; i++) {
SUMx += tempSUMx[i];
SUMy += tempSUMy[i];
SUMxy +=
__m128 xR, yR, xtR, s1, s2, s3, s4;
for (i=0; i<n; i+=4) {
xR = _mm_load_ps(&x[i]);
s1= mm_load_ps(&tempSUMx[i]);
s2 = _mm_add_ps(xR, s1);
_mm_store_ps(&tempSUMx[i], s2);
yR = _mm_load_ps(&y[i]);
s1 =_mm_load_ps(&tempSUMy[i]);
s2 = _mm_add_ps(yR, s1);
_mm_store_ps(&tempSUMy[i], s2);
s1 = _mm_mul_ps(xR, yR);
s2=_mm_load_ps(&tempSUMxy[i]);
s3 = _mm_add_ps(s1, s2);
_mm_store_ps(&tempSUMxy[i],
s3);
xtR = _mm_load_ps(&x[i]);
s1 = _mm_mul_ps(xR, xtR);
x[i], y[i], y_estimate,
res);
}
tempSUMxy[i];
SUMxx +=
tempSUMxx[i];
}
s2=_mm_load_ps(&tempSUMxx[i]);
s3 = _mm_add_ps(s1, s2);
_mm_store_ps(&tempSUMxx[i],
s3);
}
Tabela 4 – Algoritmos implementados para o Cálculo dos Mínimos Quadrados.
O primeira programa foi implementado de forma serial (Tabela 4 - Coluna 1), onde é
possível observar que a leitura dos dados, o cálculo da função e a saída do programa
foram dividos em dois loops. O primeiro obtém os dados de entrada e realiza o cálculo,
neste caso, o primeiro laço é proibitivo para vetorização automática, porque há
existência de dependência de dados (Linha 11 - x[i]*x[i]). O segundo loop realiza a
segunda parte do cálculo e gera a saída da função com os resultados. Para efeito de
análise, o estudo apenas realizou modificações no primeiro loop, também para que o
exemplo se torna-se de fácil entendimento e didático. Apenas a parte em negrito do
código foi modificado para a vetorização automática (Tabela 4 – Coluna 2), cujo trecho
de código utilizou a expansão das variáveis para vetores temporários, e após o cálculo,
a redução dos vetores para as mesmas variáveis escalares [2], por isso não foi preciso
modificar o trecho de saída do programa. Após a vetorização automática do código,
houve uma segunda modificação para vetorização intrísica do trecho em negrito do
programa (Tabela 4 – Coluna 3) para analisar se haveria mais algum ganho de
performance em relação a vetorização automática feita pelo compilador, cujo os
resultados estão na próxima seção (Figura 2).
3 – Resultados
O estudo utilizou um notebook de 32 bits, com processador Intel® Core™2 Duo
Processor T5550 (2048KB L2 Cache, 1.83 GHz, 667 MHz FSB) e memória de 3GB (667
MHz DDR2), sistema operacional Microsoft Windows Vista Business (Versão 6.0,
Compilação 6002, Service Pack 2) o ambiente de desenvolvimento Eclipse 3.5.1 (Build
20090920-1017), com o CDT Development Kit para C/C++, MingW32 e a bilbioteca PAPI
5.0.0, além da linguagem de programação C, com o GCC versão 4.6.2 no Linux Ubuntu
12.4.1 embarcado em um pendrive via porta USB como ambiente de experimentação,
com a finalidade exclusiva de execução do programa construído nesse trabalho (Seção
de solução) para coleta das métricas, através das classes instrumentadas com a
biblioteca PAPI: Tempo de Execução, CPE e MFLOPS.
Figura 1 –Análise de Vetorização com GCC para Cálculo de Vetores.
O número de linhas utilizado para o cálculo de vetorização foi 40.000.000 e conforme a
Figura 1 é possível constatar que em relação a implementação serial (compilada com
otimização –O2), a vetorização automática usando a extensão SSE3 e a modificação de
código para vetorização intrísica, está última prevaleceu com o menor tempo de
execução nos testes: A, B, C e F. Também a quantidade de MFLOPS e CPE (quantidade
de ciclo de instruções por elementos do vetor) possuem diversas informações a serem
destacadas.
O primeiro gráfico (Figura 1 - Esquerda Superior) exibe o tempo de solução dos
algoritmos implementados que foram automaticamente vetorizados. Comparando-os
diretamente com as opções de compilação do GCC para serial (gcc -O2 -I /papi/include
vetorizacao.c -o vetorizacao -L /papi/lib -lpapi) e vetorização automática (gcc -O2 -
msse3 -ftree-vectorize -ftree-vectorizer-verbose=2 -funsafe-math-optimizations -I
/papi/include vetorizacao_modificado.c -o vetorizacao_modificado -L /papi/lib -lpapi)
é possível ver no gráfico que o tempo de solução com o uso da vetorização automática
para os casos onde houve dependência de fluxo de dados, poderia ter um
desempenho melhor, caso fossem implementados de forma intrísica [2].
O segundo gráfico (Figura 1 - Direita Superior) demonstra a análise de MFLOPS e as
implementações A, C e E tiveram quase o mesma quantidade de instruções, a
implementação E se destaca por ter um maior número de instruções, pois apesar do
código parecer mais simples, após compilado para linguagem de máquina, requer um
maior controle de atribuições, cálculo e armazenamento entre memória principal,
cache e registro, o que poderia ser contornado com a criação de uma estratégia
íntrisica de acesso a variável no registro para evitar a criação e controle de variáveis
acessando áreas não contiguas nas memórias principal e cache.
No terceiro gráfico (Figura 1 - Esquerda Inferior) é apresentado os valores de CPE,
onde a vetorização automática obteve maior desempenho nos algoritmos seriais, com
excessão do algoritmo A. A vetorização automática piorou o desempenho dos
algoritmos B, C e F, os quais necessitaram ser modificados para vetorização intrísica
(gráfico direita inferior), para que as modificações tivessem um melhor desempenho
(Figura 1 – Direita Inferior).
Figura 2 –Análise de Vetorização com GCC para o Cálculo de Mínimos Quadrados.
De acordo com a Figura 2 , nota-se qua a vetorização melhorou o desempenho da
função de cálculo de mínimos quadrado, pois o tempo de solução, a quantidade de
MFLOPs e CPE foram reduzidos logo que o algoritmo foi modificado usando a
vetorização automática do GCC, entretanto o código não foi vetorizado
automáticamente, incorrendo em modificações no primeiro laço para que o benefício
fosse alcançado. Consequentemente, a vetorização intrísica melhorou ainda mais o seu
desempenho, a eficiência em relação ao CPE foi 2x melhor que a aplicação serial,
MFLOPS 3,7x e o speedup foi superior 2,6x em relação a implementação original.
4 - Conclusão
Este trabalho apresentou uma análise de vetorização automática usando o compilador
GCC, com o objetivo de analisar quais códigos de programas em C/C++ podem ser
vetorizados automaticamente e quais necessitam de esforços adicionais para alcançar
os benefícios da vetorização. O estudo foi implementado em duas partes: (i)
implementação de algoritmos para cálculo de vetores e (ii) implementação de parte de
um sistema legado que utiliza a função de mínimo quadrados. Foi possível perceber no
estudo que por causa da dependência de fluxo de dados nem sempre pode-se alcançar
um melhor desempenho automaticamente e através da vetorização intrísica,
utilizando as extensões SSE3, o programa de mínimos quadrados obteve um speedup
2,6x em relação a versão serial. Entretanto nem todos os casos foi possível detectar
uma melhoria no desempenho através de vetorização automática ou intrísica, o que
pode significar uma necessidade de melhorias em outras partes do código serial ou
carência de outras formas de otimização do GCC. Por isso este estudo sugere que em
trabalhos futuros seja feita uma análise para averiguar outras opções de compilação
do GCC e a repetição desse experimento usando outros compiladores, a exemplo do
ICC da INTEL, com o objetivo de analisar se os resultados poder-se-iam repetir em
outras arquiteturas e compiladores.
Referências
[1] Maleki, Seeed, Gao, Yaoqing, Garzaran, M. J., Wong, T., Padua, D. A.. (2011) “An
Evaluation of Vectorizing Compilers”. International Conference on Parallel
Architectures and Compilation Techniques (PACT 2011).
[2] INTEL.. (2012) “A Guide to Vectorization with Intel C++ Compilers”.
[3] Wikipedia..(2012) ”Método dos Mínimos Quadrados”.
http://pt.wikipedia.org/wiki/M%C3%A9todo_dos_m%C3%ADnimos_quadrados,
acessado em 23 de outubro de 2012.