🔝 Retour au Sommaire
L'optimisation des performances est un aspect crucial du développement logiciel professionnel. Dans le contexte du développement multi-plateforme avec FreePascal/Lazarus, cette discipline prend une dimension supplémentaire : il faut non seulement créer du code rapide, mais aussi s'assurer qu'il performe bien sur différents systèmes d'exploitation (Windows et Ubuntu/Linux), différentes architectures matérielles (x86, x64, ARM), et dans différentes conditions d'utilisation.
Ce chapitre vous guidera à travers les principes, techniques et outils pour créer des applications FreePascal/Lazarus performantes et optimisées.
"L'optimisation prématurée est la racine de tous les maux"
- Donald Knuth
Cette célèbre citation est souvent mal comprise. Elle ne signifie pas qu'il ne faut jamais optimiser, mais plutôt qu'il faut optimiser au bon moment et au bon endroit.
Ce que cela signifie vraiment :
- ❌ N'optimisez pas dès la première ligne de code
- ❌ N'optimisez pas sans mesurer d'abord
- ❌ N'optimisez pas du code qui n'a aucun impact sur les performances
- ✅ Écrivez d'abord du code correct et maintenable
- ✅ Mesurez les performances pour identifier les vrais problèmes
- ✅ Optimisez les sections critiques quand c'est nécessaire
Un développement logiciel mature suit généralement ces phases :
// Objectif : Correction et fonctionnalité
procedure TraiterDonnees(Liste: TStringList);
var i: Integer;
begin
for i := 0 to Liste.Count - 1 do
begin
// Code simple et lisible
if EstValide(Liste[i]) then
Traiter(Liste[i]);
end;
end;Focus : Correction, lisibilité, maintenabilité
// Objectif : Qualité et maintenabilité
procedure TraiterDonnees(Liste: TStringList);
var
i: Integer;
Donnee: String;
begin
if not Assigned(Liste) then
raise Exception.Create('Liste non initialisée');
for i := 0 to Liste.Count - 1 do
begin
Donnee := Liste[i];
if EstValide(Donnee) then
Traiter(Donnee);
end;
end;Focus : Gestion d'erreurs, robustesse, documentation
// Objectif : Performance (si nécessaire)
procedure TraiterDonnees(Liste: TStringList);
var
i, Count: Integer;
Donnee: String;
begin
if not Assigned(Liste) then Exit;
Count := Liste.Count; // Cache la propriété
for i := 0 to Count - 1 do
begin
Donnee := Liste[i];
if EstValide(Donnee) then
Traiter(Donnee);
end;
end;Focus : Optimisations ciblées après mesure
Important : La phase 3 n'est nécessaire que si les mesures montrent un problème de performance.
✅ Temps de réponse inacceptable
L'utilisateur attend 30 secondes pour une opération
qui devrait prendre 2 secondes
→ Optimisation nécessaire
✅ Ressources insuffisantes
L'application consomme 2 GB de RAM pour traiter un fichier de 10 MB
→ Optimisation mémoire nécessaire
✅ Scalabilité limitée
L'application gère 10 utilisateurs mais plante avec 100 utilisateurs
→ Optimisation de l'architecture nécessaire
✅ Coûts d'infrastructure élevés
Le serveur nécessite 10 cœurs CPU pour gérer la charge
→ Optimisation pourrait réduire les coûts
❌ Code exécuté rarement
Une fonction d'initialisation exécutée une fois au démarrage
qui prend 50ms au lieu de 10ms
→ Gain négligeable, ne pas optimiser
❌ Sections non critiques
Dialogue de configuration ouvert par l'utilisateur
qui prend 200ms à s'afficher
→ Perçu comme instantané, ne pas optimiser
❌ Micro-optimisations inutiles
// ❌ Temps perdu à optimiser ça
Result := X * 2; // vs
Result := X shl 1; // Gain : 0.00001%❌ Code sacrifiant la maintenabilité
// ❌ Code "optimisé" mais illisible
procedure X(a:PA;c:I);var i:I;p:P;begin for i:=0 to c-1 do begin p:=a;Inc(p,i*4);v:=p^;end;end;
// ✅ Code clair et presque aussi rapide
procedure TraiterTableau(Donnees: PByte; Taille: Integer);
var
i: Integer;
Pointeur: PByte;
Valeur: Byte;
begin
for i := 0 to Taille - 1 do
begin
Pointeur := Donnees;
Inc(Pointeur, i * 4);
Valeur := Pointeur^;
end;
end;Règle d'or : Ne jamais optimiser sans avoir mesuré d'abord.
❌ Mauvaise approche :
"Je pense que cette boucle est lente"
→ Passer 2 heures à optimiser
→ Résultat : 0.5% de gain
✅ Bonne approche :
"Je vais profiler mon application"
→ Découvrir que 80% du temps est ailleurs
→ Optimiser le vrai problème
→ Résultat : 60% de gain
Outils de mesure (détaillés dans les sections suivantes) :
- Windows : Intel VTune, Very Sleepy, Windows Performance Analyzer
- Linux : Perf, Valgrind, Callgrind
- Multi-plateforme : Chronométrage manuel, HeapTrc
Principe : 80% du temps d'exécution est passé dans 20% du code.
Implications :
- Identifier les 20% critiques (appelés hotspots)
- Concentrer les efforts d'optimisation sur ces hotspots
- Ignorer les 80% du code qui n'ont pas d'impact
Exemple concret :
Application de traitement d'images :
- Chargement fichier : 0.1s (2%)
- Parsing métadonnées : 0.2s (4%)
- Traitement pixels : 4.5s (90%) ← HOTSPOT
- Sauvegarde : 0.2s (4%)
Total : 5.0s
Optimiser le traitement pixels de 50% :
→ 4.5s → 2.25s
→ Total : 2.75s (45% plus rapide)
Optimiser le chargement de 90% :
→ 0.1s → 0.01s
→ Total : 4.91s (2% plus rapide)
Énoncé : Le gain de performance maximal est limité par la portion du code qui ne peut pas être optimisée.
Formule :
Speedup = 1 / ((1 - P) + P/S)
Où :
- P = Portion du code améliorée (en %)
- S = Facteur d'accélération de cette portion
Exemple :
Si 60% du code est optimisé et rendu 10x plus rapide :
Speedup = 1 / ((1 - 0.6) + 0.6/10)
= 1 / (0.4 + 0.06)
= 1 / 0.46
= 2.17x plus rapide
Même si la portion optimisée est 10x plus rapide,
le programme global n'est que 2.17x plus rapide.
Conséquence pratique : Il est impossible d'avoir un gain infini. Identifiez les limites théoriques avant de commencer.
L'optimisation implique souvent des compromis :
| Aspect | Non optimisé | Optimisé |
|---|---|---|
| Lisibilité | +++++ | +++ |
| Maintenabilité | +++++ | +++ |
| Débogage | Facile | Difficile |
| Performance | +++ | +++++ |
| Complexité | Faible | Élevée |
Règle de décision :
Si (Gain de performance > 20%) ET (Zone critique) :
Optimiser, documenter le code
Sinon :
Garder le code simple et lisible
L'optimisation peut se faire à différents niveaux :
Choisir le bon algorithme et la bonne structure de données
Exemple : O(n²) → O(n log n)
Caching, lazy loading, pooling de ressources
Optimisations du code Pascal
Options de compilation (-O2, -O3)
Remplacer * 2 par shl 1
Conseil : Commencez toujours par les niveaux supérieurs (architecture, conception).
Impact : Peut améliorer les performances de 10x à 1000x
Principe : Choisir le bon algorithme pour le bon problème.
Exemple : Recherche dans une liste
// ❌ Recherche linéaire : O(n)
function RechercheLineaire(Liste: TStringList; Valeur: String): Integer;
var i: Integer;
begin
Result := -1;
for i := 0 to Liste.Count - 1 do
if Liste[i] = Valeur then
begin
Result := i;
Exit;
end;
end;
// Pour 1,000,000 éléments : ~500,000 comparaisons en moyenne
// ✅ Liste triée + recherche binaire : O(log n)
function RechercheBinaire(Liste: TStringList; Valeur: String): Integer;
begin
Result := Liste.IndexOf(Valeur); // Utilise recherche binaire si triée
end;
// Pour 1,000,000 éléments : ~20 comparaisons maximum
// Gain : 25,000x plus rapide !Structures de données courantes :
| Structure | Recherche | Insertion | Usage |
|---|---|---|---|
| TList | O(n) | O(1) fin | Séquentiel |
| TStringList (triée) | O(log n) | O(n) | Recherche fréquente |
| TDictionary | O(1) | O(1) | Clé-valeur |
| THashMap | O(1) | O(1) | Cache |
Impact : Peut réduire l'utilisation mémoire de 50% à 90%
Techniques :
- Réutilisation d'objets (pooling)
- Libération précoce
- Structures compactes
- Compression de données
Exemple : Pooling d'objets
// ❌ Allocation/libération constante
for i := 1 to 10000 do
begin
Obj := TMonObjet.Create;
try
Obj.Traiter();
finally
Obj.Free;
end;
end;
// 10,000 allocations + 10,000 libérations (lent)
// ✅ Réutilisation d'un objet
Obj := TMonObjet.Create;
try
for i := 1 to 10000 do
begin
Obj.Reset();
Obj.Traiter();
end;
finally
Obj.Free;
end;
// 1 allocation + 1 libération (100x plus rapide)Impact : Peut améliorer les performances de 10x à 100x
Principe : Les opérations d'entrée/sortie (disque, réseau) sont 1000x plus lentes que la mémoire.
Techniques :
- Buffering (mise en tampon)
- Batching (traitement par lots)
- Caching (mise en cache)
- Opérations asynchrones
Exemple : Écriture fichier
// ❌ Écriture ligne par ligne
for i := 1 to 10000 do
Writeln(F, Ligne[i]);
// 10,000 appels système (très lent)
// ✅ Buffering
Buffer := TStringList.Create;
try
for i := 1 to 10000 do
Buffer.Add(Ligne[i]);
Buffer.SaveToFile('sortie.txt');
finally
Buffer.Free;
end;
// 1 appel système (100x plus rapide)Impact : Peut améliorer les performances de 2x à NxCPU (nombre de cœurs)
Principe : Utiliser plusieurs cœurs CPU en parallèle.
Exemple : Traitement de fichiers
// ❌ Séquentiel
for i := 0 to Fichiers.Count - 1 do
TraiterFichier(Fichiers[i]);
// Temps : N * T (N fichiers × T secondes par fichier)
// ✅ Parallèle avec TThread
for i := 0 to Fichiers.Count - 1 do
TThread.CreateAnonymousThread(
procedure
begin
TraiterFichier(Fichiers[i]);
end
).Start;
// Temps : (N * T) / NbCores (sur 8 cœurs : 8x plus rapide)Attention : Le parallélisme ajoute de la complexité (synchronisation, race conditions).
Impact : Peut améliorer les performances de 10% à 50%
Options de compilation FreePascal :
# Niveau 1 : Optimisations basiques
fpc -O1 programme.pas
# Niveau 2 : Optimisations standards (recommandé)
fpc -O2 programme.pas
# Niveau 3 : Optimisations agressives
fpc -O3 programme.pas
# Optimisations spécifiques CPU
fpc -O3 -CpCOREAVX2 -CfAVX2 programme.pasEffets :
-O1: Suppression du code mort, propagation de constantes-O2: Loop unrolling, inline functions-O3: Optimisations vectorielles, réorganisation du code-Cp: Optimisations pour un CPU spécifique
Les mêmes algorithmes peuvent avoir des performances différentes selon l'OS :
| Opération | Windows | Linux | Raison |
|---|---|---|---|
| Allocation mémoire | Rapide | Très rapide | Allocateurs différents |
| I/O fichiers | Moyen | Rapide | ext4 vs NTFS |
| Création threads | Lent | Rapide | Implémentations différentes |
| Appels système | Rapide | Très rapide | Overhead système |
| Rendu graphique | Rapide (GDI+) | Variable (X11/Wayland) | Drivers |
Exemple réel :
Programme de traitement de 10,000 fichiers :
- Windows 10 : 8.5 secondes
- Ubuntu 22.04 : 4.2 secondes (2x plus rapide)
Raison : Système de fichiers ext4 plus rapide que NTFS
pour de nombreuses petites opérations
Forces :
- API Windows optimisées (GDI, DirectX)
- Support natif des DLLs
- Bonne gestion des applications GUI
Optimisations :
{$IFDEF WINDOWS}
// Utiliser les API Windows natives pour I/O
uses Windows;
procedure LectureFichierOptimisee;
var
Handle: THandle;
BytesRead: DWORD;
begin
Handle := CreateFile(..., FILE_FLAG_SEQUENTIAL_SCAN, ...);
// Lecture optimisée
end;
{$ENDIF}Forces :
- I/O fichiers très rapides
- Création de threads légère
- Gestion mémoire efficace
Optimisations :
{$IFDEF LINUX}
// Utiliser les appels système Linux pour performance
uses BaseUnix;
procedure LectureFichierOptimisee;
var
fd: cint;
begin
fd := FpOpen(fichier, O_RDONLY or O_LARGEFILE);
// Lecture optimisée
end;
{$ENDIF}procedure TraiterGrandeQuantiteDonnees;
begin
{$IFDEF WINDOWS}
// Windows : privilégier la mémoire (plus disponible)
ChargerToutEnMemoire();
TraiterEnMemoire();
{$ELSE}
{$IFDEF LINUX}
// Linux : privilégier les I/O (plus rapides)
TraiterEnStreaming();
{$ENDIF}
{$ENDIF}
end;Mesure : Temps entre le début et la fin d'une opération
uses SysUtils, DateUtils;
var
StartTime, EndTime: TDateTime;
ElapsedMs: Int64;
begin
StartTime := Now;
// Code à mesurer
OperationCouteuse();
EndTime := Now;
ElapsedMs := MilliSecondsBetween(EndTime, StartTime);
WriteLn('Durée : ', ElapsedMs, ' ms');
end;Objectifs typiques :
- Opération interactive : < 100ms (perçue comme instantanée)
- Traitement court : < 1s
- Traitement long : < 10s (avec barre de progression)
Mesure : Quantité de travail par unité de temps
const
NbElements = 1000000;
var
i: Integer;
StartTime: TDateTime;
Duree: Double;
ElementsParSeconde: Double;
begin
StartTime := Now;
for i := 1 to NbElements do
TraiterElement(i);
Duree := MilliSecondsBetween(Now, StartTime) / 1000.0;
ElementsParSeconde := NbElements / Duree;
WriteLn('Débit : ', ElementsParSeconde:0:2, ' éléments/seconde');
end;Mesure : RAM consommée par l'application
uses Windows; // ou BaseUnix pour Linux
function GetMemoryUsage: Int64;
var
{$IFDEF WINDOWS}
ProcessMemoryCounters: TProcessMemoryCounters;
{$ENDIF}
begin
{$IFDEF WINDOWS}
ProcessMemoryCounters.cb := SizeOf(ProcessMemoryCounters);
if GetProcessMemoryInfo(GetCurrentProcess,
@ProcessMemoryCounters,
SizeOf(ProcessMemoryCounters)) then
Result := ProcessMemoryCounters.WorkingSetSize
else
Result := 0;
{$ELSE}
// Linux : lire /proc/self/status
Result := LireMemoireDepuisProc();
{$ENDIF}
end;Objectifs typiques :
- Application légère : < 50 MB
- Application standard : 50-500 MB
- Application lourde : > 500 MB
Latence : Temps pour une opération individuelle
Débit : Nombre d'opérations par seconde
Exemple :
Traitement de requêtes :
- Latence : 100ms par requête
- Débit : 10 requêtes/seconde (séquentiel)
Avec parallélisme (10 threads) :
- Latence : toujours 100ms par requête
- Débit : 100 requêtes/seconde
Le profiling identifie où le temps est passé :
- Windows : Intel VTune, Very Sleepy
- Linux : Perf, Valgrind
- Multi-plateforme : Chronométrage manuel
Comparer différentes implémentations :
procedure BenchmarkImplementations;
var
i, Iterations: Integer;
StartTime: TDateTime;
ElapsedA, ElapsedB: Int64;
begin
Iterations := 1000000;
// Test implémentation A
StartTime := Now;
for i := 1 to Iterations do
ImplementationA();
ElapsedA := MilliSecondsBetween(Now, StartTime);
// Test implémentation B
StartTime := Now;
for i := 1 to Iterations do
ImplementationB();
ElapsedB := MilliSecondsBetween(Now, StartTime);
WriteLn('Implémentation A : ', ElapsedA, ' ms');
WriteLn('Implémentation B : ', ElapsedB, ' ms');
WriteLn('B est ', (ElapsedA / ElapsedB):0:2, 'x plus rapide');
end;Simuler une charge réaliste :
procedure TestCharge;
var
i, NbUtilisateurs: Integer;
Threads: array of TThread;
begin
NbUtilisateurs := 100;
SetLength(Threads, NbUtilisateurs);
// Lancer 100 utilisateurs simultanés
for i := 0 to NbUtilisateurs - 1 do
Threads[i] := TThread.CreateAnonymousThread(
procedure
begin
SimulerUtilisateur();
end
);
for i := 0 to NbUtilisateurs - 1 do
Threads[i].Start;
// Attendre la fin
for i := 0 to NbUtilisateurs - 1 do
Threads[i].WaitFor;
end;Avant de commencer toute optimisation :
- Le code fonctionne correctement
- Les tests unitaires passent
- Il existe un problème de performance réel (mesuré)
- Les objectifs de performance sont définis
- Les hotspots ont été identifiés (profiling)
- L'optimisation vise les hotspots critiques
- La maintenabilité est acceptable
- Les gains sont mesurables
- Les tests de non-régression sont prêts
"Je pense que cette fonction est lente"
→ Passer du temps à l'optimiser
→ Découvrir qu'elle ne représente que 0.1% du temps total
Optimiser une fonction appelée 1 fois
Ignorer une fonction appelée 1,000,000 fois
// ❌ Code "optimisé" mais incorrect
function Diviser(a, b: Integer): Integer;
begin
Result := a div b; // Pas de vérification de b = 0
end;Gagner 1ms sur une fonction
Alors que l'architecture globale pourrait gagner 5 secondes
Optimiser uniquement pour Windows
Déployer sur Linux → Performances médiocres
Ce chapitre sur l'optimisation et la performance est organisé comme suit :
20.1 Profiling multi-plateforme (section suivante)
- 20.1.1 Intel VTune (Windows)
- 20.1.2 Perf et Valgrind (Linux)
20.2 Optimisation mémoire et caches
- Gestion efficace de la mémoire
- Localité des données
- Cache-friendly programming
20.3 SIMD et vectorisation
- Instructions SIMD (SSE, AVX)
- Auto-vectorisation du compilateur
- Optimisations manuelles
20.4 Optimisations spécifiques CPU
- Architectures x86, x64, ARM
- Branch prediction
- Pipeline CPU
20.5 Structures de données optimales
- Choisir la bonne structure
- Trade-offs temps/espace
20.6 Algorithmes haute performance
- Complexité algorithmique
- Algorithmes classiques optimisés
20.7 Memory pools et allocateurs custom
- Réduire l'overhead d'allocation
- Pooling d'objets
20.8 Lazy evaluation et memoization
- Calcul à la demande
- Cache de résultats
20.9 Benchmarking systématique
- Méthodologie de test
- Outils de benchmark
20.10 Optimisation pour différentes architectures
- x86 vs x64 vs ARM
- Optimisations spécifiques
20.11 Comparaison de performance Windows/Linux
- Différences systèmes
- Adaptations nécessaires
L'optimisation est un art qui demande :
- Rigueur : Mesurer avant et après
- Focus : Cibler les vrais problèmes
- Équilibre : Performance vs maintenabilité
- Vision : Considérer tous les niveaux (architecture → micro-optimisations)
- Multi-plateforme : Tester sur Windows ET Linux
Les sections suivantes vous donneront les outils et techniques concrètes pour optimiser efficacement vos applications FreePascal/Lazarus.
Note : Ce tutoriel fait partie de la formation "FreePascal/Lazarus - Niveau Développeur Avancé - Edition Multi-plateforme Windows/Ubuntu"