Funktionsorientiert und schnell: Die Programmiersprache Julia

Seite 2: Anbindung an C

Inhaltsverzeichnis

Zwei herausragende Eigenschaften von Julia sind die Shell-Anbindung und die Fähigkeit, C-Bibliotheken ohne Performanceverlust aufzurufen. Das folgende Beispiel reicht über die Shell einen Julia-String zu einem Compiler weiter und bindet die kompilierte Bibliothek in Julia ein:

c_code = """
#include <stddef.h>
double c_sum(size_t n, double *X) {
double s = 0.0;
for (size_t i = 0; i < n; ++i) {
s += X[i];
}
return s;
}
"""
const Clib = tempname() # gibt einen Dateinamen 
# im temp-Verzeichnis zurück
shell_command = 
`gcc -fPIC -O3 -msse3 -xc -shared -o $(Clib * "." * Libdl.dlext) -`
# do-Syntax erstellt eine anonyme Funktion, 
# die an erster Stelle des Funktionsaufrufes übergeben wird
open(shell_command, "w") do pipe
print(pipe, c_code) # printe den Code in die Pipe,
# um ihn an GCC zu übergeben
end
# erstelle eine Julia-Funktion, die die C-Funktion aufruft
c_sum(X::Vector{Float64}) =
ccall(("c_sum", Clib),
Float64, # Rückgabetyp
(Csize_t, Ptr{Float64}), # Argumententypen
length(X), X) # Argumente

Eine vergleichbare Julia-Funktion sollte nun die gleiche Performance liefern wie die C-Funktion.

Julia bietet eine breite Werkzeugpalette zur Introspektion und Metaprogrammierung. Dazu gehören Lisp-ähnliche Makros, mit denen sich Julia-Ausdrücke syntaktisch umformen lassen. Ein Julia-Makro ist somit ein Mapping von einem Ausdruck zu einem neuen.

macro time_it(in_expressions)
# ein quote-Block erlaubt es, Julia-Code zu schreiben
# und diesen als Ausdruck zu erhalten
out_expression = quote
x = time()
$(in_expressions) # man kann andere Ausdrücke
# mit $() einfügen ('splicen')
println("elapsed time: ", time() - x)
end
return out_expression
end
# Makros werden mit einem @-Zeichen aufgerufen
# und brauchen keine Klammern.
@time_it my_function_call() # alle validen Julia-Ausdrücke 
# sind erlaubt

Makros funktionieren rein syntaktisch. Entwickler haben innerhalb des Makros keinen direkten Zugriff auf Typinformationen. Um Code auf Typen zu spezialisieren, gibt es in Julia das Konzept der sogenannten "Generated Functions". Diese arbeiten wie normale Funktionen, aber beim ersten Aufruf erhält man statt der Argumente nur die Typen und kann dann einen Ausdruck zurückgeben, der zum Funktionskörper wird:

@generated function tuple_map(f::Function, 
x::NTuple{N, Values})
where {N, Values}
# man kann auch manuell einen Ausdruck
# ohne quote-Block formen
# in diesem Fall rollt man die Anwendung der Funktion f
# auf die Elemente des Tupels aus.
expr = Expr(:tuple)
for i = 1:N
push!(expr.args, :( f(x[ $i ] )))
end
println(expr) # print zum Debuggen, um den
# entstandenen Ausdruck zu zeigen
return expr
end
>tuple_map(sqrt, (1.0, 4.0, 9.0)) == (1.0, 2.0, 3.0)
printed: (f(x[1]), f(x[2]), f(x[3]))
# beim zweiten Aufruf wurde die Funktion schon 
# kompiliert und println(expr) wird nicht mehr ausgeführt
>tuple_map(sqrt, (1.0, 4.0, 9.0))

Auf diese Weise lässt sich Julia zum Skripten des Compilers benutzen, was sich derzeit viele Entwickler für hochperformante Bibliotheken für Datenbanken und Tensor-Algebra wünschen. Das erspart ihnen die aufwendige Kombination einer Skriptsprache mit einem Compiler wie LLVM oder GCC.