Der halbe Fix ist die teuerste Stelle

Ein Pfad ist schnell. Der andere sieht aus wie derselbe Code. Genau dort bleibt die alte Komplexität liegen.
Es gibt eine bestimmte Art von Optimierung, die sich im Review besonders gut anfühlt.
Man hat das Problem erkannt. Man hat den Index gebaut. Man hat ToDictionary geschrieben. Der teure Scan ist weg — jedenfalls dort, wo man hingeschaut hat.
Wochen später ist der Export immer noch langsam. Der Profiler zeigt dieselbe Familie von Methoden. Jemand sagt: «Das haben wir doch schon gemacht.»
Stimmt. Zum Teil.
Der halbe Fix ist unbequemer als gar keiner. Er erzeugt die Überzeugung, das Thema sei geschlossen. Die teure Stelle sitzt dann oft genau neben der Stelle, die man angefasst hat.
Zwei Pfade, eine Absicht
Exporte holen selten nur eine Sorte Zeilen. Es gibt interne und externe Sätze. Aktive und archivierte. Eine Abbildung für den Normalfall und eine für den Sonderfall.
Fachlich gehört das zusammen. Im Code sind es zwei Methoden, zwei Generatoren, zwei Lambdas.
Der eine Pfad sieht nach dem Fix so aus:
var byId = masters.ToDictionary(m => m.Id);
foreach (var item in movements)
{
if (!byId.TryGetValue(item.MasterId, out var master))
continue;
rows.Add(Map(item, master));
}
Einmal indexieren. Dann nachschlagen. Linear in der Summe der Mengen.
Der andere Pfad, derselbe Export, eine andere Teilmenge, sieht oft so aus:
var byId = masters.ToDictionary(m => m.Id);
var rows = movements
.Select(item => Map(item, byId.Values.ToList()))
.ToList();
Oder, noch enger am Alltag: die Methode darunter erwartet eine Liste. Die Lambda erfüllt den Vertrag, indem sie das Dictionary bei jedem Element wieder auspackt.
.Select(item => Join(item, [.. byId.Values]))
Der Index existiert. Die Suche ist wieder linear. Zusätzlich entsteht pro Zeile eine neue Liste.
Wer nur den ersten Pfad liest, hält das Modul für erledigt. Wer den zweiten nicht öffnet, sieht im Profiler «den Export» — nicht «die eine Abbildung, die niemand umgestellt hat».
Warum das so leicht stehen bleibt
Der zweite Pfad war nie falsch. Er war nur der, den man nicht angefasst hat, weil der erste schon wehgetan hat.
Reviews verstärken das. Eine sichtbare ToDictionary-Zeile gilt als Beweis. Die Signatur der Hilfsmethode bleibt List<T>. Damit ist der Rückfall in Find nicht nur möglich. Er ist der Weg des geringsten Widerstands.
Dazu kommt eine sprachliche Falle. «Wir cachen die Stammdaten.» «Wir laden alles einmal.» Beides kann wahr sein und den Join trotzdem unverändert lassen. Beschaffung und Abbildung sind zwei verschiedene Kosten.
Der halbe Fix optimiert die Beschaffung. Die Abbildung bleibt die alte, weil sie in einer anderen Datei steht, eine andere Teilmenge betrifft oder «nur» eine Liste entgegennimmt.
Woran man ihn erkennt
Drei Muster reichen meist.
Das Dictionary wird gebaut und nicht benutzt. Es gibt ToDictionary oder TryGetValue in der Nähe — und in der Lambda trotzdem Find, FirstOrDefault oder eine Kopie von .Values.
Dieselbe Abbildung existiert zweimal. Einmal mit Index, einmal mit Liste. Die Namen unterscheiden sich um ein Suffix: intern, extern, aktiv, alle. Die Logik ist kopiert, nicht geteilt.
Die Hilfsmethode hat die alte Signatur behalten. Oben steht ein Dictionary. Unten steht List<Master>. Dazwischen eine Conversion, oft in der Lambda, oft ohne eigenen Namen.
Kein Muster braucht einen Profiler. Es braucht den zweiten Blick auf den Nachbarpfad.
Was der ganze Fix ist
Nicht «noch ein Dictionary». Sondern dieselbe Abbildung für alle Teilmengen.
Die Hilfsmethode nimmt den Index entgegen, nicht die Liste:
static Row? Join(Item item, IReadOnlyDictionary<string, Master> byId)
{
if (!byId.TryGetValue(item.MasterId, out var master))
return null;
return Map(item, master);
}
Beide Pfade rufen dieselbe Methode auf. Wer später eine dritte Teilmenge baut, bekommt den Lookup geschenkt — oder stösst an der Signatur an, statt still eine Liste durchzusuchen.
Wenn eine Stelle wirklich eine Liste braucht, etwa für eine einzelne Zeile in einem anderen Use-Case, bleibt das eine eigene Überladung. Der Exportpfad darf sie nicht benutzen, indem er den Index zerstört.
Die teuerste Stelle
Gar nicht zu optimieren ist ehrlich. Man sieht die lineare Suche. Man kann sie später finden.
Der halbe Fix erzeugt eine Geschichte: das Thema sei erledigt. Die langsame Stelle trägt denselben Namen wie die schnelle. In der Telemetrie heisst beides «Export».
Deshalb ist er teurer. Nicht, weil er mehr CPU kostet als der Ursprungszustand — manchmal sogar weniger. Sondern weil er die Aufmerksamkeit abzieht, die man bräuchte, um den Rest zu sehen.
Eine Optimierung ist erst fertig, wenn alle Abbildungen derselben Daten dieselbe Komplexität haben. Nicht wenn die erste Methode hübsch aussieht.