OpenMP - OpenMP

OpenMP
Logotipo da OpenMP
Autor (es) original (is) OpenMP Architecture Review Board
Desenvolvedor (s) OpenMP Architecture Review Board
Versão estável
5.1 / 13 de novembro de 2020 ; 9 meses atrás ( 2020-11-13 )
Sistema operacional Plataforma cruzada
Plataforma Plataforma cruzada
Modelo Extensão para C , C ++ e Fortran ; API
Licença Vários
Local na rede Internet openmp .org

OpenMP ( Open Multi-Processing ) é uma interface de programação de aplicativo (API) que oferece suporte à programação de multiprocessamento de memória compartilhada em várias plataformas C , C ++ e Fortran , em muitas plataformas, arquiteturas de conjunto de instruções e sistemas operacionais , incluindo Solaris , AIX , HP-UX , Linux , macOS e Windows . Ele consiste em um conjunto de diretivas de compilador , rotinas de biblioteca e variáveis ​​de ambiente que influenciam o comportamento em tempo de execução.

O OpenMP é gerenciado pelo consórcio de tecnologia sem fins lucrativos OpenMP Architecture Review Board (ou OpenMP ARB ), definido em conjunto por uma ampla faixa de fornecedores líderes de hardware e software, incluindo Arm , AMD , IBM , Intel , Cray , HP , Fujitsu , Nvidia , NEC , Red Hat , Texas Instruments e Oracle Corporation .

O OpenMP usa um modelo portátil e escalonável que oferece aos programadores uma interface simples e flexível para desenvolver aplicativos paralelos para plataformas que vão desde o computador desktop padrão até o supercomputador .

Um aplicativo construído com o modelo híbrido de programação paralela pode ser executado em um cluster de computador usando OpenMP e Message Passing Interface (MPI), de modo que OpenMP seja usado para paralelismo dentro de um nó (multi-core) enquanto MPI é usado para paralelismo entre nós . Também houve esforços para executar o OpenMP em sistemas de memória compartilhada distribuída por software , para traduzir o OpenMP em MPI e estender o OpenMP para sistemas de memória não compartilhada.

Projeto

Uma ilustração de multithreading, em que a thread principal bifurca uma série de threads que executam blocos de código em paralelo.

OpenMP é uma implementação de multithreading , um método de paralelização por meio do qual um thread principal (uma série de instruções executadas consecutivamente) bifurca um número especificado de sub- threads e o sistema divide uma tarefa entre eles. Os encadeamentos são executados simultaneamente , com o ambiente de tempo de execução alocando encadeamentos para diferentes processadores.

A seção do código que deve ser executada em paralelo é marcada de acordo, com uma diretiva do compilador que fará com que os threads se formem antes que a seção seja executada. Cada thread tem um id anexado a ele, que pode ser obtido usando uma função (chamada omp_get_thread_num()). O id do thread é um número inteiro e o thread principal tem um id 0 . Após a execução do código paralelizado, os threads se juntam de volta ao thread principal, que continua até o final do programa.

Por padrão, cada thread executa a seção paralelizada do código de forma independente. Construções de compartilhamento de trabalho podem ser usadas para dividir uma tarefa entre os threads de modo que cada thread execute sua parte alocada do código. Ambos paralelismo de tarefas e paralelismo de dados pode ser conseguido usando OpenMP desta forma.

O ambiente de tempo de execução aloca threads para processadores dependendo do uso, carga da máquina e outros fatores. O ambiente de tempo de execução pode atribuir o número de threads com base nas variáveis ​​de ambiente ou o código pode fazer isso usando funções. As funções do OpenMP estão incluídas em um arquivo de cabeçalho denominado omp.h em C / C ++ .

História

