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


14

私は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

ただし、マルチスレッドに関するJuliaページからコピーした次のコードを実行すると

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セッションを終了することにしました)。

何が悪いのですか?

回答:


19

すばらしい質問です。

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

問題は@spawn、約の重要なオーバーヘッドがある1µsことです。そのため、スレッドを生成して未満のタスクを実行すると1µs、パフォーマンスが低下する可能性があります。の再帰的な定義はfib(n)、次数1.6180^n[1]の指数関数的な時間の複雑さを持っているため、を呼び出すとfib(43)、なんらかの順序1.6180^43スレッドを生成します。それぞれが1µsスポーンするのにかかる場合、必要なスレッドをスポーンしてスケジュールするだけで約16分かかります。これは、実際の計算を実行してスレッドを再マージ/同期するのにかかる時間も考慮していません。より多くの時間。

計算の各ステップでスレッドを生成するこのようなことは、計算の各ステップが@spawnオーバーヘッドに比べて長い時間がかかる場合にのみ意味があります。

のオーバーヘッドを減らす作業が進んでいることに注意してください。ただし@spawn、マルチコアシリコンチップの物理的性質により、上記のfib実装には十分高速であるとは思えません。


スレッド化されたfib関数を実際に有益なものに変更する方法に興味があるfib場合は1µs、実行するよりも大幅に時間がかかると考えられる場合にのみ、スレッドを生成するのが最も簡単です。私のマシン(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ジュリアにとって最適化がはるかに難しいことを意味します。この余分なオーバーヘッドにより、小さな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マクロは、コンパイル時間と平均結果をスキップして、関数を複数回実行します。


1
これは、16コアを使用すると2倍高速化するようです。16倍のスピードアップに近いものを取得することは可能ですか?
Anush、

より大きなベースケースを使用してください。ところで、これはFFTWのようなマルチスレッドプログラムが内部でどのように効果的に機能するかです。
Chris Rackauckas

大きなベースケースは役に立ちません。コツは、fibジュリアにとって最適化がに比べて難しいためFFfibforの代わりにですn< 23。私はより詳細な説明と例で私の答えを編集しました。
メイソン

それは奇妙なことですが、ブログ投稿の例を使用すると実際により良い結果が得られました...
tpdsantos

@tpdsantosの出力は何Threads.nthreads()ですか?ジュリアがシングルスレッドで実行されているのではないかと思います。
メイソン

0

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


問題は、フィボナッチ数の高速計算に関するものではありませんでした。ポイントは、「マルチスレッド化によってこの単純な実装が改善されないのはなぜですか」でした。
メイソン

私にとって、次の論理的な問題は、それを速くする方法です。したがって、これを読んでいる誰かが私の解決策を見て、そこから学ぶことができるでしょう。
シャオダイ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.