Des articles approfondis sur les technologies qui façonnent l'avenir.

Les règles de programmation de Rob Pike restent d'actualité

Rob Pike a écrit cinq règles de programmation en 1989. Elles sont plus pertinentes aujourd'hui, surtout celles qu'on continue d'ignorer.

Vieille fiche cartonnée épinglée à côté d'un bureau moderne encombré, sous une seule lampe

En 1989, Rob Pike, qui allait plus tard co-créer Go, UTF-8 et Plan 9, a rédigé cinq règles de programmation. Elles sont assez courtes pour tenir sur une fiche, et assez profondes pour que le monde du développement en débatte depuis 37 ans. La plupart des développeurs en ont déjà vu au moins quelques-unes citées dans des articles de blog ou des conférences. Moins nombreux sont ceux qui les ont vraiment intégrées, ce qui est dommage, car elles permettraient d'économiser beaucoup d'efforts inutiles.

Ces règles sont trompeusement simples. Elles ne portent ni sur la syntaxe ni sur les patterns d'architecture. Elles parlent de l'endroit où les programmeurs perdent systématiquement du temps, et de la façon d'arrêter.

Les cinq règles

Commençons par les énoncer simplement avant de les disséquer :

  1. Vous ne pouvez pas savoir où un programme va passer son temps. Les goulots d'étranglement apparaissent à des endroits surprenants, donc n'essayez pas de deviner et n'ajoutez pas de hack de performance tant que vous n'avez pas prouvé que c'est là que se trouve le goulot.
  2. Mesurez. N'optimisez pas la vitesse tant que vous n'avez pas mesuré, et même alors, ne le faites que si une partie du code écrase le reste.
  3. Les algorithmes sophistiqués sont lents quand n est petit, et n est généralement petit. Les algorithmes sophistiqués ont de grosses constantes. Tant que vous ne savez pas que n sera souvent grand, ne vous compliquez pas la vie.
  4. Les algorithmes sophistiqués sont plus buggés que les simples, et beaucoup plus difficiles à implémenter. Utilisez des algorithmes simples, tout comme des structures de données simples.
  5. Les données dominent. Si vous avez choisi les bonnes structures de données et bien organisé les choses, les algorithmes en découleront presque toujours d'eux-mêmes. Ce sont les structures de données, et non les algorithmes, qui sont au cœur de la programmation.

Les règles 1 et 2 concernent l'optimisation. Les règles 3 et 4 concernent la complexité. La règle 5 concerne la conception. Ensemble, elles forment une philosophie fondamentalement fondée sur l'humilité : admettre que nos intuitions sur la performance sont fausses, que la complexité a des coûts que l'on sous-estime, et que de bonnes structures de données comptent davantage que du code astucieux.

Règle 1 : vous ne savez pas où est le goulot d'étranglement

C'est la règle que les développeurs violent le plus allègrement. « Je sais que cette fonction est lente parce qu'elle contient une boucle imbriquée. » « Je devrais utiliser une table de hachage ici, car les recherches sont en O(1). » « Je vais préallouer ce tableau, parce que l'allocation coûte cher. » Tout cela semble raisonnable. C'est souvent faux.

J'ai profilé suffisamment de systèmes en production pour avoir une collection d'exemples où le goulot d'étranglement intuitif n'était pas le vrai. Un système où tout le monde pensait que la base de données était le problème, alors que le profilage montrait que la sérialisation JSON consommait 60 % du temps de requête. Un pipeline de données où la multiplication matricielle « coûteuse » ne représentait que 5 % de l'exécution, alors que le parsing CSV en prenait 70 %. Une application web dont l'équipe a optimisé ses requêtes SQL pendant des mois, alors que le vrai goulot était la résolution DNS à chaque requête HTTP sortante.

Le cerveau humain est un très mauvais profileur. Nous surpondérons les opérations qui semblent conceptuellement coûteuses (requêtes en base, appels réseau) et sous-pondérons celles qui semblent bon marché (concaténation de chaînes, parsing JSON, allocation mémoire). Le matériel moderne aggrave le problème : caches CPU, prédiction de branchement et exécution out-of-order font que le lien entre la complexité du code et le temps d'exécution est profondément contre-intuitif.