O OpenMP Architecture Review Board (ARB) publicou suas primeiras especificações de API, OpenMP para Fortran 1.0, em outubro de 1997. Em outubro do ano seguinte, eles lançaram o padrão C / C ++. 2000 viu a versão 2.0 das especificações Fortran com a versão 2.0 das especificações C / C ++ sendo lançada em 2002. A versão 2.5 é uma especificação C / C ++ / Fortran combinada que foi lançada em 2005.

Até a versão 2.0, o OpenMP especificava principalmente maneiras de paralelizar loops altamente regulares, conforme ocorrem na programação numérica orientada por matriz , onde o número de iterações do loop é conhecido no momento da entrada. Isso foi reconhecido como uma limitação e várias extensões paralelas de tarefas foram adicionadas às implementações. Em 2005, um esforço para padronizar o paralelismo de tarefas foi formado, que publicou uma proposta em 2007, inspirando-se nos recursos de paralelismo de tarefas em Cilk , X10 e Chapel .

A versão 3.0 foi lançada em maio de 2008. Incluído nos novos recursos do 3.0 está o conceito de tarefas e a construção de tarefas , ampliando significativamente o escopo do OpenMP além das construções de loop paralelo que compunham a maior parte do OpenMP 2.0.

A versão 4.0 da especificação foi lançada em julho de 2013. Adiciona ou melhora os seguintes recursos: suporte para aceleradores ; atômica ; Manipulação de erros; afinidade de segmento ; extensões de tarefas; redução definida pelo usuário ; Suporte SIMD ; Suporte para Fortran 2003 .

A versão atual é 5.1, lançada em novembro de 2020.

Observe que nem todos os compiladores (e sistemas operacionais) oferecem suporte ao conjunto completo de recursos para as versões mais recentes.

Elementos centrais

Gráfico de construções OpenMP

Os principais elementos do OpenMP são as construções para criação de encadeamento, distribuição de carga de trabalho (compartilhamento de trabalho), gerenciamento de ambiente de dados, sincronização de encadeamento, rotinas de tempo de execução em nível de usuário e variáveis ​​de ambiente.

Em C / C ++, OpenMP usa #pragmas . Os pragmas específicos do OpenMP estão listados abaixo.

Criação de linha

O pragma omp parallel é usado para bifurcar threads adicionais para realizar o trabalho encerrado na construção em paralelo. O encadeamento original será denotado como encadeamento mestre com ID de encadeamento 0.

Exemplo (programa C): Exibir "Olá, mundo." usando vários tópicos.

#include <stdio.h>
#include <omp.h>

int main(void)
{
    #pragma omp parallel
    printf("Hello, world.\n");
    return 0;
}

Use flag -fopenmp para compilar usando GCC:

$ gcc -fopenmp hello.c -o hello

Saída em um computador com dois núcleos e, portanto, dois threads:

Hello, world.
Hello, world.

No entanto, a saída também pode ser distorcida por causa da condição de corrida causada pelos dois threads que compartilham a saída padrão .

Hello, wHello, woorld.
rld.

(Se printfé thread-safe depende da implementação. C ++ std::cout, por outro lado, é sempre thread-safe.)

Construções de compartilhamento de trabalho

Usado para especificar como atribuir trabalho independente a um ou todos os threads.

  • omp for ou omp do : usado para dividir iterações de loop entre as threads, também chamadas de construções de loop.
  • seções : atribuição de blocos de código consecutivos, mas independentes a diferentes threads
  • único : especificando um bloco de código que é executado por apenas um thread, uma barreira está implícita no final
  • master : semelhante ao single, mas o bloco de código será executado apenas pelo thread mestre e sem barreira implícita no final.

Exemplo: inicializar o valor de uma grande matriz em paralelo, usando cada thread para fazer parte do trabalho

int main(int argc, char **argv)
{
    int a[100000];

    #pragma omp parallel for
    for (int i = 0; i < 100000; i++) {
        a[i] = 2 * i;
    }

    return 0;
}

