Perché Linux è così veloce: la spiegazione di Linus Torvalds

Linus Torvalds spiega perché Linux è così veloce: dal C alle micro-ottimizzazioni, fino all'arrivo di Rust nel kernel. Come si coniuga la visione di 14 anni fa con le novità di oggi.
Perché Linux è così veloce: la spiegazione di Linus Torvalds

Nel 2012, durante un evento svoltosi presso la Università Aalto, in Finlandia, Linus Torvalds ricevette una domanda che oggi suona ancora più interessante di allora: esistono linguaggi moderni adatti a scrivere un sistema operativo al posto del C? La risposta del “re pinguino” prese le mosse da un’osservazione concreta: Linux era già stato ottimizzato per anni intorno al linguaggio C, fino al punto da sfruttarne caratteristiche e comportamento del codice generato in porzioni estremamente sensibili alle prestazioni.

Torvalds non stava però semplicemente sostenendo che “C è più veloce“. Il suo ragionamento riguardava qualcosa di più profondo: per chi sviluppa il kernel di un sistema operativo conta poter prevedere con buona precisione cosa produrrà il compilatore, osservare l’assembly risultante e intervenire anche su dettagli che, in un normale programma applicativo, sembrerebbero trascurabili.

A distanza di 14 anni, quelle parole acquistano un significato particolare. Linux continua a poggiare in larghissima parte sul C, ma nel frattempo il kernel ha aperto le porte a Rust, proprio uno dei linguaggi che cercano di combinare prestazioni native, controllo di basso livello e maggiori garanzie sulla sicurezza della memoria. Greg Kroah-Hartman, una delle figure dipinte come più probabili sostituti per Torvalds, ha strenuamente difeso l’uso di Rust nel kernel Linux e anzi ha rimarcato che c’è bisogno di sviluppatori Rust.

Rileggere oggi l’intervento di Torvalds permette di affrontare una domanda più interessante del semplice confronto tra linguaggi: che cosa rende davvero veloce Linux e quali caratteristiche deve possedere un linguaggio per entrare nelle parti più delicate del kernel?

Linux non è veloce semplicemente perché scritto in C

Torvalds osservava che Linux risultava fortemente ottimizzato intorno al C. Il punto fondamentale, però, emerge meglio dal seguito del ragionamento: per alcune porzioni del kernel gli sviluppatori lavorano a un livello nel quale contano perfino le istruzioni prodotte dal compilatore, gli accessi alla memoria e il comportamento delle cache della CPU.

Il vantaggio ricercato da Torvalds consiste nella possibilità di ottenere codice macchina sufficientemente prevedibile e micro-ottimizzabile, non nell’esistenza di una proprietà magica del linguaggio C.

Un kernel attraversa continuamente percorsi eseguiti milioni o miliardi di volte: gestione delle chiamate di sistema, scheduler, memoria virtuale, rete, sincronizzazione tra core, gestione degli interrupt e accesso ai file. Risparmiare poche istruzioni in un percorso raramente utilizzato non cambia nulla; eliminarle da un’operazione eseguita continuamente può invece produrre una differenza misurabile sull’intero sistema. Torvalds è storicamente interessato proprio a queste ottimizzazioni apparentemente minuscole ma che possono tradursi in un grande impatto.

L’esempio dei nomi dei file spiega bene il ragionamento

Durante il suo intervento Torvalds citò come esempio la ricerca dei nomi nel file system: un’attività che Linux esegue continuamente quando un programma apre un file, esamina una directory oppure accede a un percorso.

Quando un’applicazione chiede di aprire, ad esempio, /home/mario/documenti/report.pdf il kernel deve risolvere uno dopo l’altro i componenti del percorso e associarli agli oggetti interni che rappresentano directory e file.

Linux dispone per questo della dcache, la directory entry cache: mantiene in RAM le informazioni sui nomi già incontrati in modo da evitare, quando possibile, di interrogare nuovamente il file system sottostante. La documentazione ufficiale del kernel descrive esplicitamente la dcache come un meccanismo pensato per rendere estremamente rapida la traduzione di un pathname nella corrispondente dentry.

Il lookup può inoltre usare RCU-walk, modalità progettata per percorrere i pathname evitando numerose operazioni e alcuni costi di sincronizzazione. Quando le condizioni lo permettono il kernel resta sul percorso rapido; soltanto nei casi più complessi passa alla modalità REF-walk, più costosa ma capace di gestire situazioni che richiedono riferimenti e locking tradizionale.

La velocità di Linux – spiega Torvalds – nasce spesso dall’ottimizzazione ossessiva di operazioni apparentemente insignificanti, ma presenti praticamente ovunque.

