Julia é rápida e posso provar!
Antes de ler o artigo: Meu nome é Lucas Valentim. Tenho 21 anos. Estou indo para o meu quarto período na faculdade de ciência da computação. Estou fazendo uma iniciação científica que utiliza Julia para otimização. Nunca tinha mexido com essa linguagem, dessa forma, meu orientador, para estudos, me deu o livro: “Julia High Performance” - Avik Sengupta. O texto abaixo é um resumo do seu primeiro capítulo. Pretendo, se a faculdade não me consumir antes, trazer o resumo de todo o livro. Obrigado!
Introdução:
De diversas formas, a história das linguagens de programação foi orientada pela necessidade de mexer com números e resolver problemas da área da ciência da computação. Portanto, Julia não é exceção.
Julia combina a facilidade de sintaxe que uma linguagem dinâmica proporciona (maior exemplo seria o Python) e a performance de uma linguagem estática e compilada(maior exemplo seria o C).
Esse resumo está dividido em 3 tópicos:
Julia - dinâmica e rápida
Desenhada para ser rápida
Quão rápido Julia pode ser?
Julia - dinâmica e rápida
Existe o problema das duas linguagens que resumidamente abrange o seguinte contexto: Se você quer produtividade utilize linguagens dinâmicas, mas se você quer velocidade e otimização utilize linguagens estaticamente tipadas. É muito difícil, principalmente em grandes projetos, se beneficiar das duas partes. Porém, a criação de Julia a torna a primeira linguagem moderna a tentar fortemente resolver esse problema, unindo os dois mundos.
Surpreendentemente, Julia possui um modelo de desempenho muito simples. Assim, escrever código rápido nessa linguagem se torna entender elementos chave da arquitetura de um computador e como seu compilador interage com eles.
Dessa forma, Julia abrange os dois lados do espectro da computação. Em uma mão, funciona muito bem na utilização em Raspberry Pi, por exemplo, o que cria uma ótima ambientação para ensinar programação. Em outra mão, Julia tem sido utilizada para executar, em larga escala, aplicações de machine learning em um dos maiores supercomputadores do mundo. Outro exemplo seria “The Celest” que utilizou Julia Build e o Atlas the Sky, onde os cálculos foram executados a uma impressionante taxa de 1,5 petaflops(1 petaflop é 10^15 operações de ponto flutuante por segundo), utilizando 1,3 milhão de threads. Se tornando a primeira linguagem de programação dinâmica a quebrar a barreira de petaflop.
Desenhada para ser rápida.
Quando os criadores de Julia lançaram a linguagem para o mundo, eles fizeram uma postagem em seu blog denominada “Why We Created Julia”. O seguinte trecho foi retirado dessa postagem e dizia o seguinte:
“Queremos uma linguagem de programação que seja de código aberto, com uma licença liberal. Desejamos a velocidade do C com a dinâmica do Ruby. Queremos uma linguagem homoicônica, com macros verdadeiros como o Lisp, mas com uma notação matemática clara e familiar, como o Matlab. Queremos algo tão utilizável para programação geral quanto o Python, tão fácil para estatísticas quanto o R, tão natural para processamento de strings quanto o Perl, tão poderoso para álgebra linear quanto o Matlab, tão bom em unir programas quanto o shell. Algo que seja extremamente simples de aprender, mas que mantenha os hackers mais sérios satisfeitos. Queremos que seja interativo e compilado.”
Link da postagem: https://julialang.org/blog/2012/02/why-we-created-julia/
Uma grande parte da biblioteca padrão de Julia, incluindo a mais básica operação de baixo nível, está escrita em Julia por si só. Como por exemplo a função de soma.
Além disso, os três elementos que permitem a essência da eficiência de Julia são: Compilador JIT (Just in time) de alta performance, LLVM para gerar código de máquina e um sistema de tipo que permite código expressivo.
Porém, somente adicionar LLVM em Julia não necessariamente a torna mais rápida. A forma com que Julia trata “types"! é o segredo da sua eficiência, pois sua sintaxe e semântica foram cuidadosamente desenhadas para permitir seu alto desempenho. Há muito sobre o que falar do cuidado com Julia com seus tipos, mas o capítulo atual irá se contentar somente com essa afirmação.
Porém, é possível dar um exemplo: O compilador de Julia compila códigos diferentes para a mesma função, dependendo do tipo que esta recebeu. Podemos considerar, por exemplo, a função que calcula a potenciação. Essa pode receber dois tipos diferentes, um inteiro ou um ponto flutuante. Porém, dependendo do input, a definição matemática se altera. Pois então, Julia irá compilar duas versões diferentes de código, uma para inteiros e outra para ponto flutuante . Dessa forma, inserirá a chamada apropriada, no código, durante a compilação do programa. Isso significa que, em tempo de execução, sem nenhuma verificação de tipo, o código será executado no CPU.
Uma curiosidade é que Julia permite analisar o código nativo de uma função, nesse sentido podemos perceber na prática o exemplo acima:
julia> @code_native 3^2
.text
.file "^"
.globl "julia_^_75" # -- Begin function julia_^_75
.p2align 4, 0x90
.type "julia_^_75",@function
"julia_^_75": # @"julia_^_75"
; ┌ @ intfuncs.jl:310 within `^`
.cfi_startproc
# %bb.0: # %top
push rbp
.cfi_def_cfa_offset 16
.cfi_offset rbp, -16
mov rbp, rsp
.cfi_def_cfa_register rbp
sub rsp, 32
movabs rax, offset j_power_by_squaring_77
call rax
add rsp, 32
pop rbp
ret
.Lfunc_end0:
.size "julia_^_75", .Lfunc_end0-"julia_^_75"
.cfi_endproc
; └
# -- End function
.section ".note.GNU-stack","",@progbits
julia> @code_native 4.1^2
.text
.file "^"
.section .rodata.cst8,"aM",@progbits,8
.p2align 3 # -- Begin function julia_^_127
.LCPI0_0:
.quad 0x3ff0000000000000 # double 1
.text
.globl "julia_^_127"
.p2align 4, 0x90
.type "julia_^_127",@function
"julia_^_127": # @"julia_^_127"
; ┌ @ math.jl:1246 within `^`
.cfi_startproc
# %bb.0: # %top
push rbp
.cfi_def_cfa_offset 16
.cfi_offset rbp, -16
mov rbp, rsp
.cfi_def_cfa_register rbp
sub rsp, 32
; │ @ math.jl:1247 within `^`
; │┌ @ promotion.jl:521 within `==`
test rdx, rdx
; │└
je .LBB0_1
# %bb.3: # %L4
; │ @ math.jl:1248 within `^`
movabs rax, offset j_pow_body_129
call rax
jmp .LBB0_2
.LBB0_1:
movabs rax, offset .LCPI0_0
vmovsd xmm0, qword ptr [rax] # xmm0 = mem[0],zero
.LBB0_2: # %common.ret
; │ @ math.jl within `^`
add rsp, 32
pop rbp
ret
.Lfunc_end0:
.size "julia_^_127", .Lfunc_end0-"julia_^_127"
.cfi_endproc
; └
# -- End function
.section ".note.GNU-stack","",@progbits
Percebe-se então que para a mesma função, porém tipos diferentes, temos compilações também distintas.
Quão rápido Julia pode ser?
O exemplo do livro é mais visual e, também, bastante simples. Porém a comparação é válida. Dessa forma, com o objetivo de não estender mais o assunto, deixo com os que chegaram até aqui e, portanto, os mais curiosos, o link de um artigo interessante mostrando a performance de Julia em comparação com as outras linguagens:
https://discourse.julialang.org/t/comparing-python-julia-and-c/17019