Este exemplo é embaraçosamente paralelo e depende apenas do valor de i . O sinalizador paralelo OpenMP para instrui o sistema OpenMP a dividir essa tarefa entre seus threads de trabalho. Cada um dos threads receberá uma versão exclusiva e privada da variável. Por exemplo, com dois threads de trabalho, um thread pode receber uma versão de i que vai de 0 a 49999, enquanto o segundo obtém uma versão que vai de 50000 a 99999.

Diretivas variantes

As diretivas variantes são um dos principais recursos introduzidos na especificação OpenMP 5.0 para facilitar que os programadores melhorem a portabilidade de desempenho. Eles permitem a adaptação de pragmas OpenMP e código do usuário em tempo de compilação. A especificação define características para descrever construções OpenMP ativas, dispositivos de execução e funcionalidade fornecida por uma implementação, seletores de contexto com base nas características e condições definidas pelo usuário e metadiretiva e declarar diretivas diretivas para os usuários programarem a mesma região de código com diretivas variantes.

  • A metadiretiva é uma diretiva executável que condicionalmente resolve para outra diretiva em tempo de compilação, selecionando várias variantes de diretiva com base em características que definem uma condição ou contexto OpenMP.
  • A diretiva declarar variante tem funcionalidade semelhante à metadiretiva, mas seleciona uma variante de função no site de chamada com base no contexto ou nas condições definidas pelo usuário.

O mecanismo fornecido pelas duas diretivas variantes para selecionar variantes é mais conveniente de usar do que o pré-processamento C / C ++, pois oferece suporte direto à seleção de variantes no OpenMP e permite que um compilador OpenMP analise e determine a diretiva final das variantes e do contexto.

// code adaptation using preprocessing directives

int v1[N], v2[N], v3[N];
#if defined(nvptx)     
 #pragma omp target teams distribute parallel loop map(to:v1,v2) map(from:v3)
  for (int i= 0; i< N; i++) 
     v3[i] = v1[i] * v2[i];  
#else 
 #pragma omp target parallel loop map(to:v1,v2) map(from:v3)
  for (int i= 0; i< N; i++) 
     v3[i] = v1[i] * v2[i];  
#endif


// code adaptation using metadirective in OpenMP 5.0

int v1[N], v2[N], v3[N];
#pragma omp target map(to:v1,v2) map(from:v3)
  #pragma omp metadirective \
     when(device={arch(nvptx)}: target teams distribute parallel loop)\
     default(target parallel loop)
  for (int i= 0; i< N; i++) 
     v3[i] = v1[i] * v2[i];

Cláusulas

Como o OpenMP é um modelo de programação de memória compartilhada, a maioria das variáveis ​​no código OpenMP são visíveis a todos os threads por padrão. Mas às vezes as variáveis ​​privadas são necessárias para evitar condições de corrida e há uma necessidade de passar valores entre a parte sequencial e a região paralela (o bloco de código executado em paralelo), então o gerenciamento do ambiente de dados é introduzido como cláusulas de atributo de compartilhamento de dados , anexando-os a a diretiva OpenMP. Os diferentes tipos de cláusulas são:

Cláusulas de atributo de compartilhamento de dados
  • compartilhado : os dados declarados fora de uma região paralela são compartilhados, o que significa visível e acessível por todos os threads simultaneamente. Por padrão, todas as variáveis ​​na região de compartilhamento de trabalho são compartilhadas, exceto o contador de iteração do loop.
  • privado : os dados declarados dentro de uma região paralela são privados para cada thread, o que significa que cada thread terá uma cópia local e a usará como uma variável temporária. Uma variável privada não é inicializada e o valor não é mantido para uso fora da região paralela. Por padrão, os contadores de iteração de loop nas construções de loop OpenMP são privados.
  • default : permite que o programador declare que o escopo de dados padrão dentro de uma região paralela será compartilhado , ou nenhum para C / C ++, ou compartilhado , firstprivate , privado ou nenhum para Fortran. A opção none força o programador a declarar cada variável na região paralela usando as cláusulas de atributo de compartilhamento de dados.
  • firstprivate : como privado, exceto inicializado com o valor original.
  • lastprivate : como privado, exceto o valor original é atualizado após a construção.
  • redução : uma maneira segura de unir o trabalho de todos os fios após a construção.
