20
Análise de Desempenho Usando Técnicas de Vetorização, Blocagem e Programação Concorrente 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: A busca pela melhoria de performance pode ser alcançada por diversas técnicas, cuja a técnica de programação concorrente já é bastante consolidada e utilizada no mercado. Entretanto, adicionar concorrência possui um custo adicional de conhecimento e a escolha da tecnologia pode ser benéfica ou um entrave na utilização dessa estratégia. Diversos estudos reveleram que o OpenMp é um dos frameworks mais fáceis de serem utilizados, porém ainda há dúvidas sobre a sua performance em relação aos outros. Este trabalho apresenta (1) os resultados através da mudança do paradigma sequencial para a programação concorrente, considerando o aumento do número de threads e diferentes formas de balanceamento de carga entre os processadores. Por fim, para conhecer de fato a eficiência do OpenMP, este estudo realizou uma (2) implementação híbrida utlizando três estratégias de melhoria de perfomance: (i) programação concorrente, (ii) vetorização intrísica usando extensões SSE3 do processador Intel e (iii) técnicas de blocagem para medir o speedup e eficiência em comparação aos frameworks Pthreads e Java Threads. Palavras-chaves: Performance, Programação Concorrente, OpenMP, Pthreads, Java. 1 - Introdução A dificuldade de se escrever programas representa uma grande barreira para a exploração de diversas arquiteturas. A programação é difícil especialmente por não se conseguir contornar os problemas de acesso à memória (e.g. localidade), uma vez que as linguagens de programação atuais, ferramentas e arquiteturas estão evoluindo em direções menos adequadas para esses códigos, por explorar, principalmente, as barreiras da portabilidade (e.g. Java). Por isso, é importante, quando se pensa em alta performance dos sistemas computacionais, saber escolher um paradigma de programação consonante as arquiteturas atuais de multiprocessadores com memória compartilhada ou multicores [1]. O paralelismo, atualmente, está presente em todos os níveis da computação indo desde o paralelismo de instruções em um pipeline, passando pelos processadores multicore, colocados como solução para o problema de limitação no crescimento do clock, até sistemas distribuídos de larga escala como as grades computacionais. Nos últimos 20 anos, os fabricantes de microprocessadores exploraram altos graus de paralelismo no nível de instrução ou Instruction Level Parallelism (ILP), onde muitas

Análise de Desempenho Usando Técnicas de Vetorização Blocagem e Concorrência

Embed Size (px)

DESCRIPTION

A busca pela melhoria de performance pode ser alcançada por diversas técnicas, cuja a técnica de programação concorrente já é bastante consolidada e utilizada no mercado. Entretanto, adicionar concorrência possui um custo adicional de conhecimento e a escolha da tecnologia pode ser benéfica ou um entrave na utilização dessa estratégia. Diversos estudos reveleram que o OpenMp é um dos frameworks mais fáceis de serem utilizados, porém ainda há dúvidas sobre a sua performance em relação aos outros. Este trabalho apresenta (1) os resultados através da mudança do paradigma sequencial para a programação concorrente, considerando o aumento do número de threads e diferentes formas de balanceamento de carga entre os processadores. Por fim, para conhecer de fato a eficiência do OpenMP, este estudo realizou uma (2) implementação híbrida utlizando três estratégias de melhoria de perfomance: (i) programação concorrente, (ii) vetorização intrísica usando extensões SSE3 do processador Intel e (iii) técnicas de blocagem para medir o speedup e eficiência em comparação aos frameworks Pthreads e Java Threads.

Citation preview

Page 1: Análise de Desempenho Usando Técnicas de Vetorização Blocagem e Concorrência

Análise de Desempenho Usando Técnicas de Vetorização, Blocagem e

Programação Concorrente

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: A busca pela melhoria de performance pode ser alcançada por diversas

técnicas, cuja a técnica de programação concorrente já é bastante consolidada e

utilizada no mercado. Entretanto, adicionar concorrência possui um custo adicional de

conhecimento e a escolha da tecnologia pode ser benéfica ou um entrave na utilização

dessa estratégia. Diversos estudos reveleram que o OpenMp é um dos frameworks

mais fáceis de serem utilizados, porém ainda há dúvidas sobre a sua performance em

relação aos outros. Este trabalho apresenta (1) os resultados através da mudança do

paradigma sequencial para a programação concorrente, considerando o aumento do

número de threads e diferentes formas de balanceamento de carga entre os

processadores. Por fim, para conhecer de fato a eficiência do OpenMP, este estudo

realizou uma (2) implementação híbrida utlizando três estratégias de melhoria de

perfomance: (i) programação concorrente, (ii) vetorização intrísica usando extensões

SSE3 do processador Intel e (iii) técnicas de blocagem para medir o speedup e

eficiência em comparação aos frameworks Pthreads e Java Threads.

Palavras-chaves: Performance, Programação Concorrente, OpenMP, Pthreads, Java.

1 - Introdução

A dificuldade de se escrever programas representa uma grande barreira para a exploração de diversas arquiteturas. A programação é difícil especialmente por não se conseguir contornar os problemas de acesso à memória (e.g. localidade), uma vez que as linguagens de programação atuais, ferramentas e arquiteturas estão evoluindo em direções menos adequadas para esses códigos, por explorar, principalmente, as barreiras da portabilidade (e.g. Java). Por isso, é importante, quando se pensa em alta performance dos sistemas computacionais, saber escolher um paradigma de programação consonante as arquiteturas atuais de multiprocessadores com memória compartilhada ou multicores [1]. O paralelismo, atualmente, está presente em todos os níveis da computação indo desde o paralelismo de instruções em um pipeline, passando pelos processadores multicore, colocados como solução para o problema de limitação no crescimento do clock, até sistemas distribuídos de larga escala como as grades computacionais. Nos últimos 20 anos, os fabricantes de microprocessadores exploraram altos graus de paralelismo no nível de instrução ou Instruction Level Parallelism (ILP), onde muitas

