Computação de alto desempenho

Aqui um dos principais temas é paralelização.

OpenMP

OpenMP é uma biblioteca para programação multi-processo, que permite tornar um código paralelo sem grandes alterações

Parallel

A diretiva parallel indica uma região que será executada por múltiplas threads.

Essa diretiva suporta duas diretivas: for e sections

Parâmetros que podem ser utilizados com essa diretiva:

Exemplo executando uma seção em paralelo:

#pragma omp parallel num_threads(4)
{
	printf("Hello world (%d)\n", omp_get_thread_num());
}
Saída possível:
Hello world (0)
Hello world (2)
Hello world (1)
Hello world (3)

Single

A diretiva single indica uma região que será executada por apenas uma thread. Ao final do bloco, existe uma barreira implícita a menos que nowait seja especificado.

Parâmetros que podem ser utilizados com essa diretiva:

Exemplo com uma seção que executa apenas em uma thread:

#pragma omp parallel num_threads(4)
{
	#pragma omp single
	printf("Single (%d)\n", omp_get_thread_num());
	printf("Hello world (%d)\n", omp_get_thread_num());
}
Saída possível: (note o código é executado sequencialmente dentro da thread, então Single, sempre estará antes do Hello world de sua thread)
Single (2)
Hello world (3)
Hello world (0)
Hello world (1)
Hello world (2)

Master

A diretiva master indica uma região que será executada pela thread principal. Não existe nenhuma barreira implícita.

Essa diretiva não suporta nenhum parâmetro

Exemplo com uma seção que executa apenas em uma thread:

#pragma omp parallel num_threads(4)
{
	#pragma omp master
	printf("Master (%d)\n", omp_get_thread_num());
	printf("Hello world (%d)\n", omp_get_thread_num());
}
Saída possível: (note que, como não há barreira, as linhas posteriores podem ser executadas em outras threads antes do master)
Hello world (2)
Master (0)
Hello world (0)
Hello world (1)
Hello world (3)

Reduction

Exemplo usando reduction

Produto escalar de dois vetores com double
double dot(const double *a, const double *b, size_t len) {
	double res = 0.;
	#pragma omp parallel for reduction(+ : res) default(none) shared(len, a, b) num_threads(8) schedule(static)
	for (size_t i = 0; i < len; ++i)
		res += a[i] * b[i];
	return res;
}

Essa operação pode ser paralelizada facilmente. A diretiva #pragma omp parallel cria uma zona "multi-thread", nessa zona, a diretiva for faz com que o loop a seguir seja executado de forma paralela, com cada thread executando uma parte do intervalo. E a diretiva reduction(+:res) realiza uma operação "reduction" somando a variável local res de todas as threads, e atribuindo à variável global res.

Operação de reduction no loop.

Os vetores de entrada "a" e "b" são percorridos pelo for, que multiplica os valores a[i] e b[i] e adiciona ao acumulador.

Porem, devido ao parallel for, o loop é executado em paralelo, e cada thread tem acesso a apenas uma parte do intervalo do loop. Não podemos apenas compartilhar a variável res entre as threads, pois causaria uma condição de corrida e provavelmente geraria um valor errôneo na saida. Isso poderia ser evitado com atomic, porem teria um custo significativo no desempenho.

Devido a diretiva reduction(+:res), ao invés de res ser compartilhado entre as threads, um valor local para cada thread é utilizado. Esse valor é inicializado conforme o operador utilizado (veja a tabela 2.11), e os valores produzidos por cada thread são unidos no valor final. A especificação diz que a ordem, e o local onde tal operação ocorre é "não especificado".

Programa completodot_product.c
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>

double *new_vector(size_t size) {
	double *vec = malloc(sizeof(double) * size);
	for (size_t i = 0; i < size; ++i)
		vec[i] = 1.0f;
	return vec;
}

double dot(const double *a, const double *b, size_t len) {
	double res = 0.;
#pragma omp parallel for reduction(+ : res) default(none) shared(len, a, b) num_threads(8) schedule(static)
	for (size_t i = 0; i < len; ++i)
		res += a[i] * b[i];
	return res;
}

double dot_seq(const double *a, const double *b, size_t len) {
	double res = 0.;
	for (size_t i = 0; i < len; ++i)
		res += a[i] * b[i];
	return res;
}

int main(int argc, char **argv) {
	if (argc != 2) {
		fprintf(stderr, "Usage: %s LEN\n", argv[0]);
	}
	size_t len = strtoull(argv[1], NULL, 10);
	double *a = new_vector(len), *b = new_vector(len);

	double start = omp_get_wtime();
	double result = dot(a, b, len);
	double end = omp_get_wtime();
	double time = end - start;
	printf("Parallel dot product of %zu numbers: %.2f. Took %lf seconds\n", len, result, time);
	double start_seq = omp_get_wtime();
	double result_seq = dot_seq(a, b, len);
	double end_seq = omp_get_wtime();
	double time_seq = end_seq - start_seq;
	printf("Sequencial dot product of %zu numbers: %.2f. Took %lf seconds\n", len, result_seq, time_seq);
	printf("Speedup: %.3fx\n", time_seq / time);

	free(a);
	free(b);
	return 0;
}
Saída possível:
> gcc dot_product.c -fopenmp
> ./a.out 100000000
Parallel dot product of 100000000 numbers: 100000000.00. Took 0.064056 seconds
Sequencial dot product of 100000000 numbers: 100000000.00. Took 0.251648 seconds
Speedup: 3.929x

Clauses

Private

Especifica que cada thread deve ter a sua própria variável.

private(var[, var])

Firstprivate

Especifica que cada thread deve ter a sua própria variável, e que essa variável deve ser inicializada para o valor da variável já existente

firstprivate(var[, var])

Shared

Especifica que todas as threads compartilharam a variável

shared(var[, var])

Default

Define o comportamento de variáveis não especificadas

default(shared | none)

shared torna todas as variáveis não especificadas compartilhadas, e none torna as variáveis não especificadas inacessíveis.

Num_Threads

Define o número de threads em um grupo de threads

num_threads(num)

Possui a mesma funcionalidade que a função omp_set_num_threads

Nowait

Remove uma barreira implícita em uma diretiva

nowait

OpenMPI