# Compte rendu du TD de complexité algorithmique Ce compte rendu traite le palier 1 et l'étape 4 du TD (page 68 de `R5A04_Algorithmique_MM.pdf`) : analyser la version initiale, puis atteindre les objectifs avec un commit par optimisation. Les mesures ont été réalisées le 21 octobre 2026 sur Linux x86_64, avec GCC 16.2.1 et GNU gprof 2.47. Les sections 1 à 3 décrivent la version initiale. Les données sont reproductibles grâce à la graine fixe du générateur. | Paramètre | Valeur | |---|---:| | Transactions, `n` | 30 000 | | Clients, `C` | 2 000 | | Jours, `J` | 1 000 | | Requêtes par identifiant, `Q` | 30 000 | | Somme à rendre, `S` | 400 centimes | | Types de pièces, `k` | 8 | ## 1. Mesurer la version initiale Le Makefile compile le programme avec `gcc -std=c11 -Wall -Wextra -Wpedantic -O2 banque.c -o build/banque`. La compilation s'est terminée sans avertissement. Les commandes utilisées pour compiler et enregistrer une mesure sont les suivantes, depuis la racine : ```bash make -B all (cd build && { time -p ./banque > sortie.txt; } 2> temps_avant.txt) cat build/sortie.txt build/temps_avant.txt ``` Résultat de l'exécution mesurée : ```text Clients distincts : 2000 Somme des requêtes : -2624578 Meilleure période : jours 650 à 986 (3548830) Paires opposées : 5832 Façons de rendre 400 : 1960497 Empreinte : ff68d260a7a58441 real 2.43 user 2.39 sys 0.00 ``` Le temps réel est de **2,43 s**. Le temps CPU passé dans le programme est de 2,39 s ; le temps système est arrondi à 0,00 s. Cette mesure porte sur une exécution et peut varier selon la charge de la machine. L'empreinte obtenue est bien **`ff68d260a7a58441`**, celle attendue dans le TD. Le rapport produit contient 6 017 lignes, soit 120 348 octets. Les résultats sont conservés dans [sortie.txt](build/sortie.txt) et [temps_avant.txt](build/temps_avant.txt). ## 2. Profiler et comparer aux complexités Le profil a été généré avec les commandes suivantes : ```bash make profile cp build/profil/profil.txt build/profil/profil_avant.txt ``` La cible `profile` utilise une compilation en `-O0 -g -pg`, exécute le programme, puis lance `gprof -b ./banque gmon.out` dans `build/profil`. Le profil initial est conservé dans [profil_avant.txt](build/profil/profil_avant.txt). Cette exécution produit également l'empreinte attendue. Les fichiers `build/rapport.txt` et `build/profil/rapport.txt` ont été comparés : ils sont identiques octet par octet. ### Résultats de gprof Le tableau reprend les temps propres du profil plat : ils correspondent au temps passé dans chaque fonction, hors fonctions appelées. Les pourcentages portent sur environ **6,44 s de temps CPU échantillonné**. | Fonction | Appels | Temps propre (s) | Part du temps | |---|---:|---:|---:| | F1 — `trier_par_montant` | 1 | 3,13 | 48,60 % | | F6 — `rechercher_par_id` | 30 000 | 0,85 | 13,20 % | | F7 — `nb_paires_opposees` | 1 | 0,76 | 11,80 % | | F8 — `nb_facons_rendre` | 1 appel initial | 0,66 | 10,17 % | | `longueur`, appelée par F10 | 6 017 | 0,41 | 6,37 % | | F5 — `meilleure_periode` | 1 | 0,32 | 4,97 % | | F2 — `nb_clients_distincts` | 1 | 0,11 | 1,71 % | | F3 — `total_par_client` | 1 | 0,11 | 1,71 % | | F4 — `total_par_jour` | 1 | 0,05 | 0,78 % | | F9 — `categorie` | 30 000 | 0,03 | 0,39 % | | F10 — `ajouter_ligne` | 6 017 | 0,00 | 0,00 % | Le graphe d'appels précise que F8 effectue **266 723 738 appels récursifs**, en plus de l'appel initial. Le compteur du profil plat ne montre que cet appel initial. Les écarts entre les pourcentages et les temps affichés s'expliquent par les arrondis ; quelques échantillons sont aussi attribués à `_init`. Le temps cumulé de F10, en incluant `longueur`, est d'environ **0,41 s**. Son temps propre affiché à 0,00 s ne signifie donc pas que la construction du rapport est gratuite. Plus généralement, une valeur nulle dans ce profil ne prouve pas l'absence de coût : la période d'échantillonnage est de 0,01 s. Ces temps ne se comparent pas directement aux 2,43 s de la première étape : le profil utilise `-O0` et l'instrumentation `-pg`, tandis que la mesure initiale utilise `-O2`. Le profil décrit le comportement de cette version instrumentée, pour ces données et sur cette machine. ### Les fonctions les plus coûteuses ont-elles les pires complexités ? **Non, pas systématiquement.** La complexité décrit la croissance du nombre d'opérations lorsque la taille des entrées augmente. Le profil mesure le coût d'une exécution précise. Ce coût dépend aussi des tailles effectivement utilisées, du nombre d'appels, des données et du travail effectué à chaque itération. - **F1 : le tri quadratique domine ici.** Le tri à bulles effectue `n(n - 1) / 2`, soit **449 985 000 comparaisons de paires voisines**, avec des échanges de structures lorsque les transactions sont mal ordonnées. Sa complexité est `O(n²)` et il représente 48,60 % du temps échantillonné. - **F5 : une complexité cubique ne suffit pas à en faire la fonction la plus lente.** Ses trois boucles donnent une complexité `O(J³)`, mais elles portent sur **1 000 jours**, alors que F1 traite 30 000 transactions. F5 effectue `J(J + 1)(J + 2) / 6`, soit **167 167 000 additions**, et prend environ 0,32 s, contre 3,13 s pour F1. Les variables et les tailles diffèrent : comparer uniquement les exposants serait trompeur. - **F6 : le nombre d'appels compte.** Une recherche par identifiant est en `O(n)` au pire cas, mais `main` l'effectue **30 000 fois**. Le coût de l'ensemble des recherches est donc `O(Q × n)`, soit `O(n²)` lorsque `Q = n`. Avec 0,85 s au total, F6 arrive devant F5 malgré sa complexité linéaire par appel. - **F7 : une même classe de complexité ne donne pas un même temps.** F7 examine elle aussi **449 985 000 paires**, en `O(n²)`, mais se limite à additionner deux montants, comparer le résultat à zéro et éventuellement incrémenter un compteur. Elle prend 0,76 s, environ quatre fois moins que F1, dont les itérations peuvent déplacer des transactions entières. - **F8 : les sous-problèmes recalculés multiplient les appels.** Le rendu de monnaie utilise une récursion sans mémorisation et dépasse 266 millions d'appels récursifs. Son coût dépend de `S` et de `k`, ici 400 et 8. Malgré cet arbre d'appels volumineux, son temps de 0,66 s reste inférieur à celui du tri ; le nombre d'appels ne suffit pas non plus à classer les temps. - **F10 : il faut tenir compte des fonctions appelées et des appels répétés.** Chaque ajout parcourt tout le rapport existant avec `longueur`, puis copie la nouvelle ligne. Pour un préfixe de longueur `p` et une ligne de longueur `l`, un appel coûte `O(p + l)`. Sur `m` ajouts de lignes de taille bornée, les parcours successifs coûtent `O(m²)`, soit `O(L²)` dans ce cas, où `L` est la longueur finale du rapport. Pour le rapport obtenu, les recherches de fin de chaîne relisent **340 702 496 caractères** de préfixes, ce qui explique les 0,41 s attribuées à `longueur`. Pour cette exécution instrumentée, F1, F6 et F7 représentent ensemble **73,60 %** du temps échantillonné : ce sont les premières pistes d'optimisation indiquées par le profil. ## 3. Calculer les complexités de F1 à F10 Le tableau reprend l'analyse initiale de `notes.md`, avec la complexité cyclomatique de chaque fonction. Les complexités temporelles sont données au pire cas, en faisant varier les tailles des données, même si leurs valeurs sont fixées par des constantes dans le programme. Les objectifs sont ceux du tableau de la page 67 du TD ; la section 4 présente leur réalisation. | ID | Fonction | Calcule | Actuelle | M | Objectif | |:---:|---|---|---|:---:|---| | F1 | `trier_par_montant` | tri des transactions | `O(n²)` | 6 | `O(n log n)` | | F2 | `nb_clients_distincts` | clients différents | `O(n²)` | 5 | `O(n + C)` | | F3 | `total_par_client` | solde par client | `O(n × C)` | 4 | `O(n + C)` | | F4 | `total_par_jour` | solde par jour | `O(n × J)` | 4 | `O(n + J)` | | F5 | `meilleure_periode` | période la plus rentable | `O(J³)` | 5 | `O(J)` | | F6 | `rechercher_par_id` | recherche par identifiant | `O(n)` par requête | 3 | `O(1)` par requête | | F7 | `nb_paires_opposees` | paires `a + b = 0` | `O(n²)` | 4 | `O(n log n)` | | F8 | `nb_facons_rendre` | rendu de monnaie | `O(2^(S + k))`, borne générale | 4 | `O(S × k)` | | F9 | `categorie` | catégorie d'une transaction | `O(1)` | 14 | `M ≤ 10` | | F10 | `ajouter_ligne` | construction du rapport | `O(p + l)` par ajout ; `O(L²)` au total pour des lignes de taille bornée | 2 | `O(L)` au total | `n` : transactions ; `C` : clients ; `J` : jours ; `Q` : requêtes ; `S` : somme à rendre ; `k` : types de pièces ; `L` : longueur finale du rapport ; `p` : longueur du rapport avant un ajout ; `l` : longueur de la ligne ajoutée. **F5, F7 et F8** sont signalées comme les plus difficiles dans le TD. ### Justification des complexités temporelles - **F1 et F7** : les deux boucles parcourent `n(n - 1) / 2` paires. Le travail par paire est constant, d'où `O(n²)`. - **F2** : pour chaque transaction d'indice `i`, la recherche d'un client déjà rencontré peut parcourir les `i` transactions précédentes. La somme `0 + 1 + … + (n - 1)` donne la borne `O(n²)`, atteinte si les clients sont tous différents (`C ≥ n`). - **F3 et F4** : chaque client, puis chaque jour, provoque un parcours complet des `n` transactions : respectivement `O(C × n)` et `O(J × n)`. - **F5** : chaque intervalle de jours est essayé, et sa somme est recalculée par une troisième boucle. Le nombre d'additions est `J(J + 1)(J + 2) / 6`, d'où `O(J³)`. - **F6** : une recherche peut lire les `n` transactions, soit `O(n)`. Les `Q` recherches de `main` coûtent donc `O(Q × n)` au pire cas. - **F8** : sans mémorisation, le coût suit la récurrence `T(S, k) = T(S - pièce[k - 1], k) + T(S, k - 1) + O(1)`. Chaque appel non terminal engendre deux appels ; chaque branche diminue `S` ou `k` d'au moins 1. La profondeur est donc au plus `S + k`, ce qui donne la borne générale `O(2^(S + k))`. Cette borne est large : pour `k` fixé, on peut affiner en `O(S^k)`. La notation `O(Sⁿ)` des notes initiales ne convient pas, car `n` désigne les transactions. - **F9** : le nombre de tests est borné, indépendamment de `n`, d'où `O(1)`. L'objectif porte ici sur la complexité cyclomatique, pas sur le temps. - **F10** : `longueur` lit les `p` caractères du rapport, puis la fonction copie les `l` caractères de la ligne : `O(p + l)` par ajout. Pour `m` lignes non vides de taille bornée, les préfixes relus ont des longueurs proportionnelles à `0, 1, …, m - 1` : le coût total est `O(m²)`, soit `O(L²)` dans ce cas. ### Calcul manuel de la complexité cyclomatique La convention du cours (pages 31–32) est **`M = nombre de décisions + 1`**. Chaque `if`, `else if`, `for`, `while`, ainsi que chaque opérateur `&&` ou `||`, ajoute une décision. `else`, `return` et `break` n'en ajoutent pas. Les décisions d'une fonction appelée se comptent dans cette fonction. - **F1** : 2 `for` + 1 `if` + 1 `||` + 1 `&&` = 5 décisions, donc **M = 6**. - **F2** : 2 `for` + 2 `if` = 4 décisions, donc **M = 5**. - **F3, F4 et F7** : 2 `for` + 1 `if` = 3 décisions chacune, donc **M = 4**. - **F5** : 3 `for` + 1 `if` = 4 décisions, donc **M = 5**. - **F6** : 1 `for` + 1 `if` = 2 décisions, donc **M = 3**. - **F8** : 2 `if` + 1 `||` = 3 décisions, donc **M = 4**. - **F9** : 11 `if` ou `else if` + 1 `||` + 1 `&&` = 13 décisions, donc **M = 14**. C'est la plus grande valeur de F1 à F10. - **F10** : 1 `while` = 1 décision, donc **M = 2**. La fonction auxiliaire `longueur` contient elle aussi un `while` et a **M = 2**, compté séparément. ## 4. Atteindre les objectifs Chaque optimisation est vérifiée avant son commit : compilation avec les avertissements du Makefile, tests de cas limites via `make test`, exécution en `-O2`, empreinte **`ff68d260a7a58441`** et comparaison du rapport complet avec celui de la version initiale. Les constantes, `alea`, `generer_transactions` et le format du rapport sont conservés. ### F1 — Tri fusion Le tri à bulles est remplacé par un tri fusion : chaque moitié est triée, puis les deux moitiés sont fusionnées en un passage. La comparaison porte sur le montant, puis sur l'identifiant, sans soustraction susceptible de déborder. La récurrence `T(n) = 2T(n/2) + O(n)` donne **`O(n log n)` au pire cas**, avec `O(n)` de mémoire auxiliaire et `O(log n)` de pile. La complexité cyclomatique de F1 passe de **6 à 3** ; les auxiliaires `comparer_transactions` et `trier_intervalle` ont respectivement **M = 2** et **M = 6**. Les tests comparent le résultat à un tri de référence pour les tailles 0 à 128, des doublons, des entrées déjà triées ou inversées, et les bornes des types `int` et `long`. ### F2 — Tableau des clients rencontrés Un tableau de `C` indicateurs, initialisé à zéro, remplace la recherche dans les transactions précédentes. Une transaction marque son client à son premier passage et incrémente le compteur. Le coût devient **`O(n + C)`** au lieu de `O(n²)`, avec `O(C)` de mémoire. Les identifiants de clients sont compris entre 0 et `C - 1`, comme le garantit le générateur. La complexité cyclomatique passe de **5 à 3**. Les tests couvrent l'absence de transactions, les doublons, les identifiants extrêmes et tous les clients. ### F3 — Accumulation directe par client Après la remise à zéro des `C` soldes, un seul parcours des transactions ajoute chaque montant dans `totaux[t[i].client]`. Les deux boucles sont successives : **`O(n + C)`** au lieu de `O(n × C)`, avec `O(1)` de mémoire auxiliaire en dehors du tableau de sortie. La complexité cyclomatique passe de **4 à 3**. Les tests vérifient les clients sans transaction, les débits, les crédits, les annulations et la réinitialisation entre deux appels. ### F4 — Accumulation directe par jour Le même principe s'applique aux jours : initialisation des `J` soldes, puis ajout de chaque montant dans `totaux[t[i].jour]`. Le coût passe de `O(n × J)` à **`O(n + J)`**, avec `O(1)` de mémoire auxiliaire hors sortie. La complexité cyclomatique passe de **4 à 3**. Les tests comparent chaque solde à un calcul de référence, y compris aux bornes du tableau, puis vérifient qu'un appel sans transaction remet tous les soldes à zéro. ### F5 — Meilleure période en un passage (Kadane) La somme de la meilleure période se terminant au jour courant est conservée. Si cette somme est négative, le prochain jour démarre une nouvelle période ; sinon, elle est prolongée. Le coût passe de `O(J³)` à **`O(J)`**, avec **`O(1)`** de mémoire auxiliaire, et la complexité cyclomatique de **5 à 4**. La somme initiale est celle du premier jour : une série entièrement négative renvoie bien un intervalle non vide. On redémarre uniquement pour une somme strictement négative et on remplace le meilleur résultat uniquement pour une somme strictement supérieure. Une somme nulle conserve ainsi le début le plus ancien ; à égalité, le premier début puis la première fin sont conservés comme dans les boucles initiales. Les **97 655 séries** de longueur 1 à 7 à valeurs dans `[-2, 2]` sont comparées à la version cubique, sur la somme et sur les deux bornes : elles couvrent notamment les zéros, les égalités et les séries entièrement négatives. ### F6 — Index direct par identifiant Les identifiants sont une permutation de `[0, n - 1]`. Un index de pointeurs est construit en **`O(n)`** avant la boucle des requêtes. Chaque recherche contrôle les bornes et renvoie `index_ids[id]` en **`O(1)`**, au lieu de parcourir les transactions en `O(n)`. Le coût des `Q` requêtes, construction comprise, devient **`O(n + Q)`**, avec `O(n)` de mémoire supplémentaire. Les recherches ont toutes lieu avant le tri des transactions : un déplacement des transactions imposerait de reconstruire l'index. F6 garde **M = 3** et `indexer_par_id` a **M = 2**. Les tests comparent les pointeurs renvoyés à une recherche linéaire sur des identifiants permutés pour les tailles 1 à 128, et vérifient les recherches hors bornes et dans un tableau vide. ### F7 — Tri et deux pointeurs pour les paires opposées Une copie des transactions est triée avec F1 pour préserver l'ordre du tableau d'entrée. Deux pointeurs partent ensuite des extrémités : une somme négative fait avancer le pointeur gauche, une somme positive fait reculer le droit. Pour deux montants opposés, on compte leurs multiplicités `a` et `b` et on ajoute `a × b`. Pour un groupe de `z` zéros, on ajoute `z(z - 1) / 2` : chaque paire d'indices distincts est comptée une seule fois. Le tri coûte `O(n log n)` et chaque pointeur traverse au plus `n` éléments, soit **`O(n log n)`** au total au lieu de `O(n²)`, avec `O(n)` de mémoire. F7 passe de **M = 4 à M = 3** ; l'auxiliaire `compter_paires_triees` a **M = 9**. Les tests comparent le compteur à l'énumération de toutes les paires sur les **97 655 tableaux** de longueur 1 à 7 à montants dans `[-2, 2]`, ainsi que sur une entrée vide. Ils vérifient aussi que les transactions d'origine restent inchangées, notamment en présence de doublons et de plusieurs zéros. ### F8 — Programmation dynamique pour le rendu de monnaie `facons[s]` contient le nombre de façons de rendre `s` avec les pièces déjà traitées. Il vaut initialement 1 pour la somme nulle et 0 ailleurs. Pour chaque pièce, les sommes sont parcourues dans l'ordre croissant et l'on ajoute `facons[s - piece]` à `facons[s]`. Ce sens permet la réutilisation d'une pièce ; la boucle des pièces à l'extérieur compte des combinaisons, sans distinguer l'ordre des pièces. Chaque état est calculé une seule fois par type de pièce : **`O(S × k)`** pour `k ≥ 1` (et `O(S × k + S)` en incluant l'initialisation si `k = 0`), avec **`O(S)`** de mémoire. La récursion et ses sous-problèmes répétés disparaissent. F8 passe de **M = 4 à M = 5** : l'amélioration temporelle n'implique pas une baisse de M. Les tests comparent les résultats à la récursion initiale pour les sommes de -2 à 40 et de 0 à 8 types de pièces, y compris la somme nulle et l'absence de pièces. ### F9 — Séparer les règles des débits et des crédits `categorie` répartit les transactions entre débit, montant nul et crédit. Les règles propres aux débits et aux crédits sont regroupées dans deux fonctions auxiliaires, avec des retours directs. Le test du week-end `jour % 7 >= 5` remplace les deux comparaisons reliées par `||`. Le temps reste **`O(1)`** et la complexité cyclomatique de F9 passe de **14 à 3**, sous l'objectif de 10. Les auxiliaires `categorie_debit` et `categorie_credit` ont respectivement **M = 5** et **M = 7** : les décisions ne sont pas simplement déplacées vers une autre fonction dépassant l'objectif. Les tests confrontent les 20 montants situés aux seuils et à leurs voisins, y compris `LONG_MIN` et `LONG_MAX`, à la fonction initiale, pour 20 clients et 210 jours (cycle commun des modulos 7 et 30), soit **84 000 classifications**. ### F10 — Conserver la fin du rapport `ajouter_ligne` reçoit le pointeur sur le caractère nul final, copie la ligne et renvoie le nouveau pointeur de fin. `main` le conserve entre les appels. La fonction `longueur` disparaît : les caractères déjà écrits ne sont plus relus. Un ajout coûte **`O(l)`** pour une ligne de longueur `l`, et la construction complète **`O(L)`** au lieu de `O(L²)` pour des lignes de taille bornée. La mémoire auxiliaire reste **`O(1)`** et F10 garde **M = 2**. Les tests vérifient la concaténation et le pointeur renvoyé après 200 ajouts, les lignes vides et une ligne remplissant exactement le tampon disponible. Le rapport complet garde ses **6 017 lignes et 120 348 octets**. ### Bilan des complexités | Fonction | Avant | Après | M avant → après | |---|---|---|:---:| | F1 | `O(n²)` | `O(n log n)` au pire cas | 6 → 3 | | F2 | `O(n²)` | `O(n + C)` | 5 → 3 | | F3 | `O(n × C)` | `O(n + C)` | 4 → 3 | | F4 | `O(n × J)` | `O(n + J)` | 4 → 3 | | F5 | `O(J³)` | `O(J)` | 5 → 4 | | F6 | `O(n)` par requête | `O(1)` par requête après indexation `O(n)` | 3 → 3 | | F7 | `O(n²)` | `O(n log n)` | 4 → 3 | | F8 | `O(2^(S + k))`, borne générale | `O(S × k)` pour `k ≥ 1` | 4 → 5 | | F9 | `O(1)` | `O(1)` | 14 → 3 | | F10 | `O(L²)` au total, lignes de taille bornée | `O(L)` au total | 2 → 2 | M des auxiliaires ajoutés : `comparer_transactions` **2**, `trier_intervalle` **6**, `indexer_par_id` **2**, `compter_paires_triees` **9**, `categorie_debit` **5**, `categorie_credit` **7**. Avec **M = 9** pour `main`, le maximum hors génération est désormais **9**, contre **14** initialement. ### Validation à chaque commit et mesure finale Le journal [validations_optimisations.csv](build/validations_optimisations.csv) conserve les dix vérifications successives : fonction optimisée, empreinte, égalité du rapport, temps d'une exécution et SHA-256 du fichier `banque.c` testé. Chaque ligne correspond au code du commit de cette optimisation. Les dix empreintes sont **`ff68d260a7a58441`**, et les dix rapports sont identiques octet par octet à celui de la version initiale. La validation finale comprend : - `make test` : tests de cas limites et comparaisons aux versions de référence ; - `make check` : compilation en `-O2` et vérification de l'empreinte ; - `make profile` : compilation en `-O0 -g -pg`, même sortie et même rapport ; - les tests et une exécution complète compilés avec `-O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer` : aucune erreur mémoire ou de comportement indéfini signalée ; - comparaison du code : constantes, génération des données et chaînes de formatage du rapport inchangées. La commande pour relancer les tests avec les vérifications mémoire est : ```bash make test CFLAGS='-std=c11 -Wall -Wextra -Wpedantic -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer' ``` Une nouvelle exécution de la version initiale prend **2,437683 s**. Pour la version finale compilée avec les mêmes options en `-O2`, les cinq temps réels sont **0,010910 ; 0,011111 ; 0,010734 ; 0,011242 ; 0,010525 s**, soit une médiane de **0,010910 s**, environ **11 ms**. Les mesures utilisent `time.perf_counter()` autour du processus entier et incluent l'écriture du rapport ; elles sont conservées dans [temps_apres.txt](build/temps_apres.txt). La référence étant une seule exécution et les temps dépendant de la machine, cette comparaison donne un ordre de grandeur du gain, pas un classement. ### Profil gprof après optimisation Le profil d'une exécution finale est conservé dans [profil_apres.txt](build/profil/profil_apres.txt), ainsi que dans le fichier courant [profil.txt](build/profil/profil.txt). Il a été obtenu avec : ```bash make profile cp build/profil/profil.txt build/profil/profil_apres.txt ``` Cette exécution ne fournit qu'environ **0,01 s échantillonnée**, attribuée au tri fusion. Un seul échantillon ne permet pas d'interpréter les parts de temps. Pour mieux distinguer les coûts restants, le programme complet est donc répété **200 fois dans le même processus instrumenté**, avec remise à la graine initiale entre les passages. La sortie standard est redirigée, le rapport est toujours écrit, et le dernier rapport est comparé à la référence. Cette répétition concerne uniquement le pilote de mesure ; `banque.c` et ses constantes ne sont pas modifiés. Le profil cumulé est enregistré dans [profil_apres_repete.txt](build/profil/profil_apres_repete.txt). Il représente environ **2,07 s de temps CPU échantillonné pour 200 traitements** : les nombres d'appels et les temps du tableau suivant sont ceux du lot entier. | Fonction | Appels hors récursion | Temps propre cumulé (s) | Part du temps | |---|---:|---:|---:| | `trier_intervalle` | 400 | 1,19 | 57,25 % | | `comparer_transactions` | 163 486 000 | 0,29 | 14,01 % | | `generer_transactions` | 200 | 0,14 | 6,76 % | | `compter_paires_triees` | 200 | 0,08 | 3,86 % | | `ajouter_ligne` | 1 203 400 | 0,04 | 1,93 % | | `rechercher_par_id` | 6 000 000 | 0,01 | 0,48 % | | `meilleure_periode` | 200 | 0,00 | 0,00 % | | `nb_facons_rendre` | 200 | 0,00 | 0,00 % | Les deux tris de chaque traitement, celui de F7 sur une copie et celui du rapport, expliquent les 400 appels initiaux de `trier_intervalle`. Le tri fusion et ses comparaisons représentent désormais l'essentiel du coût. Les centaines de millions d'appels récursifs de F8 ont disparu, les recherches F6 coûtent très peu et F10 ne rescane plus les préfixes du rapport. Les valeurs à 0,00 s restent des arrondis de l'échantillonnage. Les durées brutes de ce lot ne se comparent pas à celles du profil initial sur une seule exécution, ni aux temps réels mesurés en `-O2`. Pour reproduire ce profil cumulé depuis la racine : ```bash tmp_profil=$(mktemp -d) gcc -std=c11 -Wall -Wextra -Wpedantic -O0 -g -pg -x c -o "$tmp_profil/banque" - < /dev/null && gprof -b ./banque gmon.out) > build/profil/profil_apres_repete.txt rm -rf "$tmp_profil" ```