EMZETT.
Login

Kurz: Beim ersten Aufruf einer Funktion mit bestimmten Argumenttypen kompiliert Julia sie zu Maschinencode (JIT). Läuft sie danach in einer Schleife, ist sie so schnell wie C. Voraussetzung ist Typstabilität.

Teil des Kurses Julia

Kapitel 6 von 8 im Kurs Julia (Abschnitt „Wissenschaftliches Rechnen“). Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

Warum ist Julia schnell?

Beim ersten Aufruf einer Funktion mit bestimmten Argumenttypen kompiliert Julia sie zu Maschinencode (JIT). Läuft sie danach in einer Schleife, ist sie so schnell wie C. Voraussetzung ist Typstabilität: Der Typ jeder Variablen soll sich aus den Argumenttypen ergeben und nicht während der Funktion wechseln.

function summe_schleife(v)
    s = 0.0
    for x in v
        s += x
    end
    return s
end
 
function summe_instabil(v)
    s = 0                       # Int – wird beim ersten += zu Float64: Typ wechselt!
    for x in v
        s += x
    end
    return s
end
 
v = rand(1_000_000)
println(isapprox(summe_schleife(v), sum(v)))
println(isapprox(summe_instabil(v), sum(v)))
 
t = @elapsed for _ in 1:20 summe_schleife(v) end
println(t < 5.0 ? "schnell genug" : "langsam")
 
# Typinformation prüfen
println(Base.return_types(summe_schleife, (Vector{Float64},)))      # Julia leitet Float64 ab
println(Base.return_types(summe_instabil, (Vector{Float64},)))      # Union{Float64, Int64}: instabil
println(typeof(summe_schleife([1.0, 2.0])), " ", typeof(summe_instabil([1.0, 2.0])), " ", typeof(summe_instabil([1, 2])))

Ausgabe:

true
true
schnell genug
Any[Float64]
Any[Union{Float64, Int64}]
Float64 Float64 Int64

In der REPL siehst du mit @time summe_schleife(v) Zeit und Speicherverbrauch; der erste Aufruf enthält die Kompilierzeit.

Werkzeuge zur Leistungsanalyse:

WerkzeugZweck
@time, @elapsedgrobe Zeit/Allokationen (erster Aufruf enthält Kompilierzeit)
BenchmarkTools.jl@btime, @benchmark messen genau
@code_warntype f(x)zeigt Typinstabilitäten (rot markiert)
@code_llvm, @code_nativeerzeugten Code ansehen
Profile, ProfileView.jlFlammendiagramme

Faustregeln: Globale Variablen vermeiden (in Funktionen packen), Arrays vorallokieren, mit .= und @. in-place arbeiten, @views statt Kopien von Ausschnitten, @inbounds nur wenn bewiesen sicher.

# Vorallokation und fusionierte Operationen
n = 1000
x = rand(n)
y = zeros(n)
@. y = 2x^2 + 3x + 1             # @. fügt überall Punkte hinzu, eine Schleife, kein Zwischenarray
println(y[1] ≈ 2x[1]^2 + 3x[1] + 1)
 
function normiere!(v)
    m = maximum(v)
    v ./= m                       # in-place
    return v
end
println(maximum(normiere!(copy(x))))
 
teil = @view x[1:10]              # Ansicht ohne Kopie
teil[1] = -1.0
println(x[1])
 
using LinearAlgebra
A = rand(200, 200)
b = rand(200)
loesung = A \ b
println(norm(A * loesung - b) < 1e-8)
println(Threads.nthreads() >= 1)

Ausgabe:

true
1.0
-1.0
true
true

Parallel rechnen

Julia unterstützt Threads (gemeinsamer Speicher), Prozesse (Distributed), Tasks und GPUs (CUDA.jl):

using Base.Threads
 
# Aufgaben nebenläufig starten
tasks = [Threads.@spawn begin
            s = 0
            for i in 1:100_000
                s += i % (k + 1)
            end
            s
        end for k in 1:4]
println(fetch.(tasks))
 
