Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% acharam este documento útil (0 voto)
4 visualizações

TC_com_Python (1)

O trabalho de conclusão de curso de Leonardo Gomes Nunes explora o uso de multithreading em Python, destacando a linguagem como popular, mas limitada em performance devido ao GIL. A pesquisa identifica o Numba como uma solução para implementar paralelismo verdadeiro em Python e compara seu desempenho com C utilizando OpenMP em algoritmos como o método de Jacobi e multiplicação de matrizes. Testes de desempenho foram realizados, coletando tempos de execução e métricas clássicas como speedup e eficiência.
Direitos autorais
© © All Rights Reserved
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
0% acharam este documento útil (0 voto)
4 visualizações

TC_com_Python (1)

O trabalho de conclusão de curso de Leonardo Gomes Nunes explora o uso de multithreading em Python, destacando a linguagem como popular, mas limitada em performance devido ao GIL. A pesquisa identifica o Numba como uma solução para implementar paralelismo verdadeiro em Python e compara seu desempenho com C utilizando OpenMP em algoritmos como o método de Jacobi e multiplicação de matrizes. Testes de desempenho foram realizados, coletando tempos de execução e métricas clássicas como speedup e eficiência.
Direitos autorais
© © All Rights Reserved
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
Você está na página 1/ 58

Leonardo Gomes Nunes

Multithread com Python

São José dos Campos, SP


Leonardo Gomes Nunes

Multithread com Python

Trabalho de conclusão de curso apresentado ao


Instituto de Ciência e Tecnologia – UNIFESP,
como parte das atividades para obtenção do tí-
tulo de Bacharel em Ciência da Computação.

Universidade Federal de São Paulo – UNIFESP


Instituto de Ciência de Tecnologia
Bacharelado em Ciência da Computação

Orientador: Prof. Dr. Álvaro Luiz Fazenda

São José dos Campos, SP


Janeiro de 2023
Elaborado por sistema de geração automática com os dados fornecidos pelo(a) autor(a).

Gomes Nunes, Leonardo


Multithread com Python/ Leonardo Gomes Nunes
Orientador(a) Álvaro Luiz Fazenda­São José dos Campos, 2023.
58 p.

Trabalho de Conclusão de Curso­Bacharelado em Ciência da Computação­


Universidade Federal de São Paulo­Instituto de Ciência e Tecnologia, 2023.

1. Python. 2. Multithread. 3. Numba. 4. HPC. 5. GIL. I. Fazenda, Álvaro Luiz,


orientador(a). II. Título.
Leonardo Gomes Nunes

Multithread com Python

Trabalho de conclusão de curso apresentado ao


Instituto de Ciência e Tecnologia – UNIFESP,
como parte das atividades para obtenção do tí-
tulo de Bacharel em Ciência da Computação.

Trabalho aprovado em:

Prof. Dr. Álvaro Luiz Fazenda


Orientador

Arlindo Flavio da Conceição


Convidado 1

Denise Stringhin
Convidado 2

São José dos Campos, SP


Janeiro de 2023
Dedico esse trabalho de conclusão de curso ao meus pais, Maria Lucia e Lenildo, por sempre
estarem ao meu lado e me apoiarem em todas as decisões da minha vida. Nenhuma de minhas
conquistas seria possível sem eles.
“ Se fui capaz de ver mais longe,
é porque me apoiei em ombros de gigantes.“
(Sir Isaac Newton)
Resumo
Python é sem dúvidas uma das linguagens de programação mais populares no
mundo atualmente, principalmente por apresentar uma sintaxe simples, legível e ser al-
tamente versátil quanto ao desenvolvimento de diferentes aplicações. Cada vez mais vem
ganhando destaque no meio acadêmico, no ensino de disciplinas introdutórias de progra-
mação, bem como no desenvolvimento de aplicações científicas que requerem alto nível de
performance. Contudo, por ser uma linguagem interpretada e não uma linguagem compi-
lada como, C e Fortran, apresenta uma performance extremamente baixa se comparada com
essas anteriores. Além disso, aplicações de alta performance fazem uso intensivo de con-
corrência e paralelismo por meio de multithreads, porém, o interpretador padrão do Python,
chamado de CPython, é incapaz de executar threads em paralelo por conta do GIL (Global
Interpreter Lock), por não ser uma operação thread-safe, limitando a execução das threads
uma de cada vez. Esse trabalho teve como objetivo buscar na literatura qual a solução mais
utilizada para se implementar o verdadeiro paralelismo em Python, elencando assim o in-
terpretador Numba como o mais utilizado para tal. Testes de desempenho em multithread
entre Python, utilizando Numba, e C, utilizando OpenMP, foram realizados utilizando dois
algoritmos, método de Jacobi e multiplicação de matrizes. Tempos de execução foram co-
letados e métricas de desempenho clássicas como speedup e eficiência foram calculadas
para comparações e análises.

Palavras-chaves: Python, Multithread, Multiprocessamento, Numba, HPC, GIL, Desem-


penho
Abstract
Python is undoubtedly one of the most popular programming languages in the
world today, mainly because it has a simple, readable syntax and is highly versatile in terms
of developing different applications. It is increasingly gaining prominence in academia,
teaching introductory programming subjects, as well as the development of scientific ap-
plications that require a high level of performance. However, because it is an interpreted
language and not a compiled language like C and Fortran, it presents an extremely low
performance compared to these previous ones. In addition, high-performance applications
make intensive use of concurrency and parallelism through multithreads, however, the stan-
dard Python interpreter, called CPython, is unable to run threads in parallel due to the GIL
(Global Interpreter Lock), because it does not be a thread-safe operation, limiting the exe-
cution of threads one at a time. This work aimed to search the literature for the most used
solution to implement true parallelism in Python, thus listing the Numba interpreter as the
most used for this. Multithreaded performance tests between Python, using Numba, and C,
using OpenMP, were performed using two algorithms, Jacobi’s method and matrix multi-
plication. Execution times were collected and classic performance metrics like speedup and
efficiency were calculated for comparisons and analysis.

Key-words: Python, Multithread, Multiprocess, Numba, HPC, GIL, Performance


Lista de ilustrações

Figura 1 –
Processo executando duas threads em uma única CPU. . . . . . . . . . . . . 26
Figura 2 –
Exemplo de paralelismo. . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Figura 3 –
Fluxo de um algortimo em OpenMP . . . . . . . . . . . . . . . . . . . . . 28
Figura 4 –
Tempos de execução sequencial e concorrente. . . . . . . . . . . . . . . . . 30
Figura 5 –
Execução de duas threads em ambiente com uma única CPU . . . . . . . . 30
Figura 6 –
Execução de duas threads em ambiente com duas CPUs . . . . . . . . . . . 31
Figura 7 –
Vista esquemática do método de Jacobi em 2D usando um stencil de cinco
pontos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
Figura 8 – Métodos de multiplicação de matrizes. . . . . . . . . . . . . . . . . . . . . 35

Figura 9 – Tempos de Execução - Jacobi 512x512 . . . . . . . . . . . . . . . . . . . . 49


Figura 10 – Speedup - Jacobi 512x512 . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Figura 11 – Eficiência - Jacobi 512x512 . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Figura 12 – Speedup - Jacobi 1024x1024 . . . . . . . . . . . . . . . . . . . . . . . . . 50
Figura 13 – Eficiência - Jacobi 1024x1024 . . . . . . . . . . . . . . . . . . . . . . . . 50
Figura 14 – Speedup - Jacobi 2048x2048 . . . . . . . . . . . . . . . . . . . . . . . . . 51
Figura 15 – Eficiência - Jacobi 2048x2048 . . . . . . . . . . . . . . . . . . . . . . . . 51
Figura 16 – Speedup - Mult. Matrizes 5000x5000 . . . . . . . . . . . . . . . . . . . . . 52
Figura 17 – Eficiência - Mult. Matrizes 5000x5000 . . . . . . . . . . . . . . . . . . . . 52
Figura 18 – Speedup - Mult. Matrizes 10000x10000 . . . . . . . . . . . . . . . . . . . 52
Figura 19 – Eficiência - Mult. Matrizes 10000x10000 . . . . . . . . . . . . . . . . . . . 52
Lista de tabelas

Tabela 1 – Número de utilização dos diferentes interpretadores nos trabalhos analisados. 32

Tabela 2 – Tempos de execução serial. . . . . . . . . . . . . . . . . . . . . . . . . . . 45


Tabela 3 – Tempos de execução multithread (nativo) em Python . . . . . . . . . . . . . 45
Tabela 4 – Tempo de execução serial em Python utilizando JIT. . . . . . . . . . . . . . 46
Tabela 5 – Resultados Método de Jacobi - 512x512 . . . . . . . . . . . . . . . . . . . 48
Tabela 6 – Resultados Método de Jacobi - 1024x1024 . . . . . . . . . . . . . . . . . . 49
Tabela 7 – Resultados Método de Jacobi - 2048x2048 . . . . . . . . . . . . . . . . . . 50
Tabela 8 – Resultados Multiplicação de Matrizes - 5000x5000 . . . . . . . . . . . . . 51
Tabela 9 – Resultados Multiplicação de Matrizes - 10000x10000 . . . . . . . . . . . . 52
Lista de algoritmos

2.1 Exemplo de código em Python utilizando Numba. . . . . . . . . . . . . . 33


2.2 Pseudocódigo do método de Jacobi . . . . . . . . . . . . . . . . . . . . 35
2.3 Pseudocódigo da Multiplicação de Matrizes. . . . . . . . . . . . . . . . . 36
4.1 Método de Jacobi serial implementado em C. . . . . . . . . . . . . . . . 44
4.2 Método de Jacobi serial implementado em Python. . . . . . . . . . . . . 44
4.3 Método de Jacobi serial implementado em Python utilizando Numba. . 46
4.4 Método de Jacobi paralelo implementado em Python utilizando Numba. 47
Sumário

Lista de algoritmos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17

1 Introdução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
1.1 Contextualização e Motivação . . . . . . . . . . . . . . . . . . . . . . . . . . 21
1.2 Objetivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
1.2.1 Geral . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
1.2.2 Específicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
1.3 Metodologia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23

