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])))true
true
schnell genug
Any[Float64]
Any[Union{Float64, Int64}]
Float64 Float64 Int64In der REPL siehst du mit @time summe_schleife(v) Zeit und Speicherverbrauch; der erste Aufruf enthält die Kompilierzeit.
Werkzeuge zur Leistungsanalyse:
| Werkzeug | Zweck |
|---|---|
@time, @elapsed | grobe 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_native | erzeugten Code ansehen |
| Profile, ProfileView.jl | Flammendiagramme |
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)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))[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))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
Aufgabe
Schreibe eine Funktion, die π per Monte-Carlo-Simulation schätzt, und messe die Zeit.