uma análise de vetorização automática do compilador gcc

8
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 [email protected] 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

Upload: jaguaraci-silva

Post on 04-Aug-2015

147 views

Category:

Documents


41 download

DESCRIPTION

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.

TRANSCRIPT

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

[email protected]

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.