2 Fundamentação Teórica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.1 Processos e Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.2 Concorrência e Paralelismo . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.2.1 Multiprocessamento e Multithreading . . . . . . . . . . . . . . . . . . 27
2.3 Open Multi-Processing (OpenMP) . . . . . . . . . . . . . . . . . . . . . . . . 27
2.4 Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.4.1 Global Interpreter Lock (GIL) . . . . . . . . . . . . . . . . . . . . . . 28
2.4.2 Alternativas ao CPython . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.4.3 Numba . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.5 Casos de Teste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.5.1 Método de Jacobi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.5.2 Multiplicação de Matrizes . . . . . . . . . . . . . . . . . . . . . . . . 35
2.6 Métricas de Desempenho . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36

3 Revisão Bibliográfica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39

4 Resultados e Discussão . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43

5 Conclusão . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53

Referências . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
21

1 Introdução

1.1 Contextualização e Motivação

Atualmente, Python é uma das linguagens de programação mais populares e utilizadas


no mundo. De acordo com a pesquisa (OVERFLOW, 2021) realizada pela plataforma Stack
Overflow, na qual foram analisados questionários respondidos por 83.439 desenvolvedores de
software de 181 países diferentes, coloca Python como a terceira tecnologia mais popular do
mundo, atrás apenas de JavaScript e HTML/CSS. Essa mesma pesquisa a coloca no topo das
tecnologias que os desenvolvedores expressaram mais interesse em aprender/utilizar, com um
percentual de 19,04 dos questionários.
Python também se mostra uma linguagem extremamente popular no meio acadêmico,
sendo a linguagem de programação utilizada nas disciplinas introdutórias do curso de Ciência
da Computação em 8 das 10 melhores universidades dos Estados Unidos. (GUO, 2014)
Python também vem ganhando extrema notoriedade quando o assunto é desenvolvi-
mento de aplicações de alto desempenho (HPC - High Performance Computing), especialmente
no campo da computação científica. A computação científica tem se tornado um dos principais
pilares do método científico moderno, juntamente com teoria e experimentação. Isto porque a
ciência se expandiu a limites que não são práticos ou sequer possíveis de experimentação em-
pírica, tornando-se mais do que uma simples ferramenta da ciência, de modo que a modelagem
computacional de formulações teóricas é um fator crítico para avanços científicos e tecnológi-
cos. (SKUSE, 2019)
Tradicionalmente, a maioria das aplicações de HPC de grande escala são desenvolvi-
das utilizando linguagens como C, C++ ou Fortran. Embora tais linguagens obtenham um alto
desempenho nestes tipos de aplicações, as mesmas carecem de recursos que aumentam sua pro-
dutividade, como garbage collection, duck typing (conceito relacionado à tipagem dinâmica,
onde o tipo ou a classe de um objeto é menos importante do que os métodos que ele define),
entre outros. Ao se analisar aplicações HPC desenvolvidas nessas linguagens, não é usual que
uma quantidade substancial das linhas de código sejam associadas a tarefas padrões de qual-
quer linguagem, como gerenciamento de memória, parse de arquivos de entrada e tratamento
de erros, tarefas das quais não são o objetivo principal da aplicação desenvolvida. (VINCENT
et al., 2016)
Por outro lado, Python apresenta uma sintaxe fácil, limpa e simples de ser compreen-
dida, ao mesmo tempo que é uma linguagem de alto nível que dá suporte a diversos paradigmas
de programação, como orientado a objetos, imperativo e funcional. Porém, seu desempenho é
considerado lento se comparada com linguagens compiladas, como as citadas anteriormente,
22 Capítulo 1. Introdução

especialmente em processamentos de grande volumes de dados ou que exigem grande demanda


de processamento. A principal razão para o seu baixo desempenho se dá pelo fato de ser uma
linguagem interpretada.
Somando-se a isso, a implementação padrão do Python é incapaz de paralelizar o código
usando seu mecanismo de multithreading. A razão para esta limitação é que seu interpretador,
chamado de CPython, não é considerado thread-safe. Para garantir que seja thread-safe, o mó-
dulo de threading faz uso de um bloqueio global chamado Global Interpreter Lock (GIL). Isso
significa que apenas um thread pode executar uma instrução de bytecode ao mesmo tempo, com
cada uma alterando sua execução após um período de tempo. Em outras palavras, todas as thre-
ads em Python são executadas no mesmo núcleo, portanto, nenhuma melhoria de desempenho
é obtida.

1.2 Objetivos

1.2.1 Geral
O foco deste trabalho se dá em explorar na literatura quais são as alternativas que vêm
sendo utilizadas, assim elencando a mais utilizada, para se paralelizar aplicações de alto de-
sempenho em Python com o uso de multithreads, dado as limitações impostas pelo CPython
e seu Global Interpreter Lock (GIL). Definida então essa solução, testes comparativos de de-
sempenho serão realizados entre Python e C. Para tal, implementações do método de Jacobi e
do algoritmo de multiplicação de matrizes serão realizadas em ambas as linguagens, de forma
serial e paralela.

1.2.2 Específicos

• Revisão da literatura buscando elencar a solução mais utilizada no desenvolvimento de


aplicações multithread em Python;

• Implementação serial do método de Jacobi em Python e C;

• Implementações paralelas do método de Jacobi e de multiplicação de matrizes em Python,


com o uso da solução encontrada, e em C, utilizando OpenMP;

• Coleta dos tempos de execução de cada implementação;

• Cálculo das métricas de desempenho e análise comparativa entre cada implementação dos
algoritmos abordados;
1.3. Metodologia 23

1.3 Metodologia
Em um primeiro momento, o foco será em realizar uma revisão na literatura em busca da
solução mais utilizadas na implementação de algoritmos paralelos multithread em Python. Com
essa solução definida, o próximo passo será realizar a implementação de dois algoritmos muito
utilizados em processamento de alto desempenho e computação científica, sendo o método de
Jacobi, método iterativo para resolver equações lineares, e o algoritmo de multiplicação de
matrizes.
As implementações serão realizadas em Python e C, com versões serial e paralelas dos
algoritmos. Em Python, a solução encontrada após revisão da literatura será utilizada nas im-
plementações paralelas multithread. Em C, será utilizada uma programação aderente ao padrão
OpenMP.
Os tempos de execução de todas as versões implementadas serão coletados, assim sendo
possível o cálculo de métricas de desempenho clássicas como speedup e eficiência. Ao final, os
resultados obtidos serão analisados e discutidos.
25

2 Fundamentação Teórica

2.1 Processos e Threads

Primeiramente, devemos definir o que são Threads e o que são Processos no campo da
ciência da computação.
Processos podem ser definidos como uma instância específica de um software ou pro-
grama de computador que está sendo executado pelo sistema operacional. Um processo contém
tanto o código do programa que está sendo executado quanto suas interações com outras enti-
dades do sistema, possuem níveis de operação e define quais chamadas de sistema os mesmo
podem executar.
Note que um processo não é equivalente a um programa de computador. Enquanto um
programa pode ser visto apenas como uma coleção estática de instruções, sendo seu código, um
processo é a execução em si dessas instruções. Isso significa que um mesmo programa pode
disparar diversos processos para sua execução, de forma concorrente ou paralela, porém todos
eles executam o mesmo código.
Processos são isolados entre si, a nível de sistema operacional e também a nível de hard-
ware, não compartilhando memória. Tarefas sendo executadas em um mesmo processo podem
facilmente trocar informações entre si, pois compartilham o mesmo espaço de memória, po-
rém tarefas sendo executadas em processos diferentes não conseguem ter essa comunicação de
forma fácil, sendo necessário a utilização de chamadas de sistema que permitem tal ação, cha-
madas de IPC (Inter-Process Communication). Cada processo que está sendo executado possui
um identificador único, chamado de PID (Process Identifier) e o sistema operacional possui
descritores de processos, conhecidos como PCBs (Process Control Blocks) que armazenam as
informações referentes a cada processo ativo no sistema.
Threads por sua vez podem ser definidas como a menor porção de um programa de
computador que o escalonador do sistema operacional pode gerenciar e executar, sendo uma
thread tipicamente um componente de um processo específico.
Um mesmo processo pode conter uma thread (singlethread) ou várias threads (multith-
read), podendo ser executadas de forma simultânea. A diferença fundamental é que threads de
um mesmo processo compartilham os mesmos recursos, como espaço de memória, dados e etc,
porém cada uma tem seu próprio registro, memória temporária e opera sobre sua respectiva
pilha de tarefas.
26 Capítulo 2. Fundamentação Teórica

Figura 1 – Processo executando duas threads em uma única CPU.


(NGUYEN, 2018)

2.2 Concorrência e Paralelismo


Concorrência e paralelismo são conceitos relacionados, mas distintos, na ciência da
computação.
Concorrência refere-se à capacidade de um sistema de executar várias tarefas simul-
taneamente ou ao mesmo tempo. Isso pode ser obtido intercalando a execução das tarefas ou
dividindo as tarefas em unidades menores que podem ser executadas simultaneamente. A si-
multaneidade é frequentemente usada para melhorar a capacidade de resposta e o desempenho
de um sistema, permitindo que várias tarefas sejam processadas em paralelo.
O paralelismo, por outro lado, refere-se à execução de várias tarefas simultaneamente
usando vários processadores ou CPUs. O paralelismo pode ser usado para acelerar a execução
de uma tarefa, dividindo-a em unidades menores que podem ser processadas em paralelo em
diferentes processadores.
Para alcançar a simultaneidade, um sistema geralmente usa vários threads ou processos,
que são unidades independentes de execução que podem ser executadas simultaneamente em
um único processo ou em diferentes processos. O paralelismo, por outro lado, requer o uso de
múltiplos processadores ou CPUs, que podem executar tarefas simultaneamente.

Figura 2 – Exemplo de paralelismo.


(LABORATORY, 2022)
2.3. Open Multi-Processing (OpenMP) 27

2.2.1 Multiprocessamento e Multithreading