Règle 2 : mesurez d'abord, optimisez ensuite

C'est le corollaire pratique de la règle 1. N'optimisez pas à l'intuition. Profilez. Trouvez le vrai point chaud. Puis optimisez celui-là, et seulement celui-là.

La seconde moitié de cette règle, « pas tant qu'une partie du code n'écrase pas le reste », est tout aussi importante et bien moins citée. Si votre profileur montre que le temps d'exécution est réparti de façon homogène sur 20 fonctions, chacune à 5 %, il n'y a pas de goulot unique à optimiser. Rendre une fonction deux fois plus rapide ne fait gagner que 2,5 % du temps total. Cela ne vaut que rarement la complexité ajoutée. Il faut trouver une approche fondamentalement différente plutôt que d'optimiser des fonctions une à une.

# 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.

Règle 3 : les algorithmes sophistiqués sont lents quand n est petit

C'est la règle que l'enseignement de l'informatique présente à l'envers. On nous apprend que O(n log n) est meilleur que O(n²), ce qui est vrai asymptotiquement. Mais pour n = 20, un tri par insertion O(n²) bien implémenté est plus rapide qu'un tri fusion en O(n log n), à cause des constantes, du comportement du cache et des coûts annexes.

Les exemples concrets abondent. Une recherche linéaire dans un tableau trié de 50 éléments est plus rapide qu'une recherche dichotomique, car la recherche linéaire a un comportement de cache parfait et aucune mauvaise prédiction de branchement. Une simple liste chaînée bat un arbre binaire équilibré pour des collections de moins de ~100 éléments, parce que le parcours de pointeurs dans un arbre détruit la localité du cache. Les tables de hachage offrent une recherche en O(1) amorti, mais la constante est assez élevée pour qu'une recherche linéaire dans un tableau soit plus rapide en dessous de ~30-50 éléments.

Les bibliothèques standard le savent. La fonction sorted() de Python utilise Timsort, qui repasse sur un tri par insertion pour les petites sous-séquences. std::sort en C++ bascule sur un tri par insertion en dessous d'un seuil (typiquement 16 à 32 éléments). sort_unstable en Rust combine quicksort et tri par insertion. L'algorithme « sophistiqué » n'est utilisé que lorsque n est réellement assez grand pour qu'il gagne.

La leçon plus générale : connaissez votre n. Si vous hésitez entre un algorithme simple en O(n²) et un algorithme complexe en O(n log n), demandez-vous quelle sera réellement la taille de n en pratique. Si elle reste sous quelques centaines, l'algorithme simple convient presque certainement, et il sera plus facile à écrire, déboguer et maintenir.

Règle 4 : le simple vaut mieux que l'astucieux

La règle 4 prolonge la règle 3 au-delà de la performance. Les algorithmes sophistiqués ne sont pas seulement plus lents pour les petits n : ils sont aussi plus buggés. Un arbre rouge-noir a plus de cas limites qu'un tableau trié. Une structure de données concurrente sans verrou a des modes de défaillance plus subtils qu'une structure protégée par un mutex. Un allocateur mémoire maison peut corrompre la mémoire de plus de façons que l'allocateur système.

