Ś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 @spawnma 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^43wą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 @spawnnarzutu.
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 fibimplementacji.
Jeśli jesteś ciekawy, jak moglibyśmy zmodyfikować funkcję wątkową, fibaby była naprawdę korzystna, najłatwiej byłoby odrodzić fibwątek tylko, jeśli naszym zdaniem zajmie to znacznie więcej czasu niż 1µsuruchomienie. 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 fibJulia jest znacznie trudniejsza do optymalizacji. To dodatkowe obciążenie sprawia, że świat małych nskrzynek 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 < 23spowalnia nas o rząd wielkości! Jest jednak łatwa poprawka: kiedy n < 23nie 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] @btimeMakro BenchmarkTools z BenchmarkTools.jl będzie uruchamiało funkcje wiele razy, pomijając czas kompilacji i średnie wyniki.