Retour au cours

backend / go

Performance et optimisation mémoire

Leçon 241 exercice

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.Builder au 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 GOGC et GOMEMLIMIT pour 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.

ApprocheComplexitéQuand l'utiliser
resultat += mot en boucleO(n²)Jamais dans une boucle non triviale
strings.BuilderO(n)Systématiquement pour construire une chaîne en boucle
make([]T, 0, n) + appendO(n) avec n connuDè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.

go
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
}
bash
# 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'append répétées.
  • strings.Builder remplace += 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

1 disponible
1

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.

Résoudre l’exercice →