Softwareoptimierung

Hochoptimiertes Puzzle

9. Mai 2017, 13:56 Uhr | Matthias Bauer
Diesen Artikel anhören

Fortsetzung des Artikels von Teil 1

Lösung 2: Dynamische Polymorphie

UML / Redlogix
Bild 2: UML-Klassendiagramm.
© Bilder: Redlogix

Dass dieser Lösungsweg für Embedded-Systeme mit knappen Ressourcen suboptimal ist, liegt auf der Hand, denn die Lösung enthält ungenutzten Code, sie belegt also mehr Codespeicher als nötig, weil sie sämtliche konfigurierbare Varianten (blockieren/überschreiben sowie mit/ohne Synchronisation) enthält. Zudem wird für die Fallunterscheidungen zur Laufzeit Rechenzeit belegt und die beiden Member-Variablen »mMutex« sowie »mBlockIfFull« belegen Datenspeicher.

Außerdem birgt dieser Ansatz den Nachteil, dass die Softwarekomponente sämtliche Varianten umsetzen muss, so dass ein schwer zu wartender Software-Monolith entsteht, der mit jeder Konfigurationsoption komplexer wird.

Anbieter zum Thema

zu Matchmaker+
Listing 2
Listing 2: Code der Methode »write« mit dynamischer Polymorphie.
© Redlogix

Eine architektonisch saubere Problemlösung zeigt das UML-Klassendiagramm in Bild 2. Dort sind die konfigurierbaren Teile aus der Komponente »Queue« ausgelagert. Die Klasse »Queue« verwendet nun jeweils eine Instanz vom Typ »ISyncStrategy« und »IMemStrategy«, mit denen sie die parallele Ausführung kritischer Codeteile verhindert (Methoden »lock« und »unlock«) oder blockiert oder Platz schafft, wenn die Queue voll ist (Methode »requestSpace«).

Die unterschiedlichen Varianten der beiden Interfaces implementieren die »Strate­gy«-Klassen. Das gewünschte Verhalten der Klasse Queue wird definiert, indem bei der Instanziierung die richtigen Strategy-Instanzen per Referenz an den Konstruktor übergeben werden: Eine blockierende Queue ohne Synchronisation erhält man, indem man die Queue-Instanz mit »BlockingQueueMemStrategy« und »NoSyncQueueSyncStrategy« kombiniert usw. Der Code der Methode »write« sieht dann aus wie in Listing 2 gezeigt.

Man erkennt, dass in der Klasse Queue zwar die Hook-Methodenaufrufe (mit * gekennzeichnet) für die Synchronisation und das Platzschaffen vorhanden sein müssen, jedoch ist das jeweilige Verhalten der gerufenen Methoden ausgelagert. Die Queue ist nun kein Monolith mehr, ihr Verhalten kann durch Konfiguration mit der jeweiligen Strategie beeinflusst werden. Leider hat die so geschaffene Modularität ihren Preis: Da der Compiler zum Zeitpunkt der Übersetzung noch nicht wissen kann, mit welchen Strategy-Instanzen die Queue zur Laufzeit arbeiten soll, kann er praktisch keine Optimierungen durchführen.
Wird z. B. die »NoSyncQueueSyncStrategy« verwendet, werden zur Laufzeit tatsächlich deren leere Methoden lock und unlock aufgerufen, was unnötig Codespeicher und Rechenzeit kostet.

Listing 3
Listing 3: Code der Methode »write«, blockierend.
© Redlogix

Das Konzept der Codegenerierung

Beide bisher gezeigten Lösungen sind so flexibel, dass das Verhalten der Queue, also mit welchen Strategien die Warteschlange arbeiten soll, erst zur Laufzeit festgelegt werden braucht. Häufig ist diese Flexibilität indes gar nicht notwendig, weil bereits zur Compilierungszeit bekannt ist, wie die Queue betrieben werden soll. Programmierer wären also in der Lage, den für die gewünschte Lösung jeweils optimalen Code hinzuschreiben. Für eine blockierende Warteschlange ohne Synchronisierung sollte der Code der Methode write wie in Listing 3 aussehen, für eine nicht blockierende Warteschlange mit Synchronisierung ist der Code in Listing 4 abgebildet.

Listing 4
Listing 4: Code der Methode »write«, nichtblockierend.
© Redlogix

Da die gemeinsamen Teile des Codes jedoch nicht dupliziert werden sollen, erfordert dies eine Möglichkeit, den jeweiligen Code vor oder während der Compilierung abhängig vom gewünschten Verhalten erzeugen zu lassen.
Eine Möglichkeit der Codegenerierung wäre die Verwendung des C-Präprozessors: Mit bedingter Compilierung lässt sich tatsächlich das gewünschte Resultat erzielen. Allerdings machen die dafür erforderlichen Präprozessoranweisungen den Quellcode schwierig lesbar, ein Wartung des Quellcodes scheidet praktisch aus. Letztlich entsteht dadurch eine monolithische Lösung wie beim Ansatz der Fallunterscheidungen zur Laufzeit (siehe Listing 1). Was benötigt wird, ist also ein Codegenerator, der aus konfigurierbaren Einzelteilen das jeweils gewünschte Endergebnis erzeugt (Bild 3).

Codegenerierung / Redlogix
Bild 3: Konzept der Codegenerierung.
© Bilder: Redlogix

  1. Hochoptimiertes Puzzle
  2. Lösung 2: Dynamische Polymorphie
  3. Lösung 3: Codegenerator

Lesen Sie mehr zum Thema


Das könnte Sie auch interessieren

Jetzt kostenfreie Newsletter bestellen!

Weitere Artikel zu Componeers GmbH

Weitere Artikel zu Betriebssysteme