Article original : Why Your Code is Slow: Common Performance Mistakes Beginners Make
Peut-être avez-vous déjà vécu une situation similaire : vous avez écrit un code qui fonctionne, mais lorsque vous appuyez sur « run », cela prend une éternité. Vous fixez le curseur de chargement en vous demandant s'il ne serait pas plus rapide de résoudre le problème à la main.
Mais vous finissez par ressembler à peu près à ça… 😭⬇️⬇️

Voici la vérité : un code lent n'est pas une fatalité. Et c'est un rite de passage pour tout développeur.
Quand on apprend à coder, on se concentre sur le fait que les choses fonctionnent, pas sur le fait qu'elles soient rapides. Mais tôt ou tard, on finit par se heurter à un mur : votre application se fige, votre script de données prend des heures, ou votre jeu lag comme un diaporama PowerPoint.
La différence entre un code fonctionnel et un code ultra-rapide réside souvent dans l'évitement de quelques erreurs courantes. Des erreurs faciles à commettre quand on débute, comme utiliser le mauvais outil pour la tâche, écrire du code inutile ou torturer accidentellement son ordinateur avec des inefficacités cachées.
Je suis passé par là. J'ai un jour écrit un script « rapide » pour analyser des données. Il a tourné pendant 3 heures. Il s'est avéré qu'en changeant une seule ligne de code, le temps est passé à 10 secondes. Oui, j'étais maladroit quand j'apprenais – mais je ne veux pas que vous le soyez aussi.
C'est tout l'intérêt de comprendre la performance.
Dans ce guide, je vais détailler sept erreurs courantes qui peuvent réellement plomber la vitesse de votre code — et comment les corriger.
Table des matières
Erreur #2 : Utiliser les mauvaises boucles (quand il existe une alternative plus rapide)
Erreur #3 : Écrire des requêtes de base de données à l'intérieur de boucles (tueur de vitesse)
Erreur #4 : Ne pas connaître les sombres secrets de votre matériel
Problème 1 : La boule de cristal du CPU est cassée (pré-chargement de la mémoire)
Problème 2 : La taxe invisible des pages mémoire (balayage TLB)
Problème 3 : Votre code est un touriste dans le mauvais quartier CPU (NUMA)
La solution : épingler les processus à la mémoire sensible au NUMA
Problème 4 : Le CPU est une « drama queen » (exécution spéculative)
Comment les développeurs pros écrivent-ils du code plus rapide ?
Erreur #1 : Tout journaliser en production (sans s'en rendre compte)
Le logging est censé vous aider à comprendre ce qui se passe dans votre code — mais si vous journalisez tout, vous le ralentissez réellement. Une erreur courante chez les débutants est de laisser des instructions print() partout ou d'activer la journalisation détaillée même en production, là où la performance compte le plus.
Au lieu de ne journaliser que ce qui est utile, ils journalisent chaque appel de fonction, chaque entrée, chaque sortie, et parfois même des corps de requête entiers ou des requêtes de base de données. Cela peut sembler inoffensif, mais dans une application réelle traitant des milliers d'opérations par seconde, un logging excessif peut causer des ralentissements majeurs.
Pourquoi est-ce un problème
Le logging n'est pas gratuit. Chaque message de log, qu'il soit affiché dans la console ou écrit dans un fichier, ajoute un temps de traitement supplémentaire. Si la journalisation est effectuée de manière synchrone (ce qui est souvent le cas par défaut), votre application peut suspendre l'exécution en attendant que le log soit enregistré.
Cela gaspille également de l'espace disque. Si chaque requête est journalisée en détail, les fichiers de logs peuvent croître rapidement, dévorant le stockage et rendant plus difficile la recherche d'informations utiles lors du débogage.
Voici un exemple :
def process_data(data):
print(f"Processing data: {data}") # Journalisation de chaque entrée
result = data * 2
print(f"Result: {result}") # Journalisation de chaque résultat
return result
Si cette fonction s'exécute dans une boucle traitant plus de 10 000 opérations, ces instructions print ralentissent considérablement les choses.
Comment y remédier
Au lieu de tout journaliser, concentrez-vous sur ce qui compte vraiment. Un bon logging vous aide à diagnostiquer des problèmes réels sans encombrer vos journaux ni ralentir votre application.
Par exemple, supposons que vous traitiez des transactions d'utilisateurs. Vous n'avez pas besoin de journaliser chaque étape du calcul, mais journaliser le début, le succès ou l'échec d'une transaction est précieux.
// ❌ Mauvaise journalisation
logging.info(f"Received input: {data}")
logging.info(f"Processing transaction for user {user_id}")
logging.info(f"Transaction intermediate step 1 result: {some_var}")
logging.info(f"Transaction intermediate step 2 result: {another_var}")
logging.info(f"Transaction completed: {final_result}")
// ✅ Meilleure journalisation
logging.info(f"Processing transaction for user {user_id}")
logging.info(f"Transaction successful. Amount: ${amount}")
Ensuite, assurez-vous que les logs de débogage sont désactivés en production. Les logs de débogage (logging.debug()) sont parfaits pendant le développement car ils montrent des informations détaillées, mais ils ne devraient pas fonctionner sur des serveurs en direct.
Vous pouvez contrôler cela en réglant le niveau de logging sur INFO ou supérieur :
import logging
logging.basicConfig(level=logging.INFO) # Journalise uniquement les messages INFO, WARNING, ERROR, CRITICAL
def process_data(data):
logging.debug(f"Processing data: {data}") # Ne s'affichera pas en production
return data * 2
Enfin, pour les applications haute performance, envisagez d'utiliser la journalisation asynchrone. Par défaut, les opérations de logging peuvent bloquer l'exécution, ce qui signifie que votre programme attend que le message de log soit écrit avant de continuer. Cela peut être un goulot d'étranglement, surtout si vous journalisez dans un fichier ou un service de journalisation distant.
La journalisation asynchrone résout ce problème en gérant les logs en arrière-plan. Voici comment vous pouvez la configurer avec le QueueHandler de Python :
import logging
import logging.handlers
import queue
log_queue = queue.Queue()
queue_handler = logging.handlers.QueueHandler(log_queue)
logger = logging.getLogger()
logger.addHandler(queue_handler)
logger.setLevel(logging.INFO)
logger.info("This log is handled asynchronously!")
Erreur #2 : Utiliser les mauvaises boucles (quand il existe une alternative plus rapide)
Pourquoi est-ce un problème
Les boucles sont l'une des premières choses que l'on apprend en programmation, et les boucles for semblent naturelles — elles vous donnent le contrôle, elles sont faciles à comprendre et elles fonctionnent partout. C'est pourquoi les débutants ont tendance à y recourir automatiquement.
Mais ce n'est pas parce que quelque chose fonctionne que c'est la meilleure méthode. En Python, les boucles for peuvent être lentes — surtout lorsqu'il existe une alternative intégrée qui fait le même travail plus rapidement et plus efficacement.
Ce n'est pas propre à Python. La plupart des langages de programmation ont des moyens optimisés de gérer les boucles sous le capot — qu'il s'agisse d'opérations vectorisées dans NumPy, de programmation fonctionnelle en JavaScript ou de traitement de flux (streams) en Java. Savoir quand les utiliser est essentiel pour écrire un code rapide et propre.
Exemple
Disons que vous voulez mettre au carré une liste de nombres. Un débutant pourrait écrire ceci :
numbers = [1, 2, 3, 4, 5]
squared = []
for num in numbers:
squared.append(num ** 2)
Cela semble correct, n'est-ce pas ? Mais il y a deux inefficacités ici :
Vous bouclez manuellement alors que Python dispose d'un meilleur moyen intégré de gérer cela.
Vous effectuez des appels répétés à
.append(), ce qui ajoute une surcharge inutile.
Dans des cas simples, vous ne remarquerez pas de différence. Mais lors du traitement de grands ensembles de données, ces inefficacités s'accumulent rapidement.
La méthode meilleure et plus rapide
Python possède des optimisations intégrées qui accélèrent l'exécution des boucles. L'une d'elles est la compréhension de liste (list comprehension), qui est optimisée en C et s'exécute nettement plus vite que les boucles manuelles. Voici comment vous pouvez réécrire l'exemple :
# Beaucoup plus rapide et propre
squared = [num ** 2 for num in numbers]
Pourquoi est-ce mieux :
C'est plus rapide. Les compréhensions de liste s'exécutent en C sous le capot, ce qui signifie qu'elles n'ont pas la surcharge des appels de fonction Python comme
.append().Cela élimine le travail superflu. Au lieu de faire croître une liste dynamiquement (ce qui nécessite un redimensionnement en mémoire), Python pré-alloue l'espace pour toute la liste. Cela rend l'opération beaucoup plus efficace.
C'est plus lisible. L'intention est claire : « Je crée une liste en mettant chaque nombre au carré » — pas besoin de parcourir plusieurs lignes de code.
C'est moins sujet aux erreurs. Comme tout se passe dans une seule expression, il y a moins de risques de modifier accidentellement la liste de manière incorrecte (par exemple, oublier le
.append()).
Quand utiliser les boucles For vs. les compréhensions de liste
Les boucles for ont toujours leur utilité. Utilisez-les quand :
Vous avez besoin d'une logique complexe à l'intérieur de la boucle (par exemple, plusieurs opérations par itération).
Vous devez modifier des données existantes sur place plutôt que de créer une nouvelle liste.
L'opération implique des effets de bord, comme le logging, l'écriture de fichiers ou des requêtes réseau.
Sinon, les compréhensions de liste devraient être votre choix par défaut pour les transformations simples. Elles sont plus rapides, plus propres et rendent votre code Python plus efficace.
Erreur #3 : Écrire des requêtes de base de données à l'intérieur de boucles (tueur de vitesse)
Pourquoi est-ce un problème
C'est l'une des plus grosses erreurs de code lent que commettent les débutants (et même les développeurs intermédiaires). Cela arrive parce que les boucles semblent naturelles et que les requêtes de base de données paraissent simples. Mais mélangez les deux, et vous obtenez un désastre de performance.
Chaque fois que vous appelez une base de données à l'intérieur d'une boucle, vous effectuez des allers-retours répétés vers la base. Chaque requête ajoute de la latence réseau, une surcharge de traitement et une charge inutile sur votre système.
Exemple :
Imaginez que vous récupériez les détails des utilisateurs pour une liste de user_ids comme ceci :
user_ids = [1, 2, 3, 4, 5]
for user_id in user_ids:
user = db.query(f"SELECT * FROM users WHERE id = {user_id}")
print(user) # Faire quelque chose avec l'utilisateur
Qu'est-ce qui ne va pas ici ?
Vous sollicitez la base de données plusieurs fois au lieu d'une seule.
Chaque appel a une surcharge réseau (les requêtes de base de données ne sont pas instantanées).
La performance s'effondre lorsque la liste
user_idsdevient longue.
Comment y remédier : utiliser des requêtes groupées
Au lieu de faire 5 requêtes distinctes, n'en faites qu'une seule :
user_ids = [1, 2, 3, 4, 5]
users = db.query(f"SELECT * FROM users WHERE id IN ({','.join(map(str, user_ids))})")
for user in users:
print(user) # Traiter les utilisateurs efficacement
Pourquoi est-ce mieux :
Dans le code ci-dessus, nous n'avons qu'un seul appel à la base de données au lieu de plusieurs. Cela améliore considérablement les performances.
Il y a également moins de surcharge réseau, ce qui rend votre application plus réactive.
Et cela fonctionne même si
user_idscontient plus de 10 000 entrées.
Une approche plus évolutive
Si vous utilisez un ORM (comme SQLAlchemy en Python ou Sequelize en JavaScript), utilisez la récupération par lots (batch fetching) au lieu d'une boucle :
users = db.query(User).filter(User.id.in_(user_ids)).all()
Erreur #4 : Ne pas connaître les sombres secrets de votre matériel
Votre code ne s'exécute pas dans un monde imaginaire magique — il tourne sur du matériel réel. Les CPU, la mémoire et les caches ont des particularités qui peuvent transformer un code « logiquement rapide » en un bourbier léthargique. Voici ce que la plupart des tutoriels ne vous diront pas :
Problème 1 : La boule de cristal du CPU est cassée (pré-chargement de la mémoire)
Ce que vous pensez qu'il se passe :
« Je parcours les données de manière séquentielle. Le CPU devrait prédire ce dont j'aurai besoin ensuite ! »
Ce qui se passe réellement :
Les CPU modernes disposent d'un pré-chargeur de mémoire (memory prefetcher) — un assistant intelligent qui essaie de deviner les prochains emplacements mémoire dont vous aurez besoin et les charge à l'avance.
Mais voici le piège : si votre schéma d'accès est trop aléatoire, le pré-chargeur abandonne. Au lieu de récupérer les données en avance de manière fluide, le CPU se retrouve à attendre, comme quelqu'un qui rafraîchit Google Maps avec une connexion internet défaillante.
Cela arrive souvent avec les listes chaînées et les tables de hachage, où la mémoire « saute » de manière imprévisible.
Exemple :
# Parcours de liste chaînée (sauts mémoire aléatoires)
class Node:
def __init__(self, val):
self.val = val
self.next = None
head = Node(0)
current = head
for _ in range(100000): # Chaque 'next' pointe vers un emplacement mémoire aléatoire
current.next = Node(0)
current = current.next
# Parcourir cette liste = 100 000 défauts de cache
Pourquoi cela fait mal :
Chaque fois que le CPU a besoin du prochain Node, il doit le récupérer à un emplacement mémoire aléatoire, rendant le pré-chargement inutile et causant de fréquents défauts de cache.
La solution : utiliser des structures de données contiguës
Au lieu d'utiliser une liste chaînée, stockez vos données dans un bloc mémoire contigu (comme un tableau ou un tableau NumPy). De cette façon, le CPU peut facilement pré-charger les éléments suivants en séquence, ce qui accélère les choses.
# Parcours de tableau (favorable au pré-chargeur)
data = [0] * 100000 # Mémoire contiguë
for item in data:
pass # Le CPU pré-charge les éléments suivants de manière fluide
Pourquoi est-ce mieux :
Le CPU pré-charge efficacement les valeurs à venir au lieu d'attendre.
Moins de défauts de cache = exécution beaucoup plus rapide.
Les « hot loops » (boucles qui s'exécutent des millions de fois) bénéficient d'un gain de performance énorme.
📌 Hot loops : Ce sont des boucles qui s'exécutent un nombre massif de fois, comme celles dans le traitement de données, les modèles d'IA et les moteurs de jeu. Même une petite accélération dans une hot loop peut améliorer considérablement la performance globale.
Problème 2 : La taxe invisible des pages mémoire (balayage TLB)
Ce que vous pensez qu'il se passe :
« Mon jeu de données de 10 Go est juste… là. Y accéder est gratuit, non ? »
Ce qui se passe réellement :
Votre OS divise la mémoire en pages de 4 Ko. Chaque fois que votre programme accède à une nouvelle page mémoire, le CPU consulte un tampon de traduction d'adresses (TLB) — un « annuaire » pour des recherches de pages rapides.
Si votre programme saute entre trop de pages, vous obtenez des échecs TLB, et le CPU gaspille des cycles en attendant que l'OS récupère les mappages mémoire.
Exemple :
# Itération sur une liste géante avec accès aléatoire
data = [x for x in range(10_000_000)]
total = 0
for i in random_indexes: # 1 000 000 de sauts aléatoires
total += data[i] # Chaque saut touche probablement une nouvelle page
Pourquoi cela fait mal :
Les échecs TLB peuvent ajouter 10 à 100 cycles CPU par accès.
Si vous avez des millions d'accès aléatoires, cela représente des milliards de cycles gaspillés.
La solution : traiter les données par blocs
Pour réduire les échecs TLB :
Traitez les données par blocs (par exemple, 4096 éléments à la fois) au lieu de sauter partout de manière aléatoire.
Utilisez des « huge pages » (2 Mo au lieu de 4 Ko) pour que plus de données tiennent dans chaque page mémoire.
Problème 3 : Votre code est un touriste dans le mauvais quartier CPU (NUMA)
Ce que vous pensez qu'il se passe :
« Mon serveur 64 cœurs est un paradis de vitesse ! »
Ce qui se passe réellement :
Sur les serveurs multi-sockets, la mémoire est divisée en zones NUMA (Non-Uniform Memory Access). Chaque socket CPU a sa propre mémoire locale, et accéder à la mémoire d'un autre socket est lent — comme commander un Uber Eats depuis une autre ville.
Exemple :
# Exécution sur un serveur à 2 sockets :
from multiprocessing import Pool
import numpy as np
def process(chunk):
data = np.load("giant_array.npy") # Alloué sur la RAM du Socket 1
return chunk * data # Si le processus s'exécute sur le CPU du Socket 2... aïe
with Pool(64) as p:
p.map(process, big_data) # 64 cœurs se disputant la RAM distante
Pourquoi cela fait mal :
Accéder à la mémoire depuis une autre zone NUMA peut être 2 à 4 fois plus lent.
Vos 64 cœurs finissent par attendre la mémoire au lieu de calculer réellement.
La solution : épingler les processus à la mémoire sensible au NUMA
Au lieu de laisser vos processus accéder à la mémoire de manière aléatoire, vous pouvez les épingler au bon nœud NUMA.
Utilisez
numactlsur Linux pour allouer la mémoire à proximité du CPU qui l'utilisera.Utilisez des bibliothèques sensibles au NUMA dans NumPy pour garantir que les données sont allouées de manière optimale.
Problème 4 : Le CPU est une « drama queen » (exécution spéculative)
Ce que vous pensez qu'il se passe :
« Mon code s'exécute dans l'ordre où je l'ai écrit ! »
Ce qui se passe réellement :
Les CPU exécutent le code de manière spéculative à l'avance. S'ils se trompent dans leurs prévisions, ils doivent tout annuler et recommencer, ce qui ralentit les choses.
Exemple :
// Branchements imprévisibles = le pire cauchemar du CPU
if (rare_condition) { // 99 % du temps, c'est faux
do_work();
}
Pourquoi cela fait mal :
Une erreur de prédiction de branchement gaspille 15 à 20 cycles. Dans les « hot loops », cela peut vraiment nuire aux performances.
La solution : rendre les branchements prévisibles
Triez les données pour aider le CPU à faire de meilleures prédictions :
# Traiter tous les éléments 'valides' en premier, puis les 'invalides'
sorted_data = sorted(data, key=lambda x: x.is_valid, reverse=True)
for item in sorted_data:
if item.is_valid: # Le CPU apprend le motif → prédictions précises
process(item)
Pourquoi ça marche :
Le branchement devient prévisible — le CPU arrête de se tromper.
Le tri préalable réduit les annulations et les cycles gaspillés.
Comment riposter
Voici comment vous pouvez empêcher votre CPU de saboter votre code :
- Traitez la mémoire comme une autoroute : les lignes de cache comptent. Gardez les données contiguës pour que le CPU n'ait pas à les chercher.
Profilez avec
perf: utilisez l'outilperfde Linux pour repérer les défauts de cache, les fautes de page et le balayage TLB :perf stat -e cache-misses,page-faults ./votre_code
- Ne supposez rien. Testez tout (Benchmark) : les CPU ont un millier de comportements non documentés. Testez différentes dispositions de données, structures de boucles et allocations de mémoire pour voir ce qui est le plus rapide.
Erreur #5 : Fragmentation de la mémoire
Vous avez optimisé vos algorithmes. Vous maîtrisez le Big O. Pourtant, votre application plante toujours avec des erreurs « out of memory » ou ralentit progressivement. Le coupable ? La fragmentation de la mémoire — un fantôme dans la machine que la plupart des développeurs ignorent jusqu'à ce qu'il soit trop tard.
Ce qu'il se passe sous le capot
Lorsque votre code alloue et libère des blocs de mémoire de tailles variables, il laisse derrière lui un patchwork d'espaces libres et utilisés. Avec le temps, cela crée un effet « fromage suisse » dans votre RAM : beaucoup de mémoire libre totale, mais aucun bloc contigu pour de nouvelles allocations.
Exemple :
Imaginez un serveur C++ qui gère les requêtes en allouant des buffers de tailles aléatoires :
void process_request() {
// Allouer un buffer de taille aléatoire entre 1 et 1024 octets
char* buffer = new char[rand() % 1024 + 1];
// ... traitement ...
delete[] buffer;
}
Après des millions de requêtes, votre mémoire ressemble à ceci :
[UTILISÉ][LIBRE][UTILISÉ][LIBRE][UTILISÉ][LIBRE]...
Maintenant, quand vous essayez d'allouer un buffer de 2 Ko, cela échoue — non pas parce qu'il n'y a plus d'espace, mais parce qu'aucun bloc libre n'est assez grand.
Comment y remédier :
Utilisez un pool de mémoire pour allouer des blocs de taille fixe :
class MemoryPool {
public:
MemoryPool(size_t block_size) : block_size_(block_size) {}
void* allocate() { /* obtenir un bloc pré-alloué */ }
void deallocate(void* ptr) { /* retourner le bloc au pool */ }
};
// Toutes les requêtes utilisent des buffers de taille fixe (1024 octets)
MemoryPool pool(1024);
void process_request() {
char* buffer = static_cast<char*>(pool.allocate());
// ... traitement ...
pool.deallocate(buffer);
}
En standardisant la taille des blocs, vous éliminez la fragmentation.
Le piège de l'autoboxing (Java, C#, etc.)
Que se passe-t-il ?
Dans les langages qui mélangent les primitives (comme int, float) et les objets (comme Integer, Double), la conversion d'une primitive en son enveloppe objet est appelée autoboxing. Cela semble inoffensif, mais dans les « hot loops », c'est un désastre de performance.
Exemple :
// Lent : Crée 1 000 000 d'objets Integer (et des déchets !)
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
list.add(i); // Autoboxing de 'i' en Integer
}
Pourquoi cela nuit à la performance :
Surcharge mémoire : chaque objet
Integerajoute 16 à 24 octets de mémoire supplémentaire (en-têtes d'objet, pointeurs). Avec 1 000 000 de nombres, c'est un surplus de 16 à 24 Mo gaspillés rien qu'en surcharge.Pression sur le Garbage Collector (GC) : comme les objets sont alloués sur le tas (Heap), le GC doit constamment nettoyer les anciens objets
Integer, ce qui entraîne des pics de latence.Inefficacité du cache CPU : les primitives comme
intsont étroitement regroupées en mémoire, mais les objetsIntegersont dispersés sur le tas avec une indirection supplémentaire, ruinant la localité du cache.
La solution : utiliser des collections primitives
Pour éviter l'autoboxing, utilisez des structures de données qui stockent des primitives brutes plutôt que des objets. En Java, Eclipse Collections fournit des listes adaptées aux primitives comme IntList qui stockent directement des valeurs int brutes.
Exemple : la version plus rapide (Collections primitives)
// Importer une collection adaptée aux primitives
import org.eclipse.collections.api.list.primitive.IntList;
import org.eclipse.collections.impl.list.mutable.primitive.IntArrayList;
// Utiliser IntArrayList pour stocker des entiers bruts
IntList list = new IntArrayList();
for (int i = 0; i < 1_000_000; i++) {
list.add(i); // Pas d'autoboxing ! Stocke des 'int' bruts
}
Comment fonctionne cette solution :
Elle stocke des valeurs
intbrutes au lieu d'objetsInteger, éliminant la surcharge mémoire.Elle évite les allocations sur le tas, donc le ramasse-miettes ne s'en mêle pas.
Elle garde les nombres étroitement regroupés en mémoire, améliorant l'efficacité du cache CPU.
La solution pour C
En C#, vous pouvez éviter les allocations inutiles sur le tas en utilisant des struct et Span<T>, qui maintiennent les données sur la pile (Stack) ou dans une mémoire contiguë plutôt que sur le tas.
// Span<T> évite les allocations sur le tas
Span<int> numbers = stackalloc int[1_000_000];
for (int i = 0; i < numbers.Length; i++) {
numbers[i] = i; // Pas de boxing, pas d'allocation sur le tas
}
Pas d'enveloppes d'objets. Pas de pression sur le GC. Juste de la performance.
Erreur #6 : Le cache (le piège)
Vous avez entendu dire que « le cache compte », mais voici le rebondissement : vos boucles mentent à votre CPU. La façon dont vous parcourez les tableaux multidimensionnels peut transformer une différence de vitesse de 10x en un mystère qui vous fera remettre en question la réalité.
Accès par lignes (Row-Major) vs. par colonnes (Column-Major)
Ce que vous pensez qu'il se passe :
« Itérer sur un tableau 2D revient au même, que je le fasse ligne par ligne ou colonne par colonne. N'est-ce pas ? »
Ce qui se passe réellement :
La mémoire est disposée de manière linéaire, mais les CPU pré-chargent les données par blocs (lignes de cache). Parcourir à contre-sens force le CPU à récupérer de nouvelles lignes de cache à chaque étape.
Exemple en C :
// Une "petite" matrice 1024x1024
int matrix[1024][1024];
// Rapide : Parcours par lignes (favorable au cache)
for (int i = 0; i < 1024; i++) {
for (int j = 0; j < 1024; j++) {
matrix[i][j] = i + j;
}
}
// Lent : Parcours par colonnes (hostile au cache)
for (int j = 0; j < 1024; j++) {
for (int i = 0; i < 1024; i++) {
matrix[i][j] = i + j;
}
}
Le résultat :
Row-major : ~5ms (les données s'écoulent comme un fleuve).
Column-major : ~50ms (le CPU se noie dans les défauts de cache).
Pourquoi c'est pire que vous ne le pensez :
En C/C++, les tableaux sont en Row-major. Mais en Fortran, MATLAB ou Julia, ils sont en Column-major. Utilisez le mauvais ordre de parcours dans ces langages, et vous subirez la même pénalité.
Le coup de théâtre : votre langage de programmation vous ment
En C et Python (par défaut dans NumPy), les tableaux utilisent l'ordre Row-major. Mais en Fortran, MATLAB et Julia, les tableaux sont en Column-major. Si vous supposez une mauvaise disposition, vos boucles seront lentes sans raison apparente.
Exemple Python :
import numpy as np
# Row-major (style C) → Rapide pour les boucles par lignes
row_major = np.zeros((1024, 1024), order='C')
# Column-major (style Fortran) → Rapide pour les boucles par colonnes
col_major = np.zeros((1024, 1024), order='F')
# ❌ Lent : Accès par colonnes sur un tableau row-major
for i in range(1024):
for j in range(1024):
col_major[i][j] = i + j # Chaos de défauts de cache !
Pourquoi est-ce un problème :
Le Row-major (par défaut dans NumPy) attend un accès par lignes, mais la boucle y accède par colonnes, provoquant des défauts de cache.
Les tableaux de style Fortran sont stockés par colonnes, donc les boucles par lignes y seront lentes.
La solution :
Faites correspondre l'ordre du tableau à votre schéma d'accès en utilisant
order='C'(row-major) ouorder='F'(column-major).Convertissez la disposition des données avec
np.asarray()si nécessaire.
L'illusion multidimensionnelle : tableaux 3D+
Ce que vous pensez qu'il se passe :
« Les tableaux 3D ne sont que des tableaux 2D avec des étapes supplémentaires. Rien de bien méchant. »
Ce qui se passe réellement :
Chaque dimension ajoute une couche d'indirection. Un tableau 3D en C est un tableau de tableaux de tableaux. Parcourir la « mauvaise » dimension force le CPU à déréférencer des pointeurs de manière répétée, tuant la localité.
Exemple : Parcours de tableau 3D en C
// ✅ Rapide : Itérer en ordre Row-Major (dernière dimension en dernier)
int space[256][256][256];
for (int x = 0; x < 256; x++) {
for (int y = 0; y < 256; y++) {
for (int z = 0; z < 256; z++) {
space[x][y][z] = x + y + z; // Accès mémoire fluide
}
}
}
Dans ce cas, la boucle la plus interne se déplace dans une mémoire contiguë, tirant pleinement parti des lignes de cache.
// ❌ Lent : Itérer dans le mauvais ordre (dernière dimension en premier)
for (int z = 0; z < 256; z++) {
for (int y = 0; y < 256; y++) {
for (int x = 0; x < 256; x++) {
space[x][y][z] = x + y + z; // Défauts de cache constants
}
}
}
Pourquoi est-ce mauvais :
Cette boucle saute à travers la mémoire chaque fois que
xchange.Au lieu d'accéder à la mémoire contiguë, elle déréférence constamment des pointeurs.
Pénalité : jusqu'à 100x plus lent pour de grands tableaux 3D !
L'option nucléaire : algorithmes sensibles au cache
Pour une performance extrême (moteurs de jeu, HPC), vous devez concevoir pour les lignes de cache :
Tiling (Pavage) : Divisez les tableaux en petits blocs qui tiennent dans le cache L1/L2.
// Traiter des tuiles 8x8 pour exploiter les lignes de cache de 64 octets for (int i = 0; i < 1024; i += 8) { for (int j = 0; j < 1024; j += 8) { // Traiter la tuile tile[i:i+8][j:j+8] } }SoA vs. AoS : Préférez Structure de Tableaux (SoA) à Tableau de Structures (AoS) pour le SIMD.
// Lent : Tableau de Structures (AoS) struct Particle { float x, y, z; }; Particle particles[1000000]; // Rapide : Structure de Tableaux (SoA) struct Particles { float x[1000000]; float y[1000000]; float z[1000000]; };
Erreur #7 : Le piège du copier-coller
Vous ne téléchargeriez jamais 10 copies du même film. Mais dans le code ? Vous clonez probablement des données tout le temps sans vous en rendre compte. Voici comment les copies invisibles transforment votre application en un fouillis lent et gonflé — et comment y remédier.
Problème 1 : Les copies fantômes dans les opérations « inoffensives »
Ce que vous pensez qu'il se passe :
« J'ai découpé (slice) une liste — c'est juste une référence, non ? »
Ce qui se passe réellement :
Dans de nombreux langages, le slicing crée une copie complète des données. Faites cela avec de grands jeux de données, et vous doublez silencieusement l'utilisation de la mémoire et le travail du CPU.
Exemple Python :
# Une liste de données de 1 Go
big_data = [ ... ] # 1 000 000 d'éléments
# Clonage accidentel de toute la liste
snippet = big_data[:1000] # Crée une copie (inoffensif, n'est-ce pas ?)
# Mieux : utiliser une vue (si possible)
import numpy as np
big_array = np.array(big_data)
snippet = big_array[:1000] # Une vue, pas une copie (0 Mo ajoutés)
Pourquoi cela fait mal :
Copier 1 Go → 2 Go de RAM utilisés.
Si cela se produit dans une boucle, votre programme peut s'arrêter avec un
MemoryError.
La solution :
Utilisez des vues mémoire (
numpy,memoryviewen Python) ou le slicing paresseux (Pandas.iloc).En JavaScript,
slice()copie les tableaux — remplacez parTypedArray.subarraypour les buffers.
Problème 2 : Le coût caché du code « fonctionnel »
Ce que vous pensez qu'il se passe :
« Je vais enchaîner les méthodes de tableau pour un code propre et lisible ! »
Ce qui se passe réellement :
Chaque map, filter ou slice crée un nouveau tableau. Enchaînez trois opérations ? Vous avez cloné vos données trois fois.
Exemple JavaScript :
// Un tableau de 10 000 éléments
const data = [ ... ];
// Lent : Crée 3 copies (original → filtré → mappé → découpé)
const result = data
.filter(x => x.active)
.map(x => x.value * 2)
.slice(0, 100);
// Plus rapide : Le faire en une seule passe
const result = [];
for (let i = 0; i < data.length; i++) {
if (data[i].active) {
result.push(data[i].value * 2);
if (result.length === 100) break;
}
}
Pourquoi cela fait mal :
10 000 éléments → 30 000 opérations + 3x la mémoire.
La programmation fonctionnelle est élégante mais peut être coûteuse.
La solution :
Utilisez des générateurs (Python
yield, JSfunction*) pour un traitement paresseux (lazy).Remplacez les chaînes de méthodes par des boucles en une seule passe dans les chemins critiques (hot paths).
Problème 3 : L'erreur « je vais juste modifier une copie »
Ce que vous pensez qu'il se passe :
« J'ai besoin d'ajuster cet objet. Je vais le dupliquer pour éviter les effets de bord. »
Ce qui se passe réellement :
Le clonage profond (deep cloning) d'objets complexes (surtout dans des boucles) revient à photocopier un dictionnaire entier chaque fois que vous modifiez un mot.
Exemple Python :
import copy
config = {"theme": "dark", "settings": { ... }} # Données imbriquées
# Lent : Copie profonde avant chaque modification
for user in users:
user_config = copy.deepcopy(config) # Copie toute la structure imbriquée
user_config["theme"] = user.preference
# ...
# Plus rapide : réutiliser la config de base, superposer les changements
for user in users:
user_config = {"theme": user.preference, **config} # Fusion superficielle
# ...
Pourquoi cela fait mal :
deepcopyest 10 à 100 fois plus lent que les copies superficielles (shallow copies).Multiplié par 1 000 utilisateurs, vous gaspillez des minutes.
La solution :
Utilisez des modèles immuables : créez de nouveaux objets par fusion plutôt que par clonage.
Pour le Big Data, utilisez le partage structurel (bibliothèques comme
immutablesen Python).
Comment échapper à l'enfer du copier-coller ?
Demandez-vous : « Ai-je besoin d'une copie ? » : 90 % du temps, non. Utilisez des vues, des générateurs ou des modifications sur place.
Profilez l'utilisation de la mémoire : des outils comme
memory_profiler(Python) ou Chrome DevTools (JS) montrent la surcharge liée aux copies.Apprenez les particularités de votre langage :
Python : le slicing de listes copie, mais pas celui des tableaux NumPy.
JavaScript :
[...array]clone, mais pasarray.subarray(TypedArray).
Comment les développeurs pros écrivent-ils du code plus rapide ?
La plupart des débutants pensent que le « code rapide » signifie simplement écrire une syntaxe plus propre ou utiliser un Framework différent. Mais en réalité, la performance n'est pas seulement une question de langage ou de framework — c'est une façon de réfléchir.
Les développeurs pros ne se contentent pas d'écrire du code. Ils le mesurent, le testent et l'optimisent. Voici comment ils s'y prennent.
1. Ils profilent leur code au lieu de deviner
🔥 Débutants : « Cette fonction semble lente… je devrais peut-être la réécrire ? »
💡 Pros : « Profilons-la pour voir ce qui est réellement lent. »
Au lieu de réécrire le code au hasard, les développeurs pros mesurent d'abord en utilisant des outils de profilage.
Exemple : En Python, vous pouvez utiliser cProfile pour analyser où votre code passe le plus de temps :
import cProfile
def slow_function():
total = 0
for i in range(10**6):
total += i
return total
cProfile.run('slow_function()')
👀 Ce que cela vous dit :
Quelle fonction prend le plus de temps
Combien de fois une fonction est appelée
Où se trouve le véritable goulot d'étranglement
✅ À retenir : Avant d'optimiser, profilez toujours votre code. On ne peut pas corriger ce qu'on ne mesure pas.
Autres outils utiles :
Python :
cProfile,line_profilerJavaScript : Onglet Performance de Chrome DevTools
Java : JProfiler
Général :
perf,Valgrind
2. Ils évitent l'optimisation prématurée
🔥 Débutants : « Je vais passer des heures à optimiser cette boucle avant de la tester. »
💡 Pros : « Je vais d'abord faire en sorte que ça marche, puis je n'optimiserai que ce qui compte. »
Donald Knuth a dit la célèbre phrase : « L'optimisation prématurée est la racine de tous les maux. » Beaucoup de débutants perdent du temps à optimiser des choses qui ne sont pas réellement lentes.
Exemple : Un débutant pourrait passer des heures à optimiser une boucle qui s'exécute en 0,001 seconde, alors que le vrai ralentissement provient d'une requête de base de données supplémentaire qui prend 500 ms.
✅ À retenir :
D'abord, faites fonctionner votre code.
Ensuite, profilez et n'optimisez que ce qui est lent.
3. Ils choisissent les bonnes structures de données (pas seulement celles qui leur sont familières)
🔥 Débutants : « Je vais juste utiliser une liste. »
💡 Pros : « Quelle structure de données est optimale pour cette tâche ? »
La plupart des ralentissements surviennent à cause de mauvais choix de structures de données. Les développeurs pros choisissent le bon outil au lieu de se contenter de l'option par défaut.
Exemple : recherches rapides
❌ Lent (Liste - O(n))
users = ["alice", "bob", "charlie"]
if "bob" in users: # Parcourt toute la liste
print("Found")
✅ Rapide (Set - O(1))
users = {"alice", "bob", "charlie"}
if "bob" in users: # Utilise une table de hachage pour une recherche instantanée
print("Found")
✅ À retenir : Lorsque la performance est importante, choisissez la structure de données appropriée, pas seulement la plus familière.
4. Ils automatisent les contrôles de performance
🔥 Débutants : « Je vérifierai les problèmes de performance quand j'en aurai envie. »
💡 Pros : « Je vais utiliser des outils pour détecter automatiquement les goulots d'étranglement. »
Au lieu de chercher manuellement le code lent, les développeurs pros s'appuient sur des outils automatisés qui signalent les inefficacités.
Exemple :
Python :
py-spy(profileur d'échantillonnage léger)JavaScript : Chrome DevTools Performance Monitoring
Java : JMH (Java Microbenchmark Harness)
Revues de code assistées par IA : Il existe des outils comme CodeAnt qui analysent et corrigent automatiquement votre code lorsque vous poussez sur GitHub (ou ailleurs) et suggèrent des améliorations de performance.
✅ À retenir : Mettez en place des contrôles automatisés pour détecter les problèmes de performance tôt — avant qu'ils n'arrivent en production.
5. Ils pensent à la performance dès le premier jour
🔥 Débutants : « J'optimiserai plus tard. »
💡 Pros : « J'écrirai un code efficace dès le départ. »
Si l'optimisation prématurée est mauvaise, écrire du code lent dès le début est pire. Les développeurs pros évitent les pièges courants avant qu'ils ne deviennent de réels problèmes.
Exemple : écrire des boucles efficaces dès le départ
❌ Lent (appel à .append() inutile)
result = []
for i in range(10**6):
result.append(i * 2) # C'est lent
✅ Rapide (Compréhension de liste - optimisée dès le départ)
result = [i * 2 for i in range(10**6)] # Plus rapide, plus efficace
✅ À retenir : Les petits choix s'accumulent. Pensez à la performance pendant que vous écrivez, plutôt que d'essayer de la réparer plus tard.
🚀 Réflexions finales : leçons apprises à la dure
Merci de m'avoir lu ! Voici quelques-uns des conseils que j'ai personnellement notés pour moi-même — des choses que j'ai apprises à la dure en codant, en discutant avec des amis développeurs et en travaillant sur des projets réels.
Quand j'ai commencé, j'avais l'habitude de deviner pourquoi mon code était lent au lieu de mesurer. J'optimisais des parties aléatoires de mon code et je me demandais encore pourquoi les choses ne s'accéléraient pas. Avec le temps, j'ai réalisé que les développeurs pros ne « pondent » pas du code rapide par instinct — ils utilisent des outils, mesurent et optimisent ce qui compte réellement.
J'ai écrit ceci pour vous éviter de commettre les mêmes erreurs que moi. J'espère que vous avez maintenant une feuille de route plus claire pour écrire un code plus rapide et plus efficace — sans la frustration que j'ai connue ! 🚀
Si vous avez trouvé cela utile, enregistrez cet article pour plus tard et n'hésitez pas à le partager avec un collègue développeur qui pourrait lui aussi être aux prises avec un code lent.
Bon code ! 😊