# Parallele Schleife mit festem Ergebnis pro Index
ergebnis = zeros(Int, 8)
@threads for i in 1:8
    ergebnis[i] = i^2
end
println(ergebnis)
 
# Thread-sichere Summe mit Lock
summe = Ref(0)
l = ReentrantLock()
@threads for i in 1:1000
    lock(l) do
        summe[] += i
    end
end
println(summe[])
 
# Channels zwischen Tasks
kanal = Channel{Int}(5)
produzent = @async begin
    for i in 1:5 put!(kanal, i * i) end
    close(kanal)
end
println(collect(kanal))

Ausgabe:

[50000, 100000, 150000, 200000]
[1, 4, 9, 16, 25, 36, 49, 64]
500500
[1, 4, 9, 16, 25]

Starte Julia mit julia --threads=auto (oder -t 4), damit @threads echte Parallelität bekommt. Das gleiche Programm funktioniert mit einem Thread weiter, nur langsamer.

Numerik in Kürze

using LinearAlgebra, Statistics
 
# Newton-Verfahren
function newton(f, df, x0; tol = 1e-12, maxiter = 50)
    x = x0
    for k in 1:maxiter
        fx = f(x)
        abs(fx) < tol && return x, k
        x -= fx / df(x)
    end
    return x, maxiter
end
x, k = newton(x -> x^2 - 2, x -> 2x, 1.0)
println(x, " nach ", k, " Schritten, Fehler ", abs(x - √2))
 
# Numerische Integration (Trapezregel) und Ableitung
integriere(f, a, b, n = 1000) = (h = (b - a) / n; h * (f(a) / 2 + sum(f(a + i * h) for i in 1:n-1) + f(b) / 2))
println(integriere(sin, 0, π), " ", integriere(x -> x^2, 0, 3))
ableitung(f, x; h = 1e-6) = (f(x + h) - f(x - h)) / 2h
println(round(ableitung(sin, 0.0), digits = 6), " ", round(ableitung(x -> x^3, 2.0), digits = 4))
 
# Lineare Regression von Hand
xs = [1.0, 2, 3, 4, 5, 6]
ys = [2.2, 4.1, 6.2, 7.8, 10.1, 12.1]
X = [ones(length(xs)) xs]
beta = X \ ys
println(round.(beta, digits = 3), " R² = ", round(1 - sum((ys .- X * beta) .^ 2) / sum((ys .- mean(ys)) .^ 2), digits = 4))
 
# Differentialgleichung y' = -2y mit dem Euler-Verfahren
function euler(h, schritte)
    y = 1.0
    for _ in 1:schritte
        y += h * (-2y)
    end
    return y
end
println(round(euler(0.001, 2000), digits = 4), " exakt ", round(exp(-4), digits = 4))

Ausgabe:

1.4142135623730951 nach 6 Schritten, Fehler 0.0
1.9999983550656624 9.000004500000005
1.0 12.0
[0.173, 1.974] R² = 0.9986
0.0182 exakt 0.0183

Für ernsthafte Numerik gibt es die Pakete DifferentialEquations.jl, Optim.jl, JuMP.jl, FFTW.jl, Zygote.jl (automatische Ableitung).

Merke

  • JIT-Kompilierung: erster Aufruf langsam, danach C-Tempo; Voraussetzung ist Typstabilität
  • In Funktionen arbeiten (keine Globalen), vorallokieren, @., @views
  • Messen mit @time, BenchmarkTools @btime, analysieren mit @code_warntype
  • Parallel: Threads.@spawn, @threads, Channel, Distributed, GPU
  • Numerik-Ökosystem: LinearAlgebra, DifferentialEquations.jl, Optim.jl, JuMP

Übungsaufgabe

Schreibe eine Funktion, die π per Monte-Carlo-Simulation schätzt, und messe die Zeit.

Quiz zur Selbstkontrolle

Weiter im Kurs

Zurück: Structs, Module und Fehler

Weiter: Projekte, Pakete und Ökosystem

Alle Kapitel: Julia im Überblick