Julia'daki Fibonacci dizisi ile çok iş parçacıklı paralellik performans sorunu (1.3)


14

Julia 1.3Aşağıdaki donanım ile multithread işlevini deniyorum :

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

Aşağıdaki komut dosyasını çalıştırırken:

function F(n)
if n < 2
    return n
    else
        return F(n-1)+F(n-2)
    end
end
@time F(43)

bana şu çıktıyı veriyor

2.229305 seconds (2.00 k allocations: 103.924 KiB)
433494437

Bununla birlikte, Julia sayfasından kopyalanan aşağıdaki kodu çalıştırırken, çoklu kullanım hakkında

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)

ne olur RAM / CPU kullanımının herhangi bir çıkış olmadan 3.2GB /% 6'dan 15GB /% 25'e sıçramasıdır (en az 1 dakika boyunca, julia oturumunu öldürmeye karar verdim)

Neyi yanlış yapıyorum?

Yanıtlar:


19

Harika bir soru.

Fibonacci işlevinin bu çok iş parçacıklı uygulaması , tek iş parçacıklı sürümden daha hızlı değildir . Bu işlev, blog yazısında sadece yeni iş parçacığı özelliklerinin nasıl çalıştığına dair bir oyuncak örneği olarak gösterildi, farklı işlevlerde çok sayıda iş parçacığının ortaya çıkmasına izin verdiğini ve zamanlayıcının en uygun iş yükünü bulacağını vurguladı.

Sorun şu ki @spawn, önemsiz bir ek yüke sahip 1µs, bu nedenle daha az süren bir görev yapmak için bir iş parçacığı ortaya çıkarırsanız 1µs, muhtemelen performansınıza zarar vermiş olursunuz. Özyinelemeli tanımının fib(n)sıralamanın üstel zaman karmaşıklığı vardır 1.6180^n[1], dolayısıyla aradığınızda fib(43), sipariş 1.6180^43iplikleri ortaya çıkar. Her birinin 1µsyumurtlaması gerekiyorsa, sadece gerekli iş parçacıklarının ortaya çıkması ve zamanlanması yaklaşık 16 dakika sürer ve bu, gerçek hesaplamaları yapmak için gereken süreyi hesaba katmaz ve daha fazla zaman.

Bir hesaplamanın her adımı için bir iş parçacığı oluşturduğunuz gibi şeyler yalnızca hesaplamanın her adımı @spawnyüke göre uzun zaman alıyorsa mantıklıdır .

Ek yükü azaltmaya yönelik bir çalışma olduğunu unutmayın @spawn, ancak çok çekirdekli silikon çiplerin fiziği ile yukarıdaki fibuygulama için yeterince hızlı olabileceğinden şüpheliyim .


İş parçacığı fibişlevini gerçekten yararlı olacak şekilde nasıl değiştirebileceğimizi merak ediyorsanız , yapılacak en kolay şey, yalnızca çalıştırılmasından fibçok daha uzun süreceğini düşünürsek bir iş parçacığı 1µsoluşturmaktır. Makinemde (16 fiziksel çekirdek üzerinde çalışıyor) alıyorum

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)

yani bir ipliğin yumurtlama maliyeti üzerinde büyüklükte iyi bir iki sipariş thats. Kullanmak için iyi bir kesme gibi görünüyor:

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

şimdi, BenchmarkTools.jl [2] ile uygun kıyaslama metodolojisini takip edersem

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 yorumlarda soruyor: Bu, 16 çekirdeği kullanarak 2 hızlanma faktörü gibi görünüyor. 16 faktöre daha yakın bir şey elde etmek mümkün müdür?

Evet öyle. Yukarıdaki işlevle ilgili sorun, işlev gövdesinin, Fbirçok koşullu, işlev / iş parçacığı yumurtlama ve tüm bunlardan daha büyük olmasıdır. Sizi karşılaştırmaya davet ediyorum @code_llvm F(10) @code_llvm fib(10). Bu fib, Julia'nın optimizasyonunun çok daha zor olduğu anlamına gelir . Bu ekstra yük, küçük nvakalar için bir dünya yaratır .

julia> @btime F(20);
  28.844 μs (0 allocations: 0 bytes)

julia> @btime fib(20);
  242.208 μs (20 allocations: 320 bytes)

Oh hayır! hiçbir zaman dokunulmayan ekstra kodlar n < 23bizi bir büyüklük sırasına göre yavaşlatıyor! Yine de kolay bir düzeltme var: ne zaman n < 23, geri çekilmeyin fib, bunun yerine tek dişliyi çağırın 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

bu da pek çok iş parçacığı için beklediğimiz şeye daha yakın bir sonuç verir.

[1] https://www.geeksforgeeks.org/time-complexity-recursive-fibonacci-program/

[2] BenchmarkTools.jl @btimedosyasındaki BenchmarkTools makrosu, derleme süresini ve ortalama sonuçları atlayarak işlevleri birden çok kez çalıştıracaktır.


1
Bu 16 çekirdek kullanarak 2 hızlanma faktörü gibi görünüyor. 16 faktöre daha yakın bir şey elde etmek mümkün müdür?
Anush

Daha büyük bir taban kasası kullanın. BTW, FFTW gibi çok iş parçacıklı programlar da bu şekilde etkili bir şekilde çalışıyor!
Chris Rackauckas

Daha büyük taban kasa yardımcı olmaz. İşin püf noktası, fibjulia'nın optimizasyonundan daha zor F, bu yüzden for Fyerine kullanıyoruz . Cevabımı daha ayrıntılı bir açıklama ve örnekle düzenledim. fibn< 23
Mason

Bu garip, aslında blog yazısı örneğini kullanarak daha iyi sonuçlar aldım ...
tpdsantos

@tpdsantos Sizin için çıktı nedir Threads.nthreads()? Ben sadece tek bir iplik ile çalışan Julia olabilir sanıyorum.
Mason

0

@Anush

Not ve elle çoklu kullanım kullanımına örnek olarak

_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)

Ancak hızlanma, memmiozasyondan geldi ve çok fazla iş görmedi. Burada ders, çoklu okumadan önce daha iyi algoritmalar düşünmemiz gerektiğidir.


Soru asla Fibonacci sayılarını hızlı bir şekilde hesaplamakla ilgili değildi. Mesele şuydu: 'Neden çoklu iş parçacığı bu naif uygulamayı iyileştirmiyor?'
Mason

Benim için bir sonraki mantıksal soru şudur: hızlı nasıl yapılır. Bunu okuyan biri benim çözümümü görebilir ve belki de ondan öğrenebilir.
xiaodai
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.