Świetne pytanie.
Ta wielowątkowa implementacja funkcji Fibonacciego nie jest szybsza niż wersja z jednym wątkiem. Ta funkcja została pokazana tylko w poście na blogu jako zabawkowy przykład działania nowych możliwości wątków, podkreślając, że pozwala ona na tworzenie wielu wielu wątków w różnych funkcjach, a harmonogram zaplanuje optymalne obciążenie pracą.
Problem polega na tym, że @spawn
ma około trywialny narzut 1µs
, więc jeśli odrodzisz wątek, aby wykonać zadanie, które zajmuje mniej niż 1µs
, prawdopodobnie zepsułeś swoją wydajność. Rekursywna definicja fib(n)
ma wykładniczą złożoność czasową rzędu 1.6180^n
[1], więc kiedy wywołujesz fib(43)
, odradzasz się z 1.6180^43
wątków porządku . Jeśli każdy bierze1µs
odrodzi się, zajmie to około 16 minut po prostu odrodzenie i zaplanowanie potrzebnych wątków, a to nawet nie uwzględnia czasu potrzebnego na wykonanie obliczeń i ponownego scalenia / synchronizacji wątków, co zajmuje nawet więcej czasu.
Rzeczy takie jak ten, w których spawnowany jest wątek dla każdego kroku obliczenia, mają sens tylko wtedy, gdy każdy krok obliczenia zajmuje dużo czasu w porównaniu do @spawn
narzutu.
Zauważ, że trwają prace nad zmniejszeniem narzutów @spawn
, ale przez samą fizykę wielordzeniowych chipów silikonowych wątpię, że może być wystarczająco szybki dla powyższej fib
implementacji.
Jeśli jesteś ciekawy, jak moglibyśmy zmodyfikować funkcję wątkową, fib
aby była naprawdę korzystna, najłatwiej byłoby odrodzić fib
wątek tylko, jeśli naszym zdaniem zajmie to znacznie więcej czasu niż 1µs
uruchomienie. Na mojej maszynie (działającej na 16 fizycznych rdzeniach) dostaję
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)
więc to dobre dwa rzędy wielkości ponad koszt odrodzenia nici. Wydaje się, że to dobry sposób na użycie:
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
teraz, jeśli zastosuję odpowiednią metodologię testu porównawczego z BenchmarkTools.jl [2] znajdę
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 pyta w komentarzach: Wydaje się, że jest to współczynnik przyspieszenia 2 przy użyciu 16 rdzeni. Czy można uzyskać coś bliższego 16-krotnemu przyspieszeniu?
Tak to jest. Problem z powyższą funkcją polega na tym, że treść funkcji jest większa niż z F
, z dużą liczbą warunków, odradzaniem funkcji / wątku i tym podobne. Zapraszam do porównania @code_llvm F(10)
@code_llvm fib(10)
. Oznacza to, że fib
Julia jest znacznie trudniejsza do optymalizacji. To dodatkowe obciążenie sprawia, że świat małych n
skrzynek stanowi różnicę .
julia> @btime F(20);
28.844 μs (0 allocations: 0 bytes)
julia> @btime fib(20);
242.208 μs (20 allocations: 320 bytes)
O nie! cały ten dodatkowy kod, którego nigdy nie dotykamy, n < 23
spowalnia nas o rząd wielkości! Jest jednak łatwa poprawka: kiedy n < 23
nie powracaj do fib
, zamiast tego wywołaj pojedynczy wątek 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
co daje wynik bliższy oczekiwaniom dla tak wielu wątków.
[1] https://www.geeksforgeeks.org/time-complexity-recursive-fibonacci-program/
[2] @btime
Makro BenchmarkTools z BenchmarkTools.jl będzie uruchamiało funkcje wiele razy, pomijając czas kompilacji i średnie wyniki.