Page 2: Análise de Desempenho Usando Técnicas de Vetorização Blocagem e Concorrência

gerações de processadores foram construídas aumentando-se cada vez mais a frequência do clock e com pipelines cada vez mais profundos. Como benefício, as aplicações atingiram um maior desempenho por delegar aos compiladores a tarefa de explorar eficientemente o ILP. Porém, devido a vários fatores, em especial às limitações físicas, tornou-se necessário alterar o foco do paralelismo do ILP para o TLP (Thread Level Parallelism). Nesse, o ganho de desempenho é alcançado através da replicação das unidades de execução (cores) ao passo que se mantêm os clocks em uma faixa na qual o consumo de energia e dissipação de calor são menores e, portanto, menos problemáticos. Ao longo dos anos, é possível encontrar diversas tentativas direcionadas aos compiladores [3], entretanto, esses se demonstraram úteis apenas em algumas classes restritas de problemas. Assim, no contexto multicore, não é possível fundamentar-se apenas nos compiladores para obter ganhos de desempenho e é extremamente necessário reescrever as aplicações de modo a tirar proveito do paralelismo de granularidade fina, onde a granularidade fina dos cores

promovem uma melhoria no acesso as suas memórias locais (cache L1), o que resulta na reorganização das tarefas para que estas operem sob pequenas porções de dados para reduzir o tráfego no barramento e aumentar a localidade de dados, evitando entre outros problemas o cache miss [4]. O objetivo deste trabalho é comparar os resultados através da mudança do paradigma sequencial para a programação concorrente, considerando o aumento do número de threads e diferentes formas de balanceamento de carga entre os processadores. Com esse fim, o estudo selecionou dois algoritmos: um de ordenação e um cálculo simples de verificação de número primos para mostrar se é possível obter desempenho em funções auxiliares. Na segunda parte do estudo foi feita uma análise para conhecer qual a melhor implementação híbrida entre as estratégias de memória compartilhada em conjunto com as técnicas de vetorização [3] e blocagem [4] aferindo medições de speedup e eficiência quando da adoção do OpenMP, Pthreads e Java Threads em uma aplicação que realiza o cálculo de produtos de matrizes.

2 – Solução

De um modo geral, os paradigmas paralelos podem ser classificados como: memória compartilhada com tópicos explícitos (Pthreads, Java Threads) ou de memória compartilhada com paralelismo de dados (OpenMP). A abordagem de memória compartilhada usando Pthreads (POSIX threads) é um modelo de programação onde o paralelismo utiliza invocações das funções paralelas. Um corpo de uma função quando paralelizado é executado por muitas threads, que podem acessar dados compartilhados de forma global. Pthreads se consolidou como uma implementação subjacente de paralelismo, enquanto Java Threads, por utilizar uma linguagem de programação de propósito geral, promove o suporte ao paralelismo na forma de tópicos. Os programas paralelos construídos em Java são SMPs (Symmetric

Multiprocessing), e assim, se assemelham aos programas implementados com a abordagem utilizada com Pthreads. Desta forma, Pthreads e Java estão disponíveis apenas em SMPs, enquanto a bliblioteca OpenMP é um modelo de memória compartilhada onde o paralelismo é definido usando directivas paralelas nos loops e funções, cujas directivas especificam quais laços ou iterações devem ser executadas

Page 3: Análise de Desempenho Usando Técnicas de Vetorização Blocagem e Concorrência

em paralelo, bem como as funções. Essas diretrizes adicionais especificam também quais dados devem ser compartilhados ou privados para cada segmento. Os compiladores traduzem os programas OpenMP em um código que se assemelham aos programas construídos usando Pthreads, onde o código dos loops paralelos são executados através de funções paralelas [1]. Para comparar a eficiência das abordagens de memória compartilhada, este estudo foi divido em duas partes: (2.1) medição de performance através da mudança do paradigma sequencial para concorrente e (2.2) implementação híbrida das estratégias de vetorização, blocagem e programação concorrente. 2.1 Medição de Performance Através da Mudança do Paradigma Sequencial para o

Concorrente

O algoritmo utilizado no exprimento realiza uma ordenação de números do tipo float

através de 3 loops. Conforme a listagem 1, é possível perceber que os loops mais

internos realizam uma ordenação separando a tarefa entre números pares e ímpares.

Código Sequencial Código com Diretivas do OpenMp

void ordenaVetor(int n, float *a) {

int i, j, nHalf, lastEven, lastOdd;

nHalf = n/2;

if (n%2 == 0) {

lastEven=nHalf-1; lastOdd=nHalf-2;

}

else {

lastEven=nHalf-1; lastOdd=nHalf-1;

}

for (i=0; i<n-1; i++) {

for (j=0; j<=lastOdd; j++) ce(&a[2*j+1],

&a[2*j+2]); /* odd */

for (j=0; j<=lastEven; j++) ce(&a[2*j],

&a[2*j+1]); /* even */

}}

void ordenaVetorOpenMP(int n, float *a, int

nthreads) {

int i, j, nHalf, lastEven, lastOdd;

nHalf = n/2;

if (n%2 == 0) {

lastEven=nHalf-1; lastOdd=nHalf-2;

}

else {

lastEven=nHalf-1; lastOdd=nHalf-1;

}

#pragma omp parallel private(i)

for (i=0; i<n-1; i++) {

omp_set_num_threads(nthreads);

#pragma omp parallel for

for (j=0; j<=lastOdd; j++) ce(&a[2*j+1],

&a[2*j+2]); /* odd */

omp_set_num_threads(nthreads);

#pragma omp parallel for

for (j=0; j<=lastEven; j++) ce(&a[2*j],

&a[2*j+1]); /* even */}} Listagem1 – Mudança de Paradigma para Melhoria de Performance na Ordenação.