Cláusulas de sincronização
  • crítico : o bloco de código incluído será executado por apenas um segmento de cada vez, e não simultaneamente executado por vários segmentos. Geralmente é usado para proteger dados compartilhados de condições de corrida .
  • atômica : a atualização da memória (escrever ou ler-modificar-escrever) na próxima instrução será executada atomicamente. Isso não torna toda a declaração atômica; apenas a atualização da memória é atômica. Um compilador pode usar instruções especiais de hardware para melhor desempenho do que ao usar o crítico .
  • ordenado : o bloco estruturado é executado na ordem em que as iterações seriam executadas em um loop sequencial
  • barreira : cada thread espera até que todos os outros threads de uma equipe tenham alcançado este ponto. Uma construção de compartilhamento de trabalho tem uma sincronização de barreira implícita no final.
  • nowait : especifica que os threads que concluem o trabalho atribuído podem prosseguir sem esperar que todos os threads da equipe sejam concluídos. Na ausência desta cláusula, os threads encontram uma sincronização de barreira no final da construção de compartilhamento de trabalho.
Cláusulas de agendamento
  • cronograma (tipo, pedaço) : Isso é útil se a construção de compartilhamento de trabalho for um do-loop ou for-loop. As iterações na construção de compartilhamento de trabalho são atribuídas a threads de acordo com o método de agendamento definido por esta cláusula. Os três tipos de programação são:
  1. estático : aqui, todos os threads são iterações alocadas antes de executarem as iterações do loop. As iterações são divididas entre os threads igualmente por padrão. No entanto, a especificação de um número inteiro para o fragmento do parâmetro alocará o número do fragmento de iterações contíguas a um segmento específico.
  2. dinâmico : aqui, algumas das iterações são alocadas para um número menor de threads. Depois que um determinado thread termina sua iteração alocada, ele retorna para obter outro a partir das iterações restantes. O parâmetro chunk define o número de iterações contíguas que são alocadas para um encadeamento por vez.
  3. guiado : um grande pedaço de iterações contíguas é alocado para cada thread dinamicamente (como acima). O tamanho do bloco diminui exponencialmente com cada alocação sucessiva para um tamanho mínimo especificado no bloco do parâmetro
Controle IF
  • if : Isso fará com que os threads paralelizem a tarefa apenas se uma condição for atendida. Caso contrário, o bloco de código é executado em série.
Inicialização
  • firstprivate : os dados são privados para cada thread, mas inicializados usando o valor da variável usando o mesmo nome do thread mestre.
  • lastprivate : os dados são privados para cada thread. O valor desses dados privados será copiado para uma variável global usando o mesmo nome fora da região paralela se a iteração atual for a última iteração no loop paralelizado. Uma variável pode ser firstprivate e lastprivate .
  • threadprivate : os dados são globais, mas são privados em cada região paralela durante o tempo de execução. A diferença entre threadprivate e private é o escopo global associado a threadprivate e o valor preservado em regiões paralelas.
Cópia de dados
  • copyin : semelhante ao firstprivate para variáveis privadas , as variáveis threadprivate não são inicializadas, a menos que use copyin para passar o valor das variáveis ​​globais correspondentes. Nenhuma cópia é necessária porque o valor de uma variável threadprivate é mantido durante a execução de todo o programa.
  • copyprivate : usado com single para suportar a cópia de valores de dados de objetos privados em um thread (o thread único ) para os objetos correspondentes em outros threads na equipe.