Completamente relacionados aos conceitos anteriores, estão os conceitos de multipro-
cessamento e multithreading, sendo ambas duas técnicas que podem ser utilizadas para a ob-
tenção de paralelismo e concorrência no desenvolvimento de aplicações.
O multiprocessamento envolve o uso de vários processos, ou unidades independentes
de execução, que podem ser executados simultaneamente em um único sistema. Como já apre-
sentado, cada processo é executado em seu próprio espaço de memória separado e pode ter suas
próprias variáveis, dados e recursos. O multiprocessamento é frequentemente usado para para-
lelizar a execução de uma tarefa, dividindo-a em unidades menores que podem ser processadas
em paralelo em diferentes processadores.
Já multithread envolve o uso de vários threads em um único processo. Um thread é uma
unidade de execução dentro de um processo que compartilha o espaço de memória e os recursos
do processo. Multithread permite que várias tarefas sejam executadas simultaneamente em um
único processo, tornando mais fácil escrever e gerenciar códigos concorrentes.
Tanto o multiprocessamento quanto o multithreading podem ser úteis para melhorar o
desempenho e a capacidade de resposta de um sistema, mas eles têm diferentes compensações e
considerações. O multiprocessamento geralmente consome mais recursos e pode ser mais difícil
de implementar, mas pode oferecer maior paralelismo e melhor desempenho para determinados
tipos de tarefas. O multithreading geralmente é mais fácil de implementar e gerenciar, mas pode
ser limitado pelo número de processadores disponíveis e pode ser mais complexo de depurar.

2.3 Open Multi-Processing (OpenMP)


Open Multi-Processing (OpenMP) é um padrão de programação paralela que permite
a criação de programas paralelos para sistemas com multiprocessadores de memória compar-
tilhada. Ele permite que os desenvolvedores escrevam código paralelo de maneira familiar e
direta, usando um conjunto de diretivas de compilador e rotinas de biblioteca (a partir de fun-
ções de uma API). O OpenMP é suportado por muitos compiladores, incluindo GCC, Intel e
Clang, e é amplamente usado para paralelizar aplicações de computação científica e de enge-
nharia.
Um conceito chave na programação em OpenMP é seu modelo fork-join. Esse modelo
refere-se ao processo de criação de múltiplas threads (conhecidos como fork) para executar uma
seção de código em paralelo e, em seguida, unir (conhecido como join) todas essas mesmas
threads quando a execução paralela for concluída.
O modelo é implementado usando a diretiva em OpenMP pragma omp parallel, que cria
um time de threads e especifica o bloco de código a ser executado em paralelo por essas threads.
As threads trabalham juntas para executar o bloco de código e, quando o bloco de código é
28 Capítulo 2. Fundamentação Teórica

concluído, todas as threads são unidas novamente. Esse modelo permite a paralelização fácil
de loops e outros blocos de código, bem como um controle refinado sobre como as threads são
criadas e gerenciadas. A figura 3 representa o fluxo de um algoritmo em OpenMP que evidencia
essas características.

Figura 3 – Fluxo de um algortimo em OpenMP


(HAGER; WELLEIN, 2010)

2.4 Python
Python é uma linguagem de programação de alto nível e de uso geral. É uma linguagem
interpretada, o que significa que é executada por um interpretador em vez de ser compilada em
um programa de linguagem de máquina. Python é tipado dinamicamente, o que significa que o
tipo de uma variável é determinado em tempo de execução e não quando a variável é definida. O
Python também é orientado a objetos, o que significa que é baseado no conceito de "objetos"que
podem conter dados e métodos que operam nesses dados.
O Python é conhecido por sua simplicidade e legibilidade, pois a sintaxe é projetada
para ser fácil de entender e escrever. Python também é muito versátil e pode ser usado para
desenvolver uma ampla variedade de aplicações, incluindo desenvolvimento web, computação
científica, ferramentas de análise de dados e muito mais. É amplamente utilizado na indústria e
na academia e possui uma comunidade grande e ativa de desenvolvedores e usuários.

2.4.1 Global Interpreter Lock (GIL)


Um dos grandes problemas quanto à implementação de aplicações que se valem do uso
de multithread em Python é o Global Interpreter Lock (GIL). Em linhas gerais, ele atua como
uma espécie de trava, permitindo que apenas uma thread por vez tenha o acesso e controle ao
2.4. Python 29

interpretador do Python, CPython, limitando o possível ganho que implementações multithread


poderiam obter.
Para entendermos o motivo de sua existência e porque ele traz consigo esse tipo de
limitação, primeiramente devemos conhecer como é o funcionamento do gerenciamento de
memória em Python.
Uma grande diferença de Python para outra linguagens de programação populares, se dá
em como são gerenciados os objetos em espaço de memória. Em linguagens de programação
como C e C++, uma variável é exatamente o local de memória onde o valor da mesma será
escrito, ou seja, o valor que está sendo atribuído a essa variável será armazenado no local de
memória para o qual a mesma aponta. Quando uma variável é atribuída com outra, com essa
última não sendo um ponteiro, o local de memória está sendo copiado, implicando que nenhuma
conexão entre ambas será mantida após a atribuição.
Python, por sua vez, considera cada variável apenas como um nome, uma marcação,
sendo que o verdadeiro valor de suas variáveis estão isoladas em outra região no espaço de
memória. Ou seja, quando é realizada a atribuição de um valor a uma variável, essa mesma
variável em questão está recebendo a referência do local de memória onde se encontra esse
valor. Dessa maneira, atribuição de variáveis em Python pode ser entendido como a troca dessas
referências, nunca o valor propriamente dito, implicando que múltiplas variáveis podem fazer
referência ao mesmo valor, onde alterações realizadas em uma vão se refletir em todas.
Python utiliza um contador interno para realizar o controle de quantas vezes determi-
nado valor foi referenciado por variáveis. Esse contador é intrinsecamente um recurso compar-
tilhado, no qual toda e qualquer thread utilizando o mesmo interpretador precisa conhecer e
interagir, sendo uma sessão crítica. Seções críticas de um sistema podem estar sujeitas a con-
dições de corrida, quando duas ou mais entidades integram com um único recurso, e caso essa
interação não seja realizada de forma cuidadosa, pode acarretar em problemas na execução da
aplicação. Nesse caso, pode se haver a incorreta interpretação de quantas variáveis então refe-
renciado um valor em particular, resultando em execuções ineficientes ou até mesmo liberando
o espaço de memória no qual variáveis estão fazendo referência, ocasionando na perda de seu
valor atual.
A implementação do GIL busca resolver esses problemas supracitados, sendo ele uma
trava única para toda a execução do Python. Qualquer instrução em Python que deseja ser exe-
cutada precisa necessariamente obter esta trava antes de ser iniciada, garantindo a integridade
da quantidade de referências de determinado valor e de outras seções críticas. No início do de-
senvolvimento da linguagem Python, outras soluções foram propostas também com o intuito
de resolver esses mesmo problemas, porém o GIL se mostrou a mais eficiente e simples de ser
implementada.
Em (NGUYEN, 2018), é realizado um teste para demonstrar a perda de desempenho
30 Capítulo 2. Fundamentação Teórica

(ao teoricamente esperado) na execução de um algoritmo em multithread com relação a sua


execução de forma sequencial. Nele, é implementado um contador no qual seu objetivo é ser
decrementado até atingir o valor zero ou negativo. Em sua versão sequencial, o valor inicial do
contador é de 50.000.000. Em sua versão multithread, são implementadas duas threads, cada
uma iniciando com o valor 25.000.000, exatamente metade do valor da execução de forma
sequencial.
Intuitivamente, espera-se que a versão em multithread demore metade do tempo para
executar do que a versão sequencial. Porém, o resultado foi o seguinte:

Figura 4 – Tempos de execução sequencial e concorrente.


(NGUYEN, 2018)

A versão em multithread levou praticamente a mesma quantidade de tempo para ser


executada que a versão sequencial. A divisão da tarefa em duas threads não teve praticamente
nenhum efeito no desempenho. Isso é um efeito direto do GIL, limitando a execução das threads
uma por vez. (NGUYEN, 2018) também pontua que em determinadas ocasiões, um programa
em multithread pode demorar mais tempo para ser executado do que sua versão sequencial,
dado que existe um tempo atrelado a aquisição e liberação da trava imposta pelo GIL, além do
tempo necessário para serem criadas as demais threads.
Em (BEAZLEY, 2010) é apresentando uma forma de visualizar a limitação imposta
pelo GIL de forma gráfica. Nas figuras a seguir, os ticks do interpretador do Python são mostra-
dos ao longo do eixo X, enquanto as duas barras indicam duas threads diferentes em execução.
As regiões brancas indicam os momentos nos quais determinada thread está ociosa, ou seja,
não está sendo executada. As regiões verdes indicam quando uma thread conseguiu efetiva-
mente adquirir o bloqueio do GIL e está sendo executada. Por último, as regiões vermelhas
indicam quando uma thread foi agendada para execução pelo sistema operacional porém não é
efetivamente executada pois o GIL não está disponível no momento.
A figura 5 apresenta a execução de duas threads em um ambiente com uma unica CPU,
de tal forma que as threads alternam sua execução.

Figura 5 – Execução de duas threads em ambiente com uma única CPU


(BEAZLEY, 2010)
2.4. Python 31

Já na figura 6, em um ambiente com duas CPUs disponíveis para execução, o verdadeiro


paralelismo não é atingido pois enquanto uma thread está sendo executada e o possui o GIL, a
outra está ocisa aguardando sua liberação.

Figura 6 – Execução de duas threads em ambiente com duas CPUs


(BEAZLEY, 2010)

2.4.2 Alternativas ao CPython


Conforme foi apresentado anteriormente, a existência do GIL impõe restrições quanto a
implementação de aplicações em multithread, limitando suas execuções a uma por vez. Por essa
razão, a comunidade de Python como um todo passou a enxergar o GIL de forma negativa. Jeff
Knupp em seu artigo Python’s Hardest Problem (KNUPP, 2012) comenta, em livre tradução:

"Por mais de uma década, nenhum problema causou mais frustração ou curiosidade
para novatos e especialistas em Python do que o Global Interpreter Lock."

Dessa maneira, diversos esforços foram realizados pela comunidade de Python ao longo
dos anos com o intuito de se remover completamente o GIL, assim eliminando suas restrições.
Porém, sua implementação está tão intrinsecamente atrelada a linguagem que a execução de
diversos pacotes e bibliotecas são dependentes de seu funcionamento, por não serem thread-
safe, de tal forma que sua remoção implica em muitas incompatibilidades e bugs.
Ainda em (KNUPP, 2012), é apresentado um desses esforços para a remoção do GIL.
No ano de 1999, com Python em sua versão 1.5, Greg Stein aplicou um série de alterações
ao CPython, removendo completamente o GIL e em seu lugar aplicando o controle do fluxo
de execução de threads por meio de granularidade fina (finer grained locking). Por meio dessa
mudança, obteve-se uma melhora no desempenho de execução de duas threads, porém, notou-
se uma piora na execução de aplicações em single-thread, em torno de 40% se comparado com
a versão padrão. Além disso, o desempenho não se mostrou escalável de forma linear conforme
o aumento no número de threads e núcleos de processamento (cores).
Algumas maneiras de lidar com o GIL na execução de aplicações em multithreads são
pelo uso de extensões nativas em Python, que realizam de forma manual a liberação do bloqueio
imposto pelo GIL, liberando assim a execução, como é o caso de um dos pacotes mais populares
de computação científica, o NumPy. Por outro lado, como o GIL é implementado apenas no
interpretador padrão do Python, CPython, é possível realizar o uso de diferentes interpretadores,
como foi o caso dos trabalhos apresentado na sessão de revisão bibliográfica.
32 Capítulo 2. Fundamentação Teórica