O código utilizado na versão serial do programa (Listagem 1 - lado esquerdo) promoveu uma certa facilidade na identificação dos pontos que deveriam ter as diretivas de paralelização do OpenMP (Listagem 1 – lado direito). A primeira estrutura de repetição foi modificada com a diretiva #pragma omp parallel private(i), pois o

Page 4: Análise de Desempenho Usando Técnicas de Vetorização Blocagem e Concorrência

objetivo neste caso foi garantir que o índice que controla o laço não fosse compartilhado entre as threads. Nos laços internos a granularidade de cada operação permitiu, através da diretiva #pragma omp parallel for, um incremente do número de threads, cujo os gráficos podem ser vistos na seção de resultados para comparação do desempenho entre o tempo da versão serial e a execução do programa paralelo (Figura 1). A inserção das diretivas do OpenMP não prejudica o reuso do código para fins de execução serial, pois não foi necessário qualquer modifcação para alcançar tal benefício. Além disso, as diretivas podem ser ignoradas quando da compilação, para executar o trecho paralelo usando o GCC basta inserir a seguinte linha de compilação: gcc -O2 -fopenmp -I /papi/include primo.c -o primo -L /papi/lib -lpapi –lm, onde a

inclusão da flag –fopenmp, torna-se possível a inserção da diretiva e sem a flag a opção de não transformar o código serial em segmentos paralelizáveis.

O segundo programa (Listagem 2 – Lado Esquerdo) utiliza a geração de números para verificação se cada número gerado é primo, neste caso foram analisadas diferentes formas de balanceamento de cargas entre os processadores. Da mesma forma que na situação anterior, um laço foi paralelizado com a diretiva #pragma omp parallel for, a novidade foi a inclusão das claúsulas de escalonamento [5] quando do incremente do número de threads (Listagem 2 – Lado Direito). As análises de desempenho também podem ser vistas na seção de resultados (Figura 2 e 3).

Código com Diretivas do OpenMP para

Escalonamento

Execução do Programa Usando

Cláusula de Escalonamento

A = alocaMemoria(n);

s = PAPI_get_real_usec();

#pragma omp parallel for

for (i=0; i<n; i++) {

numero = rand_r(&semente)%1000000000 +1;

A[i] = ehPrimo(numero,n);

}

e = PAPI_get_real_usec();

setPapiFinal(e, retval, EventSet, s);

A = liberaMemoria(n, A);

int ehPrimo(int i, int n){

long int max;

int ret = 1;

max = (long int) sqrt(n);

if(n%2==0) ret = 0;

for(i=3; i<=max && ret==1; i+=2) {

if(n%i==0) ret = 0; }

return ret;

}

export OMP_SCHEDULE="static,10"

./primo 1 > static10/primo_1.csv

./primo 2 > static10/primo_2.csv

./primo 4 > static10/primo_4.csv

./primo 6 > static10/primo_6.csv

./primo 8 > static10/primo_8.csv

export OMP_SCHEDULE="dynamic,3"

./primo 1 > dynamic3/primo_1.csv

./primo 2 > dynamic3/primo_2.csv

./primo 4 > dynamic3/primo_4.csv

./primo 6 > dynamic3/primo_6.csv

./primo 8 > dynamic3/primo_8.csv

export OMP_SCHEDULE="guided,3"

./primo 1 > guided3/primo_1.csv

./primo 2 > guided3/primo_2.csv

./primo 4 > guided3/primo_4.csv

./primo 6 > guided3/primo_6.csv

./primo 8 > guided3/primo_8.csv

Listagem 2 – Implementação com OpenMP Utilizando Cláusulas de Escalonamento.

As claúsulas de esclalonamento descrevem como as iterações do laço são divididas

entre as threads e existem três tipos no OpenMP: estática, dinâmica e guiada. Na

estática as iterações do laço são divididas em pedaços e depois atribuídas

Page 5: Análise de Desempenho Usando Técnicas de Vetorização Blocagem e Concorrência

estaticamente as threads. Se um tamanho não for especificado, as iterações são

executadas uniformemente, se possível, dividindo-as entre as threads de forma

contígua. Na dinâmica as iterações são divididas em pedaços a serem distribuídos

sistematicamente entre as threads, quando uma thread termina um pedaço é

atribuído um outro. O tamanho do bloco padrão no escalonamento dinâmico é 1 e

máximo a ser atribuído depende da quantidade de linhas que podem ser endereçadas

na memória, já que o chunk size está ligado diretamente ao tamanho da memória

cache. Já na cláusula guiada as iterações são atribuídas dinamicamente para os

segmentos em forma de blocos, até que sejam solicitados, os blocos continuam a ser

atribuídos para cada thread. O escalonamento guiado é semelhante ao dinâmico,

exceto pelo fato que o tamanho do bloco diminui cada vez que uma pedaço do

trabalho é atribuído a uma thread.

2.2 Implementação Híbrida das Técnicas de Vetorização, Blocagem e Programação

Concorrente

A multiplicação de cada elemento de uma matriz de resultados é gerada por um produto escalar, onde uma sequência de números de ponto flutuante são multiplicados e acumulados, exigindo assim duas operações de ponto flutuante para cada par de números com operando de produto escalar. Uma técnica de multiplicação padrão para máquinas com hierarquia de memória comum (e.g. Registradores, Cache L1, Cache L2 e Memória Principal) é a blocagem, onde o objetivo fundamental é quebrar uma multiplicação de matrizes grandes em uma série de sub-matrizes menores, cujo os dados necessários cabem inteiramente na cache (Figura 1).

Figura 1 – Multiplicação de Matrizes usando a Técnica de Blocagem [8].

