Salta al contenuto
È uscito il Corso Java Completo
Torna al blog

Cos'è Julia e perché promette la velocità del C con la semplicità di Python

Cos'è Julia: il problema dei due linguaggi, la compilazione JIT, il multiple dispatch, la latenza della prima esecuzione e dove ha davvero senso usarlo.

Edoardo Midali

Edoardo Midali

Developer · Content Creator

9 min di lettura

Hai scritto una simulazione in Python, funziona, ma impiega ore. Ti dicono di vettorizzare tutto con NumPy, poi di riscrivere il ciclo critico in C, e a quel punto hai due programmi da mantenere invece di uno. Julia nasce esattamente da questa frustrazione. In questo articolo trovi cos'è, come riesce a essere veloce senza chiederti di scrivere C, e perché resta comunque un linguaggio di nicchia.

Cos'è Julia

Julia è un linguaggio open source per il calcolo scientifico e numerico, con una sintassi di alto livello simile a Python o MATLAB, che compila il codice in istruzioni macchina al momento dell'esecuzione e raggiunge prestazioni paragonabili ai linguaggi compilati tradizionali.

È nato al MIT da un piccolo gruppo di ricercatori (Jeff Bezanson, Stefan Karpinski, Viral Shah e Alan Edelman) che lavoravano ogni giorno con strumenti diversi e volevano un linguaggio solo che li sostituisse tutti. Il loro manifesto iniziale chiedeva, in sostanza, la velocità del C, la dinamicità di Ruby, la notazione matematica di MATLAB e la facilità d'uso di Python.

Il problema dei due linguaggi

Per capire Julia devi capire il problema che vuole risolvere, perché è lo stesso che hai probabilmente vissuto.

Nel calcolo scientifico il lavoro si svolge storicamente così:

  1. Prototipi in un linguaggio comodo: Python, MATLAB o R. Esplori, provi, cambi idea in fretta.
  2. Riscrivi le parti lente in un linguaggio veloce: C, C++ o Fortran. Il codice diventa rapido ma difficile da modificare.

Questo si chiama problema dei due linguaggi, e ha costi nascosti: chi sa fare la ricerca spesso non sa scrivere C ottimizzato, ogni modifica all'algoritmo va portata in due posti, e i bug si annidano proprio nel punto di passaggio tra i due mondi.

Anche NumPy, a ben vedere, è una soluzione a due linguaggi: la parte veloce è scritta in C, e tu scrivi Python che la chiama. Finché il tuo problema si esprime come operazioni su array intere va benissimo. Appena ti serve un ciclo con una logica particolare, torni alla lentezza di Python puro.

Julia prova a eliminare la seconda fase: scrivi il ciclo nel modo più naturale, e il ciclo è già veloce.

function somma_quadrati(v)
    s = zero(eltype(v))
    for x in v
        s += x^2
    end
    return s
end

dati = rand(10_000_000)
somma_quadrati(dati)

In Python puro un ciclo del genere su dieci milioni di elementi è lento. In Julia viene compilato in codice macchina specializzato per un array di numeri a virgola mobile, e gira alla velocità che otterresti scrivendolo a mano in C.

Come fa a essere veloce: JIT e LLVM

Julia usa una compilazione just-in-time basata su LLVM, la stessa infrastruttura di compilazione usata da molti compilatori moderni.

Il meccanismo è questo: quando chiami una funzione per la prima volta con certi tipi di argomenti, Julia genera una versione di quella funzione specializzata per quei tipi e la compila in codice macchina. Se poi la richiami con gli stessi tipi, usa la versione già compilata. Se la chiami con tipi diversi, ne compila un'altra.

La conseguenza importante è che la velocità dipende da quanto il compilatore riesce a capire i tipi. Se il codice è scritto in modo che i tipi siano prevedibili (si dice "type-stable"), Julia produce codice eccellente. Se i tipi cambiano in modo imprevedibile dentro una funzione, le prestazioni crollano e non sempre te ne accorgi.

Il multiple dispatch: l'idea centrale

Se c'è una cosa che distingue davvero Julia, non è la velocità ma il multiple dispatch.

Nei linguaggi a oggetti classici, quando scrivi a.collidi(b) il metodo scelto dipende dal tipo di a, cioè di un solo argomento. In Julia la funzione eseguita dipende dai tipi di tutti gli argomenti.

abstract type Forma end

struct Cerchio <: Forma
    r::Float64
end

struct Quadrato <: Forma
    lato::Float64
end

area(c::Cerchio) = π * c.r^2
area(q::Quadrato) = q.lato^2

collisione(a::Cerchio, b::Cerchio) = "controllo tra due cerchi"
collisione(a::Cerchio, b::Quadrato) = "controllo cerchio-quadrato"
collisione(a::Quadrato, b::Cerchio) = collisione(b, a)

Non ci sono classi che "possiedono" i metodi: ci sono tipi di dati e funzioni con più versioni, e Julia sceglie la versione giusta guardando tutti gli argomenti insieme.

Sembra un dettaglio tecnico, ma spiega il fenomeno più interessante dell'ecosistema: i pacchetti si compongono tra loro senza essersi mai conosciuti. Se un pacchetto definisce un nuovo tipo numerico, per esempio numeri con incertezza di misura, e un altro pacchetto risolve equazioni differenziali scritte in modo generico, puoi passare i primi al secondo e spesso funziona subito. Il caso classico citato dalla comunità è proprio DifferentialEquations.jl usato insieme a Measurements.jl, per propagare l'incertezza lungo una simulazione. Nessuno dei due autori ha dovuto scrivere codice di collegamento.