Redução
  • redução (operador | intrínseco: lista) : a variável tem uma cópia local em cada thread, mas os valores das cópias locais serão resumidos (reduzidos) em uma variável compartilhada global. Isso é muito útil se uma operação particular (especificada no operador para esta cláusula particular) em uma variável for executada iterativamente, de modo que seu valor em uma iteração particular dependa de seu valor em uma iteração anterior. As etapas que levam ao incremento operacional são paralelizadas, mas os threads atualizam a variável global de uma maneira segura para os threads. Isso seria necessário para paralelizar a integração numérica de funções e equações diferenciais , como um exemplo comum.
Outros
  • flush : O valor desta variável é restaurado do registro para a memória para usar este valor fora de uma parte paralela
  • master : Executado apenas pela thread master (a thread que separou todas as outras durante a execução da diretiva OpenMP). Sem barreira implícita; outros membros da equipe (threads) não são obrigados a alcançar.

Rotinas de tempo de execução em nível de usuário

Usado para modificar / verificar o número de threads, detectar se o contexto de execução está em uma região paralela, quantos processadores no sistema atual, definir / remover bloqueios, funções de temporização, etc.

Variáveis ​​ambientais

Um método para alterar os recursos de execução de aplicativos OpenMP. Usado para controlar o agendamento de iterações de loop, número padrão de threads, etc. Por exemplo, OMP_NUM_THREADS é usado para especificar o número de threads para um aplicativo.

Implementações

OpenMP foi implementado em muitos compiladores comerciais. Por exemplo, Visual C ++ 2005, 2008, 2010, 2012 e 2013 suportam (OpenMP 2.0, nas edições Professional, Team System, Premium e Ultimate), bem como Intel Parallel Studio para vários processadores. Os compiladores e ferramentas do Oracle Solaris Studio suportam as especificações OpenMP mais recentes com aprimoramentos de produtividade para Solaris OS (UltraSPARC e x86 / x64) e plataformas Linux. Os compiladores Fortran, C e C ++ do The Portland Group também suportam OpenMP 2.5. O GCC também oferece suporte a OpenMP desde a versão 4.2.

Compiladores com uma implementação de OpenMP 3.0:

  • GCC 4.3.1
  • Compilador Mercurium
  • Compiladores Intel Fortran e C / C ++ versões 11.0 e 11.1, Intel C / C ++ e Fortran Composer XE 2011 e Intel Parallel Studio.
  • Compilador IBM XL
  • Sun Studio 12 update 1 tem uma implementação completa do OpenMP 3.0
  • Computação com vários processadores ( "MPC" .)

Vários compiladores são compatíveis com OpenMP 3.1:

  • GCC 4.7
  • Compiladores Intel Fortran e C / C ++ 12.1
  • Compiladores IBM XL C / C ++ para AIX e Linux, V13.1 e compiladores IBM XL Fortran para AIX e Linux, V14.1
  • LLVM / Clang 3.7
  • Absoft Fortran Compilers v. 19 para Windows, Mac OS X e Linux

Compiladores que suportam OpenMP 4.0:

  • GCC 4.9.0 para C / C ++, GCC 4.9.1 para Fortran
  • Compiladores Intel Fortran e C / C ++ 15.0
  • IBM XL C / C ++ para Linux, V13.1 (parcial) e XL Fortran para Linux, V15.1 (parcial)
  • LLVM / Clang 3.7 (parcial)

Vários compiladores que suportam OpenMP 4.5:

  • GCC 6 para C / C ++
  • Compiladores Intel Fortran e C / C ++ 17.0, 18.0, 19.0
  • LLVM / Clang 12

Suporte parcial para OpenMP 5.0:

  • GCC 9 para C / C ++
  • Compiladores Intel Fortran e C / C ++ 19.1
  • LLVM / Clang 12

Compiladores de paralelização automática que geram código-fonte anotado com diretivas OpenMP:

Vários profilers e depuradores oferecem suporte expresso para OpenMP:

Prós e contras