Primeiro são definidas duas matrizes A e B com dimensões M x K e K x N, linhas e colunas de cada matriz, respectivamente, então C = A x B, onde M x N será o resultado. Se todos os elementos de A, B e C não couberem inteiramente na cache L1 simultaneamente, cada vez que for necessário obter esses elementos estes serão retirados seja da cache L2 ou pior, da memória principal. A estratégia de blocagem visa melhorar o desempenho para que as obtenções dos dados permaneçam no mais baixo nível hierárquico possível, sendo A M/m x B N /n, ou seja pequenas matrizes, c = a x b

Page 6: Análise de Desempenho Usando Técnicas de Vetorização Blocagem e Concorrência

com uma escala a~( m x k), b~(k x n) e c~(m x n). Os blocos m, n e k de a, b e c são pequenos e por isso cabem na cache, assim todos os dados necessários para a multiplicação corrente estarão no mesmo nível da cache e no mais baixo nível a ser acessado. Para realizar a multiplicação, por exemplo, o bloco c2,2 é gerado a partir da operação c2,2 = a,2,1 x b1,2 + a2,2 x b2,2 + a2,3 x b2,3 e assim sucessivamente [8].

Com objetivo de melhorar a performance da multiplicação de matrizes ainda é possível

realizar a multiplicação dos blocos simultaneamente utilizando unidades de

vetorização dos registradores, ou seja, o nível mais baixo da hierarquia de memória.

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/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 através de

compiladores tais como GCC e ICC da Intel (ICC) que podem gerar automaticamente o

código vetorizado.

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 registradores 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, a vetorização é uma das transformações do compilador que pode ter

um impacto significativo no desempenho das aplicações [3].

Conforme visto em [3] nem sempre o compilador consegue vetorizar automaticamente

o laço, no estudo de Silva, JB foi possível ver alguns entraves por conta da dependência

de dados nos laços e uma alternativa foi implementar a vetorização íntrisica, que no

estudo de caso em uma aplicação de cálculo de mínimos quadrados realizado no

estudo obteve sppedup 2,7x em relação a versão escalar. Outras dificuldades

encontradas para alcançar a vetorização automática podem se vistas detalhamente em

[9]. Neste experimento o compilador utilizado (GCC) não conseguiu vetorizar

automaticamente os laços da multiplicação, por isso, adiante serão apresentados os

detalhes necessários para que a vetorização pudesse ser realizada nos laços do

algoritmo de blocagem.

Algoritmo de Multiplicação Utilizando

Blocagem

Algoritmo de Multiplicação Utilizando

Blocagem, Vetorização e OpenMP

int ii,jj,kk,i,j,k;

for (ii=0; ii<n; ii+=tamBloco){

__m128 v1,v2,v3,v4,total;

float soma __attribute__((aligned(16)));

int ii,jj,kk,i,j,k;

Page 7: Análise de Desempenho Usando Técnicas de Vetorização Blocagem e Concorrência

for (jj=0; jj<n; jj+=tamBloco){

for (kk=0; kk<n; kk+=tamBloco){

for (j=ii; j<min(ii+tamBloco,n); j++){

for (k=jj; k<min(jj+tamBloco,n); k++){

for (i=kk; i<min(kk+tamBloco,n); i++){

mr[i][j] += a[i][k] * b[k][j];

}

}

}

}

}

}

for (ii=0; ii<n; ii+=tamBloco){ for (jj=0; jj<n; jj+=tamBloco){ for (kk=0; kk<n; kk+=tamBloco){ omp_set_num_threads(nthreads);

#pragma omp parallel for collapse(2)

for (j=ii; j<min(ii+tamBloco,n); j++){ for (k=jj; k<min(jj+tamBloco,n); k++){ for (i=kk; i<min(kk+tamBloco,n); i++){ soma = 0;

v1 = _mm_loadu_ps(&X[i][k]);

v2 = _mm_loadu_ps(&Y[k][j]);

v3 = _mm_mul_ps(v1,v2);

v4 = _mm_loadu_ps(&Z[i][j]);

total =_mm_add_ps(v4,v3);

_mm_store_ps(&soma,total);

Z[i][j] = soma; } } } } } }

Listagem 3 – Multiplicação usando Blocagem e Vetorização com OpenMP.

A Listagem 3 exibe o algoritmo de blocagem na sua forma serial e sua versão com vetorização intrísica utilizado no experimento. Segundo [4], dentre as implementações comuns para obtenção do produto de matrizes, foi a implementação que obteve o menor número de L2 cache misses, ou seja conseguiu o melhor o desempenho em relação as outras versões (e.g. Naive e Strassem) por explorar eficazmente o acesso à memória cache usando a técnica de multiplicação em blocos. Na versão implementada com vetorização intrísica é possível notar também a existência de duas diretivas OpenMP: omp_set_num_threads(nthreads) e #pragma omp parallel for collapse(3) que serão explicadas adiante. A versão implementada com a técnica de vetorização utiliza variáveis que acessam diretamente os registradores e são defindas com o tipo __128, onde a leitura dos dados da cache para obtenção dos dados das matrizes sendo alinhada (_mm_load_ps) ou não (_mm_loadu_ps), não causa prejuízos em relação a performance. O algoritmo realiza a leitura dos blocos e cada operação de multiplicação é realizada no nível dos registradores (mm_mul_ps) e (_mm_store_ps(&soma,total)), por conseguinte, os dados são colocados no endereço de memória da cache para atualizar os valores obtidos do registrador (Z[i][j] = soma). Performando desta maneira a multiplicação em blocos, teoricamente, é esperado uma redução no tempo de processamento do cálculo, uma vez que evita-se obter e colocar os dados em todas as operações do cálculo na memória cache (Seção 2.2). Ainda para melhorar a performance da multiplicação foram utilizadas 3 implementações da técnica de blocagem e vetorização com: (i) OpenMp, (ii) Pthreads e (iii) Java Threads, adicionando a técnica de programação concorrente, conforme as Listagens 3 e 4.

