Skip to content

Latest commit

 

History

History
1114 lines (827 loc) · 30.4 KB

File metadata and controls

1114 lines (827 loc) · 30.4 KB

🔝 Retour au Sommaire

12.4 Profilage et optimisation des performances

Introduction

Imaginez que vous avez créé une application Delphi qui fonctionne correctement, mais qui semble "lente" ou qui "rame" parfois. Comment savoir quelle partie du code est responsable de cette lenteur ? Comment améliorer les performances sans tout réécrire ? C'est exactement à ces questions que répond le profilage.

Le profilage est le processus qui consiste à mesurer et analyser les performances de votre application pour identifier les parties du code qui consomment le plus de temps ou de ressources. Une fois ces "goulots d'étranglement" identifiés, vous pouvez les optimiser de manière ciblée et efficace.

Pour un débutant, il est important de comprendre que l'optimisation prématurée est souvent contre-productive. La règle d'or est : "Faites d'abord fonctionner votre code correctement, puis mesurez avant d'optimiser."

Pourquoi le profilage est-il important ?

L'intuition peut tromper

En tant que développeur, vous pourriez penser savoir quelle partie de votre code est lente. Mais l'expérience montre que les développeurs se trompent souvent sur les vraies causes de lenteur. Le profilage vous donne des données objectives plutôt que des suppositions.

Optimiser au bon endroit

Il est inutile de passer des heures à optimiser une fonction qui ne représente que 0,1% du temps d'exécution total de votre programme. Le profilage vous aide à concentrer vos efforts là où ils auront le plus d'impact.

Éviter les régressions

Une fois votre application optimisée, le profilage régulier vous permet de détecter rapidement si de nouvelles modifications ont introduit des problèmes de performance.

Comprendre le comportement de votre code

Le profilage vous aide à mieux comprendre comment votre code s'exécute réellement, ce qui améliore vos compétences de développeur.

Concepts de base de la performance

Avant de plonger dans le profilage, il est important de comprendre quelques concepts fondamentaux.

Temps d'exécution (Execution Time)

C'est le temps que prend une fonction ou une portion de code pour s'exécuter. On le mesure généralement en millisecondes (ms) ou en microsecondes (μs).

Nombre d'appels (Call Count)

Le nombre de fois qu'une fonction est appelée pendant l'exécution du programme. Une fonction rapide appelée un million de fois peut causer plus de problèmes qu'une fonction lente appelée une seule fois.

Temps inclusif vs exclusif

  • Temps inclusif : Le temps total passé dans une fonction, incluant le temps des fonctions qu'elle appelle
  • Temps exclusif : Le temps passé uniquement dans le code de la fonction elle-même, sans compter les appels à d'autres fonctions

Exemple :

FonctionA (100ms au total)
├── Son propre code (20ms)
├── Appelle FonctionB (50ms)
└── Appelle FonctionC (30ms)

Temps inclusif de FonctionA : 100ms
Temps exclusif de FonctionA : 20ms

La règle des 80/20 (Principe de Pareto)

Dans la plupart des applications, environ 80% du temps d'exécution est passé dans environ 20% du code. Le profilage vous aide à identifier ces 20% critiques.

Complexité algorithmique