Porém, os trabalhos apresentados na sessão de revisão bibliográfica são apenas um pe-


queno recorte da literatura quanto ao uso de diferentes interpretadores em aplicações multith-
read com Python. Ao todo, foram analisados 15 trabalhos como forma de realizar um levan-
tamento de qual o interpretador mais utilizado atualmente para esse tipo de implementação.
Todos os trabalhos analisados utilizavam os diferentes interpretadores para acelerarem determi-
nada aplicação ou realizavam comparativos entre si ou com outras linguagens.
Esse levantamento servirá de base para a escolha do interpretador que será utilizado
para os testes e comparativos. O resultado é apresentado na tabela 1.

Interpretador Nº de Utilizações
Numba 8
Cython 6
PyPy 2
Jython 2
IronPython 1
Pythran 1
Tabela 1 – Número de utilização dos diferentes interpretadores nos trabalhos analisados.

2.4.3 Numba
Numba é um compilador just-in-time (JIT) para Python projetado para melhorar o de-
sempenho de aplicações de computação numérica e científica desenvolvidas em Python. É es-
pecialmente útil para códigos que usam matrizes e funções NumPy, que são parte fundamental
da computação científica em Python atualmente.
Conforme apresentado na tabela 1, Numba é o interpretador mais utilizado entre os
trabalhos analisados, de tal maneira que foi o escolhido para os testes e comparativos que serão
apresentados mais adiante.
Numba funciona compilando código Python para código de máquina em tempo de exe-
cução, usando a infraestrutura do compilador LLVM. Isso permite que seja alcançado um de-
sempenho muitas vezes equivalente a códigos escritos em linguagens compiladas, como C ou
Fortran.
Um dos principais recursos do Numba é sua capacidade de gerar código de máquina
otimizado para uma ampla variedade de arquiteturas de hardware, incluindo x86, x86-64, ARM
e GPUs. Isso o torna uma ferramenta poderosa para desenvolver aplicações de alto desempenho
que podem ser executadas em uma variedade de plataformas.
Para usar o Numba, basta utilizar o decorator @jit nas funções em Python. Numba então
analisará a função e gerará um código de máquina otimizado para a mesma, assim armazenando
em cache e o utilizando na próxima vez que a função for chamada. Isso permite que você escreva
2.5. Casos de Teste 33

0 from numba i m p o r t j i t
1
2 @ j i t ( n o P y t h o n = True , n o g i l = True , c a c h e = T r u e )
3 def mult ( x , y ) :
4 return x * y

Algoritmo 2.1 – Exemplo de código em Python utilizando Numba.

código em uma linguagem expressiva de alto nível como Python, enquanto ainda obtém os
benefícios de desempenho do código compilado.
Em 2.1 é apresentando um pequeno exemplo de uma função em Python utilizando
Numba. Como mencionado, o decorator @jit irá designar que essa função seja compilada on–
the-fly, assim gerando um código otimizado. Além disso, três argumentos são passados para
o decorator, sendo o primeiro noPython=true indicando ao compilador para que essa mesma
função seja executada sem o envolvimento do interpretador padrão do Python, aumentando seu
desempenho. O segundo argumento, nogil=true, instrui explicitamente ao compilador para li-
berar o GIL durante toda a execução, permitindo o uso efetivo de múltiplas threads. Por último,
o argumento cache=true instrui ao compilador para armazenar em memória cachê o código
compilado assim evitando que execuções subsequentes percam tempo compilando novamente
o mesmo código.
Além do decorator @jit, o Numba também fornece vários outros decoratores e funções
que podem ser usados para otimizar e ajustar ainda mais o desempenho do seu código. Por
exemplo, você pode usar o decorator @vectorize para gerar código otimizado para operações
elementares em arrays ou o decorator @cuda.jit para gerar código que pode ser executado em
uma GPU.

2.5 Casos de Teste

2.5.1 Método de Jacobi


O método de Jacobi é um método iterativo para resolver sistemas de equações lineares,
que pode ser usado para resolver equações diferenciais parciais (EDPs), discretizando o EDP
em uma grade e expressando-o como um sistema de equações lineares. Computações de estêncil
são algoritmos que usam um pequeno conjunto fixo de valores de entrada (o stencil, em inglês)
para calcular um único valor de saída. No contexto da resolução de EDPs, cálculos de estêncil
podem ser usados para implementar eficientemente o método Jacobi usando um estêncil para
calcular os valores atualizados das variáveis em cada iteração do método.
Para usar o método de Jacobi com um cálculo de estêncil, o EDP é primeiro discretizado
em uma grade multidimensional, como um vetor ou uma matriz, o que resulta em um sistema
de equações lineares que pode ser resolvido usando o método de Jacobi. Em cada iteração do
34 Capítulo 2. Fundamentação Teórica

método Jacobi, o cálculo do estêncil é usado para computar os valores atualizados das variáveis
com base nos valores das variáveis na iteração anterior. O cálculo do estêncil usa um conjunto
fixo de valores de entrada que são obtidos dos valores das variáveis de sua vizinhança, e o valor
de saída é calculado usando alguma função dos valores de entrada.

Figura 7 – Vista esquemática do método de Jacobi em 2D usando um stencil de cinco pontos


(WELLEIN et al., 2009)

É comumente utilizada em aplicações de computação científica e processamento de


imagem, podendo ser utilizado para se implementar uma grande variedade de operações como
convoluções, filtro de imagens, processamento de sinais e simulações intensivas. É também
amplamente utilizada em computação de alta desempenho, podendo ser implementada de forma
eficiente em arquiteturas paralelas como unidades de processamento gráfico (GPU - Graphics
Processing Unit) e field-programmable gate array (FPGAs).
A eficiência de uma computação de estêncil depende de quão bem a computação pode
ser paralelizada e quão bem ela pode ser vetorizada. Como foi apresentado, a paralelização
permite que a computação seja distribuída por vários processadores ou unidades de hardware, o
que pode melhorar a velocidade da computação permitindo que ela seja computada em paralelo.
A vetorização permite que a computação seja implementada usando instruções SIMD (Single
Instruction, multiple data - instrução única, dados múltiplos), que podem melhorar a velocidade
da computação, permitindo que várias operações sejam executadas em uma única instrução.
Por conta de sua natureza paralela, a implementação do método de Jacobi será utilizada
para a realização dos testes da linguagem Python em um contexto de multithread. O pseudocó-
digo de sua implementação é apresentado em 2.2.
2.5. Casos de Teste 35

0 w h i l e ( e r r > t o l && i t e r < i t e r _ m a x )


1 err = 0
2 for j = 1 to n − 1
3 for i = 1 to m − 1
4 Anew [ j ] [ i ] = 0 . 2 5 * (A[ j ] [ i + 1 ] + A[ j ] [ i − 1 ] + A[ j − 1 ] [ i ] + A[ j
+1][ i ] )
5 e r r = max ( e r r , a b s ( Anew [ j ] [ i ] − A[ j ] [ i ] ) )
6 end f o r
7 end f o r
8 for j = 1 to n − 1
9 for i = 1 to m − 1
10 A[ j ] [ i ] = Anew [ j ] [ i ]
11 end f o r
12 end f o r
13 iter = iter + 1
14 end w h i l e

Algoritmo 2.2 – Pseudocódigo do método de Jacobi

2.5.2 Multiplicação de Matrizes


A multiplicação de matrizes é uma operação comum da Álgebra Linear que multiplica
uma linha inteira de uma matriz A em uma coluna inteira de uma matriz B para produzir cada
elemento de uma matriz C, sendo um tipo de operação essencial para diversas aplicações com-
putacionais nos campos científicos, de multimédia e, mais recentemente, para algoritmos de
aprendizado de máquina.
Existem diferentes formas de se realizar a implementação deste tipo de operação, as
mais comuns sendo o mapeamento básico, matriz transposta, permutação e por blocos. A repre-
sentação visual de cada método é apresentada na Figura 8.

Figura 8 – Métodos de multiplicação de matrizes.


(AKOUSHIDEH; SHAHBAHRAMI, 2022)

Por ser um tipo de operação computacionalmente intensiva e extremamente paralelizá-


vel, será também utilizada para a realização dos testes e comparativos, sendo a implementação
básica da multiplicação a escolhida para tal.
36 Capítulo 2. Fundamentação Teórica

0 f o r i = 1 t o n do
1 f o r j = 1 t o m do
2 f o r k = 1 t o p do
3 C [ i ] [ j ] = C [ i ] [ j ] + A[ i ] [ k ] * B [ k ] [ j ]

Algoritmo 2.3 – Pseudocódigo da Multiplicação de Matrizes.

2.6 Métricas de Desempenho


Dois dos principais objetivos no desenvolvimento de aplicações paralelas são a obtenção
de desempenho, sendo a capacidade de reduzir o tempo de execução de um algoritmo a medida
que os recursos computacionais disponíveis aumentam, e escalabilidade, sendo a capacidade
de manter ou aumentar o desempenho à medida que os recursos computacionais disponíveis
aumentam. Assim, é extremamente necessário que o desempenho de determinado algoritmo
paralelo seja superior ao de sua versão sequencial. (FAZENDA; STRINGHINI, 2019)
Dessa maneira, podemos estabelecer algumas métricas a fim de realizar a comparação
com a versão sequencial do algoritmo para verificar se de fato houve ganho de desempenho.
Dentre as métricas que podemos utilizar estão o tempo de execução, sendo a medida mais
básica, speedup e eficiência.
O speedup é uma das métricas mais populares quando se busca medir o desempenho de
aplicações paralelas, sendo definida com a seguinte equação:

𝑇 𝑒𝑥𝑒𝑐(1)
𝑆𝑝𝑒𝑒𝑑𝑢𝑝(𝑃 ) = (2.1)
𝑇 𝑒𝑥𝑒𝑐(𝑃 )

(FAZENDA; STRINGHINI, 2019)

Nos quais,

• 𝑃 constitui o número de processadores

• 𝑇 𝑒𝑥𝑒𝑐(1) é o tempo de execução total da versão sequencial do algoritmo (sendo execu-


tado em uma única CPU)

• 𝑇 𝑒𝑥𝑒𝑐(𝑃 ) constitui o tempo total de execução pelo código paralelizado em um número


𝑃 de CPUs

Todo algoritmo, por mais bem paralelizado que seja, irá contar com regiões sequenciais.
Algumas instruções são únicas e outras podem possuir dependência entre si e entre interações.
2.6. Métricas de Desempenho 37

Apenas as regiões paralelas da aplicação irão se beneficiar de múltiplos núcleos, aumentando o


desempenho da execução.
Alguns casos são possíveis ao se calcular a aceleração de um algoritmo. O cenário mais
comum é que seu cálculo obtenha um resultado que fique no intervalo 1 ≤ 𝑆𝑝𝑒𝑒𝑑𝑢𝑝 ≤ 𝑃 .
Quanto mais próximo de P, maior será a eficiência da paralelização.
Como já mencionado, não se espera atingir o valor𝑃 de aceleração por conta da carga de
trabalho sequencial que todo algoritmo possui, além do fato de também ser custoso o processo
de criar a alocar recursos para as threads, chamado de Overhead Call. Assim, quanto maior
for o número de threads alocadas em uma região paralela, maior será o tempo gasto com o
Overhead Call.
Por último, temos a métrica de eficiência, que pode ser obtida através da seguinte for-
mula:

𝑆𝑝𝑒𝑒𝑑𝑢𝑝(𝑃 )
𝐸𝑓 𝑖𝑐𝑖𝑒𝑛𝑐𝑖𝑎(𝑃 ) = (2.2)
𝑃

(FAZENDA; STRINGHINI, 2019)

Eficiência indica o grau de aproveitamento dos recursos computacionais disponíveis,


com seu resultado ficando no intervalo 1 ≤ 𝐸𝑓 𝑖𝑐𝑖𝑒𝑛𝑐𝑖𝑎 ≤ 𝑃 .
39

3 Revisão Bibliográfica

Diversos trabalhos na literatura abordam de alguma forma as limitações impostas pelo


GIL, que será abordado mais adiante, na utilização de implementações em multithread com
Python, evidenciando principalmente seu ganho nulo na performance de execução. Em busca
de soluções a esse problemas, muitos desses mesmo trabalhos partem para a utilização de multi-
processos, com cada tarefa da aplicação sendo executada em um processo diferente, com seu
próprio espaço de memória, atingindo assim o paralelismo. (NGUYEN, 2018) cita que essa
alternativa de se implementar multi-processos em vez de multithread é provavelmente a maneira
mais fácil e popular.
Porém, este trabalho focou-se em buscar e apresentar trabalhos nos quais as soluções
apresentadas ainda se mantivessem no campo de multithreads, e, como será evidenciado, sua
grande maioria se valeu da utilização de diferentes interpretadores ao Python, dado que a o GIL
apenas existe no interpretador padrão do Python, chamado de CPython.
Em (GMYS et al., 2020) pretende-se fornecer um ponto de dados útil para ajudar a
pesquisadores do campo das meta-heurísticas paralelas na difícil decisão do quão válido é des-
pender tempo e esforço no aprendizado de uma nova linguagem de programação, dado que o
campo das meta-heurísticas paralelas requer linguagens de programação que provém alta per-
formance e um alto nível de “programabilidade”. Para tal, foi realizada a comparação da per-
formance (paralela), escalabilidade e produtividade de três linguagens, Chapel, Python e Julia,
em duas meta-heurísticas (uma baseada em trajetória e outra baseado na população) aplicada
ao problema de atribuição tridimensional quadrática (Q3AP). A implementação dessas mesmas
meta-heurísticas em C/OpenMP foi utilizada como “padrão ouro” na comparação em termos de
performance.
Em relação aos testes com a linguagem Python, é comentado o problema que o GIL traz
consigo e como forma de solução é buscado o uso de diferentes interpretadores. São menciona-
dos os seguintes: PyPy, Cython, Numba e Nuitka. Ao final, Numba foi o escolhido pois oferece
opções para acelerar e paralelizar o código em Python de forma incremental e exigindo poucas
alterações.
Em (YANG; MENARD, 2019), é apresentado que o modelo LRDFIT, amplamente uti-
lizado para reconstruir o equilíbrio magnetohidrodinâmico (MHD) e também para prever a evo-
lução do campo de vácuo, juntamente de seu programa de computador (originalmente em For-
tran), foi construído utilizando uma plataforma comercial paga, implicando que seu uso requer
a compra de uma licença comercial. Assim, o trabalho em questão busca realizar o porte desse
modelo para a linguagem de programação Python, de forma open-source, com o uso do pa-
radigma de Programação Orientada a Objetos, a fim de ser possível sua utilização de forma
40 Capítulo 3. Revisão Bibliográfica

gratuita.
Nesse mesmo trabalho, é discutido que para atingir uma performance de execução oti-
mizada, métodos números requerem muitos níveis de otimizações. Uma implementação padrão
em Python com apenas otimizações básicas da própria linguagem, não iria trazer velocidades
de execução comparáveis a linguagens como C ou Fortran. Para tal, foi utilizado o interpretador
Numba, a fim de se melhorar a performance dos módulos números de baixo nível, para desta
maneira tais módulos serem capazes de atingir velocidades comparáveis a de C e Fortran. Como
forma de paralelização do código, GIL também se mostra uma pedra no caminho e, mais uma
vez, Numba foi o interpretador escolhido para uma implementação satisfatória em multithread.
(BOULLE; KIEFFER, 2019) apresenta como acelerar a execução de computação cris-
talográfica com Python utilizando diferentes interpretadores como NumExpr, Numba, Pythran
e Cython. Foram utilizados quatro algoritmos como forma de exemplificação, de complexida-
des crescentes. Após os testes, a performance da cada uma das implementações foi comparada,
juntamente com a implementação estado-da-arte em Python utilizando NumPy.
O foco na utilização de diferentes interpretadores foi em se obter o real paralelismo no
uso de multithreads em Python, dado o já comentado problema advindo do CPython e sua im-
plementação do GIL. Ao final de todos os testes e discussões levantadas, o trabalho conclui que
NumPy pode ser considerada a biblioteca padrão a ser utilizada em aplicações de computação
científica, citando que apenas com o seu uso já é possível atingir altas performances compu-
tacionais em algoritmos adequadamente vetorizados, mantendo o código simples e limpo. Em
casos contrários, onde o aumento da performance é algo crucial, os diferentes interpretadores
entram em ação. Pythran e Cython no geral apresentam uma performance similar, sendo Pyth-
ran mais fácil de se utilizar. Por outro lado, Cython permite acesso a mais opções avançadas em
relação ao gerenciamento de threads e memória. Por último, Numba é avaliado como um pro-
jeto relativamente recente, com muitas capacidades interessantes, como compatibilidade com
GPUs, concluindo que deve ser levado em consideração em um contexto de programação de
alta performance com Python.
Na mesma linha do trabalho anterior, (MEIER; GROSS, 2019) também realiza uma
comparação de alternativas disponíveis ao interpretador de padrão Python, sendo: Jython, IronPython
e PyPy-STM. Essa comparação tem como base suas compatibilidades, desempenho e escala-
bilidades, e definindo as diferenças entre duas abordagens de implementação concorrente, em
linha gerais, duas formas de sincronizar a execução paralela do código, sendo elas fine-grained
locking no caso do Jython e IronPython, e memória transacional (STM, em inglês) no caso do
PyPy-STM.
De forma sumarizada, o trabalho acima conclui que os interpretadores testados podem
aumentar a performance paralela em hardware modernos. As duas abordagens de implementa-
ção concorrente estudadas exibem suas qualidades em diferentes benchmarks realizados. PyPy-
STM leva vantagem sobre os outros interpretadores pois apresenta um melhor balanço entre
41

performance sequencial e paralela em aplicações multithread com Python.


Por último, (MAROWKA, 2018) realiza um estudo no qual examina até que ponto a
linguagem Python é adequada para o ensino de programação paralela a estudantes inexperientes.
Para tal, o trabalho em questão elenca algumas capacidades que uma linguagem deve apresentar
para ser apropriada, das quais é válido mencionar:

• Suporte ao um alto nível de abstração e sintaxe simples de ser entendida;

• Suporte a distintos hardware e plataformas de software;

• Suporte aos principais paradigmas de programação paralela: Memória compartilhada


(multithread), memória distribuída (comunicação inter-processos) e programação hete-
rogênea;

• Facilidade de demonstração em algoritmos simples que apresentam escalabilidade;

A metodologia desse estudo se resumiu em uma extensa análise da literatura, técnicas,


ferramentas disponíveis aos desenvolvedores e sua utilização na prática por meio de diversos
algoritmos. Foram examinadas soluções apresentadas na documentação oficial da linguagem,
bem como Jython, IronPython, PyPy, Cython, pyCUDA, PyOpenCL e Numba. Porém, o tra-
balho se foca nas seguintes: módulo padrão de threads (Python threading), módulo padrão de
multiprocessamento (Python multiprocessing), Pacote mpi4Py e Numba. Essas soluções foram
escolhidas pois abordam os três principais paradigmas de programação paralela e também por
refletirem os prós e contras de outras mencionadas.
Com relação especificamente a multithread, o estudo conclui que a linguagem não dá
o suporte de forma adequada por meio de seu módulo padrão, pelos obstáculos impostos pelo
GIL, assim não servindo como ferramenta de ensino, dado que esse é o paradigma mais básico
em programação paralela atualmente.
43

4 Resultados e Discussão

Conforme apresentado na sessão 1.3, o primeiro passo deste trabalho constituiu da


