Le regole di programmazione di Rob Pike valgono ancora
Nel 1989 Rob Pike scrisse cinque regole di programmazione. Oggi sono più attuali che mai, soprattutto quelle che continuiamo a ignorare.

Nel 1989 Rob Pike — che in seguito avrebbe co-creato Go, UTF-8 e Plan 9 — mise per iscritto cinque regole di programmazione. Sono abbastanza brevi da stare su un bigliettino e abbastanza profonde da far discutere il mondo della programmazione per 37 anni. La maggior parte degli sviluppatori ne ha visto almeno qualcuna citata in post o talk, ma sono in pochi ad averle davvero interiorizzate, ed è un peccato, perché ci risparmierebbero un bel po' di fatica sprecata.
Le regole sono di una semplicità ingannevole. Non parlano di sintassi o pattern architetturali. Parlano di dove i programmatori sprecano sistematicamente tempo e di come smettere di farlo.
Le cinque regole
Le enuncio chiaramente prima di analizzarle:
- Non puoi sapere in anticipo dove un programma spenderà il suo tempo. I colli di bottiglia compaiono in posti sorprendenti, quindi non cercare di indovinare e non infilare un hack di velocità finché non hai dimostrato che è lì il collo di bottiglia.
- Misura. Non ottimizzare per la velocità finché non hai misurato, e anche allora non farlo a meno che una parte del codice non domini tutte le altre.
- Gli algoritmi sofisticati sono lenti quando n è piccolo, e n di solito è piccolo. Gli algoritmi sofisticati hanno costanti moltiplicative elevate. Finché non sai che n sarà spesso grande, non complicarti la vita.
- Gli algoritmi sofisticati sono più bacati di quelli semplici e sono molto più difficili da implementare. Usa algoritmi semplici così come strutture dati semplici.
- I dati dominano. Se hai scelto le strutture dati giuste e hai organizzato bene le cose, gli algoritmi saranno quasi sempre evidenti. Sono le strutture dati, non gli algoritmi, al centro della programmazione.
Le regole 1 e 2 riguardano l'ottimizzazione. Le regole 3 e 4 riguardano la complessità. La regola 5 riguarda il design. Insieme formano una filosofia che è fondamentalmente una questione di umiltà: ammettere che le nostre intuizioni sulle prestazioni sono sbagliate, che la complessità ha costi che sottovalutiamo e che le buone strutture dati contano più di un codice intelligente.
Regola 1: non sai dov'è il collo di bottiglia
È la regola che gli sviluppatori violano con più sicurezza. «So che questa funzione è lenta perché ha un ciclo annidato». «Dovrei usare una hash map qui perché le ricerche sono O(1)». «Preallocherò questo array perché allocare memoria è costoso». Suonano ragionevoli. Spesso sono sbagliati.
Ho profilato abbastanza sistemi di produzione da avere una piccola collezione di casi in cui il collo di bottiglia intuitivo non era quello reale. Un sistema in cui tutti davano per scontato che fosse il database, ma il profiling ha mostrato che la serializzazione JSON assorbiva il 60% del tempo di richiesta. Una pipeline di dati in cui la «costosa» moltiplicazione di matrici occupava il 5% del tempo di esecuzione mentre il parsing dei CSV ne prendeva il 70%. Un'applicazione web in cui il team aveva ottimizzato le query del database per mesi, mentre il vero collo di bottiglia era la risoluzione DNS a ogni richiesta HTTP in uscita.
Il cervello umano è un pessimo profiler. Sovrappesiamo le operazioni che sembrano concettualmente costose (query al database, chiamate di rete) e sottovalutiamo quelle che sembrano economiche (concatenazione di stringhe, parsing JSON, allocazione di memoria). L'hardware moderno peggiora le cose: cache della CPU, branch prediction ed esecuzione fuori ordine fanno sì che il rapporto tra complessità del codice e tempo di esecuzione sia profondamente contro-intuitivo.
Regola 2: misura prima, poi ottimizza
È la conseguenza pratica della regola 1. Non ottimizzare in base all'intuito. Fai il profiling, trova l'hotspot reale, poi ottimizza quello e solo quello.
La seconda metà di questa regola — «non farlo a meno che una parte del codice non domini tutte le altre» — è altrettanto importante e viene citata molto meno spesso. Se il profiler mostra che il tempo di esecuzione è distribuito in modo uniforme su 20 funzioni, ciascuna con il 5%, non c'è un singolo collo di bottiglia da ottimizzare. Rendere una funzione due volte più veloce fa risparmiare il 2,5% del tempo totale. Di solito non vale la complessità aggiuntiva. Serve un approccio fondamentalmente diverso, invece di ottimizzare le singole funzioni.
# Profile first. Always.
# Python
python -m cProfile -s cumulative my_script.py
# Node.js
node --prof app.js
node --prof-process isolate-*.log > profile.txt
# Go
go test -bench=. -cpuprofile=cpu.prof
go tool pprof cpu.prof
# Rust
cargo flamegraph
# General (Linux)
perf record ./my_program && perf report
# The flamegraph (brendangregg.com/flamegraphs) is the most
# informative visualization. It shows you exactly where time
# is spent, in a format that makes bottlenecks visually obvious.
Regola 3: gli algoritmi sofisticati sono lenti quando N è piccolo
È la regola che l'insegnamento dell'informatica capovolge. Insegniamo che O(n log n) è meglio di O(n²), ed è vero asintoticamente. Ma per n = 20, un'insertion sort O(n²) ben implementata è più veloce di un merge sort O(n log n) per via delle costanti, del comportamento della cache e dell'overhead.
Gli esempi reali abbondano. La ricerca lineare in un array ordinato di 50 elementi è più veloce della ricerca binaria, perché la ricerca lineare ha un comportamento perfetto nella cache e nessuna previsione errata dei branch. Una semplice lista concatenata batte un albero binario bilanciato per collezioni sotto i ~100 elementi, perché inseguire puntatori attraverso un albero distrugge la località della cache. Le hash map hanno un costo di ricerca O(1) ammortizzato, ma la costante è così alta che la ricerca lineare in un array è più veloce per collezioni sotto i ~30-50 elementi.
Le librerie standard lo sanno. Il sorted() di Python usa Timsort, che ricorre all'insertion sort per le sottosequenze piccole. Il std::sort di C++ passa all'insertion sort sotto una soglia (tipicamente 16-32 elementi). Il sort_unstable di Rust usa una combinazione di quicksort e insertion sort. L'algoritmo «sofisticato» viene usato solo dove n è davvero abbastanza grande da farlo vincere.
La lezione più ampia: conosci il tuo n. Se scegli tra un semplice algoritmo O(n²) e uno complesso O(n log n), chiediti quanto sarà grande n in pratica. Se è sotto qualche centinaio, l'algoritmo semplice è quasi certamente sufficiente, e sarà più facile da scrivere, correggere e mantenere.
Regola 4: semplice è meglio che intelligente
La regola 4 estende la regola 3 oltre le prestazioni. Gli algoritmi sofisticati non sono solo più lenti per n piccolo: sono anche più bacati. Un albero rosso-nero ha più casi limite di un array ordinato. Una struttura dati concorrente lock-free ha modalità di guasto più sottili di una protetta da mutex. Un allocatore di memoria personalizzato ha più modi di corrompere la memoria rispetto all'allocatore di sistema.
Ho visto team passare settimane a implementare e fare debug di una cache LRU personalizzata con operazioni O(1), quando un semplice array limitato con eviction lineare sarebbe stato scritto in un pomeriggio, corretto al primo tentativo e abbastanza veloce per il loro carico di lavoro (che aveva al massimo qualche centinaio di voci in cache).
Il costo della complessità non sta solo nell'implementazione iniziale. Sta in ogni sviluppatore futuro che dovrà capire, modificare e fare debug del codice. Un algoritmo semplice che tutto il team comprende vale più di uno intelligente che solo l'autore originale sa mantenere. E l'autore originale, sei mesi dopo, è in pratica un'altra persona che ha anche dimenticato come funziona.
Il debug è due volte più difficile che scrivere il codice in primo luogo. Quindi, se scrivi il codice nel modo più intelligente possibile, per definizione non sei abbastanza intelligente per fare il debug. — Brian Kernighan
Regola 5: i dati dominano
Questa è la regola più importante di Pike, e quella più trascurata da chi si concentra su algoritmi e pattern di design. L'affermazione: se le strutture dati sono giuste, gli algoritmi seguono naturalmente. Se le strutture dati sono sbagliate, nessuna quantità di intelligenza algoritmica ti salverà.
Fred Brooks disse qualcosa di simile: «Mostrami i tuoi diagrammi di flusso e nascondi le tue tabelle, e continuerò a essere perplesso. Mostrami le tue tabelle e di solito non avrò bisogno dei tuoi diagrammi di flusso». Linus Torvalds ha fatto eco: «I cattivi programmatori si preoccupano del codice. I buoni programmatori si preoccupano delle strutture dati e delle loro relazioni».
Questo principio emerge continuamente nella pratica. Una codebase che memorizza i permessi utente come una lista piatta di stringhe accumulerà logica di controllo complessa e soggetta a errori, sparsa per tutto il codice. Ristruttura i dati come gerarchia di ruoli, e la logica di controllo diventa banale. Un sistema che memorizza gli eventi come blob JSON richiederà parsing e validazione complessi in ogni consumer. Strutturando gli eventi come record tipizzati con schemi espliciti, i consumer si semplificano drasticamente.
# Bad data structure → complex algorithm
class OrderSystem:
def __init__(self):
self.orders = [] # flat list of all orders
def get_user_orders(self, user_id):
return [o for o in self.orders if o.user_id == user_id] # O(n)
def get_pending_orders(self):
return [o for o in self.orders if o.status == 'pending'] # O(n)
def get_user_pending_orders(self, user_id):
return [o for o in self.orders
if o.user_id == user_id and o.status == 'pending'] # O(n)
# Better data structure → algorithms become obvious
class OrderSystem:
def __init__(self):
self.orders_by_user = {} # user_id → [orders]
self.orders_by_status = {} # status → [orders]
def get_user_orders(self, user_id):
return self.orders_by_user.get(user_id, []) # O(1)
def get_pending_orders(self):
return self.orders_by_status.get('pending', []) # O(1)
# The right data structure makes the code self-evident.
# You don't need to think about algorithms at all.
Come Go incarna queste regole
È difficile guardare le regole di Pike senza vedere in embrione la filosofia di design di Go. Go, che Pike ha co-creato vent'anni dopo averle scritte, è un linguaggio che privilegia sistematicamente la semplicità rispetto all'intelligenza.
- Niente generici (inizialmente): costringono a strutture dati semplici. (I generici sono arrivati in Go 1.18, ma solo dopo anni di resistenza, finché non è stato trovato un design abbastanza semplice.)
- Niente overloading degli operatori: il codice significa ciò che sembra.
- Niente conversioni di tipo implicite: esplicitezza invece di intelligenza.
- Niente eccezioni: gestisci gli errori dove si verificano.
- Algoritmi della libreria standard ridotti al minimo: usa slice e mappe, non strutture dati sofisticate.
- Profiling integrato (
pprof): misura, non tirare a indovinare.
Go viene spesso criticato come «noioso» da sviluppatori che preferiscono linguaggi più espressivi. Ed è proprio il punto. Le regole di Pike sono una ricetta per codice noioso: semplice, misurabile e costruito su buone strutture dati anziché su algoritmi intelligenti. Go è ciò che succede quando trasformi quella ricetta in un linguaggio.
Dove le regole non si applicano
Nessun insieme di regole è universale, e quelle di Pike hanno eccezioni legittime. I sistemi critici per le prestazioni (motori di gioco, internals dei database, compilatori) a volte hanno bisogno di algoritmi sofisticati perché il loro n è davvero grande. Il codice infrastrutturale che gira milioni di volte al secondo giustifica ottimizzazioni che il codice applicativo non giustifica. E a volte l'algoritmo «semplice» ha complessità O(n³), davvero inaccettabile anche per n modesti.
Le regole sono euristiche, non leggi. Il loro valore sta nel correggere il bias più comune: gli sviluppatori tendono a ottimizzare troppo presto, a usare algoritmi troppo complessi e a pensare troppo al codice e troppo poco alle strutture dati. Le regole di Pike spingono in direzione opposta. Se ti trovi nella rara situazione in cui vale il bias contrario, dove hai davvero bisogno di più complessità e non di meno, allora datti pure da fare con le soluzioni sofisticate. Ma misura prima.
Trentasette anni dopo che Pike le ha scritte, le regole restano tra i migliori consigli di programmazione mai pubblicati. Non perché siano sorprendenti: la maggior parte degli sviluppatori esperti, leggendole, pensa «sì, ovvio». Il valore sta nell'averle enunciate abbastanza chiaramente da poterle applicare con coerenza. La prossima volta che ti viene voglia di un albero rosso-nero, di un allocatore personalizzato o di un'«ottimizzazione» che non hai mai profilato, ricorda: misura prima, mantieni semplice e sistema le strutture dati.


