Quadratura Adaptativa - DI PUC-Rio

Propaganda
Quadratura Adaptativa
José Eduardo Talavera Herrera
[email protected]
1. Introdução
Este trabalho apresenta algumas variantes do algoritmo da quadratura adaptativa [1], utilizando
pthreads e OpenMP. Não seção 2. É apresentado o conceito da quadratura adaptativa. O
problema que se pretende resolver neste trabalho é descrito na seção 3. Uma solução sequencial
para a quadratura adaptativa é definida na seção 4. A seção 5 e 6 são usadas para desenvolver a
quadratura adaptativa usando pThread e OpenMP. Por último os resultados são apresentados
não seção 7.
2. Quadratura Adaptativa
Considere uma função f(x) continua e não negativa, e também dois valores l e r, onde l < r. Ver
Figura 1 . Uma maneira de aproximar o valor da área embaixo da curva é dividir o intervalo
[l,r] em uma serie de subintervalos, e usar trapézios para aproximar a área de cada subintervalo.
Por exemplo, para aproximar da área de uma função f no intervalo [a,b], primeiro obter o valor
da base do trapézio b-a , depois obter o tamanho dos lados f(a) e f(b) e usar a formula clássica:
Area = (f(a) + f(b)) * (b-a) *0.5
O problema da quadratura pode ser resolvido estaticamente o dinamicamente. A abordagem
estática usa um número fixo de intervalos de igual tamanho, computa a área do trapézio para
cada intervalo e suma os resultados. O processo é repetido para cada intervalo, normalmente
cada um é dividido em dois novos subintervalos, acabando quando as aproximações das áreas
são próximas a um valor aceitável. A abordagem dinâmica começa com um intervalo [l,r] e é
computado o ponto médio entre eles m. Depois ela computa a área de três trapézios: A1) a
primeira limitada por l, r, f(l), f(r) que é a área de maior tamanho embaixo a função; A2) a
segunda limitada por l, m, f(l), f(m); e A3) m, r, f(m), f(r). Ver Figura 1. Continuando com o
processo, a abordagem dinâmica compara a área de maior tamanho com a suma das áreas
menores. Se estas áreas são muito próximas, a abordagem considera a suma de estas últimas
áreas como uma aproximação aceitável da área embaixo de f . Em caso contrario, o problema é
repetido para resolver os dois subproblemas gerados, ou seja, computar a área dos intervalos
[l,m] e [m,r]. Este processo é repetido recursivamente até a solução de cada problema ser
aceitável. No final do processo, a abordagem suma as respostas dos subprocessos para obter a
resposta final.
A abordagem dinâmica é chamada de Quadratura Adaptativa, porque a abordagem adapta por se
mesma a forma da curva. Em particular, em lugares onde a curva é plana uma área ampla de
trapézio pode se aproximar com a área da função. Em lugares onde a curva muda de forma,
especificamente onde a tangente de f(x) é quase vertical, pequenos subproblemas serão gerados,
conforme seja necessário.
Figura 1 Quadratura Adaptativa
3. Problema
Neste trabalho deve-se implementar algumas variantes do algoritmo da quadratura adaptativa
[1], utilizando pthreads e OpenMP.
O programa deve utilizar X threads para computar a aproximação, pelo método de trapézios, da
área abaixo da curva formada por uma função f.. O número de trapézios a ser calculado para
realizar a aproximação depende de uma tolerância. A tolerância na diferença entre um trapézio
e os dois subtrapézios “seguintes” deve ser fixada inicialmente em 10^-20, mas deve ser um
valor facilmente alterado no programa.
1. Na primeira variante, cada thread calcula um subintervalo pelo qual será responsável e
calcula o resultado para esse subintervalo inteiro. Quando todas as threads terminarem,
a thread principal deve mostrar o resultado final.
2. Na segunda variante, a thread principal inicialmente cria uma lista de tarefas, contendo
os extremos dos intervalos, com NUMINICIAL tarefas. Cada thread executa uma
tarefa, e se ela gerar subtarefas, coloca uma delas na fila global e processa a outra, até
que não encontre mais tarefas na fila (Escreva operaçoes InsereTarefa e RetiraTarefa
para manipular essa fila.). A thread principal espera as demais terminarem e mostra o
resultado final.
O programa deve aceitar facilmente a mudança para outras funções e intervalos, parametrizando
o cálculo da integral pelos extremos e por um ponteiro de função.
Execute cada variante para diferentes números de threads (2, 4 e 8), com algumas medidas
preliminares dos tempos obtidos para cada combinação. Deve-se experimentar com diferentes
tolerâncias e com diferentes funções. (Nas próximas aulas iremos discutir questões relacionadas
a medidas de desempenho.) Procure funções (pode inventar loops e muitas operações com
doubles) que criem uma carga de processamento relevante. Elabore um pequeno relatório com
seus resultados, explicitando também a arquitetura em que executou os testes. Discuta também o
que considerou vantagens ou desvantagens de cada um dos ambientes.
Fazer o programa em C. Nas variantes em pthreads, não misture semáforos e monitores.
4. Solução sequencial da Quadratura Adaptativa
A seguir se apresenta a solução da quadratura adaptativa de forma sequencial. Esta solução
presenta uma recursividade que ajuda a identificar as possíveis seções paralelizáveis, e outras
que podem virar seções criticas, como por exemplo, a suma das áreas, no caso seja uma
variável compartilhada.
1. void integral(double l, double r, double area)
2.{
3. double m = (r+l)/2.0;
4. double larea = trape_area(l,m);
5. double rarea = trape_area(m,r);
6. double sumOfAreas = larea + rarea;
7. double relError = fabs(area - sumOfAreas);
8. if(relError <= EPSILON)
9. { return sumTotal = sumTotal + sumOfAreas; }
10. else
11. { integral(l,m, larea) ; integral(m,r, rarea); }
12.}
13.
14.double trape_area(double a, double b)
15.{
16. double f_a = function(a);
17. double f_b = function(b);
18. double c = fabs(b-a);
19. return 0.5 * (f_a + f_b) * c;
20.}
Algoritmo. 1 Quadratura Adaptativa Sequencial
No Algoritmo 1 os subproblemas gerados devem manter salvo o resultado de área do intervalo
que processam e devolver a chamado ao subproblema principal (ver linha 9). A variável
sumTotal mantém o resultado computado por cada subprocesso, no caso sequencial não terá
concorrência de acessos, no entanto em um ambiente paralelo seria um problema que precisa-se
tratar cuidadosamente.
A segunda variante do problema apresentado anteriormente, as seções paralelizáveis, podem ser
facilmente detectadas na linha 11 do algoritmo. Uma delas pode ser resolvida por uma thread e
outra agendada em uma fila global compartilhada pelas threads, assim alguma delas a resolverá
em algum momentos. A primeira variante do problema depende muito da quantidade de tarefas
definidas inicialmente, pois cada subintervalo gerado é atribuído a uma thread e resolvida
usando uma recursividade como a definida no Algoritmo 1.
5. Solução da Quadratura Adaptativa usando pThread
5.1 Variante 1
No algoritmo 2 apresentamos a variante 1 resolvida com pthread para o problema definido na
seção 3. Nas linhas 1-5 é definida uma estrutura de dados que representa um intervalo e na linha
7 um semáforo, ele ajuda a proteger a seção critica identificada na seção 4. Nas linhas 17-25
são criada uma quantidade de trabalhos segundo o número de thread definido previamente. Para
cada thread criado é atribuída uma função Worker definida na linha 26, cada uma de estas
funções resolve um intervalo atribuído na linha 19 do Algoritmo 2.
Cada Worker faz uma chamada à função integral da mesma que o algoritmo sequencial da seção
4, com a diferença que na linha 41-43 protegem a computação da área total.
1. typedef struct{
2. double a;
3. double b;
4. double area;
5. }Interval;
6. .....
7. sem_t mutex;
8. .....
9. int main ()
10. {
11. pthread_t work[NUMBER_THREAD];
12. pthread_attr_t attr;
13. pthread_attr_init(&attr);
14. pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
15. sem_init (&mutex, SHARED, 1);
16. . double width = fabs(b-a)/NUMBER_THREAD;
17. for(int i= 0; i < NUMBER_THREAD; i++)
18. {
19.
Interval *inteval = create(a+ (width*i), a + (width*(i+1)));
20.
pthread_create (&work[i], &attr, Worker, (void *) point);
21. }
22. for(int j= 0; j < NUMBER_THREAD; j++)
23. {
pthread_join(work[j], NULL); }
24. printf("Total = %f\n", sumTotal);
25. }
26. void *Worker(void *arg) {
27. Interval *interval = (Interval*) arg;
28. integral(interval->a,interval->b, interval->area);
29. }
30.
31. void integral(double r, double l, double area )
32. {
33. double m,larea,rarea,sumOfAreas,relError;
34. m = (l+r)*0.5;
35. larea = trape_area(l,m);
36. rarea = trape_area(m,r);
37. sumOfAreas = larea + rarea;
38. relError = fabs(area - sumOfAreas);
39. if(relError <= EPSILON)
40. {
41.
sem_wait(&mutex);
42.
sumTotal = sumTotal + sumOfAreas;
43.
sem_post(&mutex);
44. }
45. else
46. { integral(l,m,larea) ; integral(m,r,rarea); }
47.}
Algoritmo. 2 Quadratura Adaptativa Variante 1 com pThread
5.1 Variante 2
No algoritmo 3 apresentamos a variante 2 resolvida com pThread para o problema definido na
seção 3. Para resolver esta variante é necessário uma estrutura de dados de tipo queue para
armazenar as tarefas geradas pelas diferentes thread ( ver linhas 13-18 ). Nas linhas 1-7 se
define uma estrutura de dados para representar as tarefas a processar (taskInterval). Três
semáforos são necessários nesta solução, um para manter a integridade da suma das áreas (linha
22), outro para sincronizar o armazenamento de novas tarefas (linha 23) e mais um para
sincronizar o final do processamento (linha 24).
Na linha 38 uma chamada a função createInitialTask ajuda a criar um número de tarefas
iniciais. Nas linhas 96-103 é defina a função createInitialTask, ali novos sub-intervalos são
gerados e empilhados na queue. Na linhas 39-41 são criados os diferentes thread que chamamos
de consumer, que no final também fazem o role de producer, pois eles podem criar novas subtarefas. Para cada consumer[i] é atribuída a funão Consumer definida na linha 46, nesta função
são aplicadas as diferentes regras de sincronização para resolver este problema, a seguir se
definem estas regrras:
1) Manter a suma das tarefas, as linhas 84-86 ajudam a sincronizar a suma total da área gerada
pelas diferentes sub-tarefas.
2) Armazenar novas tarefas, nas linhas 54-57 controlam o acesso coordenado da queue para
extrair uma tarefa e processá-la, na linha 90-92 (dentro da chamada da função integral) é gerada
uma nova subtarefa e empilhada na queue.
3) Sincronizar o final do processamento, as linhas 50-52 e 70-72 são necessárias para saber
quantos thread estão processando alguma tarefa. As linhas 64-69 avaliam se o número total de
thread é igual ao número total definido previamente, e também se na queue não tem nenhuma
tarefa para processar. Depois disto último o processo acaba.
Na linha 60 a chamada para a função integral serve para processar as tarefas atribuídas em cada
consumer[i]. Na linha 93 assegura que o mesmo cosumer[i] processe parte do intervalo inicial,
e na linha 91 o cosumer[i] empilha uma sub-tarefa na queue global do processamento.
1. typedef struct taskInterval
2. {
3. double r;
4. double l;
5. double area;
6. struct taskInterval *next;
7. } taskInterval;
....
13. typedef struct queue
14. {
15. int contains; // no. of elements currently in the queue
16. struct taskInterval *front; // the front ptr
17. struct taskInterval *rear; // the rear ptr
18. } queue;
19.
20. queue *q;
21.
22. sem_t mutexsum;
23. sem_t mutexenqueue;
24. sem_t mutexidle;
25.
26. int main()
27. {
28. pthread_t consumer[NUMBER_THREAD];
29. pthread_attr_t attr;
30. pthread_attr_init(&attr);
31. pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
32. sem_init (&mutexsum, SHARED, 1);
34. sem_init (&mutexenqueue, SHARED, 1);
35. sem_init (&mutexidle, SHARED, 1);
36. queue_init ();
37. activeconsumer= 0;
38. createInitialTask(r, l);
39. for(int i= 0; i < NUMBER_THREAD; i++)
40. { pthread_create (&consumer[i], &attr, Consumer, (void *) &i);
41.
42. for(int j= 0; j < NUMBER_THREAD; j++)
43. { pthread_join(consumer[j], NULL); }
44. printf("Total = %f\n", sumTotal);
45. }
46. void *Consumer(void *arg)
47. {
48. while(1)
49. {
50. sem_wait(&mutexidle);
51. idle++;
52. sem_post(&mutexidle);
53. struct taskInterval *task=NULL;
54. sem_wait(&mutexenqueue);
55. if(q->contains > 0)
56. { task = dequeue (); }
57. sem_post(&mutexenqueue);
58. if(task!=NULL)
59. {
60.
integral(task->l, task->r, task->area);
61.
free(task);
62.
task =NULL;
63. }
64. sem_wait(&mutexidle);
65. if(idle == NUMBER_THREAD && q->contains==0)
66. { sem_post(&mutexidle);
67.
break;
68. }
69. sem_post(&mutexidle);
70. sem_wait(&mutexidle);
71. idle--;
72. sem_post(&mutexidle);
}
73. }
74.}
75.void integral(double l, double r, double area)
76.{
77. double m,larea,rarea,sumOfAreas,relError;
78. m = (r+ l)*0.5;
79. larea = trape_area(l,m);
80. rarea = trape_area(m,r);
81. sumOfAreas = larea + rarea;
82. relError = fabs(area - sumOfAreas);
83. if(relError <= EPSILON)
83. {
84.
sem_wait(&mutexsum);
85.
sumTotal = sumTotal + sumOfAreas;
86.
sem_post(&mutexsum);
87. }
88. else
89. {
90. sem_wait(&mutexenqueue);
91. enqueue(l,m,larea);
92. sem_post(&mutexenqueue);
93. integral(m,r,rarea);
94. }
95.}
96.void createInitialTask(double l, double r)
97.{
98. double width = fabs(r-l)/(double)NUMBER_TASK;
99. for(int i=0; i < NUMBER_TASK; i++)
100. {
101.
enqueue (l+ (width*i), l + (width*(i+1)), -1.0);
102. }
103.}
Algoritmo. 3 Quadratura Adaptativa Variante 2 com pThread
6. Solução da Quadratura Adaptativa usando openMP
6.1 Variante 1
No Algoritmo 4 resolve a variante 1 usando openMP. Na linha 4 do Algoritmo 4 se define o
número de threads usado no processamento, na linha 5 é computado o tamanho da altura dos
trapézios, que no eixo x pode ser considerado como a largura de cada área atribuída para cada
thread. Na linha 8 os intervalos são atribuídos para cada thread criado implicitamente por
openMP.
Antes de seguir explicando é melhor definir cada #pragma usada por openMP para paralelizar o
código nesta variante apresentada. Utilizando as diretivas #pragma disponíveis na linguagem, o
programador explicita os locais do programa que serão paralelizados. A etapa de précompilação do programa converte as diretivas em chamadas para threads comuns, como
pThreads por exemplo. O programador não precisa usar as APIs de threads diretamente, isto é
feito pelo OpenMP. Por exemplo, o pragma #pragma omp parallel for usado no algoritmo 4
na linha 6 atribui cada intervalo a uma thread gerado implicitamente por openOMP. O pragma
#pragma omp critical usado na linha 33 serve para proteger a variável compartilhada entre eles
e que mantém a suma total da área. Na linha 32 se apresenta a recursividade necessária para
computar as possíveis subtarefas que cada thread gera.
1. int main ()
2. {
3. omp_set_dynamic(0);
4. omp_set_num_threads(NUMBER_THREAD);
5. double width = fabs(r-l)/NUMBER_THREAD;
6. #pragma omp parallel for
7. for (int i=0; i < NUMBER_THREAD; i++)
8. { paralelIntegral(l+ (width*i), l + (width*(i+1))); }
9. printf("Total = %f\n", sumTotal);
10. return 0;
11. }
12. void paralelIntegral(double l , double r)
13. {
14. double area = trape_area(l,r);
15.
integral(l,r,area);
16. }
17. void integral(double l, double r, double area )
18. {
19. double m,larea,rarea,sumOfAreas,relError;
20. m = (l+r)*0.5;
21. larea = trape_area(l,m);
22. rarea = trape_area(m,r);
23. sumOfAreas = larea + rarea;
24. relError = fabs(area - sumOfAreas);
25. if(relError <= EPSILON)
26. {
27.
#pragma omp critical
28.
sumTotal = sumTotal+ sumOfAreas;
29. }
30. else
31. {
32.
integral(a,m,larea) ; integral(m,b,rarea);
35. }
36. }
Algoritmo. 4 Quadratura Adaptativa Variante 1 com openOMP
6.1 Variante 2
A variante 2 resolvida com openMP é apresentada no algoritmo 5. Neste algoritmo o openOMP
faz a gestão de produtor consumidor implicitamente. Nas linhas 6-10 o pragma #pragma
omp parallel for ajuda a criar um número de threads definido, um intervalo é atribuído a cada
thread para ela resolver.
Na linha 33 o pragma #pragma omp task untied é usado para criar novas tarefas para alguma
outra thread resolver. Neste pragma é usado a tag untied, que significa qualquer outra thread
pode executar o porção de código ligada a ela. Por tanto, nas linhas 33-34 uma nova tarefa é
criada e armazenada em um pool de tarefas. Assim, se alguma thread está disponível atendera
está nova tarefa. Por último, a linha 28 protege a variável que mantém consistente a suma das
áreas.
1. int main()
2. {
3.
4. omp_set_num_threads(NUMBER_THREAD);
5. double width = fabs(r-l)/NUMBER_THREAD;
6. #pragma omp parallel for
7. for (int i=0; i < NUMBER_THREAD; i++)
8. { parallelIntegral(l+ (width*i), l + (width*(i+1))); }
9. printf("Total = %f\n", sumTotal); return 0;
10. }
11. void parallelIntegral(double l , double r)
12. {
13. double area = trape_area(l,r);
14. integral(l,r,area);
15. }
17. void integral(double l, double r, double area )
18. {
19. double m,larea,rarea,sumOfAreas,relError;
20. m = (l+r)*0.5;
21. larea = trape_area(l,m);
22. rarea = trape_area(m,r);
23. sumOfAreas = larea + rarea;
24. relError = fabs(area - sumOfAreas);
26. if(relError <= EPSILON)
27. {
28.
#pragma omp critical
29.
sum = sum + sumOfAreas;
30. }
31. else
32. {
33. #pragma omp task untied
34. integral(l,m,larea) ;
35. integral(m,r,rarea);
36. }
37. }
Algoritmo. 5 Quadratura Adaptativa Variante 1 com openOMP
7. Comparação das variantes em openMP e pThread.
Nesta seção apresenta os resultados dos diferentes testes usando OpenMP e pThread, diferentes
opções são usadas para rodar as variantes da quadratura adaptativa. A figura 2 representa os
diferentes testes rodados para encontrar a área da função e^(x), no intervalo [0,15], a seguir a se
define a integral desta função:
Figura 1 Integral da função e^x
Na figura 2 são apresentados os comportamentos de cada teste. Nestes gráficos cada linha
representa uma diferente configuração, por exemplo, “C2-T2” significa que foi rodado dois
thread em dois cores. Assim, em total foi rodado para cada variante 9 diferentes testes. O eixo
“X” da imagem é define o EPSILON permitido na computação das diferentes áreas . O eixo “Y”
representa o tempo em segundos que gasta cada teste.
(a) Variante 1
(b) Variante 2
Figura 2. Apresenta uma comparativa entre número de cores (Cx) e o número de threads por core
(Tx) usadas em pThread, isto para a variante 1 (a) e para variante 2 (b).
Pode se observar que as linhas nas Figura 2 cresce seguindo a forma da função original, a forma
da função e^x faz que o processamento se incremente rapidamente. Quanto a função cresça
novas subtarefas são geradas e o tempo de processamento aumenta.
Da Figura 2, pode-se ressaltar que o teste C8-T2, para ambas variantes, se mantem estável
durante o processamento. Um mesmo comportamento segue o teste C8-T4 que parece ter uma
relação entre a quantidade usada para executar alguma tarefa e os possíveis recursos necessários
para mente a execução ativa.
Os testes para openMP seguem o mesmo padrão definido anteriormente. Da mesma forma, as
linhas da figura 3 também seguem o comportamento da função e^x. A principal observação,
comparando as duas variantes 1 (ver figura 2 e 3), em OpenMP a solução para a primeira
variante gasta mais tempo que a variante 2. Na variante 2 openMP faz a gestão implícita do
produtor/consumidor e em pThread um abstração é definida para compartilhar a bolsa de
tarefas.
(b) Variante 1
(a) Variante 2
Figura 3. Apresenta uma comparativa entre número de cores (Cx) e o número de threads por core
(Tx) usadas em openOMP, isto para a variante 1 (a) e para variante 2 (b).
As figuras4,5 apresentam uma comparativa entre as duas bibliotecas na computação da integral
da função e^x no intervalo [0,15]. Para o desenho de esta figura foram considerados os testes
que gastaram maior tempo de processamento.
Figura 4 Comparativa entre PThread e openOMP para a Variante 1.
Figura 5 Comparativa entre PThread e openOMP para a Variante 2.
Estas imagens reflexam que OpemMP uma vantagen sobre pThread nos diferentes testes
desenvolvidos neste trabalho.
8. Vantagens ou Desvantagens
Sobre OpenMP, ela resulta fácil de usar e não precisa de conceitos avançados para definir
um código paralelizável. Por outro lado, em pThread é necessário definir com atenção os
componentes que serão coordenados no processamento, um ponto positivo de pThread é
que o programados tem conhecimento de como o processamento está sendo feito.
9. Referências
[1]. G. Andrews. Paradigms for process interaction in distributed programs. ACM
Computing Surveys, 23(1), Mar. 1991, pp. 49–90.
Download