La façon dont le temps d'exécution d'un algorithme évolue avec la taille des données :

  • O(1) : Temps constant (accès à un élément d'un tableau)
  • O(n) : Temps linéaire (parcourir une liste)
  • O(n²) : Temps quadratique (boucles imbriquées)
  • O(log n) : Temps logarithmique (recherche dichotomique)

Comprendre ces concepts vous aide à choisir les bonnes structures de données et algorithmes.

Mesures simples sans outils spécialisés

Avant d'utiliser des outils complexes, vous pouvez effectuer des mesures basiques avec du code simple.

Utiliser TStopwatch

Delphi fournit la classe TStopwatch qui permet de mesurer précisément le temps d'exécution.

uses
  System.Diagnostics;

procedure MesureTempsExecution;  
var  
  Chrono: TStopwatch;
  TempsEcoule: Int64;
begin
  // Démarrer le chronomètre
  Chrono := TStopwatch.StartNew;

  // Code à mesurer
  EffectuerTraitementComplexe;

  // Arrêter et obtenir le temps
  Chrono.Stop;
  TempsEcoule := Chrono.ElapsedMilliseconds;

  ShowMessage(Format('Temps d''exécution : %d ms', [TempsEcoule]));
end;

Avantages :

  • Très simple à utiliser
  • Précis pour des mesures de base
  • Ne nécessite aucun outil externe

Limitations :

  • Mesure manuelle, nécessite de modifier le code
  • Donne uniquement le temps total, pas de détails sur les sous-fonctions
  • Les appels répétés nécessitent du code supplémentaire

Mesurer des portions de code spécifiques

procedure AnalyserPerformances;  
var  
  Chrono: TStopwatch;
  TempsPartie1, TempsPartie2, TempsPartie3: Int64;
begin
  // Mesurer la partie 1
  Chrono := TStopwatch.StartNew;
  TraitementPartie1;
  TempsPartie1 := Chrono.ElapsedMilliseconds;

  // Mesurer la partie 2
  Chrono := TStopwatch.StartNew;
  TraitementPartie2;
  TempsPartie2 := Chrono.ElapsedMilliseconds;

  // Mesurer la partie 3
  Chrono := TStopwatch.StartNew;
  TraitementPartie3;
  TempsPartie3 := Chrono.ElapsedMilliseconds;

  // Afficher les résultats
  Memo1.Lines.Add(Format('Partie 1 : %d ms', [TempsPartie1]));
  Memo1.Lines.Add(Format('Partie 2 : %d ms', [TempsPartie2]));
  Memo1.Lines.Add(Format('Partie 3 : %d ms', [TempsPartie3]));
  Memo1.Lines.Add(Format('Total : %d ms', [TempsPartie1 + TempsPartie2 + TempsPartie3]));
end;

Mesurer des opérations répétées

Pour des opérations très rapides, il faut les répéter plusieurs fois pour obtenir une mesure significative :

procedure MesurerOperationRapide;  
var  
  Chrono: TStopwatch;
  i: Integer;
  NombreIterations: Integer;
  TempsTotal: Int64;
  TempsMoyen: Double;
begin
  NombreIterations := 100000;

  Chrono := TStopwatch.StartNew;
  for i := 1 to NombreIterations do
  begin
    // Opération à mesurer
    EffectuerCalculSimple(i);
  end;
  Chrono.Stop;

  TempsTotal := Chrono.ElapsedMilliseconds;
  TempsMoyen := TempsTotal / NombreIterations;

  ShowMessage(Format('Temps moyen par opération : %.6f ms', [TempsMoyen]));
end;

Compter les allocations mémoire

Utilisez GetProcessMemoryInfo (Windows) pour surveiller l'utilisation mémoire :

uses
  Winapi.Windows, Winapi.PsAPI;

function ObtenirMemoireUtilisee: Cardinal;  
var  
  MemCounters: TProcessMemoryCounters;
begin
  MemCounters.cb := SizeOf(MemCounters);
  if GetProcessMemoryInfo(GetCurrentProcess, @MemCounters, SizeOf(MemCounters)) then
    Result := MemCounters.WorkingSetSize div 1024  // En Ko
  else
    Result := 0;
end;

procedure AnalyserMemoire;  
var  
  MemoireAvant, MemoireApres: Cardinal;
begin
  MemoireAvant := ObtenirMemoireUtilisee;

  // Code à analyser
  CreerBeaucoupObjets;

  MemoireApres := ObtenirMemoireUtilisee;

  ShowMessage(Format('Mémoire utilisée : %d Ko', [MemoireApres - MemoireAvant]));
end;

Outils de profilage pour Delphi

Sampling Profiler intégré à l'IDE

Delphi inclut un profileur de base dans certaines éditions (Professional, Enterprise, Architect).

Comment l'utiliser :

  1. Ouvrez votre projet dans Delphi
  2. Allez dans Run > Run with Profiling (Exécuter avec profilage)
  3. Utilisez votre application normalement, en exécutant les fonctionnalités que vous souhaitez analyser
  4. Fermez l'application
  5. Delphi affiche automatiquement les résultats du profilage

Ce que vous verrez :

Le profileur affiche un rapport montrant :

  • Les fonctions qui ont consommé le plus de temps
  • Le nombre d'appels pour chaque fonction
  • Le pourcentage du temps total pour chaque fonction
  • Une arborescence des appels de fonctions

Limitations :

  • Disponible uniquement dans certaines éditions de Delphi
  • Profilage par échantillonnage (sampling), donc moins précis que le profilage instrumenté
  • Interface parfois limitée

AQtime (Outil commercial)

AQtime est un profileur professionnel très puissant pour Delphi (et d'autres langages).

Fonctionnalités :

  • Profilage de performance détaillé
  • Analyse de mémoire et détection de fuites
  • Profilage de l'utilisation de ressources
  • Nombreuses vues et rapports
  • Intégration avec l'IDE Delphi

Avantages :

  • Très complet et précis
  • Interface utilisateur riche
  • Support professionnel
  • Idéal pour les projets d'entreprise

Inconvénients :

  • Payant (coût élevé)
  • Courbe d'apprentissage
  • Peut ralentir significativement l'application pendant le profilage

Quand l'utiliser :

Pour des projets professionnels où les performances sont critiques et où le budget le permet.

Nexus Quality Suite (gratuit)

Une alternative gratuite qui offre un profilage de base.

Avantages :

  • Gratuit
  • Interface simple
  • Suffisant pour des besoins basiques

Inconvénients :

  • Moins de fonctionnalités qu'AQtime
  • Documentation limitée
  • Support communautaire

Profilage avec des outils Windows

Performance Monitor (PerfMon) : Outil Windows natif pour surveiller les performances système.

Process Explorer : Outil gratuit de Sysinternals (Microsoft) pour analyser les processus en détail.

Visual Studio Profiler : Peut être utilisé avec des applications Delphi si vous avez Visual Studio.

Approche manuelle avec instrumentation

Vous pouvez créer votre propre système de profilage simple :

unit ProfilageSimple;

interface

uses
  System.SysUtils, System.Diagnostics, System.Generics.Collections;

type
  TProfileurSimple = class
  private
    FMesures: TDictionary<string, Int64>;
    FChronos: TDictionary<string, TStopwatch>;
  public
    constructor Create;
    destructor Destroy; override;

    procedure Commencer(const Nom: string);
    procedure Terminer(const Nom: string);
    procedure AfficherResultats;
  end;

var
  Profileur: TProfileurSimple;

implementation

constructor TProfileurSimple.Create;  
begin  
  FMesures := TDictionary<string, Int64>.Create;
  FChronos := TDictionary<string, TStopwatch>.Create;
end;

destructor TProfileurSimple.Destroy;  
begin  
  FMesures.Free;
  FChronos.Free;
  inherited;
end;

procedure TProfileurSimple.Commencer(const Nom: string);  
var  
  Chrono: TStopwatch;
begin
  Chrono := TStopwatch.StartNew;
  FChronos.AddOrSetValue(Nom, Chrono);
end;

procedure TProfileurSimple.Terminer(const Nom: string);  
var  
  Chrono: TStopwatch;
  Temps: Int64;
begin
  if FChronos.TryGetValue(Nom, Chrono) then
  begin
    Chrono.Stop;
    Temps := Chrono.ElapsedMilliseconds;

    if FMesures.ContainsKey(Nom) then
      FMesures[Nom] := FMesures[Nom] + Temps
    else
      FMesures.Add(Nom, Temps);

    FChronos.Remove(Nom);
  end;
end;

procedure TProfileurSimple.AfficherResultats;  
var  
  Paire: TPair<string, Int64>;
begin
  WriteLn('=== Résultats du profilage ===');
  for Paire in FMesures do
    WriteLn(Format('%s : %d ms', [Paire.Key, Paire.Value]));
end;

initialization
  Profileur := TProfileurSimple.Create;

finalization
  Profileur.AfficherResultats;
  Profileur.Free;

end.

Utilisation :

procedure MonTraitement;  
begin  
  Profileur.Commencer('Chargement données');
  ChargerDonnees;
  Profileur.Terminer('Chargement données');

  Profileur.Commencer('Traitement');
  TraiterDonnees;
  Profileur.Terminer('Traitement');

  Profileur.Commencer('Sauvegarde');
  SauvegarderResultats;
  Profileur.Terminer('Sauvegarde');
end;

Identifier les goulots d'étranglement

Que rechercher ?

Lors de l'analyse des résultats de profilage, concentrez-vous sur :

Les fonctions "chaudes" (hot spots) : Les fonctions qui consomment beaucoup de temps d'exécution (généralement les 5-10 premières du rapport).

Les fonctions appelées très fréquemment : Une fonction rapide appelée des millions de fois peut être un problème.

Les boucles imbriquées : Particulièrement celles avec une complexité O(n²) ou pire.

Les accès aux bases de données : Souvent la cause principale de lenteur dans les applications.

Les allocations/libérations mémoire fréquentes : La création et destruction répétée d'objets peut ralentir l'application.

Les opérations de chaînes de caractères : La concaténation répétée de chaînes est coûteuse.

Exemple d'analyse

Supposons que le profileur vous montre :

Fonction                    Temps    %      Appels
================================================
ChargerClients             2500ms   50%    1  
RechercherClient           1000ms   20%    50000  
AfficherInterface          500ms    10%    1  
CalculerTotal              400ms    8%     10000  
ValiderEmail               300ms    6%     5000  
Autres                     300ms    6%     -  

Analyse :

  1. ChargerClients prend 50% du temps. C'est votre priorité n°1 d'optimisation.

  2. RechercherClient est appelée 50 000 fois. Même si chaque appel est rapide (~0.02ms), le total est significatif. Peut-être faut-il réduire le nombre d'appels ou utiliser une meilleure structure de données.

  3. CalculerTotal et ValiderEmail méritent aussi d'être examinées mais avec moins de priorité.

Méthode d'investigation

Une fois un goulot identifié :

1. Comprendre pourquoi c'est lent

Examinez le code de la fonction. Utilisez TStopwatch pour mesurer ses différentes parties et identifier précisément où le temps est perdu.

2. Chercher les opérations coûteuses

  • Accès base de données non optimisés
  • Boucles inefficaces
  • Allocations mémoire inutiles
  • Conversions de type répétées
  • Opérations sur les chaînes

3. Vérifier les appels multiples

Parfois le problème n'est pas la fonction elle-même, mais le fait qu'elle est appelée trop souvent. Peut-être pouvez-vous mettre en cache certains résultats ?

Techniques d'optimisation

Une fois les goulots identifiés, voici les techniques courantes pour optimiser votre code Delphi.

1. Optimisation des algorithmes et structures de données

Choisir la bonne structure de données :

// LENT : Recherche linéaire dans une liste
function TrouverClient(Liste: TList<TClient>; ID: Integer): TClient;  
var  
  Client: TClient;
begin
  Result := nil;
  for Client in Liste do
  begin
    if Client.ID = ID then
    begin
      Result := Client;
      Break;
    end;
  end;
end;

// RAPIDE : Utiliser un dictionnaire
var
  Clients: TDictionary<Integer, TClient>;

function TrouverClientRapide(ID: Integer): TClient;  
begin  
  Clients.TryGetValue(ID, Result);
end;

Éviter les boucles imbriquées inutiles :

// LENT : O(n²)
procedure TrouverDoublons(Liste: TList<Integer>);  
var  
  i, j: Integer;
begin
  for i := 0 to Liste.Count - 1 do
    for j := i + 1 to Liste.Count - 1 do
      if Liste[i] = Liste[j] then
        ShowMessage('Doublon trouvé');
end;

// RAPIDE : O(n)
procedure TrouverDoublonsRapide(Liste: TList<Integer>);  
var  
  Vus: TDictionary<Integer, Boolean>;
  Valeur: Integer;
begin
  Vus := TDictionary<Integer, Boolean>.Create;
  try
    for Valeur in Liste do
    begin
      if Vus.ContainsKey(Valeur) then
        ShowMessage('Doublon trouvé')
      else
        Vus.Add(Valeur, True);
    end;
  finally
    Vus.Free;
  end;
end;

2. Optimisation des chaînes de caractères

Éviter la concaténation répétée :

// LENT : Chaque += crée une nouvelle chaîne
function GenererRapport(Lignes: TStringList): string;  
var  
  Ligne: string;
begin
  Result := '';
  for Ligne in Lignes do
    Result := Result + Ligne + #13#10;  // Très inefficace !
end;

// RAPIDE : Utiliser TStringBuilder
function GenererRapportRapide(Lignes: TStringList): string;  
var  
  Builder: TStringBuilder;
  Ligne: string;
begin
  Builder := TStringBuilder.Create;
  try
    for Ligne in Lignes do
      Builder.AppendLine(Ligne);
    Result := Builder.ToString;
  finally
    Builder.Free;
  end;
end;

Utiliser les bonnes fonctions de comparaison :

// Pour comparaisons insensibles à la casse
if SameText(Chaine1, Chaine2) then  // Plus rapide que UpperCase()
  // ...

// Pour vérifier si une chaîne commence par
if Chaine.StartsWith('Bonjour') then  // Plus rapide que Pos()
  // ...

3. Mise en cache (Caching)

Mettre en cache les calculs coûteux :

type
  TCalculateurCache = class
  private
    FCacheResultats: TDictionary<string, Double>;
  public
    constructor Create;
    destructor Destroy; override;

    function CalculerComplexe(const Param: string): Double;
  end;

function TCalculateurCache.CalculerComplexe(const Param: string): Double;  
begin  
  // Vérifier si déjà calculé
  if FCacheResultats.TryGetValue(Param, Result) then
    Exit;  // Retourner la valeur en cache

  // Sinon, effectuer le calcul coûteux
  Result := CalculComplexeEtLong(Param);

  // Mettre en cache pour les prochaines fois
  FCacheResultats.Add(Param, Result);
end;

4. Optimisation des accès base de données

Réduire le nombre de requêtes :

// LENT : Une requête par client
procedure ChargerCommandesLent(Clients: TList<TClient>);  
var  
  Client: TClient;
begin
  for Client in Clients do
  begin
    // Requête SQL pour chaque client
    ChargerCommandesDuClient(Client.ID);
  end;
end;

// RAPIDE : Une seule requête pour tous
procedure ChargerCommandesRapide(Clients: TList<TClient>);  
var  
  IDsClients: string;
begin
  // Créer une liste d'IDs : '1,2,3,4,5'
  IDsClients := CreerListeIDs(Clients);

  // Une seule requête SQL avec IN
  ExecuterSQL('SELECT * FROM Commandes WHERE ClientID IN (' + IDsClients + ')');
end;

Utiliser des index appropriés : Assurez-vous que vos tables ont des index sur les colonnes fréquemment recherchées.

Limiter les colonnes récupérées :

// LENT : Récupérer toutes les colonnes
SELECT * FROM Clients WHERE ...

// RAPIDE : Récupérer uniquement ce qui est nécessaire
SELECT ID, Nom, Prenom FROM Clients WHERE ...

Utiliser les transactions pour les opérations multiples :

procedure InsererPlusieursFois;  
begin  
  // Démarrer une transaction
  FDConnection.StartTransaction;
  try
    // Insérer 1000 enregistrements
    for i := 1 to 1000 do
      InsererEnregistrement(i);

    // Valider toutes les insertions en une fois
    FDConnection.Commit;
  except
    FDConnection.Rollback;
    raise;
  end;
end;

5. Optimisation de l'interface utilisateur

Suspendre les mises à jour visuelles :

procedure RemplirListeRapide;  
begin  
  ListView1.Items.BeginUpdate;
  try
    for i := 1 to 10000 do
      AjouterElementListe(i);
  finally
    ListView1.Items.EndUpdate;
  end;
end;

Virtualiser les contrôles pour grandes quantités de données :

Utilisez des composants virtuels (comme TListView en mode virtuel) qui ne créent que les éléments visibles à l'écran.

Charger les images en arrière-plan :

procedure ChargerImageAsync(const URL: string);  
begin  
  TTask.Run(procedure
  var
    Image: TBitmap;
  begin
    // Télécharger/charger l'image (opération longue)
    Image := TelechargerImage(URL);

    // Mettre à jour l'interface dans le thread principal
    TThread.Synchronize(nil, procedure
    begin
      Image1.Picture.Assign(Image);
      Image.Free;
    end);
  end);
end;

6. Gestion de la mémoire

Réutiliser les objets plutôt que les recréer :

// LENT : Créer/détruire à chaque fois
procedure TraiterDonnees;  
var  
  Liste: TStringList;
begin
  for i := 1 to 1000 do
  begin
    Liste := TStringList.Create;
    try
      // Traiter...
    finally
      Liste.Free;
    end;
  end;
end;

// RAPIDE : Réutiliser
procedure TraiterDonneesRapide;  
var  
  Liste: TStringList;
begin
  Liste := TStringList.Create;
  try
    for i := 1 to 1000 do
    begin
      Liste.Clear;
      // Traiter...
    end;
  finally
    Liste.Free;
  end;
end;

Utiliser les objets de taille appropriée :

// Dimensionner correctement les listes
Liste := TList<string>.Create;  
Liste.Capacity := 1000;  // Pré-allouer si on connaît la taille  

7. Parallélisation et multithreading

Pour les tâches qui peuvent s'exécuter en parallèle :

uses
  System.Threading;

procedure TraiterEnParallele;  
begin  
  TParallel.For(0, 999, procedure(i: Integer)
  begin
    // Traiter l'élément i en parallèle
    TraiterElement(i);
  end);
end;

Attention : Le multithreading ajoute de la complexité. N'utilisez-le que si le gain de performance en vaut la peine.

8. Optimisations spécifiques au compilateur

Activer l'optimisation du compilateur :

Dans Project > Options > Delphi Compiler > Compiling :

  • Cochez Optimization pour le mode Release
  • Décochez Debug information en Release (réduit la taille de l'exécutable)

Utiliser les directives de compilation :

{$OPTIMIZATION ON}    // Activer optimisation pour cette unité
{$RANGECHECKS OFF}    // Désactiver les vérifications de bornes (attention !)
{$OVERFLOWCHECKS OFF} // Désactiver les vérifications de débordement

Versions courtes équivalentes : {$O+}, {$R-}, {$Q-}.

Attention : Désactiver les vérifications peut masquer des bugs (accès tableau hors bornes, overflow d'entiers). Faites-le uniquement sur du code éprouvé et testé en profondeur.

Bonnes pratiques d'optimisation

1. Mesurez avant d'optimiser

Ne jamais optimiser sans avoir d'abord mesuré. Vous pourriez perdre du temps sur des parties qui n'ont aucun impact réel.

2. Optimisez les gros gains d'abord

Concentrez-vous sur les fonctions qui représentent le plus de temps d'exécution. Optimiser une fonction qui prend 0,1% du temps n'a quasiment aucun impact.

3. Un code lisible avant tout

N'optimisez pas au détriment de la lisibilité sauf nécessité absolue. Un code maintenable est plus important qu'un code ultra-optimisé mais incompréhensible.

// Préférez ceci (clair)
function CalculerMoyenne(Valeurs: TArray<Integer>): Double;  
var  
  Somme, i: Integer;
begin
  Somme := 0;
  for i := 0 to High(Valeurs) do
    Somme := Somme + Valeurs[i];
  Result := Somme / Length(Valeurs);
end;

// Plutôt que ceci (obscur mais marginalement plus rapide)
function CalculerMoyenne(V: TArray<Integer>): Double;  
var S,i:Integer;begin S:=0;for i:=0to High(V)do S:=S+V[i];Result:=S/Length(V);end;  

4. Testez après chaque optimisation

Après avoir optimisé quelque chose :

  • Vérifiez que ça fonctionne toujours correctement (tests unitaires)
  • Mesurez l'amélioration réelle
  • Si le gain est négligeable, annulez l'optimisation

5. Documentez les optimisations

Si vous écrivez du code optimisé qui n'est pas évident, ajoutez un commentaire expliquant pourquoi :

// Utilisation de TStringBuilder plutôt que concaténation
// car cette fonction peut traiter jusqu'à 100 000 lignes
// (gain mesuré : 5000ms → 200ms)
function GenererGrosRapport: string;  
var  
  Builder: TStringBuilder;
begin
  // ...
end;

6. Évitez l'optimisation prématurée

La citation célèbre de Donald Knuth : "L'optimisation prématurée est la racine de tous les maux."

Développez d'abord un code propre et fonctionnel. Optimisez seulement quand :

  • Vous avez identifié un problème de performance réel
  • Vous avez mesuré où est le problème
  • L'optimisation apporte un gain significatif

7. Pensez à l'échelle

Un code parfait pour 100 éléments peut être catastrophique pour 100 000 éléments. Pensez à comment votre code se comportera avec de grandes quantités de données.

Optimisations à éviter (anti-patterns)

Micro-optimisations inutiles

// N'optimisez PAS ce genre de choses
// (le compilateur le fait déjà)
Result := X * 2;  // Vs  Result := X + X;  // Aucune différence réelle

Sacrifier la maintenabilité

N'écrivez pas du code incompréhensible juste pour gagner quelques microsecondes.

Optimiser partout

Il est inutile d'optimiser chaque ligne de code. Concentrez-vous sur les 20% qui comptent vraiment.

Ignorer les tests après optimisation

Une optimisation qui casse des fonctionnalités ne vaut rien. Testez toujours après avoir optimisé.

Outils de surveillance en production

Journalisation (Logging)

Ajoutez des journaux de performance dans votre application en production :

procedure TraiterCommande(Commande: TCommande);  
var  
  Chrono: TStopwatch;
begin
  Chrono := TStopwatch.StartNew;
  try
    // Traitement...
    TraiterLaCommande(Commande);
  finally
    Chrono.Stop;
    Logger.Log(Format('Commande %d traitée en %d ms',
                     [Commande.ID, Chrono.ElapsedMilliseconds]));
  end;
end;

Métriques et télémétrie

Pour les applications professionnelles, envoyez des métriques de performance à un système de monitoring pour détecter les problèmes en production.

Alertes sur les seuils

Configurez des alertes si certaines opérations dépassent un temps acceptable :

procedure TraiterAvecAlerte(Temps: Int64);  
begin  
  if Temps > 5000 then  // Plus de 5 secondes
    EnvoyerAlerte('Traitement anormalement lent : ' + IntToStr(Temps) + ' ms');
end;

Checklist d'optimisation

Lorsque vous optimisez une application, suivez cette checklist :

□ Phase 1 : Mesure

  • Identifier les fonctionnalités lentes (feedback utilisateurs)
  • Profiler l'application
  • Identifier les 3-5 plus gros goulots d'étranglement

□ Phase 2 : Analyse

  • Comprendre pourquoi chaque goulot est lent
  • Estimer le gain potentiel d'optimisation
  • Prioriser les optimisations par impact/effort

□ Phase 3 : Optimisation

  • Optimiser le goulot #1
  • Tester que tout fonctionne encore
  • Mesurer l'amélioration réelle
  • Répéter pour les autres goulots si nécessaire

□ Phase 4 : Validation

  • Exécuter tous les tests unitaires
  • Tester l'application complète
  • Vérifier les performances en conditions réelles
  • Documenter les optimisations effectuées

Conseils pour débutants

Commencez simple

Utilisez d'abord TStopwatch pour identifier les parties lentes de votre code avant d'investir dans des outils complexes.

Ne vous perdez pas dans les détails

Concentrez-vous sur les optimisations qui ont un impact visible. Gagner 1ms sur une opération qui prend déjà 2ms n'aura aucun impact perceptible.

Apprenez à reconnaître les patterns de lenteur

Avec l'expérience, vous apprendrez à reconnaître les structures de code susceptibles d'être lentes (boucles imbriquées, requêtes dans des boucles, etc.).

Gardez des versions

Avant d'optimiser, sauvegardez votre code (utilisez Git). Vous pourrez ainsi revenir en arrière si l'optimisation cause des problèmes.

L'expérience utilisateur prime

Parfois, améliorer la perception de performance (barre de progression, retour immédiat) est plus important qu'optimiser le code lui-même.

Acceptez les compromis

Parfois, la meilleure optimisation consiste à accepter qu'une opération prenne du temps et à l'exécuter en arrière-plan plutôt que d'essayer de l'accélérer.

Cas pratiques d'optimisation

Cas 1 : Chargement lent d'une grille

Problème : Une grille avec 10 000 lignes prend 30 secondes à charger.

Investigation :

  • Profiler montre que 90% du temps est dans la boucle de remplissage
  • Chaque ajout de ligne déclenche un rafraîchissement visuel

Solution :

// AVANT
for i := 0 to 9999 do
  AjouterLigne(Donnees[i]);  // Rafraîchit à chaque ligne

// APRÈS
StringGrid1.BeginUpdate;  
try  
  for i := 0 to 9999 do
    AjouterLigne(Donnees[i]);
finally
  StringGrid1.EndUpdate;  // Rafraîchit une seule fois
end;

Résultat : Temps réduit de 30s à 0.5s.

Cas 2 : Recherche lente dans une liste

Problème : Rechercher un client par ID prend de plus en plus de temps au fur et à mesure que la liste grandit.

Investigation :

  • 50 000 clients dans une TList
  • Recherche linéaire O(n) utilisée

Solution :

// AVANT
FClients: TList<TClient>;

function TrouverClient(ID: Integer): TClient;  
var  
  Client: TClient;
begin
  for Client in FClients do
    if Client.ID = ID then
      Exit(Client);
  Result := nil;
end;

// APRÈS
FClients: TDictionary<Integer, TClient>;

function TrouverClient(ID: Integer): TClient;  
begin  
  FClients.TryGetValue(ID, Result);
end;

Résultat : Temps de recherche de O(n) à O(1), passage de ~25ms à <0.1ms.

Cas 3 : Génération de rapport lent

Problème : Générer un rapport PDF de 100 pages prend 2 minutes.

Investigation :

  • La concaténation de chaînes prend 80% du temps
  • Des milliers de petites concaténations

Solution :

// AVANT
function GenererRapport: string;  
var  
  i: Integer;
begin
  Result := '';
  for i := 1 to 100000 do
    Result := Result + Ligne[i];  // Crée une nouvelle chaîne à chaque fois
end;

// APRÈS
function GenererRapport: string;  
var  
  Builder: TStringBuilder;
  i: Integer;
begin
  Builder := TStringBuilder.Create(100000 * 80);  // Pré-allocation
  try
    for i := 1 to 100000 do
      Builder.Append(Ligne[i]);
    Result := Builder.ToString;
  finally
    Builder.Free;
  end;
end;

Résultat : Temps réduit de 120s à 8s.

Conclusion

L'optimisation des performances est un aspect crucial du développement professionnel avec Delphi. Les points essentiels à retenir :

Méthodologie :

  • Mesurez toujours avant d'optimiser
  • Utilisez le profilage pour identifier les vrais problèmes
  • Concentrez vos efforts sur les goulots d'étranglement significatifs
  • Testez après chaque optimisation

Techniques clés :

  • Choisissez les bonnes structures de données et algorithmes
  • Optimisez les opérations sur les chaînes avec TStringBuilder
  • Mettez en cache les résultats de calculs coûteux
  • Minimisez les accès aux bases de données
  • Suspendez les mises à jour visuelles lors d'opérations groupées

Principes :

  • La lisibilité du code prime sur les micro-optimisations
  • L'optimisation prématurée est à éviter
  • Documentez vos optimisations non évidentes
  • Pensez à l'échelle dès la conception

Outils :

  • TStopwatch pour les mesures simples
  • Profileurs intégrés ou tiers pour l'analyse approfondie
  • Tests unitaires pour valider les optimisations
  • Journalisation pour surveiller en production

En maîtrisant ces concepts et techniques, vous serez capable de créer des applications Delphi non seulement fonctionnelles, mais aussi performantes et réactives. N'oubliez pas que l'optimisation est un processus itératif : mesurez, optimisez, validez, et répétez jusqu'à atteindre les performances souhaitées.

⏭️ Gestion des exceptions et journalisation