TC_com_Python (1)
TC_com_Python (1)
Denise Stringhin
Convidado 2
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
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.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
• 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
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
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.
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.
"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
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
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.
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.
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 ]
𝑇 𝑒𝑥𝑒𝑐(1)
𝑆𝑝𝑒𝑒𝑑𝑢𝑝(𝑃 ) = (2.1)
𝑇 𝑒𝑥𝑒𝑐(𝑃 )
Nos quais,
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
𝑆𝑝𝑒𝑒𝑑𝑢𝑝(𝑃 )
𝐸𝑓 𝑖𝑐𝑖𝑒𝑛𝑐𝑖𝑎(𝑃 ) = (2.2)
𝑃
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
4 Resultados e Discussão
• Python v3.9.6;
• Numba v0.56.4;
• NumPy v1.23.5;
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.
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
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
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 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
Na Tabela 6 são apresentados os resultados obtidos através da execução com uma matriz
de tamanho 1024x1024.
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
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.
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
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.
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
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
Referências
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.
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.
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.