busca pela solução mais utilizada atualmente na implementação de aplicações multithread em
Python, constatando-se assim que a forma mais usual é a utilização de outro interpretador da
linguagem para superar as limitações advindas do GIL (2.4.1). Através de uma breve revisão
bibliográfica determinou-se que o interpretador Python mais utilizado para paralelismo mul-
tithread foi o Numba (1), sendo este, portanto, o escolhido para implementação dos testes de
desempenho que serão apresentados posteriormente. O código completo de todas as implemen-
tações realizadas pode ser encontrado em: https://github.com/gnunesleonardo/
python-multithread-tcc
Aa especificações de hardware e software no qual todos os testes foram realizados, em
um nó de processamento do cluster SCAD/Unifesp, contém as seguintes especificações:

• CentOS Linux release 7.2.1511 (Core);

• Intel(R) Xeon(R) CPU E5-2660 v4 @ 2.00GHz 14 Cores/28 Threads;

• Python v3.9.6;

• Numba v0.56.4;

• NumPy v1.23.5;

• gcc (GCC) v11.2.0;

Em um primeiro momento, o método de Jacobi, que implementa uma computação nu-


mérica em estêncil sob uma matriz bidimensional, foi implementado de forma serial, sob uma
quantidade iterativa de vezes, restringindo a utilização apenas de recursos nativos da própria
linguagem. A fim de atestar a correta implementação do algoritmo, ao final da execução foi
comparada o número de iterações para se atingir o erro tolerado nas rodadas iterativas, e a ma-
triz resultante final. O valor máximo de erro tolerado é de (MAX_TOL = 0.0001) e o número
máximo de iterações permitido (MAX_ITER = 100000).
Nos algoritmos 4.1 e 4.2 são apresentados o trecho de código considerado o core (ou
núcleo) da implementação do método de Jacobi, codificado em linnguagem C e Python, respec-
tivamente.
44 Capítulo 4. Resultados e Discussão

0 w h i l e ( e r r > MAX_TOL && i t e r < MAX_ITER )


1 {
2 err = 0.0;
3 f o r ( i n t i = 1 ; i < MAX_M − 1 ; i ++)
4 {
5 f o r ( i n t j = 1 ; j < MAX_N − 1 ; j ++)
6 {
7 a_new [ i ] [ j ] = 0 . 2 5 * ( a [ i ] [ j + 1 ] + a [ i ] [ j − 1 ] + a [ i − 1 ] [ j ] +
a [ i + 1][ j ]) ;
8 e r r = fmax ( e r r , f a b s ( a_new [ i ] [ j ] − a [ i ] [ j ] ) ) ;
9 }
10 }
11
12 f o r ( i n t i = 1 ; i < MAX_M − 1 ; i ++)
13 {
14 f o r ( i n t j = 1 ; j < MAX_N − 1 ; j ++)
15 {
16 a [ i ] [ j ] = a_new [ i ] [ j ] ;
17 }
18 }
19
20 i t e r ++;
21 }

Algoritmo 4.1 – Método de Jacobi serial implementado em C.

0 w h i l e ( e r r > MAX_TOL and i t e r < MAX_ITER ) :


1 err = 0.0
2
3 f o r i i n r a n g e ( 1 , (MAX_M − 1 ) ) :
4 f o r j i n r a n g e ( 1 , (MAX_N − 1 ) ) :
5 a_new [ i ] [ j ] = 0 . 2 5 * ( a [ i ] [ j + 1 ] + a [ i ] [ j − 1 ] + a [ i − 1 ] [ j ] +
a [ i + 1][ j ]) ;
6 e r r = max ( e r r , a b s ( a_new [ i ] [ j ] − a [ i ] [ j ] ) ) ;
7
8 f o r i i n r a n g e ( 1 , (MAX_M − 1 ) ) :
9 f o r j i n r a n g e ( 1 , (MAX_N − 1 ) ) :
10 a [ i ] [ j ] = a_new [ i ] [ j ]
11
12 i t e r += 1

Algoritmo 4.2 – Método de Jacobi serial implementado em Python.


45

Neste primeiro teste, foi utilizada uma matriz quadrada 𝑁 𝑥𝑁 , sendo 𝑁 = 100. Os
tempos coletados de cada execução se referem apenas à parte de cálculo central do método de
Jacobi, não considerando o tempo de inicialização das matrizes. Essa característica também se
repete para todos os testes posteriores.
Os tempos de execução (em segundos) das implementações seriais registrados são os
apresentados na Tabela 2.

Linguagem Tempo(s) Desvio Padrão


C 0,583457 0,01219438203
Python 427,8583531 2,182820581
Tabela 2 – Tempos de execução serial.

Como podemos observar, o tempo de execução do algoritmo serial implementado em


Python foi extremamente mais lento que o tempo de execução da implementação do mesmo al-
goritmo em linguagem C, chegando a ser 733 vezes mais lento. Isso se deve basicamente ao fato
de Python ser uma linguagem de programação interpretada (apesar de algumas implementações
da linguagem utilizar de pré-compilação em BytCode), enquanto a linguagem C é uma lin-
guagem compilada. Programas codificados em linguagens interpretadas geralmente produzem
resultados com desempenho mais lento que códigos binários executáveis produzidos a partir da
compilação de um código-fonte em linguagem compilada.
O próximo teste efetuado faz uso de um pacote nativo de Python para a implementação
em multithread. No caso a seguir, o teste foi realizado utilizando-se de apenas 2 threads, na
mesma configuração de tamanho de problema do teste anterior, e seu resultado pode ser visto
na tabela 3.

Linguagem Threads Tempo(s) Desvio Padrão


Python 2 636,8537574 34,00024788
Tabela 3 – Tempos de execução multithread (nativo) em Python .

Conforme esperado, devido as conhecidas restrições do GIL, não houve ganho de de-
sempenho na execução em multithread, observando ainda uma perda de desempenho quando
comparada a execução serial, evidenciando a limitação imposta pelo GIL neste tipo de proces-
samento, assim como demonstrado na sessão 2.4.1.
Após esses testes, fica evidente que devemos buscar outras alternativas para o desenvol-
vimento de aplicações em multithread com Python e, como mencionado, a solução escolhida foi
a utilização do interpretador Numba, o qual permite aplicar o decorator @jit disponível na fun-
ção que desejamos que seja compilada em tempo de execução, gerando assim um código mais
rápido, que é chamado toda vez que essa mesma função é executada, conforme o Algoritmo
4.3.
46 Capítulo 4. Resultados e Discussão

0 @ j i t ( ) # numba d e c o r a t o r
1 d e f c o m p u t e _ s t e n c i l ( a , a_new , m, n ) :
2 err = 0.0
3 f o r i i n r a n g e ( 1 , (m − 1 ) ) :
4 f or j in range (1 , ( n − 1) ) :
5 a_new [ i ] [ j ] = 0 . 2 5 * ( a [ i ] [ j + 1 ] + a [ i ] [ j − 1 ] + a [ i − 1 ] [ j ] +
a [ i + 1][ j ]) ;
6 e r r = max ( e r r , a b s ( a_new [ i ] [ j ] − a [ i ] [ j ] ) )
7
8 fo r i in range (1 , m − 1) :
9 f or j in range (1 , n − 1) :
10 a [ i ] [ j ] = a_new [ i ] [ j ]
11
12 return err

Algoritmo 4.3 – Método de Jacobi serial implementado em Python utilizando Numba.

A Tabela 4 apresenta o tempo de execução serial com matriz 𝑁 𝑥𝑁 , sendo 𝑁 = 100,


para o código em Python utilizando o decorator @jit do Numba.

Linguagem Tempo(s) Desvio Padrão


Python/JIT 1,139639854 0,02442838294
Tabela 4 – Tempo de execução serial em Python utilizando JIT.

Como podemos notar, o tempo de execução foi significativamente impactado apenas


com o uso do decorator @jit na função de cálculo, com uma melhora de 375 vezes se comparada
com a versão serial "pura"em Python.
Dessa maneira, é possível concluir que as implementações em serial e em multithread
(nativo) em Python não oferecem um bom desempenho numérico e escalabilidade na execução
do método de Jacobi, principalmente se comparado com os resultados obtidos ma versão do
código com a linguagem C. Somente após a utilização do Numba, com seu decorator @jit
aplicado à função, foi possível observar um grande impacto no desempenho, com uma melhora
considerável no desempenho e com tempos comparáveis aos obtidos na execução serial em C.
De agora em diante, serão apresentados e discutidos os resultados obtidos dos testes em
multithread com Python, utilizando Numba, e com C, utilizando uma programação aderente ao
padráo OpenMP. Para a implementação em Python com Numba, foi utilizada a mesma função
apresentada em 4.3 porém com duas pequenas modificações, sendo primeira mudança feita nos
argumentos do decorator @jit e a segunda mudança nos laços da função.
Para se paralelizar um código em Numba, deve-se utilizar o argumento parallel=True no
decorator @jit. Isso permite ao Numba realizar a tentativa de se paralelizar a função de forma
automática, além de executar diversas outras otimizações, caso possível. Isso se dá pois diver-
sas operações são conhecidas por terem uma semântica paralela, sendo possível identificá-las.
Numba também apresenta suporte ao uso explícito de laços paralelos, substituindo a instrução
47

0 @ j i t ( p a r a l l e l = True , n o g i l = True , c a c h e = T r u e )
1 d e f c o m p u t e _ s t e n c i l ( a , a_new , m, n ) :
2 err = 0.0
3 f o r i i n p r a n g e ( 1 , (m − 1 ) ) :
4 fo r j in range (1 , ( n − 1) ) :
5 a_new [ i ] [ j ] = 0 . 2 5 * ( a [ i ] [ j + 1 ] + a [ i ] [ j − 1 ] + a [ i − 1 ] [ j ] +
a [ i + 1][ j ]) ;
6 e r r = max ( e r r , a b s ( a_new [ i ] [ j ] − a [ i ] [ j ] ) )
7
8 f or i in prange (1 , m − 1) :
9 fo r j in range (1 , n − 1) :
10 a [ i ] [ j ] = a_new [ i ] [ j ]
11
12 return err

Algoritmo 4.4 – Método de Jacobi paralelo implementado em Python utilizando Numba.

