Grande question.
Cette implémentation multithread de la fonction Fibonacci n'est pas plus rapide que la version à thread unique. Cette fonction n'a été présentée dans le billet de blog que comme un exemple de jouet du fonctionnement des nouvelles capacités de threading, soulignant qu'elle permet de générer de nombreux threads dans différentes fonctions et que le planificateur trouvera une charge de travail optimale.
Le problème est que la @spawn
surcharge n'est pas anodine 1µs
, donc si vous générez un thread pour effectuer une tâche qui prend moins de temps 1µs
, vous avez probablement nui à vos performances. La définition récursive de fib(n)
a une complexité temporelle exponentielle de l'ordre 1.6180^n
[1], donc lorsque vous appelez fib(43)
, vous générez quelque chose de 1.6180^43
threads d' ordre . Si chacun prend1µs
pour apparaître, cela prendra environ 16 minutes juste pour générer et planifier les threads nécessaires, et cela ne tient même pas compte du temps qu'il faut pour effectuer les calculs réels et re-fusionner / synchroniser les threads, ce qui prend même plus de temps.
Des choses comme celle-ci où vous générez un thread pour chaque étape d'un calcul n'ont de sens que si chaque étape du calcul prend beaucoup de temps par rapport à la @spawn
surcharge.
Notez qu'il y a du travail pour réduire les frais généraux de @spawn
, mais par la physique même des puces en silicone multicœurs, je doute que cela puisse jamais être assez rapide pour la fib
mise en œuvre ci-dessus .
Si vous êtes curieux de savoir comment nous pourrions modifier la fib
fonction threadée pour qu'elle soit réellement bénéfique, la chose la plus simple à faire serait de ne générer un fib
thread que si nous pensons que cela prendra beaucoup plus de temps que 1µs
son exécution. Sur ma machine (fonctionnant sur 16 cœurs physiques), je reçois
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)
c'est donc deux bons ordres de grandeur par rapport au coût de création d'un thread. Cela semble être une bonne coupure à utiliser:
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
maintenant, si je suis la méthodologie de référence appropriée avec BenchmarkTools.jl [2] je trouve
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 demande dans les commentaires: C'est un facteur de 2 accélération en utilisant 16 cœurs semble-t-il. Est-il possible de rapprocher quelque chose d'un facteur 16?
Oui, ça l'est. Le problème avec la fonction ci-dessus est que le corps de la fonction est plus grand que celui de F
, avec beaucoup de conditions, la génération de fonctions / threads et tout ça. Je vous invite à comparer @code_llvm F(10)
@code_llvm fib(10)
. Cela signifie que fib
julia a beaucoup plus de mal à optimiser. Cette surcharge supplémentaire fait toute la différence pour les petits n
boîtiers.
julia> @btime F(20);
28.844 μs (0 allocations: 0 bytes)
julia> @btime fib(20);
242.208 μs (20 allocations: 320 bytes)
Oh non! tout ce code supplémentaire qui n'est jamais touché n < 23
nous ralentit d'un ordre de grandeur! Cependant, il existe une solution simple: quand n < 23
, ne récapitulez pas fib
, appelez plutôt le thread unique 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
ce qui donne un résultat plus proche de ce que nous attendions pour tant de threads.
[1] https://www.geeksforgeeks.org/time-complexity-recursive-fibonacci-program/
[2] La @btime
macro BenchmarkTools de BenchmarkTools.jl exécutera les fonctions plusieurs fois, ignorant le temps de compilation et les résultats moyens.