web-dev-qa-db-ja.com

Julia(1.3)のフィボナッチ数列でのマルチスレッド並列処理パフォーマンスの問題

次のハードウェアで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セッションを終了することにしました)。

何が悪いのですか?

14
ecjb

すばらしい質問です。

このフィボナッチ関数のマルチスレッド実装は、シングルスレッドバージョンよりも高速です。この関数は、新しいスレッド機能がどのように機能するかを示すおもちゃの例としてブログ投稿にのみ示され、さまざまな関数で多数のスレッドを生成でき、スケジューラーが最適なワークロードを計算することを強調しています。

問題は、_@spawn_には_1µs_程度の重要なオーバーヘッドがあるため、スレッドを生成して_1µs_よりも少ないタスクを実行すると、パフォーマンスが低下する可能性があります。 。 fib(n)の再帰的な定義は、_1.6180^nfib(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_マクロは関数を複数回実行し、コンパイル時間と平均結果をスキップします。

19
Mason

@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によるもので、マルチスレッドによるものではありませんでした。ここでの教訓は、マルチスレッド化の前に、より良いアルゴリズムを考える必要があるということです。

0
xiaodai