Algoritmo de Multiplicação Utilizando

Blocagem, Vetorização e Pthreads

Algoritmo de Multiplicação Utilizando

Blocagem e Java Threads

void *runner(void *param) {

struct v *data = param;

int i,j,k; __m128 v1,v2,v3,v4,total;

float x_, y_, z_, soma

public class MatrixMultiple implements

Runnable{

public void run() {

Page 8: Análise de Desempenho Usando Técnicas de Vetorização Blocagem e Concorrência

__attribute__((aligned(16)));

for (j=data->ii; j<min(data->ii+data-

>tamBloco,data->n); j++){

for (k=data->jj; k<min(data->jj+data-

>tamBloco,data->n); k++){

for (i=data->kk; i<min(data->kk+data-

>tamBloco,data->n); i++){

soma = 0;

x_ = data->X[i][k];

y_ = data->Y[k][j];

z_ = data->Z[i][j];

v1 = _mm_loadu_ps(&x_);

v2 = _mm_loadu_ps(&y_);

v3 = _mm_mul_ps(v1,v2);

v4 = _mm_loadu_ps(&z_);

total =_mm_add_ps(v4,v3);

_mm_store_ps(&soma,total);

data->Z[i][j] = soma;

} } }

pthread_exit(0);

}

for (ii=0; ii<n; ii+=tamBloco){

for (jj=0; jj<n; jj+=tamBloco){

for (kk=0; kk<n; kk+=tamBloco){

pthread_t thread[nthreads];

struct v *data = (struct v *)

malloc(sizeof(struct v));

data->ii=ii; data->jj=jj; data->kk=kk; data-

>n=n;

data->X=X; data->Y=Y; data->Z=Z; data->n=n;

data->tamBloco=tamBloco;

for(t=0; t<nthreads; t++) {

rc = pthread_create(&thread[t],

NULL,runner, data);

if (rc) {

printf("ERROR:create=%d\n", rc);

exit(-1);

}

}

for(t=0; t<nthreads; t++) {

rc = pthread_join(thread[t], &status);

if (rc) {

printf("ERROR:join code=%d\n", rc);

exit(-1);

for (int j=ii; j<Math.min(ii+tamBloco,n); j++){

for (int k=jj; k<Math.min(jj+tamBloco,n);

k++){

for (int i=kk; i<Math.min(kk+tamBloco,n);

i++){

C[i][j] += A[i][k] * B[k][j];

} } }

}

...

public static void main(String[] args) {

int n = 1000;

int tamBloco = 200;

long MAX_THREADS=8;

for (int ii=0; ii<n; ii+=tamBloco){

for (int jj=0; jj<n; jj+=tamBloco){

for (int kk=0; kk<n; kk+=tamBloco){

for(icount=0; icount<MAX_THREADS;

icount++) {

s = "Thread " + ((char) (65+icount));

new Thread(new

MatrixMultiple(MAX_COUNT), s).start();

}}}}

}

Page 9: Análise de Desempenho Usando Técnicas de Vetorização Blocagem e Concorrência

}

}

}}}

Listagem 4 – Multiplicação usando Blocagem e Vetorização com Pthreads e Java Threads.

O experimento utilizou matrizes de tamanho 100 e 1000 variando o número de

threads (1, 2, 4, 6 e 8) e tamanho de blocos 25 e 200, respectivamente, de acordo com

o tamanho das matrizes. O estudo infelizmente não conseguiu alocar dinamicamente

memória suficiente para tamanhos maiores (e.g. 10^4 e 10^5) utilizando alocação

dinâmica na arquitetura de teste com a linguagem C/C++. Neste caso o estudo poderia

utilizar estratégias avançadas de alocação de memória, porém para não distorcer as

comparações entre as estratégias, o estudo preferiu comparar apenas esse conjunto

de valores. O estudo também preferiu não adicionar as vantagens provenientes da

configuração de escalonamento analisadas na parte 1 com o OpenMP e o algoritmo

implementado usando blocagem e vetorização seguiu a mesma padronização de

código, após analisar a, dentre as possíveis configurações das diretivas do OpenMP, a

que obteve maior speedup na fase de planejamento do experimento.

Dependo das versões do OpenMP, trabalhar com a paralelização de laços internos e

externos pode ser um problema, por isso durante a fase de planejamento do

experimento foram vistas algumas informações importantes, cuja a solução adotada

para habilitar paralelização do algoritmo de blocagem é exibida na Listagem 5.

Código com Erro em OpenMP Código Válido em OpenMP

#pragma omp parallel for

for (j=ii; j<min(ii+tamBloco,n); j++){ #pragma omp parallel for

for (k=jj; k<min(jj+tamBloco,n); k++){ #pragma omp parallel for

for (i=kk; i<min(kk+tamBloco,n); i++){ ... } } }

#pragma omp parallel for collapse(2)

for (j=ii; j<min(ii+tamBloco,n); j++){ for (k=jj; k<min(jj+tamBloco,n); k++){ for (i=kk; i<min(kk+tamBloco,n); i++){ ... } } }

Listagem 5 – Multiplicação usando Blocagem com OpenMP Erro e Acerto.

A multiplicação usando a técnica de blocagem necessita de 3 laços (Listagem 5), nesse

caso as versões anteriores a 3.0 do OpenMP aceitam a inclusão da diretiva #pragma

omp parallel for, no entanto, os laços mais internos serão executados de forma

sequencial, isso porque a biblioteca libgomp do GCC, incluída nas versões inferiores a

4.4, detecta que já existe um grupo de threads sendo executado no laço externo e não

cria novas threads para os laços mais internos. A partir da versão 3.0 do OpenMP é

possível contornar esse problema com a diretiva collapse (Listagem 5). Além dessa,

podem ser encontradas diversas práticas de performance para utilização do OpenMP

na literatura [10][11].

A partir da primeira implementação com OpenMP, as outras estratégias assemelham-

se por executar de forma paralela os 3 laços mais internos do algoritmo de blocagem

Page 10: Análise de Desempenho Usando Técnicas de Vetorização Blocagem e Concorrência

com vetorização (Listagem 3 - Esquerda), apenas se diferenciam na forma particular de

implementação das suas próprias tecnologias. O OpenMP foi executado com: gcc -O2 -

msse3 -ftree-vectorize -ftree-vectorizer-verbose=2 -funsafe-math-optimizations -

std=c99 -fopenmp -I /papi/include multiplicacao_omp.c -o multiplicacao_omp -L

/papi/lib –lpapi, apenas a inclusão das diretivas e o flag de compilação tornaram a

estratégia a mais fácil de ser implementada dentre as analisadas. A utilização da

biblioteca Pthreads requereu criar adicionalmente uma estrutura com os dados a

serem passados por referência para as threads (struct v *data), invocar a criação e

execução das threads usando o modelo fork-join (pthread_create e pthread_join) com:

gcc -O2 -msse3 -ftree-vectorize -ftree-vectorizer-verbose=2 -funsafe-math-

optimizations -I /papi/include multiplicacao_pthreads.c -o multiplicacao_pthreads -L

/papi/lib -lpapi –pthread, por isso foi uma das mais trabalhosas. A programação de

threads na linguagem Java é mais simples, apenas a criação de uma classe que

implementa a interface Runnable e o método run() com bloco de código a ser

executado pelas threads. De forma que a programação concorrente na linguagem Java

também utiliza o modelo fork-join, também foi necessário invocar a criação e

executação das threads, porém a união após o processamento (join) é realizado

automaticamente, por isso não é preciso invocar as threads para este fim, como é feito

na implementação com Pthreads.

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 das linguagens de programação C e Java 7, com o GCC versão 4.6.2 e

OpenMP 3.1 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 para os testes com OpenMP e Pthreads (Seção de solução).

A coleta das métricas: tempo de execução, L2 Cache Miss, MFLOPS e CPI foram

realizadas através das classes instrumentadas com a biblioteca PAPI [6] para a

linguagem C/C++ e no caso do Java, apenas foi possível obter o tempo de execução

com instrumentos de medição da própria linguagem.

Page 11: Análise de Desempenho Usando Técnicas de Vetorização Blocagem e Concorrência

Figura 2 – Análise de Desempenho do Algoritmo de Ordenação.

A Figura 2 exibe as métricas coletas nas classes que foram instrumentas com o PAPI

[6]. Foram analisados: o tempo de execução do programa com o incremento do

número de threads (1, 2, 4, 6 e 8), a quantidade de MFLOPS (milhões de instruções de

ponto flutuante por segundo), a quantidade de cache misses [4], o CPI, o speedup [7] e

a eficiência [7]. O menor tempo de execução do programa foi obtido quando foram

utilizadas apenas 2 threads, isso se justifica pelo ao fato da arquitetura utilizada no

experimento possuir apenas 2 cores. Como consequência, o speedup alcançado

também foi superior em 2,4x a versão serial e a quantidade de MFLOPS foi a maior

entre os testes realizados. Entretanto, não foi possível reduzir a quantidade de cache

misses com apenas 2 threads em relação ao número de threads 1, 6 e 8 e também o

número de ciclos por instrução (CPI), porém em todos os casos em houve uma

melhoria em todas as métricas em relação a versão serial do programa (s).

Page 12: Análise de Desempenho Usando Técnicas de Vetorização Blocagem e Concorrência

Figura 3 – Análise de Desempenho do Algoritmo de Números Primos.

Após a avaliação do experimento com o programa de ordenação ser satisfatório em

relação a versão serial de um programa, além de incrementar o número de threads

(Figura 3) o experimento também analisou o desempenho com e sem o uso das 3

cláusulas de escalonamento e diferentes tamanhos de blocos. Foi implementado um

programa simples para verificação de números primos, onde o tamanho do laço foi

definido por cada elemento do conjunto N={10�, 10�, 10�, 10�}, perfazendo assim 4

execuções para cada número de thread. A primeira comparação (Figura 3) refere-se ao

incremento dos valores de N e análise das métricas de tempo de execução, speedup e

eficiência sem o uso de claúsulas de escalonamento. É possível notar, que variando-se

o tamanho do problema e o número de threads, o speedup não é linear, exceto no

primeiro gráfico (Figura 3 – Esquerda superior) e em todos os gráficos a melhor

eficiência foi obtida com 2 threads .

Page 13: Análise de Desempenho Usando Técnicas de Vetorização Blocagem e Concorrência

Figura 4 – Análise de Desempenho das Claúsulas de Escalonamento.

Ainda analisando o experimento com números primos (Figura 4) é possível visualizar

que o maior speedup foi alcançado, na maior parte das vezes, pela figura em verde

que representa o escalonamento estático. O motivo da melhora na performance se

deve ao uso da cláusula de escalonamento estática com chunk ou bloco de tamanho 5.

É posível notar que o escalonamento estático também se destaca pelo maior speedup

em todos os momentos do experimento para um número de threads igual a 4 que é o

dobro da quantidade de cores disponíveis na arquitetura, com exceção ao primeiro e

penúltimo momento do estudo (Figura 4 - Esquerda). O maior speedup entre as

execuções para tamanhos muitos grandes (Figura 4 – Direita Inferior) foi obtido com a

claúsula estática com chunk de tamanhos 10 e 5, porém a eficiência foi piorando com o

aumento do número de threads (Figura 5).

Page 14: Análise de Desempenho Usando Técnicas de Vetorização Blocagem e Concorrência

Figura 5 – Análise de Eficiência das Claúsulas de Escalonamento.

As Figuras 6, 7 e 8 exibem os resultados da multiplicação utilizando a estratégia híbrida

de blocagem, vetorização e programação concorrente usando OpenMP, Pthreads e

Java Threads (Seção 2.2). Todos os gráficos realizam uma comparação entre a versão

serial do programa, que utiliza apenas técnica de blocagem, em relação as outras que

utilizam blocagem, vetorização e programação concorrente. Para que o experimento

pudesse ser comparado de forma justa em relação a plataforma forma, o estudo

procurou interferir o mínimo possível na convenção de código e realizou comparações

entre as versões seriais dos programas em suas respectivas plataformas, C/C++ com

Linux e Java 7 com Windows.

Page 15: Análise de Desempenho Usando Técnicas de Vetorização Blocagem e Concorrência

Figura 6 – Multiplicação de Matrizes usando Vetorização, Blocagem e Threads – N=100.

A Figura 6 exibe os gráficos com os resultados coletados para a multiplicação de

matrizes 100 x 100 de acordo com o aumento do número de threads para OpenMP,

Pthreads e Java Threads. O primeiro gráfico (Figura 6 – Esquerda Superior) demonstra

que não houve uma redução no tempo de execução do programa em relação a versão

serial, apenas que a implementação com OpenMP foi mais rápida que Pthreads a partir

da inclusão de 4 ou mais threads. No segundo gráfico (Figura 6 – Esquerda Superior) é

possível conhecer o motivo do alto desempenho da versão com OpenMP em relação a

versão Pthreads, pois o algoritmo com OpenMP obteve uma quantidade menor de

MFLOPS que a implementação com OpenMP e também de cache misses L2, inclusive

em relação a versão serial do programa (Figura 6 – Direita Superior). No entanto, o

speedup em comparação as versões seriais dos programas e as implementações

específicas de cada plataforma, a versão com Java obteve o maior speedup em relação

ao aumento do número de threads até igualar-se com as outras soluções com o

número máximo de 8 threads, o que pode signifcar que a plataforma forma Java não é

escalável quando existe uma quantidade muito elevada de threads acima do número

de processadores (Figura 6 - Direita Inferior).

Page 16: Análise de Desempenho Usando Técnicas de Vetorização Blocagem e Concorrência

Figura 7 – Multiplicação de Matrizes usando Vetorização, Blocagem e Threads – N=1000.

A Figura 7 exibe os resultados da mesma forma que a Figura 6, com a diferença do

tamanho das matrizes serem 1000 x 1000. Da mesma forma que a análise anterior, a

versão serial executou a multiplicação em tempo inferior as estratégias concorrentes

(Figura 7 – Esqueda Superior) e a implementação com Pthreads se mostrou superior a

OpenMP quando o número de threads é igual ao número de cores. Porém a

implementação com Pthreads foi piorando com o aumento do número de threads.

Esse fato pode ser explicado por conta do OpenMP traballhar com o modelo de

memória compartilhada com dados ao invés de tópicos explícitos (Seção 2.2), assim

mantém o desempenho mesmo quando da inclusão de mais threads que número de

cores. A versão serial também obteve maior número de MFLOPS em relação as

estratégias concorrentes (Figura 7 – Esquerda Inferior) quando o número de threads é

igual ao número de cores e mais uma vez a estratégia OpenMP se manteve constante

com o aumento do número de threads o que não aconteceu com Pthreads, entretanto

em relação ao número de L2 cache misses a melhor estratégia foi a implementação

híbrida com Pthreads (Figura 7 – Direita Superior). Por fim, o speedup obtivo entre as

versões hibridas implementadas foi igual a análise anterior, sendo que a plataforma

Java conseguiu ser até 1.190x mais rápida com a execução de uma thread e o

desempenho foi caindo, mostrando mais uma vez que a estratégia pode não ser

escalável quando do aumento do número de threads (Figura 7 - Direita Inferior).

Page 17: Análise de Desempenho Usando Técnicas de Vetorização Blocagem e Concorrência

Figura 8 – Multiplicação de Matrizes usando Vetorização, Blocagem e Threads - Eficiência.

A Figura 8 mostra a quantidade de cache misses L2 das estratégias concorrentes e a

versão serial do programa de multiplicação de matrizes 1000 x 1000 com o uso da

linguagem C/C++ no gráfico superior, o que certifica que a versão utilizando blocagem,

vetorização e Pthreads é a melhor estratégia para reduzir o número de cache miss L2

entre as versões analisadas. Em relação a eficiência das estratégias (Figura 8 - Inferior)

a linguagem Java obteve o melhor desempenho sem dúvida quando da execução de

uma única thread e se manteve superior as demais estratégias, porém nota-se que o

incremento do número de threads piora a sua eficiência 3x em relação ao número de

threads anterior.

4 - Conclusão

Este trabalho apresentou uma análise dos resultados obtidos com a mudança do

paradigma sequencial para a programação concorrente usando o OpenMP,

considerando o aumento do número de threads (1-8) e diferentes formas de

balanceamento de carga entre os processadores (e.g. estática, dinâmica e guiada).

Também o estudo realizou uma implementação do jogo da vida proposto por Martin

Gardner com objetivo de analisar as duas versões do programa: serial e concorrente

para saber sobre a eficiência da programação concorrente em conjunta com outras

técnicas (e.g. vetorização intrísica e técnicas de blocagem) através da análise das

Page 18: Análise de Desempenho Usando Técnicas de Vetorização Blocagem e Concorrência

métricas de speedup e eficiência coletas quando do uso dos frameworks analisados no

estudo: OpenMp, Pthreads e Java Threads.

Na primeira parte do trabalho foram analisados os laços paralelizados nos programas

de ordenação e números primos com o OpenMP e tiveram um CPI próximo de 1, o que

significa que a computação de números floats foi mais intensificada, melhorando a

performance da aplicações. A aplicação de ordenação teve um speedup superior a 2x

em relação a versão serial reduzindo drasticamente o tempo de execução do programa

e em todos os gráficos a melhor eficiência foi obtida com 2 threads, que é o número

físico de cores da arquitetura. Já o programa de número de primos quando variou-se o

tamanho do problema e o número de threads, o desempenho não foi linear. O maior

speedup entre as execuções para tamanhos muitos grandes (100.000.000) foi obtido

com a claúsula estática com chunk de tamanhos 10 e 5, porém a eficiência foi piorando

com o aumento do número de threads, o que suscita afirmar que a melhor estratégia

de escalonamento foi deixar que o sistema operacional a gerencie.

A segunda parte do estudo analisou-se a multiplicação de matrizes utilizando uma

estratégia híbrida com técnicas de blocagem, vetorização e programação concorrente

usando OpenMP, Pthreads e Java Threads. O estudo procurou interferir o mínimo

possível na convenção de código e realizou comparações entre as versões seriais dos

programas em suas respectivas plataformas, C/C++ com Linux e Java 7 com Windows,

sendo que a implementação com Java threads não foi possível implementar

vetorização intrísica com SSE3. As versões implementadas com OpenMP e Pthreads

não conseguiram reduzir o tempo de execução em relação a versão serial, já a

implementação com Java Threads conseguiu ser até 1.190x mais rápida com a

execução de uma thread, porém o seu desempenho foi decaindo. Foi constatado no

experimento que o incremento do número de threads piora a sua eficiência 3x em

relação ao número de threads anterior. O estudo ainda conclui que a melhor estratégia

para reduzir o número de cache misses L2 entre as versões analisadas na linguagem

C/C++ foi a implementação híbrida envolvendo as técnias de blocagem, vetorização e

Pthreads, já na plataforma Java não foi possível coletar essa métrica.

Este estudo sugere como trabalhos futuros analisar porque a máquina virtual Java não

é escalável quando do aumento do números de threads e explorar também um

catálogo de melhores práticas de codificação com OpenMP e Pthreads com o objetivo

de paralelização de laços aninhados. Por conta do estudo realizar comparações entre

as plataformas Java e C/C++ não foi possível variar os algoritmos, pois a plataforma

Java não suporta vetorização intrísica e o estudo não aprofundou a comparação entre

OpenMP e Pthreads para melhorar o speedup alterando a convenção de código para

não distorcer ainda mais as variáveis de controle do experimento. Também outra

sugestão de trabalho futuro é investigar formas de coletar as métricas de MFLOPS, CPI

e L2 cache miss na plataforma Java, porque a bilbioteca PAPI, utilizada no

Page 19: Análise de Desempenho Usando Técnicas de Vetorização Blocagem e Concorrência

experimento, permite apenas a instrumentação de programas em C/C++ para coletar

essas métricas.

Referências

[1] Berlin, K., Huan, J., Jacob, M., Kochhar, G., Prins, J., Pugh, B., Sadayappan, P. , Spacco, J., Tseng, C.. (2012) “Evaluating the Impact of Programming Language Features on the Performance of Parallel Applications on Cluster Architectures”. Acesso em 07 de novembro de 2012, http://www.cs.umd.edu/Honors/reports/lcpc03.pdf. [2] Milane, R. C.. (2010) “Computação Verificada Aplicada à Resolução de Ssistemas

Lineares Intervalares Densos em Arquiteturas Multicore”. Dissertação de Mestrado

PUC-RS. [3] Silva, J. B.. (2012) “Uma Análise de Vetorização Automática do Compilador GCC”. Relatório de Pesquisa, Instituto de Ciência e Tecnologia, Universidade Federal de São Paulo, Campus São José dos Campos, São Paulo-SP. Acesso em 07 de novembro de 2012,http://www.jaguaracisilva.blogspot.com.br/2012/10/uma-analise-de-vetorizacao-automatica.html. [4] Silva, J. B.. (2012) “Análise de Performance na Obtenção de Produtos de Matrizes”. Relatório de Pesquisa, Instituto de Ciência e Tecnologia, Universidade Federal de São Paulo, Campus São José dos Campos, São Paulo-SP. Acesso em 07 de novembro de 2012,http://www.jaguaracisilva.blogspot.com.br/2012/10/analise-de-performance-na-obtencao-de.html. [5] OpenMp..(2012) “OpenMP Tutorial”. Acesso em 07 de novembro de 2012, https://computing.llnl.gov/tutorials/openMP/. [6] PAPI...(2012) “The Performance API”. Acesso em 07 de novembro de 2012, http://icl.cs.utk.edu/papi/overview/index.html. [7] Oliveira, O..(2012) “Computação Paralela - Medidas de Performance”. Acesso em 07 de novembro de 2012, http://nautilus.fis.uc.pt/personal/orlando/aulas/CParalela2005/Performance.pdf. [8] D., Aberdeen, J., Jonathan..(2000) “Emmerald: A Fast Matrix-Matrix Multiply Using Intel’s SSE Instructions”. Acessado em 10 de novembro de 2012, http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.23.6754&rep=rep1&type=pdf. [9] 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).

Page 20: Análise de Desempenho Usando Técnicas de Vetorização Blocagem e Concorrência

[10] Helsgaun, K.. (2012) “How to get Good Performance by Using OpenMP”. Acesso em 10 de novembro de 2012, http://www.akira.ruc.dk/~keld/teaching/IPDC_f10/Slides/pdf4x/4_Performance.4x.pdf [11] Chandra, R., Dagum, L., kohr, D., Maydan, D., McDonald, J., Menon, R..(2001) “Parallel Programming in OpenMP”. Capítulos 3 e 4. Elsevier, San Diego.