Hallo zusammen,
ich überlege schon länger, ob ich diesen Post verfassen und meine Erkenntnisse mit der Community teilen sollte. Heute habe ich mich schließlich dazu überwunden.
Unsere Vorgeschichte: Wir setzen aktuell Delphi 11.3 ein. Vor einigen Jahren wurde im Rahmen der Modernisierungsarbeiten eine interne Empfehlung herausgegeben, wann immer möglich, generische Typen, entweder aus der internen Basisbibliothek oder aus dem Spring-Framework abzuleiten und zu verwenden. Grundsätzlich ist dieser Schritt begrüßenswert und richtig. Doch keiner aus unserem Team konnte zu diesem Zeitpunkt ahnen, welche langfristigen Folgen dies haben wird.
Die Folgen sind:
- Die DCUs werden aufgebläht, da jeder in der entsprechenden Unit deklarierte generische Typ stumpf nach Template-Manier reinkompiliert wird. Wenn z.B. ein TDictionary<K,V> deklariert wurde, so ist jede Methode oder Feld mit K oder V auch in der DCU enthalten. Das an sich scheint noch nicht so schlimm zu sein, denn..
- Nach erfolgter Kompilierung macht sich der Linker auf die Arbeit und sucht in allen zu linkenden DCUs nach gleichen generischen Typen, um sie nur einmalig in die Exe zu ziehen. Das ist ein löblicher Schritt, denn dadurch soll der Code-Bloat in der Exe so weit wie möglich eingedämmt werden.
Beispiel: TDictionary<K,V> kommt in UnitA, UnitB und UnitC vor. In dem Fall befindet sich zwar in jeder DCU der replizierte Code dafür, aber nach getaner Arbeit des Linkers, kommt der Code für den konkreten generischen Typ nur einmal ins Binary.
Hört sich doch gut an, wo ist das Problem?...
- Nun leider kommt dieser Vorgang nicht ohne Aufwand aus. Der erste große Nachteil ist der Zeitaufwand, nochmals: Sowohl der Compiler als auch der Linker muss für jeden generischen Typ Arbeit leisten und zwar deutlich mehr als bei non-generic Typen. Ich muss zugeben, dass dieser Aufwand ziemlich lange kaum auffällt, aber ab einem bestimmten Punkt kippt. Ein interessanter Vergleich hierzu ist das CO2, welches die Menschheit nach und nach in die Atmosphäre entlässt, das war viele Jahrzehnte auch kein Problem…aber ich schweife ab 😉.
Hier ein paar Zahlen von den zwei häufigsten generischen Typen aus dem Spring-Framework, die wir verwenden:
IList<T> über 4000x in über 600 Units
IDictionary<K,V> über 1000x in über 300 Units
Bei uns braucht der Linker schon mal ca. 10-20 Sekunden.
Hört sich ja nicht so schlimm an! Wo ist das Problem, schließlich haben wir genug Zeit…
- Jetzt kommen wir zu einem Punkt, den wir nicht so einfach relativieren können:
Der RAM-Verbrauch der dcc32, dem Delphi-Compiler, ist bei Verwendung von Generics immens. Wir können schon seit einigen Monaten nicht mehr in der IDE kompilieren, weil wir ständig EOutOfMemory-Exceptions bekommen. Die Lösung ist aktuell die Projektoption "MSBuild extern für die Compilierung verwenden". Damit hat die IDE scheinbar wieder Luft, aber bei der Erzeugung des Projekts sieht man im Task-Manager wie der RAM-Verbrauch der dcc32.exe so langsam über die 3 GB-Marke schreitet und dennoch hin und wieder mal mit einem EOutOfMemory abbricht.
Ich weiß, dass die Compiler in der 12er-Version auch als 64-Bit-Versionen vorliegen und der Versionssprung steht bei uns auch an.
Also doch alles gut?
- Leider Nein: Sobald man das Projekt debuggt, welches extern über MS-Build erzeugt wurde, muss die IDE die RSM-Datei einlesen und da ist wieder der RAM-Verbrauch. Einmal den Debugger gestartet und schon steht die bds.exe auf 2,6 GB und man weiß schon, gleich passiert es "Zu wenig Arbeitsspeicher".
Ich weiß nicht, wie die Prioritäten bei Embarcadero bzgl. der Umstellung der IDE auf 64-bit sind. Gelesen habe ich bisher nur etwas von "Fernziel". Auf meine Anfrage im Support hat man mir auch nichts in dieser Hinsicht sagen können.
- Als wenn das schon nicht genug wäre…die DelphiLSP-Prozesse, welche für die Code-Vervollständigung verwendet werden, werden durch massiven Einsatz von Generics ebenfalls langsam, bis nicht mehr zu gebrauchen. Mal schauen, wie sich die 64-Bit-Versionen davon schlagen werden.
Ich bin ein Fan von Generics, aber unter diesen Umständen kann ich vom weiträumigen Einsatz in großen Delphi-Projekten nur abraten!
Das Spring4D-Framework, so sehr ich es auch mag, sticht hier leider nochmals unangenehm hervor, wahrscheinlich weil die Ableitungshierarchien sowohl in den Interfaces als auch in den implementierenden Klassen tief verschachtelt sind. In Spring4D v2 hat Stefan schon sehr viel gegen den Code-Bloat getan, aber gegen den hohen
RAM-Verbrauch des Compilers/Linkers kann er nicht viel tun.
Dies kann nur Embarcadero fixen.
So, jetzt habe ich mal meinen Frust hier kund getan und bin gespannt, ob es irgendjemandem hier auch so ergeht?