Skip to content

Latest commit

 

History

History
838 lines (663 loc) · 20.5 KB

File metadata and controls

838 lines (663 loc) · 20.5 KB

🔝 Retour au Sommaire

20. Optimisation et Performance

Introduction

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.

Philosophie de l'optimisation

La citation de Donald Knuth

"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

Les trois phases du développement

Un développement logiciel mature suit généralement ces phases :

Phase 1 : Faire fonctionner le code ✅

// 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é

Phase 2 : Rendre le code propre 🧹

// 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

Phase 3 : Rendre le code rapide ⚡

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

Quand optimiser ?

Situations nécessitant l'optimisation

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

Situations ne nécessitant PAS l'optimisation

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;

Principes fondamentaux de l'optimisation

1. Mesurer avant d'optimiser

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

2. La loi de Pareto (règle des 80/20)

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)

3. La loi d'Amdahl

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

4. Compromis performance vs maintenabilité

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

5. Optimisation multi-niveau

L'optimisation peut se faire à différents niveaux :

Niveau 1 : Architecture (impact majeur ⭐⭐⭐⭐⭐)

Choisir le bon algorithme et la bonne structure de données  
Exemple : O(n²) → O(n log n)

Niveau 2 : Conception (impact important ⭐⭐⭐⭐)

Caching, lazy loading, pooling de ressources

Niveau 3 : Implémentation (impact moyen ⭐⭐⭐)

Optimisations du code Pascal

Niveau 4 : Compilateur (impact moyen ⭐⭐⭐)

Options de compilation (-O2, -O3)

Niveau 5 : Micro-optimisations (impact faible ⭐)

Remplacer * 2 par shl 1

Conseil : Commencez toujours par les niveaux supérieurs (architecture, conception).

Types d'optimisation

1. Optimisation algorithmique

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

2. Optimisation mémoire

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)

3. Optimisation I/O

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)

4. Optimisation parallèle

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

5. Optimisation du compilateur

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

Effets :

  • -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

Spécificités multi-plateformes

Différences de performance Windows vs Linux

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

Optimisations spécifiques par plateforme

Windows

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}

Linux

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}

Compilation conditionnelle pour optimisations

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;

Métriques de performance

Temps d'exécution

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)

Débit (Throughput)

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;

Utilisation mémoire

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 vs Débit

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

Outils et techniques

Profiling (détaillé dans section 20.1)

Le profiling identifie où le temps est passé :

  • Windows : Intel VTune, Very Sleepy
  • Linux : Perf, Valgrind
  • Multi-plateforme : Chronométrage manuel

Benchmarking

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;

Tests de charge

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;

Checklist d'optimisation

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

Erreurs courantes à éviter

1. Optimiser sans mesurer

"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

2. Optimiser le mauvais endroit

Optimiser une fonction appelée 1 fois  
Ignorer une fonction appelée 1,000,000 fois

3. Sacrifier la correction pour la performance

// ❌ Code "optimisé" mais incorrect
function Diviser(a, b: Integer): Integer;  
begin
  Result := a div b;  // Pas de vérification de b = 0
end;

4. Micro-optimiser sans vision globale

Gagner 1ms sur une fonction  
Alors que l'architecture globale pourrait gagner 5 secondes

5. Ignorer les différences de plateformes

Optimiser uniquement pour Windows  
Déployer sur Linux → Performances médiocres

Structure du chapitre

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

Conclusion

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"

⏭️ Profiling multi-plateforme