Matlab, Objektorientierte Programmierung (OOP) und Laufzeiten

Matlab (MATrix LABoratory) wird oft in wissenschaftlichen und ingenieurtechnischen Feldern angewendet. Es ist eine Spezialsoftware für numerische und symbolische Mathematik. Weniger bekannt hingegen ist, dass Matlab auch objektorientierte Programmierung (OOP) unterstützt. In diesem Blogartikel möchte ich auf diese Möglichkeit und ihre Tücken kurz eingehen.
Wer mit Matlab arbeitet, kennt es allzu gut. Es können schnell lange, unübersichtliche Skripte entstehen, denn Matlab wird auch gern genutzt, um eben mal Daten grafisch in einem Plot darzustellen. Oft höre ich „das ist historisch gewachsen“.  Ich selbst denke mir dann, das muss doch irgendwie hübscher und weniger redundant gehen? Außerdem wäre es schön, wenn das Skript auf einfache Weise erweiterbar wäre. Also wie es bestimmte Techniken der objektorientierten Programmierung erlauben.

Es gibt OOP in Matlab?

Wie bereits erwähnt ist OOP auch in Matlab möglich. Seit der Version R2008a gibt es in Matlab Klassen mit Properties, Methoden und Vererbung. Jedoch musst du bei der Verwendung von solchen Mitteln in Matlab ganz schön auf die Laufzeiten aufpassen. Vor allem sind die Laufzeiten stark abhängig davon, welche Version du verwendest. Dazu später mehr.

Beispiel „Log Levels“

Schauen wir uns anhand eines Beispiels an, wie OOP in Matlab aussehen kann:
Ich möchte gern Log-Nachrichten konfigurieren und den entsprechenden Log-Level anzeigen. Ein ähnliches Konzept gibt es bereits in anderen Programmiersprachen, zum Beispiel in Python (Logging Levels).  Die verschiedenen Typen sollen also eine Ordnung erhalten, die den Grad der Risikobewertung widerspiegelt (Order). Je niedriger die Ordnungszahl ist, desto kritischer soll die Lognachricht sein.

1. Variante: Enumeration

Ein plausibles erstes Mittel sind Aufzählungstypen (Enumeration Types). Tatsächlich gibt es auch in Matlab Enumerations. Es ist daher naheliegend, diese Variante auch auszuprobieren. Um die Ordnung zu integrieren, leitet die Basisklasse LoggingEnum von der Integer-Datentypgröße uint8 (8-Bit-Integer-Arrays ohne Vorzeichen) ab. Eine Vererbung von Klassen wird in Matlab durch das Zeichen < angegeben. Um die Bezeichnung eines Log-Levels später in Texten anzeigen zu können, füge ich noch eine Methode get_description hinzu, welche mir den Namen als String zurück gibt.

classdef LoggingEnum < uint8
     %Defines default log-level types to use it later at log messages.
     %The lower the order, the more critical the log message.

     enumeration
         Debug (4)
         Info (3)
         Warning (2)
         Error (1)
         FatalError (0)
     end

     methods
         function description = get_description(loglevel)
            description = char(loglevel);
         end
     end
end

Die Ausgabe eines Enumeration im Command Window in Matlab sieht dann so aus:

>> info = LoggingEnum.Info
info = 
 LoggingEnum enumeration   
   Info

Die Methoden einer Klasse lassen sich entweder durch die neue Syntax obj.method

>> this_description = LoggingEnum.Info.get_description 
this_description = Info

oder auch klassisch durch die Syntax method(obj) aufrufen:

>> this_description = get_description(LoggingEnum.Info)
this_description = Info

Jedoch möchte ich hier schon erwähnen, dass der Aufruf über die Syntax obj.method sehr langsam und nicht zu empfehlen ist! Generell sorgen Enumerations in Matlab für schlechte Performance. Deshalb lohnt es sich, auch andere Implementierungen anzuschauen.

2. Variante: Klasse mit konstanten Properties

Eine andere Möglichkeit wäre es, eine Klasse (siehe auch doc classdef) mit dem Namen LogLevel und einer Ordnung und Typbeschreibung als Properties zu erstellen. Der Konstruktor weist die Typbezeichnung und die Ordnung zu. In Matlab sieht das dann folgendermaßen aus:

classdef LogLevel
     %Defines description and order for log levels
     properties
         Description
         Order
     end
     methods
         function obj = LogLevel(this_description, this_lvl)
            obj.Description = this_description;
            obj.Order  = this_lvl;
         end
     end
end

Außerdem erstelle ich eine Klasse Logging mit konstanten Properties, welche mir die Log-Level mit den passenden Typbezeichnungen definiert, indem entsprechend Objekte von der Klasse LogLevel erstellt werden:

classdef Logging
     %Defines default log-level types to use it later at log messages.

     properties (Constant = true)
         Debug = LogLevel('Debug', 4)
         Info  = LogLevel('Info', 3)
         Warning = LogLevel('Warning', 2)
         Error = LogLevel('Error', 1)
         FatalError = LogLevel('FatalError', 0)
     end
