次のハードウェアでJulia 1.3
のマルチスレッド機能を試しています。
Model Name: MacBook Pro
Processor Name: Intel Core i7
Processor Speed: 2.8 GHz
Number of Processors: 1
Total Number of Cores: 4
L2 Cache (per Core): 256 KB
L3 Cache: 6 MB
Hyper-Threading Technology: Enabled
Memory: 16 GB
次のスクリプトを実行すると:
function F(n)
if n < 2
return n
else
return F(n-1)+F(n-2)
end
end
@time F(43)
それは私に次の出力を与えます
2.229305 seconds (2.00 k allocations: 103.924 KiB)
433494437
ただし、 マルチスレッドに関するジュリアのページ からコピーした次のコードを実行すると
import Base.Threads.@spawn
function fib(n::Int)
if n < 2
return n
end
t = @spawn fib(n - 2)
return fib(n - 1) + fetch(t)
end
fib(43)
何が起こるかというと、RAM/CPUの使用率が出力なしで3.2GB/6%から15GB/25%にジャンプすることです(少なくとも1分間、その後Juliaセッションを終了することにしました)。
何が悪いのですか?
すばらしい質問です。
このフィボナッチ関数のマルチスレッド実装は、シングルスレッドバージョンよりも高速です。この関数は、新しいスレッド機能がどのように機能するかを示すおもちゃの例としてブログ投稿にのみ示され、さまざまな関数で多数のスレッドを生成でき、スケジューラーが最適なワークロードを計算することを強調しています。
問題は、_@spawn
_には_1µs
_程度の重要なオーバーヘッドがあるため、スレッドを生成して_1µs
_よりも少ないタスクを実行すると、パフォーマンスが低下する可能性があります。 。 fib(n)
の再帰的な定義は、_1.6180^n
fib(43)
を呼び出すと、_1.6180^43
_のスレッドが生成されます。それぞれが_1µs
_を使用してスポーンする場合、必要なスレッドをスポーンしてスケジュールするだけで約16分かかり、実際の計算を実行して再マージするのにかかる時間も考慮されません/さらに時間がかかる同期スレッド。
計算の各ステップでスレッドを生成するこのようなことは、計算の各ステップに_@spawn
_オーバーヘッドと比較して長い時間がかかる場合にのみ意味があります。
_@spawn
_のオーバーヘッドを減らす作業が行われていることに注意してください。ただし、マルチコアシリコンチップの物理的性質により、上記のfib
実装には十分高速であるとは思えません。
スレッド化されたfib
関数を実際に有益なものに変更する方法に興味がある場合は、実行するのに_1µs
_よりも大幅に時間がかかると思われる場合にのみ、fib
スレッドを生成するのが最も簡単です。私のマシン(16物理コアで実行)では、
_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)
_
これは、スレッドを生成するコストよりも2桁優れています。それは使用するのに良いカットオフのようです:
_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
_
今、私がBenchmarkTools.jl [2]で適切なベンチマーク方法論に従っている場合、私は見つけます
_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はコメントで質問します。これは、16コアを使用すると2倍高速になると思われます。 16倍のスピードアップに近いものを取得することは可能ですか?
はい、そうです。上記の関数の問題は、関数本体がF
のそれよりも大きく、多くの条件、関数/スレッドの生成などがあることです。 @code_llvm F(10)
@code_llvm fib(10)
を比較してください。これは、fib
はJuliaが最適化するのがはるかに難しいことを意味します。この余分なオーバーヘッドは、小さなn
の場合に大きな違いをもたらします。
_Julia> @btime F(20);
28.844 μs (0 allocations: 0 bytes)
Julia> @btime fib(20);
242.208 μs (20 allocations: 320 bytes)
_
大野! _n < 23
_の影響を受けない余分なコードはすべて、桁違いに遅くなっています!ただし、簡単な修正があります。_n < 23
_の場合は、fib
に再帰しないで、代わりにシングルスレッドの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
_
これにより、非常に多くのスレッドで期待される結果により近い結果が得られます。
[1] https://www.geeksforgeeks.org/time-complexity-recursive-fibonacci-program/
[2] BenchmarkTools.jlのBenchmarkTools _@btime
_マクロは関数を複数回実行し、コンパイル時間と平均結果をスキップします。
@Anush
メモ化とマルチスレッドを手動で使用する例として
_fib(::Val{1}, _, _) = 1
_fib(::Val{2}, _, _) = 1
import Base.Threads.@spawn
_fib(x::Val{n}, d = zeros(Int, n), channel = Channel{Bool}(1)) where n = begin
# lock the channel
put!(channel, true)
if d[n] != 0
res = d[n]
take!(channel)
else
take!(channel) # unlock channel so I can compute stuff
#t = @spawn _fib(Val(n-2), d, channel)
t1 = _fib(Val(n-2), d, channel)
t2 = _fib(Val(n-1), d, channel)
res = fetch(t1) + fetch(t2)
put!(channel, true) # lock channel
d[n] = res
take!(channel) # unlock channel
end
return res
end
fib(n) = _fib(Val(n), zeros(Int, n), Channel{Bool}(1))
fib(1)
fib(2)
fib(3)
fib(4)
@time fib(43)
using BenchmarkTools
@benchmark fib(43)
しかし、スピードアップはmemmiozationによるもので、マルチスレッドによるものではありませんでした。ここでの教訓は、マルチスレッド化の前に、より良いアルゴリズムを考える必要があるということです。