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 @spawnsurcharge 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^43threads 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 @spawnsurcharge.
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 fibmise en œuvre ci-dessus .
Si vous êtes curieux de savoir comment nous pourrions modifier la fibfonction threadée pour qu'elle soit réellement bénéfique, la chose la plus simple à faire serait de ne générer un fibthread que si nous pensons que cela prendra beaucoup plus de temps que 1µsson 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 fibjulia a beaucoup plus de mal à optimiser. Cette surcharge supplémentaire fait toute la différence pour les petits nboî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 < 23nous 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 @btimemacro BenchmarkTools de BenchmarkTools.jl exécutera les fonctions plusieurs fois, ignorant le temps de compilation et les résultats moyens.