backend / go
Performance et optimisation mémoire
Explication
Ce que vous allez apprendre
- Pré-allouer un slice avec
make([]T, 0, n)pour éviter des réallocations répétées - Utiliser
strings.Builderau lieu de la concaténation+=en boucle - Comprendre le padding mémoire d'une struct et comment le réduire
- Passer un pointeur pour éviter de copier une grosse struct
- Régler
GOGCetGOMEMLIMITpour ajuster le comportement du garbage collector
Dans quel contexte ?
Un service qui traite un gros fichier CSV ligne par ligne construit une chaîne de caractères de rapport en concaténant resultat += ligne + "\n" dans une boucle de plusieurs millions d'itérations. Ce traitement, censé prendre quelques secondes, en prend plusieurs minutes — un ralentissement quadratique classique, invisible sur un petit fichier de test mais catastrophique en production sur un vrai volume de données.
D'abord, il faut comprendre pourquoi append sans capacité coûte cher
Quand un slice grandit sans capacité connue à l'avance, chaque dépassement de capacité déclenche une réallocation complète du tableau sous-jacent (généralement en doublant la taille), avec copie de toutes les données existantes. Sur un million d'éléments, cela représente environ 20 réallocations successives (log2 de 1 million), chacune recopiant tout ce qui a déjà été inséré.
make([]int, 0, n) élimine ce problème d'un coup : en connaissant n à l'avance, une seule allocation suffit pour toute la durée du remplissage.
Une fois ce principe compris, le même raisonnement s'applique aux chaînes
Une string en Go est immuable : chaque resultat += mot crée en réalité une toute nouvelle chaîne, recopiant l'intégralité du contenu précédent. Sur une boucle de N itérations, cela donne une complexité quadratique (O(n²)) au lieu de linéaire.
| Approche | Complexité | Quand l'utiliser |
|---|---|---|
resultat += mot en boucle | O(n²) | Jamais dans une boucle non triviale |
strings.Builder | O(n) | Systématiquement pour construire une chaîne en boucle |
make([]T, 0, n) + append | O(n) avec n connu | Dès que la taille finale est prévisible |
Prérequis
Il faut connaître les slices en profondeur (leçon dédiée) : cette leçon applique directement les notions de len/cap déjà vues à des cas concrets de performance.
Il reste un détail moins connu mais tout aussi impactant : le padding mémoire
Le compilateur Go aligne les champs d'une struct en mémoire selon des règles précises, ce qui peut introduire du "padding" (des octets gaspillés) selon l'ordre de déclaration des champs. Réordonner les champs du plus gros au plus petit type réduit souvent la taille totale de la struct, un gain particulièrement notable sur des structs allouées en très grand nombre.
Piège fréquent
Alterner des champs de tailles très différentes (bool, puis int64, puis bool à nouveau) maximise le padding gaspillé — jusqu'à 33% de mémoire perdue inutilement dans certains cas, comme le montre l'exemple MalAligne face à BienAligne. Ce gaspillage devient significatif dès qu'on manipule des millions d'instances (une struct de log, un événement réseau...).
Enfin, deux leviers pour ajuster le comportement du garbage collector
Bonne pratique
GOGC contrôle la fréquence du garbage collector (une valeur plus haute = GC moins fréquent, plus de mémoire utilisée, moins de CPU consommé par le GC), et GOMEMLIMIT (depuis Go 1.19) fixe un plafond mémoire absolu en complément. Ajuste ces variables uniquement après avoir mesuré un vrai problème via profiling, jamais de façon préventive et arbitraire.
Maintenant que tu sais optimiser mémoire et allocations, la suite du cours attaque des patterns indispensables pour faire tourner un service Go fiable en production : le graceful shutdown et les worker pools.
Commandes & code
Performance et optimisation mémoire
Comprendre l'allocation heap/stack, réduire la pression sur le garbage collector.
package main
import "fmt"
// Pre-allouer la capacite d'un slice evite les reallocations successives
func mauvaisePratique(n int) []int {
var s []int // capacite 0, va reallouer a chaque doublement (log2(n) fois)
for i := 0; i < n; i++ {
s = append(s, i)
}
return s
}
func bonnePratique(n int) []int {
s := make([]int, 0, n) // une seule allocation, capacite connue a l'avance
for i := 0; i < n; i++ {
s = append(s, i)
}
return s
}
// strings.Builder evite les concatenations couteuses (chaque += cree une nouvelle string)
func concatenationLente(mots []string) string {
resultat := ""
for _, m := range mots {
resultat += m + " " // O(n^2) : recree la chaine entiere a chaque tour
}
return resultat
}
// Struct alignment : l'ordre des champs impacte la taille memoire (padding)
type MalAligne struct {
A bool // 1 octet + 7 de padding
B int64 // 8 octets
C bool // 1 octet + 7 de padding
} // taille totale : 24 octets
type BienAligne struct {
B int64 // 8 octets
A bool // 1 octet
C bool // 1 octet
} // + 6 de padding final : taille totale 16 octets, moins de gaspillage
// Passer un pointeur pour eviter de copier une grosse struct
type GrosSeau struct {
Donnees [1024]byte
}
func traiterParValeur(s GrosSeau) int { return len(s.Donnees) } // copie 1024 octets
func traiterParPointeur(s *GrosSeau) int { return len(s.Donnees) } // copie 8 octets (l'adresse)
func main() {
grand := bonnePratique(1_000_000)
fmt.Println(len(grand))
var sb fmt.Stringer
_ = sb
seau := &GrosSeau{}
fmt.Println(traiterParPointeur(seau))
var mal MalAligne
var bien BienAligne
fmt.Println("tailles struct (voir unsafe.Sizeof en pratique)")
_ = mal
_ = bien
}# GOGC controle l'agressivite du garbage collector (defaut: 100)
GOGC=200 ./app # GC moins frequent, plus de memoire utilisee, moins de CPU en GC
# GOMEMLIMIT (Go 1.19+) : plafond memoire absolu, complementaire a GOGC
GOMEMLIMIT=512MiB ./app
# Verifier la taille reelle d'une struct
# fmt.Println(unsafe.Sizeof(MaStruct{}))Résumé
make([]T, 0, n)avec capacité connue élimine les réallocations d'appendrépétées.strings.Builderremplace+=pour les concaténations en boucle (complexité linéaire au lieu de quadratique).- Réordonner les champs d'une struct (gros vers petit) réduit le padding mémoire.
- Passer un pointeur pour les grosses structs évite des copies coûteuses en pile ou en paramètre.
Exercices pratiques
Mission : un rapport CSV de 5 minutes qui devrait tenir en 5 secondes
Objectif : Corriger une concaténation de chaînes quadratique, réordonner une struct pour réduire le padding, et raisonner sur les limites de ces deux optimisations.
Contexte
Un traitement nocturne lit un fichier CSV de plusieurs millions de lignes et construit un rapport texte avec resultat += ligne + "\n" dans une boucle. Ce traitement, qui prenait quelques secondes sur le fichier de test de 1000 lignes utilisé en développement, prend maintenant plus de 5 minutes sur le vrai fichier de production.