J'ai vu des équipes passer des semaines à implémenter et déboguer un cache LRU maison en O(1), alors qu'un simple tableau borné avec éviction linéaire aurait été écrit en un après-midi, correct du premier coup, et assez rapide pour leur charge (qui comptait au plus quelques centaines d'entrées en cache).

Le coût de la complexité ne se limite pas à l'implémentation initiale. Il réside dans chaque futur développeur qui devra la comprendre, la modifier et la déboguer. Un algorithme simple que tout le monde dans l'équipe comprend vaut plus qu'un algorithme astucieux que seul son auteur peut maintenir. Et cet auteur, six mois plus tard, est en fait une autre personne, qui a aussi oublié comment ça marche.

Déboguer est deux fois plus difficile que d'écrire le code. Donc, si vous écrivez le code le plus astucieusement possible, vous n'êtes, par définition, pas assez malin pour le déboguer. — Brian Kernighan

Règle 5 : les données dominent

C'est la règle la plus importante de Pike, et celle que les développeurs obsédés par les algorithmes et les patterns de conception négligent le plus. L'affirmation : si vos structures de données sont bonnes, les algorithmes en découlent naturellement. Si elles sont mauvaises, aucune astuce algorithmique ne vous sauvera.

Fred Brooks l'a dit à sa façon : « Montrez-moi vos organigrammes et cachez-moi vos tables, et je resterai perplexe. Montrez-moi vos tables, et je n'aurai généralement pas besoin de vos organigrammes. » Linus Torvalds a repris l'idée : « Les mauvais programmeurs se soucient du code. Les bons programmeurs se soucient des structures de données et de leurs relations. »

Ce principe apparaît constamment en pratique. Une base de code qui stocke les permissions utilisateur sous forme de liste plate de chaînes accumulera une logique de vérification complexe et sujette aux erreurs, dispersée partout. Restructurez les données en hiérarchie de rôles, et la logique de vérification devient triviale. Un système qui stocke des événements sous forme de blobs JSON exigera un parsing et une validation complexes chez chaque consommateur. Structurez les événements en enregistrements typés avec des schémas explicites, et les consommateurs se simplifient radicalement.

# 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.

Comment Go incarne ces règles

Difficile de lire les règles de Pike sans voir déjà en germe la philosophie de conception de Go. Go, que Pike a co-créé 20 ans après avoir écrit ces règles, est un langage qui privilégie systématiquement la simplicité à l'astuce.

  • Pas de génériques (au départ) : on force des structures de données simples. (Les génériques sont arrivés avec Go 1.18, mais seulement après des années de résistance, le temps de trouver un design suffisamment simple.)
  • Pas de surcharge d'opérateurs : le code signifie ce qu'il paraît dire.
  • Pas de conversions de types implicites : l'explicite plutôt que l'astucieux.
  • Pas d'exceptions : gérez les erreurs là où elles surviennent.
  • Algorithmes de la bibliothèque standard réduits au minimum : utilisez des slices et des maps, pas des structures de données sophistiquées.
  • Profilage intégré (pprof) : mesurez, ne devinez pas.

Go est souvent critiqué comme « ennuyeux » par les développeurs qui préfèrent des langages plus expressifs. C'est précisément le but. Les règles de Pike sont une recette pour du code ennuyeux : simple, mesurable, et bâti sur de bonnes structures de données plutôt que sur des algorithmes astucieux. Go, c'est ce qui arrive quand on transforme cette recette en langage.

Là où les règles ne s'appliquent pas

Aucun ensemble de règles n'est universel, et celles de Pike ont des exceptions légitimes. Les systèmes critiques pour la performance (moteurs de jeu, internes de bases de données, compilateurs) ont parfois besoin d'algorithmes sophistiqués, parce que leur n est vraiment grand. Le code d'infrastructure qui s'exécute des millions de fois par seconde justifie des optimisations que le code applicatif n'a pas besoin. Et parfois, l'algorithme « simple » a une complexité en O(n³) vraiment inacceptable, même pour un n modeste.

Ces règles sont des heuristiques, pas des lois. Leur valeur tient à la correction d'un biais courant : les développeurs ont tendance à optimiser trop tôt, à utiliser des algorithmes trop complexes et à trop penser au code et pas assez aux structures de données. Les règles de Pike vont à contre-courant de ces tendances. Si vous vous trouvez dans la rare situation où le biais inverse s'applique, où il vous faut vraiment plus de complexité et non moins, alors allez-y, soyez sophistiqué. Mais mesurez d'abord.

Trente-sept ans après que Pike les a couchées sur le papier, ces règles restent parmi les meilleurs conseils de programmation jamais publiés. Non parce qu'elles sont surprenantes : la plupart des développeurs expérimentés les lisent en pensant « oui, évidemment ». Leur valeur tient à les avoir énoncées assez clairement pour les appliquer de façon constante. La prochaine fois que vous serez tenté par un arbre rouge-noir, un allocateur maison ou une « optimisation » que vous n'avez pas profilée, rappelez-vous : mesurez d'abord, gardez-le simple, et placez bien vos structures de données.