end

Im Command Window kann ich diese konstanten Properties direkt verwenden:

>> info = Logging.Info
info = 
  LogLevel with properties:
    Description: 'Info'
          Order: 3

Der Nachteil ist, dass ich nun zwei Klassen brauche. Außerdem lassen sich die Enumerations direkt vergleichen, während ich in der Klasse LogLevel das Property Order dafür erstellen musste. Hier nochmal der Vergleich für Klassen mit konstanten Properties vs den Enumerations:

>> is_loglevel_info = Logging.Info.Order == 3
is_loglevel_info = 1
>> is_enum_info = LoggingEnum.Info == 3
is_enum_info = 1

3. Variante: Funktion mit „struct“ als Ausgabe

Eine eher herkömmliche Variante in Matlab ist eine Funktion, die den Datentyp „struct“ zurückgibt, welcher den Level und die Typbezeichnung des gewünschten Log-Typs enthält:

function logging = func_loglevel(lvl)
    %Defines loglevel types
    loglevel = struct( 'Description', [],...
                       'Order', lvl);
    if lvl == 4 
        loglevel.Description = 'Debug';
    elseif lvl == 3
        loglevel.Description = 'Info';
    elseif lvl == 2
        loglevel.Description = 'Warning';
    elseif lvl == 1
        loglevel.Description = 'Error';
    elseif lvl == 0
        loglevel.Description = 'FatalError';
    end
end

Die Ausgabe im Command Window würde dann folgendermaßen aussehen:

>> info = func_loglevel(3)
info = 
  struct with fields:
    Description: 'Info'
          Order: 3
>> this_lvl = info.Order
this_lvl = 3

Laufzeitanalyse

Nachdem ich einige Varianten vorgestellt habe, möchte ich auf die Laufzeiten eingehen. Andrew Janke hat eine nette Übersicht über die möglichen Laufzeiten in Abhängigkeit der Matlab-Version erstellt. Der Quellcode ist auch auf GitHub hochgeladen. Damit kannst du recht unkompliziert dein System testen.
Ich habe die Laufzeiten auf zwei unterschiedlichen Systemen getestet, indem ich die jeweilige Description-Ausgaben meiner Log-Level-Varianten aufgerufen habe. Diese ließ ich mit 100.000 Iterationen durchlaufen.

nIters = 100000;

name = 'enumeration, using .method';
t0 = tic;
for i = 1:nIters
    obj = LoggingEnum.Debug;
    obj.get_description;
end
te = toc(t0);
show_result(name, nIters, te);

name = 'enumeration, using method()';
t0 = tic;
for i = 1:nIters
   obj = LoggingEnum.Debug;
    get_description(obj);
end
te = toc(t0);
show_result(name, nIters, te);

name = 'class, constant properties';
t0 = tic;
for i = 1:nIters
    obj = Logging.Debug;
    obj.Description;
end
te = toc(t0);
show_result(name, nIters, te);

name = 'struct';
t0 = tic;
for i = 1:nIters
    obj = func_loglevel(4);
    obj.Description;
end
te = toc(t0);
show_result(name, nIters, te);

Dabei haben sich folgende Zeiten ergeben:

R2013b, Windows 8, [sec]/100.000 R2021b, Windows 10, [sec]/100.000
Funktion mit Ausgabeparameter vom Typ „struct“ 9.32 3.77
Enumeration mit Syntax „obj.method“ 13.08 18.63
Enumeration mit Syntax „method(obj)“ 10.38 8.66
Klasse mit konstanten Properties 7.80 2.40

Die schnellste Variante ist also die Verwendung einer Klasse mit konstanten Properties. Wenn ich die Zeiten nach der schnellsten Laufzeit normiere, dann ist die Verwendung von Enumerations im besten Fall (R2013, „method(obj)“) 1,3- und im schlechtesten Fall (R2021, „obj.method“) sogar 7,7-fach langsamer. Bei der Verwendung einer Funktion, die ein „struct“ zurückgibt, ist die Laufzeit 1,2- bis 1,6-fach langsamer.
Besonders ins Auge fallen allerdings die Zeiten für die unterschiedlichen Syntaxvarianten für die Methoden einer Klasse. Mit dem Release R2013b ist der Aufruf mittels „obj.method“ 1.26-fach langsamer als der Aufruf mittels „method(obj)“ und mit dem Release R2021b sogar 2.15-fach langsamer.

Fazit

Durch den Einsatz von OOP kann die Wartbarkeit von Software sehr profitieren. Diese Vorteile lassen sich auch in Matlab nutzen, wovon ich mit diesem Artikel einen ersten Eindruck bieten will. Bei der Benutzung von Klassen oder Enumerations können die Laufzeiten allerdings je nach Matlab-Version deutlich variieren.

Mehr Informationen zum Thema Software Engineering findest du hier in unserem Kompetenzfeld.