Clojure Direct-Linking

Seit der Version 1.8 besitzt Clojure die Fähigkeit, den Compiler anzuweisen, die Funktionen direkt zu linken. Aber was bedeutet dies im Detail und wie kann es nützlich sein?

Beim Linken im Sinne von Clojure geht es um die Art und Weise, wie Funktionsaufrufe im compilierten Code aufgelöst werden. Konkret geht es dabei um Funktionen, die an Variablen mit der Clojure-Anweisung def oder defn gebunden werden.

Wie funktioniert das klassische, nicht-statische Linken in Clojure?

Eine Variable ist eine Referenz auf den zugewiesenen Wert.

Das nicht-statische Linken funktioniert prinzipiell so, dass ein Wert einer Variablen bei der Benutzung derselben zuvor dereferenziert wird. Die Auflösung (Dereferenzierung) erfolgt also während der Programmausführung. Das Variablenkonzept ermöglicht so eine nachträgliche Redefinition einer Variablen im Programm.

Jede Clojure-Funktion Fun, die eine andere Funktion RefFun aus einen Namespace Nspace aufruft, definiert diese Kombination als statische Klassen-Variable const__n in der Funktionsklasse Fun.class. RefFun ist dabei die Variable, die auf die Funktion zeigt.

Die Klasse Fun.class initialisiert ihre eigene statische Konstante (hier const__1) indem sie die (zuvor im Namespace Nspace definierte Variable RefFun) benutzt.

S1: public static final Var const__1 =(Var)RT.var(Nspace, RefFun)

Der Aufruf der Funktion RefFun in der Funktion Fun ist dann (vereinfacht) folgendermaßen im compilierten Code kodiert:

S2: ((IFn) const__1.getRawRoot()).invoke(…)

Mit Hilfe der Methode getRawRoot wird hier die Variable dereferenziert, d.h. ihr Wert (hier die Funktion) aufgelöst, das Ergebnis als Funktions-Interface gecastet und schließlich diese Funktion aufgerufen.

Was passiert beim statischen (direkten) Linken?

Im Vergleich zum normalen Linken entfällt die statische Initialisierung der Konstanten für die Variable der Funktion RefFun. Der Aufruf der Funktion erfolgt direkt mit der statischen Klassen-Methode invokeStatic der aufzurufenden Funktion.

S3: Nspace.RefFun.invokeStatic(…)

Die Benutzung und Auflösung der Variablen wird somit umgangen. Damit wird deutlich, dass eine nachträgliche Änderung der Variablen keine Auswirkungen im Programmcode hat.

Die Redefinition einer Variable

Mit den Clojure Funktionen alter-var-root, with-redefs und with-redefs-fn kann der Wert einer Variablen nach einer ersten Zuweisung – die Variable ist nun „gebunden“ – während der Programmausführung geändert werden.

Auch wenn statisch gelinkt wurde, wird dennoch der Inhalt der Variablen mit diesen Funktionen geändert! Nur hat dies in diesem Fall keinerlei Auswirkung, da die fest compilierte Funktion direkt aufgerufen wird.

Sicherlich kann man sich die Frage stellen: Wozu sollte man im fertigen Programm überhaupt eine Funktion neu definieren?

Diese Redefinitionen sind vor allem während der Programmentwicklung, also während des Testens und Debuggens in der REPL nützlich, um vorübergehend Funktionen durch andere Funktionen zu ersetzen.

Aber auch während des Programmablaufs ist eine Redefinition nützlich: Ich verwende z.B, alter-var-root im Programm heskudi-gpx um einen Funktionszeiger zu realisieren: In Abhängigkeit von bestimmten, vergleichsweise seltenen, aber zeitintensiven Entscheidungen, wird eine normale Funktion durch eine gecachte Variante während der Laufzeit ausgetauscht.

Die Funktion with-redefs eignet sich hervorragend für den Aufruf einer Funktion in einem ganz anderen Kontext. Ich benutze sie z.B. beim Kartendaten-Download, um dem Programmablauf alternative Bildschirmgrößen vorzutäuschen.

Die Möglichkeit Funktionen mittels alter-var-root zu modifizieren ist auch besonders im Rahmen der aspektorientierten Programmierung nützlich.

Statisches Linken aktivieren

Durch die Angabe der Java-Property clojure.compiler.direct-linking=true wird das statische Linken aktiviert. Zweckmäßigerweise verwendet man für die Compilation ein Projektmanagement wie beispielsweise Leiningen.

lein new app heskudi.sample.directlink
cd heskudi.sample.directlink/

Mittels Einfügen der rot gekennzeichneten JVM-Option in die automatisch von Leiningen erzeugte Datei project.clj wird die Uberjar-Datei statisch gelinkt:

(defproject heskudi.sample.directlink „0.1.0-SNAPSHOT“
:description „FIXME: write description“
:url „http://example.com/FIXME“
:license {:name „Eclipse Public License“
:url „http://www.eclipse.org/legal/epl-v10.html“}
:dependencies [[org.clojure/clojure „1.8.0“]]
:main ^:skip-aot heskudi.sample.directlink
:target-path „target/%s“
:profiles {:uberjar {:jvm-opts [„-Dclojure.compiler.direct-linking=true“] :aot :all}})

Das statische Linken kann für einzelne Funktionen verhindert werden, wenn diese mit dem Metadaten-Schlüsselwort :redef gekennzeichnet sind. Selbstverständlich werden auch die mit dem Metadaten-Schlüsselwort :dynamic gekennzeichneten Funktionen nicht statisch gelinkt.

Statisches Linken im Detail

Das statisches Linken der Variablen erfolgt prinzipiell nur für Funktionen.

Für numerische Werte, Strings oder ähnliche Zuweisungen gibt es das Metadaten-Schlüsselwort :const (der übrigens auch für Funktionen anwendbar ist!). Hier wird dann ähnlich wie beim statischen Linken der definierte Wert im Programm anstelle der Variablen benutzt. Auch hier ist dann eine Redefinition möglich, aber folgenlos.

Funktionen können vielfältig an Variablen gebunden werden. Ich habe anhand des compilierten Codes ausgewertet, ob die Funktion je nach Definitionsvariante statisch gelinkt wurde, wenn das Direct-Linking aktiviert wurde. Die Ergebnisse liefert die folgende Tabelle:

Beschreibung

Definition

Aufruf

Statisch gelinkt?

Normal

(defn fun [x] …)

(fun y)

ja

Schlüsselwort :redef

(defn ^:redef fun [x] …)

(fun y)

nein

Schlüsselwort :dynamic

(defn ^:dynamic fun [x] …)

(fun y)

nein

Variadische Funktion, Aufruf mit mindestens einem variablen Argument

(defn fun [& more] …)

(fun y)

ja

Variadische Funktion, Aufruf ohne ein variables Argument

(defn fun [& more] …)

(fun)

nein

Zeiger auf Funktion

(defn fun [x] …)

(def zei fun)

(zei y)

ja

Vorwärtsdeklaration und Aufruf ohne Definition

 

(declare fun)

(fun y)

(defn fun [x] …)

 (fun y)

 nein

Vorwärtsdeklaration und Aufruf mit Definition

(declare fun)…

(defn fun [x] …)

(fun y)

 (fun y)ja

Definition einer Funktion über eine Closure

(def fun
(let [a 1]
(fn [x] (+ a x))))

(fun y)

nein

Man kann feststellen, dass dass statische Linken nur für diejenigen Variablen aktiviert wird, an denen eindeutig eine Funktion gebunden ist.

Geschwindigkeit

Wie schon oben unter (S1) beschrieben, entfällt für statisch gelinkte Funktionen das Initialisieren der statischen Konstanten. Damit wird natürlich auch die Codegröße verringert. Das müsste sich vorteilhaft auf die Geschwindigkeit beim Starten eines Programms auswirken.

Der direkte Aufruf der Funktionen (S3) während der Programmausführung anstelle der sonst zuvor nötigen Dereferenzierung der Konstanten und das IFn-Castings (S2) sollte sich auch günstig auf die Geschwindigkeit auswirken.

Eigene Laufzeitanalysen belegten wie erwartet einen geringen Geschwindigkeitsvorteil der statisch gelinkten Funktionen, wenn das Programm im Java-Client-Modus gestartet wurde. Im Server-Modus hingegen konnte ich keinen Vorteil der statisch gelinkten Funktionen erkennen, was für den JVM-Optimierer (JIT-Compiler) spricht. Es ist aber möglich, dass unter anderen Umständen in der Praxis das direkte Linken einen Geschwindigkeitsvorteil bewirkt.

Die Messung des Programmstarts am Beispiel meines Programms heskudi-gpx lieferte keine signifikante Verbesserung des Startgeschwindigkeit.

Risiken

Die Verwendung des Direct-Linking birgt Risiken in sich, die vielleicht auf den ersten Blick nicht ganz offensichtlich sind.

Wenn man für bestehende Programm das Direct-Linking anwenden will, muss erst einmal der Code auf die Verwendung der Funktionen alter-var-root, with-redefs und with-redefs-fn untersucht werden. Dazu kann auch die Verwendung von Low-Level-Methoden der Klasse clojure.lang.Var gezählt werden, mit denen ähnliche „Manipulationen“ möglich sind. Die entsprechenden Variablen müssen dann mit :redef vor dem direkten Linken geschützt werden.

Viel bedeutsamer ist allerdings, dass man üblicherweise in eigenen Programmen Clojure-Bibliotheken verwendet, deren Source-Code z.B. bei der Erstellung einer Uberjar auch erst zuvor (dann implizit mit dem Direct-Linking-Flag) compiliert werden muss. Wenn aber dort in den Quellen, die man gewöhnlich nicht kennt oder kennen will, die Funktionen mittels alter-var-root o.ä. überschrieben werden, ist der Ärger „vorprogrammiert“!

Selbst wenn man für jede verwendete Bibliothek das statische Linken ausschaltet, so werden doch ihre Funktionen von den eigenen Funktionen nach wie vor direkt aufgerufen.

Fazit

Das direkte oder statische Linken ist seit Clojure 1.8 eingeführt worden. Es ist als eine Optimierungsstrategie hinsichtlich Größe und Ausführungsgeschwindigkeit zu betrachten.

Das ist erst dann sinnvoll, wenn man ein Programm als Uberjar-Datei ausliefern möchte. Wenn fremde Bibliotheken verwendet werden, muss durch Sichtung der Quellen und ausführliche Test sichergestellt werden, dass dort keine Funktionen überschrieben werden.

Anhand meines Programms heskudi-gpx Version 0.5.1, welches statisch gelinkt ist, konnte ich nur geringe Geschwindigkeitsvorteile erkennen. Die Codegröße der Uberjar-Datei wurde kleiner.

Ob der nötige Aufwand angesichts eher geringer Vorteile gerechtfertigt ist, muss im Einzelfall entschieden werden.