C è davvero “vicino all’hardware”?

Su una CPU non basta però contare le istruzioni assembly. Un accesso ai dati già presenti nelle cache più vicine al core del processore può costare enormemente meno rispetto alla lettura dalla memoria principale.

A livello kernel, quindi, strutture dati, dimensioni degli oggetti, ordine degli accessi e percorsi condizionali possono essere progettati per evitare letture inutili e aumentare la probabilità che il processore trovi ciò che gli serve nella cache.

Un algoritmo con qualche istruzione in più può quindi batterne uno apparentemente più corto se provoca meno cache miss. Analogamente, eliminare un puntatore da inseguire o evitare una sincronizzazione può contare più di una trasformazione aritmetica particolarmente sofisticata. In questo senso, il linguaggio C è “vicino all’hardware“.

C’è da dire, a voler spaccare il capello, che C non costituisce certo una rappresentazione diretta dell’hardware. Un chip x86-64 può decodificare le istruzioni in micro-operazioni interne, riordinarne l’esecuzione, eseguirne diverse contemporaneamente, prevedere i salti, speculare sui risultati e utilizzare più livelli di cache. L’ordine descritto in assembly non coincide necessariamente con l’ordine temporale secondo cui il silicio completa materialmente il lavoro. In mezzo c’è poi il compilatore.

Il vantaggio del linguaggio C, per uno sviluppatore del kernel esperto, consiste nella possibilità di esprimere operazioni di basso livello senza imporre un grande runtime e lascia comunque la possibilità di compilare, disassemblare il risultato, misurarlo e modificare il sorgente finché il codice macchina assume le caratteristiche desiderate.

Nel 2012 mancava però un protagonista: Rust

Il dettaglio che rende particolarmente interessante riascoltare oggi l’intervento di Torvalds è la data: 2012.

Dieci anni dopo, infatti, durante il ciclo di sviluppo di Linux 6.1, Torvalds avrebbe accettato ufficialmente l’infrastruttura iniziale per Rust. All’epoca si trattava di preparare il terreno per permettere lo sviluppo futuro, non di una conversione del kernel né dell’introduzione immediata di grandi sottosistemi scritti nel nuovo linguaggio.

La situazione si è poi evoluta. La documentazione ufficiale corrente del kernel prevede esplicitamente il supporto Rust tramite CONFIG_RUST; il codice viene compilato con rustc e dispone di binding e astrazioni che consentono di interagire con le parti esistenti scritte in C.

Rust offre qualcosa che il C non può garantire allo stesso livello: molte proprietà relative alla sicurezza della memoria sono direttamemente controllate dal compilatore.

Come spiegato nel nostro articolo citato in precedenza, ownership, borrowing e lifetime permettono di impedire – al momento della compilazione – numerosi errori che in C possono trasformarsi in use-after-free, double free, iterator invalidation o accessi concorrenti non corretti. La differenza può essere particolarmente importante per i driver, una categoria nella quale errori di gestione della memoria possono provocare crash o vulnerabilità del kernel.

Ma Rust non richiede necessariamente di rinunciare alle caratteristiche che Torvalds considerava importanti nel 2012. Non utilizza un garbage collector obbligatorio, può produrre codice nativo ottimizzato e consente di ispezionare l’assembly prodotto dal compilatore esattamente come accade con C o C++.

Il kernel non sarà completamente riscritto in Rust

L’ingresso di Rust nel kernel Linux non implica la sostituzione di milioni di righe di C funzionanti e ottimizzate da decenni. L’architettura adottata permette invece una convivenza: C continua a rappresentare la base del kernel mentre Rust può essere utilizzato progressivamente dove le sue caratteristiche offrono vantaggi concreti. Le astrazioni Rust del kernel incapsulano funzionalità esistenti sul lato C e i binding permettono ai due mondi di interagire.

Riscrivere codice maturo comporterebbe il rischio di introdurre regressioni, perdere ottimizzazioni accumulate nel tempo e dover nuovamente validare comportamenti già collaudati su un numero enorme di configurazioni hardware.

A distanza di 14 anni, quindi, la parte più interessante dell’intervento di Torvalds non è la preferenza personale per C. È il metodo.

Per ottenere prestazioni elevate nelle parti fondamentali di un sistema operativo bisogna conoscere il costo delle operazioni. C ha fornito a Linux una risposta estremamente efficace per oltre 30 anni; Rust sta cercando di aggiungere un’altra proprietà al medesimo obiettivo: mantenere prestazioni e controllo di basso livello impedendo al compilatore di accettare intere categorie di errori.

Ti consigliamo anche

Link copiato negli appunti