Prós:

  • Código multithreading portátil (em C / C ++ e outras linguagens, normalmente é necessário chamar primitivas específicas da plataforma para obter multithreading).
  • Simples: não precisa lidar com a passagem de mensagens como o MPI faz.
  • O layout e a decomposição dos dados são controlados automaticamente por diretivas.
  • Escalabilidade comparável ao MPI em sistemas de memória compartilhada.
  • Paralelismo incremental: pode funcionar em uma parte do programa ao mesmo tempo, nenhuma mudança drástica no código é necessária.
  • Código unificado para aplicativos seriais e paralelos: construções OpenMP são tratadas como comentários quando compiladores sequenciais são usados.
  • As instruções de código original (serial) não precisam, em geral, ser modificadas quando paralelizadas com OpenMP. Isso reduz a chance de introduzir bugs inadvertidamente.
  • Ambos de grão grosso e de grão fino paralelismo são possíveis.
  • Em aplicativos multifísicos irregulares que não aderem apenas ao modo de computação SPMD , conforme encontrado em sistemas de partículas de fluido fortemente acoplados, a flexibilidade do OpenMP pode ter uma grande vantagem de desempenho sobre o MPI .
  • Pode ser usado em vários aceleradores, como GPGPU e FPGAs .

Contras:

  • Risco de introdução de bugs de sincronização e condições de corrida difíceis de depurar .
  • A partir de 2017, só funciona de forma eficiente em plataformas de multiprocessador de memória compartilhada (veja, no entanto, o Cluster OpenMP da Intel e outras plataformas de memória compartilhada distribuída ).
  • Requer um compilador que suporte OpenMP.
  • A escalabilidade é limitada pela arquitetura da memória.
  • Sem suporte para comparar e trocar .
  • Falta tratamento confiável de erros.
  • Carece de mecanismos refinados para controlar o mapeamento do processador de thread.
  • Alta chance de gravar acidentalmente um código de compartilhamento falso .

Expectativas de desempenho

Pode-se esperar obter um aumento de velocidade N vezes ao executar um programa paralelizado usando OpenMP em uma plataforma de processador N. No entanto, isso raramente ocorre por estes motivos:

  • Quando existe uma dependência, um processo deve esperar até que os dados dos quais depende sejam calculados.
  • Quando vários processos compartilham um recurso de prova não paralelo (como um arquivo para gravar), suas solicitações são executadas sequencialmente. Portanto, cada thread deve esperar até que o outro thread libere o recurso.
  • Uma grande parte do programa pode não ser paralelizada pelo OpenMP, o que significa que o limite superior teórico de speedup é limitado de acordo com a lei de Amdahl .
  • N processadores em um multiprocessamento simétrico (SMP) podem ter N vezes o poder de computação, mas a largura de banda da memória geralmente não aumenta N vezes. Freqüentemente, o caminho da memória original é compartilhado por vários processadores e a degradação do desempenho pode ser observada quando eles competem pela largura de banda da memória compartilhada.
  • Muitos outros problemas comuns que afetam a aceleração final na computação paralela também se aplicam ao OpenMP, como balanceamento de carga e sobrecarga de sincronização.
  • A otimização do compilador pode não ser tão eficaz ao invocar o OpenMP. Normalmente, isso pode levar a um programa OpenMP de thread único rodando mais lentamente do que o mesmo código compilado sem um sinalizador OpenMP (que será totalmente serial).

Afinidade de linha

Alguns fornecedores recomendam definir a afinidade do processador em threads OpenMP para associá-los a núcleos de processador específicos. Isso minimiza a migração de encadeamentos e o custo de troca de contexto entre os núcleos. Também melhora a localidade dos dados e reduz o tráfego de coerência do cache entre os núcleos (ou processadores).

Benchmarks

Uma variedade de benchmarks foi desenvolvida para demonstrar o uso do OpenMP, testar seu desempenho e avaliar a correção.

Exemplos simples

Os benchmarks de desempenho incluem:

Os benchmarks de exatidão incluem:

Veja também

Referências

Leitura adicional

links externos