que implementa um laço padrão de Python, range(), pela nova instrução prange(), a qual per-
mite distribuir as iterações do laço entre as threads. O código apresentado no Algoritmo 4.4
reflete as mudanças mencionadas.
Ainda em relação a paralelização de algoritmos com Numba, algumas informações se
fazem pertinentes. De acordo com sua documentação, Numba nunca cria novas threads além
da quantidade que foi criada no momento da primeira execução paralela. Consequentemente,
não é possível aumentar dinamicamente o número de threads durante a execução do algoritmo,
mas apenas diminuir para um valor menor ou igual ao inicial. Como forma de "mascarar"essa
limitação, Numba sempre cria o número máximo de threads suportado pela máquina utilizada,
porém apenas utiliza a quantidade setada por meio da função set num threads(n). Por exemplo,
em uma máquina com 16 CPU Cores, se um programa executar set num threads(4), Numba
terá 16 threads disponíveis para execução, porém irá utilizar apenas 4, mantendo as outras 12
ociosas.
Com isso em mente, certo cuidado deve ser tomado quanto a implementação em C
utilizando OpenMP. Como apresentado na sessão 2.3, OpenMP trabalha em um modelo fork-
join, com threads sendo criadas dinamicamente nas regiões paralelas do código e encerradas ao
final de sua sessão. Por mais rápido e eficiente que seja, o processo de criação e destruição de
threads demanda certo tempo, e caso a região paralela esteja dentro de um laço, esse processo irá
ocorrer diversas vezes, aumentando o tempo de execução total do algoritmo. Como em Numba
o processo de criação das threads ocorre apenas uma vez, esse tempo não se torna relevante em
sua execução.
Com relação às variáveis em uma função paralelizada pelo Numba, não há uma maneira
explícita de as declarar como compartilhada entre threads, privada ou até mesmo indicar redu-
ções, sendo essa inferência feita de forma implícita e automática, porém atendendo às seguintes
regras, em ordem de importância:
48 Capítulo 4. Resultados e Discussão

• Se uma variável for atualizada por uma função/operador binário suportado usando seu
valor anterior no laço, é inferida como redução;

• Variáveis que sofrem alguma atribuição são inferidas como privadas;

• Variáveis que são apenas lidas são inferidas como compartilhadas entre todas as threads;

Note que no Algoritmo 4.4, na linha 6, uma redução é automaticamente inferida por
conta do uso da função max().
Outra característica em Numba importante de ser mencionada é chamada de seriali-
zação de laço. A serialização de laço ocorre quando um ou mais laços paralelos prange() são
aninhados. Neste caso, apenas o laço externo é efetivamente paralelizado, com os laços internos
sendo executados de forma serial, funcionando como um laço padrão em Python, range().
Os resultados que se seguem foram obtidos através de execuções do método de Jacobi
com matrizes 𝑁 𝑥𝑁 de tamanhos 512𝑥512, 1024𝑥1024 e 2048𝑥2048. Para cada tamanho de
matriz, testes com 1, 2, 4, 8, 16 e 28 threads foram realizados em ambas as linguagens.
No Algoritmo 5 são apresentados os resultados obtidos através da execução com uma
matriz de tamanho 512𝑥512.
Tabela 5 – Resultados Método de Jacobi - 512x512

Threads Linguagem Tempo(s) Desvio Padrão Speedup Eficiência


1 OpenMP 108.1079926 0.5776338847 1 1
Numba 138.7769408 0.3042729204 1 1

2 OpenMP 55.545681 0.5795717116 1.946289803 0.9731449017


Numba 72.07241702 0.08719761201 1.925520838 0.962760419

4 OpenMP 28.27863189 0.5798702181 3.822956958 0.9557392396


Numba 37.46488833 0.1126874441 3.704186693 0.9260466733

8 OpenMP 14.14375062 0.577939949 7.643516593 0.9554395742


Numba 19.95205712 0.2578876414 6.955520424 0.869440053

16 OpenMP 8.8185949 0.005368338833 12.25909499 0.7661934369


Numba 12.08143854 0.1426813726 11.48678945 0.7179243409

28 OpenMP 9.8255751 1.255433258 11.00271398 0.3929540709


Numba 10.11858273 1.420473012 13.7150572 0.4898234713

Como é possível observar, com o uso de 1 até 16 threads, o código em Pytho/Numba


obteve um tempo de execução em torno de 30% maior do que o obtido com o algoritmo em
C/OpenMP. Esse cenário apenas é alterado quando se é utilizado 28 threads, com Numba apre-
sentando um leve ganho de desempenho enquanto OpenMP piora seu tempo de execução com
relação a 16 threads, porém ainda sim menor que o tempo obtido com Numba. Porém, isso
sugere que um pico de desempenho em OpenMP foi atingido neste cenário com matriz de ta-
manho 512𝑥512. Com relação ao speedup e eficiência, apresentado respectivamente de forma
gráfica nas Figuras 10 e 11, ambas as versões obtiveram resultados semelhantes.
49

Figura 9 – Tempos de Execução - Jacobi 512x512

Figura 10 – Speedup - Jacobi 512x512 Figura 11 – Eficiência - Jacobi 512x512

Na Tabela 6 são apresentados os resultados obtidos através da execução com uma matriz
de tamanho 1024x1024.

Tabela 6 – Resultados Método de Jacobi - 1024x1024

Threads Linguagem Tempo(s) Desvio Padrão Speedup Eficiência


1 OpenMP 430.4296187 0.0001065045188 1 1
Numba 554.5815649 5.029148641 1 1

2 OpenMP 216.2159241 0.5777175073 1.990739676 0.9953698379


Numba 285,9708209 0.06793977916 2.00956595 0,9696471185

4 OpenMP 109.1087555 1.733937667 3.944959475 0.9862398689


Numba 350.3187826 0.1130153984 1.583076879 0.3957692197

8 OpenMP 55.55233617 0.579814117 7.748182135 0.9685227669


Numba 176.8036981 0.2084921019 3.136707948 0.3920884935

16 OpenMP 29.29130846 2.095718843 14.69478973 0.918424358


Numba 90.71165776 0.06143875407 6.113674676 0.3821046673

28 OpenMP 21.21153026 0.001854068752 20.29224735 0.7247231196


Numba 56.09671974 0.2889421053 9.886167452 0.353077409

Neste cenário de teste, Numba obteve resultados bem discrepantes e não muito satis-
fatórios com relação a execuções com matrizes de tamanhos 512x512 e 2048x2048 (que será
apresentado adiante). Como podemos observar, o tempo de execução com 4 threads foi maior
que o tempo de execução com 2 threads. Além disso, a eficiência a partir de 2 threads sofreu
50 Capítulo 4. Resultados e Discussão

Figura 12 – Speedup - Jacobi 1024x1024 Figura 13 – Eficiência - Jacobi 1024x1024

uma enorme alteração, despencando da casa dos 90% para a casa dos 40% em relação a versão
em C/OpenMP. Diversas execuções foram realizadas com este cenário e todas apresentaram
resultados semelhantes, não sendo possível explicar até o momento, com exatidão, os motivos
pelos quais esses resultados foram observados.
Por último, a Tabela 7 apresenta os resultados obtidos com a utilização de uma matriz
de tamanho 2048x2048.

Tabela 7 – Resultados Método de Jacobi - 2048x2048

Threads Linguagem Tempo(s) Desvio Padrão Speedup Eficiência


1 OpenMP 1977.199776 2.645515759 1 1
Numba 2142.01861 1.876940165 1 1

2 OpenMP 989.9892365 19.11804302 1.997193205 0.9985966025


Numba 1120.40023 0.6187116904 1.911833425 0.9559167127

4 OpenMP 505.5051705 6.563815871 3.911334427 0.9778336067


Numba 564.0198202 7.315054585 3.797771874 0.9494429686

8 OpenMP 267.2662926 1.000356056 7.397864344 0.924733043


Numba 284.537473 1.262757238 7.528072093 0.9410090117

16 OpenMP 143.1437176 2.311358086 13.81268986 0.8632931161


Numba 146.5381601 1.556489832 14.6174799 0.9135924938

28 OpenMP 105.1048413 5.691807435 18.81169081 0.6718461003


Numba 119.6934962 13.11781615 17.89586466 0.6391380237

Este último cenário foi onde Numba obteve seus melhores resultados, com seus tempos
de execução em torno de 10% maiores que aqueles obtidos com OpenMP. Destaque para o
tempo de execução com 16 threads, onde Numba atingiu uma diferença de apenas 3% mais
lento em relação ao desempenho em OpenMP. O Speedup e eficiência se demonstraram mais
uma vez bem semelhantes entre as versões com as duas linguagens, como podemos observar
nas Figuras 14 e 15. Esses resultados sugerem que Numba obtém uma melhor paralelização em
cenários com matrizes grandes.
O próximo caso de teste a ser analisado é o de multiplicação de matrizes. Neste cenário,
foram realizados testes com dois tamanhos de matrizes: 5000x5000 e 10000x10000. Assim
51

Figura 14 – Speedup - Jacobi 2048x2048 Figura 15 – Eficiência - Jacobi 2048x2048

como anteriormente, foram feitas execuções com 1, 2, 4, 8, 16 e 28 threads, com seus tempos
de execução coletados e métricas de desempenho calculadas para posterior análise. Na Tabela
8 são apresentados os resultados da multiplicação de matrizes de tamanho 5000x5000.

Tabela 8 – Resultados Multiplicação de Matrizes - 5000x5000

Threads Linguagem Média Desvio Padrão Speedup Eficiência


1 OpenMP 64.645388 0.000384799 1 1
Numba 168.75083 0.281906 1 1

2 OpenMP 42.417629 0.00053545144 1.5240217 0.76201086


Numba 91.889958 0.20794857 1.8364447 0.91822237

4 OpenMP 21.215148 0.57858516 3.04713339 0.76178334


Numba 45.982472 0.049942534 3.66989467 0.91747366

8 OpenMP 11.114285 0.576771 5.81642335 0.72705291


Numba 23.223665 0.0825018 7.26633044 0.9082913

16 OpenMP 6.6331373 0.577293515 9.74582393 0.60911399


Numba 12.487527 0.265701898 13.5135501 0.84459688

28 OpenMP 4.4366648 0.573089227 14.57071724 0.52038275


Numba 9.110691 0.494801976 18.52228657 0.66151023

Diferentemente dos resultados obtidos com a execução do método de Jacobi, nos quais
foram observados tempos de execução em Numba apenas 10% maiores com relação ao valor
obtido em OpenMP, na multiplicação de matrizes o tempo de processamento em Numba prati-
camente dobraram com relação ao mesmo tipo de comparação. Porém, foi observado melhores
valores de speedup e eficiência, como se pode ver nas Figuras 16 e 17, respectivamente.
Os mesmos tipos de resultados também foram observados na execução com matriz de
tamanho 10000x10000, conforme a Tabela 9 e Figuras 18 e 19.
52 Capítulo 4. Resultados e Discussão

