Optimiser F10 en conservant la fin du rapport

This commit is contained in:
rabot
2026-10-21 11:23:07 +02:00
parent 3f98548446
commit f2eb2c2f1e
12 changed files with 590 additions and 123 deletions
+136 -2
View File
@@ -163,7 +163,7 @@ 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 ; ils restent à atteindre.
du tableau de la page 67 du TD ; la section 4 présente leur réalisation.
| ID | Fonction | Calcule | Actuelle | M | Objectif |
|:---:|---|---|---|:---:|---|
@@ -261,7 +261,7 @@ 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
densement compris entre 0 et `C - 1`, comme le garantit le générateur.
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.
@@ -364,3 +364,137 @@ 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" - <<EOF
#define main banque_main
#include "$(pwd)/banque.c"
#undef main
int main(void) {
for (int i = 0; i < 200; i++) {
etat_alea = GRAINE;
if (banque_main() != 0) return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
EOF
(cd "$tmp_profil" && ./banque > /dev/null && gprof -b ./banque gmon.out) > build/profil/profil_apres_repete.txt
rm -rf "$tmp_profil"
```