Gute Frage.
Diese Multithread-Implementierung der Fibonacci-Funktion ist nicht schneller als die Single-Threaded-Version. Diese Funktion wurde im Blog-Beitrag nur als Spielzeugbeispiel für die Funktionsweise der neuen Threading-Funktionen gezeigt. Dabei wurde hervorgehoben, dass viele, viele Threads in verschiedenen Funktionen erzeugt werden können und der Scheduler eine optimale Arbeitslast ermittelt.
Das Problem ist, dass @spawn
der Aufwand nicht trivial 1µs
ist. Wenn Sie also einen Thread erstellen, um eine Aufgabe zu erledigen, die weniger als dauert 1µs
, haben Sie wahrscheinlich Ihre Leistung beeinträchtigt. Die rekursive Definition von fib(n)
hat eine exponentielle zeitliche Komplexität der Reihenfolge 1.6180^n
[1]. Wenn Sie also aufrufen fib(43)
, erzeugen Sie etwas von Auftragsthreads 1.6180^43
. Wenn jeder benötigt 1µs
, um zu spawnen, dauert es ungefähr 16 Minuten, nur um die benötigten Threads zu spawnen und zu planen, und das berücksichtigt nicht einmal die Zeit, die benötigt wird, um die eigentlichen Berechnungen durchzuführen und Threads neu zusammenzuführen / zu synchronisieren, was gerade dauert mehr Zeit.
Solche Dinge, bei denen Sie für jeden Schritt einer Berechnung einen Thread erzeugen, sind nur dann sinnvoll, wenn jeder Schritt der Berechnung im Vergleich zum @spawn
Overhead lange dauert .
Beachten Sie, dass daran gearbeitet wird, den Overhead von zu verringern @spawn
, aber aufgrund der Physik von Multicore-Silikonchips bezweifle ich, dass dies für die obige fib
Implementierung jemals schnell genug sein kann .
Wenn Sie neugierig sind, wie wir die Thread- fib
Funktion so ändern können, dass sie tatsächlich von Vorteil ist, ist es am einfachsten, einen fib
Thread nur dann zu erzeugen, wenn wir der Meinung sind, dass die 1µs
Ausführung erheblich länger dauert als die Ausführung. Auf meinem Computer (läuft auf 16 physischen Kernen) bekomme ich
function F(n)
if n < 2
return n
else
return F(n-1)+F(n-2)
end
end
julia> @btime F(23);
122.920 μs (0 allocations: 0 bytes)
Das sind also gut zwei Größenordnungen über den Kosten für das Laichen eines Fadens. Das scheint ein guter Cutoff zu sein:
function fib(n::Int)
if n < 2
return n
elseif n > 23
t = @spawn fib(n - 2)
return fib(n - 1) + fetch(t)
else
return fib(n-1) + fib(n-2)
end
end
Wenn ich nun mit BenchmarkTools.jl [2] die richtige Benchmark-Methodik befolge, finde ich
julia> using BenchmarkTools
julia> @btime fib(43)
971.842 ms (1496518 allocations: 33.64 MiB)
433494437
julia> @btime F(43)
1.866 s (0 allocations: 0 bytes)
433494437
@Anush fragt in den Kommentaren: Dies ist ein Faktor von 2 Beschleunigung mit 16 Kernen, wie es scheint. Ist es möglich, etwas näher an einen Faktor von 16 zu bringen?
Ja, so ist es. Das Problem mit der obigen Funktion ist, dass der Funktionskörper größer ist als der von F
, mit vielen Bedingungen, Funktions- / Thread-Laichen und all dem. Ich lade Sie zum Vergleich ein @code_llvm F(10)
@code_llvm fib(10)
. Dies bedeutet, dass fib
es für Julia viel schwieriger ist, sie zu optimieren. Dieser zusätzliche Aufwand macht für die kleinen n
Fälle einen großen Unterschied .
julia> @btime F(20);
28.844 μs (0 allocations: 0 bytes)
julia> @btime fib(20);
242.208 μs (20 allocations: 320 bytes)
Ach nein! All dieser zusätzliche Code, der niemals berührt n < 23
wird, verlangsamt uns um eine Größenordnung! Es gibt jedoch eine einfache Lösung: Wann n < 23
, nicht zurückgreifen auf fib
, sondern den einzelnen Thread aufrufen F
.
function fib(n::Int)
if n > 23
t = @spawn fib(n - 2)
return fib(n - 1) + fetch(t)
else
return F(n)
end
end
julia> @btime fib(43)
138.876 ms (185594 allocations: 13.64 MiB)
433494437
Dies ergibt ein Ergebnis, das näher an dem liegt, was wir für so viele Threads erwarten würden.
[1] https://www.geeksforgeeks.org/time-complexity-recursive-fibonacci-program/
[2] Das BenchmarkTools- @btime
Makro von BenchmarkTools.jl führt Funktionen mehrmals aus und überspringt die Kompilierungszeit und die durchschnittlichen Ergebnisse.