Figura 16 – Speedup - Mult. Matrizes 5000x5000 Figura 17 – Eficiência - Mult. Matrizes 5000x5000

Tabela 9 – Resultados Multiplicação de Matrizes - 10000x10000

Threads Linguagem Média Desvio Padrão Speedup Eficiência


1 OpenMP 498.4980296 1.000705108 1 1
Numba 1344.101343 1.459651332 1 1

2 OpenMP 322.3225691 1.155512879 1.54658121 0.773290605


Numba 736.8213334 0.366854248 1.82418896 0.912094481

4 OpenMP 163.16227453 0.577213019 3.05522849 0.763807122


Numba 368.0907945 1.448140878 3.65154837 0.912887094

8 OpenMP 86.86605975 1.539457486 5.73869738 0.717337172


Numba 185.6512882 0.953860146 7.23992468 0.904990585

16 OpenMP 48.48397634 0.584038827 10.2817068 0.642606675


Numba 99.2691853 0.498056729 13.5399654 0.846247843

28 OpenMP 34.34189242 0.001087084 14.5157414 0.518419335


Numba 68.40574359 1.236760651 19.6489545 0.701748375

Figura 18 – Speedup - Mult. Matrizes 10000x10000 Figura 19 – Eficiência - Mult. Matrizes 10000x10000
53

5 Conclusão

Python sem dúvidas é uma das linguagens de programação mais populares e utilizadas
atualmente, principalmente por apresentar uma sintaxe limpa e simples, sendo seu uso possível
para o desenvolvimento de diversas aplicações. Porém, por ser uma linguagem interpretada
(considerando ainda que algumas implementações da mesma utilizam um código pré-compilado
em ByteCode), não oferece um bom desempenho em operações aritméticas quando comparada
com linguagens compiladas.
Testes de desempenho entre implementações do algoritmo do método numérico de Ja-
cobi, para computação em padrão estêncil sobre um array bidimensional, escritos em linguagem
Python original e o mesmo algoritmo codificado para a linguagem C, executando serialmente,
mostraram que o tempo de execução em Python foi 733 vezes mais lento que a mesma versão
em C. Além disso, a implementação do mesmo algoritmo com o uso do módulo padrão de mul-
tithread em Python se mostrou ineficaz, não mostrando nenhum ganho de speedup, por conta
do GIL (Python Global Interpreter Lock).
Por outro lado, a comunidade de usuários e desenvolvedores em Python se mostra extre-
mamente ativa ultimamente em busca de desenvolver soluções para melhorar seu desempenho,
como ocorre com os bem conhecidos pacotes para desenvolvimento de aplicações científicas e
cálculos numéricos como NumPy e SciPy, por exemplo. No que diz respeito a aplicações parale-
las com programação multithread, também existem pacotes disponíveis que buscam minimizar
ou eliminar as restrições do GIL, permitindo implementar paralelismo de fato em códigos im-
plementados em Python. Após uma simples revisão da literatura, foi observado que o pacote
mais utilizado para essa finalidade chama-se Numba.
Dessa maneira, implementações em Python utilizando o pacote Numba e em C utili-
zando OpenMP foram aplicadas aos algoritmos que implementam o já citado método de Jacobi
e também para resolver uma multiplicação de matrizes bidimensionais. Para ambos os casos,
foram feitas variações nos tamanhos das matrizes e no número de threads utilizados na execu-
ção, obtendo assim seus tempos de execução. Com isso foi possível cálcular métricas clássicas
para se medir desempenho, como o speedup e eficiência.
Em ambos os algoritmos, foi observado que as versões codificadas em C/OpenMP, ob-
tiveram menores tempos de execução em todos os cenários, porém a diferença foi considera-
velmente menor para o método de Jacobi. Com uma matriz de 2048x2048, executando com 16
threads, a versão Python/Numba mostrou um tempo de execução apenas 3% mais lento. Em
compensação, os tempos de execução para a multiplicação de matrizes em Python/Numba é
praticamente o dobro daqueles encontrados na versão C/OpenMP.
Isso mostra que, dependendo do algoritmo que está sendo utilizado e paralelizado, có-
54 Capítulo 5. Conclusão

digos implementados em Python/Numba podem apresentar desempenho semelhante ao encon-


trado para versões em C/OpenMP. Python/Numba conta com suporte a pacotes de cálculo nu-
mérico como NumPy, que, se utilizado pode também contribuir na melhora do desempenho em
aplicações numéricas.
Algo importante presenciado neste trabalho, é que códigos escritos em Python/Numba
não se mostraram completamente predizíveis e consistente em seu desempenho, apresentando
variações não esperadas para algumas configurações dos problemas. Notadamente, os tempos
de execução, speedup e eficiência vistos na execução do método de Jacobi com uma matriz
de tamanho 1024x1024 não foram coerentes com o esperado. Não está claro quais os motivos
dessa discrepância nos resultados, sendo necessária uma análise de mais apurada, provavel-
mente necessitando se analisar códigos gerados (pré-compilados e compilados) considerados
de mais "baixo nível"na implementação.
Na maioria das vezes, os códigos em Python/Numba se mostraram eficientes na parale-
lização dos algoritmos, apresentando um speedup compatível, e até mesmo superior em alguns
casos, em comparação com a escalabilidade medida para a versão em C/OpenMP, porem, sem-
pre inferior em desempenho quando a mesma comparação é feita considerando apenas o tempo
decorrido de processamento. Os códigos Python/Numba também se mostraram bastante simples
de ser desenvolvido e utilizado, bastando apenas o uso do decorator @jit na função desejada,
além do uso da instrução prange ao invés do usual range.
O escopo desse trabalho se limitou em analisar o paralelismo em multithread em códi-
gos em Python, porém, outra forma possível de paralelismo é através de multiprocessamento
com múltiplos processos. Como trabalho futuro, análises de desempenho entre o módulo padrão
de multiprocessamento em Python e outros pacotes disponíveis, como mpi4py, podem ser rea-
lizados. Também é perfeitamente plausível a comparação entre diferentes pacotes disponíveis
para programação multithread em Python, tal como os citados Numba, Cython, PyPy, Jython,
etc. O pacote Numba também apresenta suporte a paralelismo com GPU, através de seu mó-
dulo cuda.jit, passível também de testes e análises, podendo ser comparados com códigos em
C/CUDA, C/OpenMP for accelerators ou C/OpenAcc.
55

Referências

AKOUSHIDEH, A.; SHAHBAHRAMI, A. Performance evaluation of matrix-matrix


multiplication using parallel programming models on cpu platforms. 2022. Citado na página
35.

BEAZLEY, D. The Python GIL Visualized. 2010. Disponível em: <http://dabeaz.blogspot-


.com/2010/01/python-gil-visualized.html>http://dabeaz.blogspot.com/2010/01/python-gil-
visualized.html. Citado 2 vezes nas páginas 30 e 31.

BOULLE, A.; KIEFFER, J. High-performance python for crystallographic computing. Journal


of Applied Crystallography, International Union of Crystallography, v. 52, n. 4, p. 882–897,
2019. Citado na página 40.

FAZENDA, L.; STRINGHINI, D. Como programar aplicações de alto desempenho com


produtividade. XXXIX Congresso da Sociedade Brasileira de Computação, 2019. Citado 2
vezes nas páginas 36 e 37.

GMYS, J. et al. A comparative study of high-productivity high-performance programming


languages for parallel metaheuristics. Swarm and Evolutionary Computation, Elsevier, v. 57,
p. 100720, 2020. Citado na página 39.

GUO, P. Python Is Now the Most Popular Introductory Teaching Language at Top U.S.
Universities. 2014. Disponível em: <https://cacm.acm.org/blogs/blog-cacm/176450-python-
is-now-the-most-popular-introductory-teaching-language-at-top-us-universities/fulltext>.
Citado na página 21.

HAGER, G.; WELLEIN, G. Introduction to high performance computing for scientists and
engineers. [S.l.]: CRC Press, 2010. Citado na página 28.

KNUPP, J. Python’s Hardest Problem. 2012. Disponível em: <https://jeffknupp.com/blog-


/2012/03/31/pythons-hardest-problem/>. Citado na página 31.

LABORATORY, L. L. N. Introduction to Parallel Computing Tutorial. 2022. Disponível


em: <https://hpc.llnl.gov/documentation/tutorials/introduction-parallel-computing-tutorial>.
Citado na página 26.

MAROWKA, A. On parallel software engineering education using python. Education and


Information Technologies, Springer, v. 23, n. 1, p. 357–372, 2018. Citado na página 41.

MEIER, R.; GROSS, T. R. Reflections on the compatibility, performance, and scalability of


parallel python. In: Proceedings of the 15th ACM SIGPLAN International Symposium on
Dynamic Languages. [S.l.: s.n.], 2019. p. 91–103. Citado na página 40.

NGUYEN, Q. Mastering Concurrency in Python: Create faster programs using concurrency,


asynchronous, multithreading, and parallel programming. [S.l.]: Packt Publishing Ltd, 2018.
Citado 4 vezes nas páginas 26, 29, 30 e 39.

OVERFLOW, S. Stack Overflow Developer Survey 2021. 2021. Disponível em: <https:/-
/insights.stackoverflow.com/survey/2021technology>. Citado na página 21.
56 Referências

SKUSE, B. The third pillar. Physics World, IOP Publishing, v. 32, n. 3, p. 40, 2019. Citado na
página 21.

VINCENT, P. et al. Towards green aviation with python at petascale. In: IEEE. SC’16:
Proceedings of the International Conference for High Performance Computing, Networking,
Storage and Analysis. [S.l.], 2016. p. 1–11. Citado na página 21.

WELLEIN, G. et al. Efficient temporal blocking for stencil computations by multicore-aware


wavefront parallelization. In: IEEE. 2009 33rd Annual IEEE International Computer Software
and Applications Conference. [S.l.], 2009. v. 1, p. 579–586. Citado na página 34.

YANG, F.; MENARD, J. E. Pyisolver—a fast python oop implementation of lrdfit model. IEEE
Transactions on Plasma Science, IEEE, v. 48, n. 6, p. 1793–1798, 2019. Citado na página 39.

Você também pode gostar