In Python ottenere lo stesso risultato richiederebbe che le librerie fossero progettate apposta per parlarsi.

Una sintassi pensata per la matematica

La sintassi di Julia è vicina alla notazione che trovi su un libro di analisi numerica:

x = range(0, 2π, length=100)
y = sin.(x) .* exp.(-x ./ 5)

A = [4.0 -2.0; 1.0 1.0]
b = [2.0, 3.0]
soluzione = A \ b

Il punto dopo la funzione (sin.(x)) applica l'operazione elemento per elemento, il backslash risolve un sistema lineare come in MATLAB, e puoi usare simboli come π direttamente nel codice.

Gli indici partono da 1, come in MATLAB, R e Fortran. Per chi arriva da Python o C è la prima trappola; per chi arriva dal mondo scientifico è la cosa più naturale.

Il problema onesto: il "time to first plot"

Ecco la critica più famosa, e va detta senza giri di parole. La prima volta che esegui qualcosa in una sessione nuova, Julia è lenta. Carichi un pacchetto per i grafici, chiedi il primo grafico, e aspetti.

Il motivo è la stessa compilazione JIT che rende Julia veloce: prima di eseguire, deve compilare tutto il codice che serve, compreso quello dei pacchetti. La comunità ha dato a questo fenomeno un nome preciso, "time to first plot", perché il caso tipico era proprio aspettare il primo grafico.

Le versioni recenti hanno migliorato molto la situazione, salvando in anticipo più codice già compilato dei pacchetti. Ma il modello resta quello: Julia paga all'inizio per andare veloce dopo. Per una simulazione che gira un'ora è irrilevante. Per uno script da due secondi lanciato da riga di comando, è fastidioso.

Dove ha senso (e dove no)

AmbitoJulia ha senso?Perché
Simulazioni numericheMoltoCicli veloci senza riscrivere in C
Equazioni differenzialiMoltoDifferentialEquations.jl è tra i pacchetti più completi
Ottimizzazione matematicaMoltoJuMP è uno strumento maturo e usato in ricerca
Ricerca e calcolo accademicoSìPrototipo e codice finale coincidono
Machine learning in aziendaPocoL'ecosistema è in Python, vedi machine learning
Analisi dati e statisticaPocoPython e R hanno più pacchetti e più persone
Script brevi e automazioneNoLa latenza iniziale pesa più del guadagno
Sviluppo web e appNoNon è il suo territorio

Per l'analisi dati classica, il confronto che conta è ancora quello tra Python e R: Julia ci si affianca, non li sostituisce.

L'ecosistema e il mercato

L'ecosistema di Julia è molto più piccolo di quello di Python. Per i problemi numerici centrali ci sono pacchetti eccellenti, ma appena esci dal calcolo scientifico trovi librerie meno mature, documentazione più scarna e meno risposte già pronte online.

Sul lavoro, onestamente: in Italia le offerte che chiedono Julia sono quasi nulle. Lo incontri soprattutto in università, centri di ricerca e in qualche gruppo di modellazione quantitativa. Se il tuo obiettivo è un impiego, Julia non è la leva giusta; può esserlo se fai ricerca o un dottorato con calcolo intensivo.

Errori comuni

Misurare le prestazioni alla prima chiamata. Il primo tempo include la compilazione. Per sapere quanto è veloce una funzione chiamala due volte, o usa strumenti di benchmark come BenchmarkTools.jl.

Scrivere codice nello scope globale. Le variabili globali non hanno un tipo fisso, e il compilatore non può ottimizzarle. Metti il codice dentro funzioni: è la regola numero uno per avere codice veloce.

Cambiare il tipo di una variabile dentro una funzione. Partire con s = 0 (un intero) e sommarci numeri decimali rende la funzione instabile sui tipi. Per questo nell'esempio sopra c'è zero(eltype(v)).

Ragionare con indici da 0. Un ciclo for i in 0:length(v)-1 in Julia va fuori dall'array. Usa eachindex(v) e il problema sparisce.

Aspettarsi la libreria di Python. Se ti serve un pacchetto specifico per un servizio web, un formato di file raro o un'API aziendale, verifica prima che esista e sia mantenuto.

In sintesi

Julia nasce per eliminare il problema dei due linguaggi: prototipare in un linguaggio comodo e riscrivere in C o Fortran per la velocità. Con la compilazione JIT basata su LLVM, un ciclo scritto in modo naturale è già veloce.

Il multiple dispatch è la sua idea più originale: la funzione eseguita dipende dai tipi di tutti gli argomenti, e questo permette a pacchetti indipendenti di comporsi tra loro senza codice di collegamento.

I limiti sono concreti: la latenza della prima esecuzione, un ecosistema molto più piccolo di Python, indici da 1 che confondono chi viene da altri linguaggi e un mercato del lavoro italiano quasi inesistente.

Quando ha senso impararlo: se fai simulazioni, equazioni differenziali, ottimizzazione o ricerca con calcolo intensivo, e sei stanco di mantenere due versioni dello stesso algoritmo. Per un primo lavoro nei dati, Python resta la scelta più solida.

Per vedere dove si colloca rispetto a tutto il resto, il quadro completo è in tutti i linguaggi di programmazione.