ปัญหาประสิทธิภาพขนานหลายเธรดกับลำดับฟีโบนักชีในจูเลีย (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

อย่างไรก็ตามเมื่อเรียกใช้รหัสต่อไปนี้คัดลอกจากหน้าจูเลียเกี่ยวกับมัลติเธรด

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

เป็นคำถามที่ดีมาก

การใช้งานแบบมัลติเธรดของฟังก์ชัน Fibonacci ไม่เร็วกว่าเวอร์ชั่นเธรดเดี่ยว ฟังก์ชั่นนั้นแสดงเฉพาะในโพสต์บล็อกเป็นตัวอย่างของเล่นเกี่ยวกับความสามารถในการเธรดใหม่โดยเน้นว่ามันช่วยให้วางไข่หลายเธรดในฟังก์ชั่นต่าง ๆ และตัวกำหนดตารางเวลาจะคำนวณปริมาณงานที่เหมาะสมที่สุด

ปัญหาคือว่า@spawnมีค่าใช้จ่ายที่ไม่น่ารำคาญ1µsดังนั้นถ้าคุณวางไข่เธรดเพื่อทำงานที่ใช้เวลาน้อยกว่า1µsคุณอาจทำร้ายการแสดงของคุณ ความหมายของ recursive 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)

ดังนั้นมันจึงเป็นคำสั่งที่ดีสองคำในเรื่องของการวางไข่ ดูเหมือนว่าเป็นการตัดยอดเยี่ยมที่จะใช้:

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 ถามในความคิดเห็น: นี่คือปัจจัยของ 2 ความเร็วในการใช้ 16 แกนดูเหมือนว่า เป็นไปได้หรือไม่ที่จะเพิ่มความเร็ว 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] @btimeมาโครBenchmarkTools จาก BenchmarkTools.jl จะเรียกใช้ฟังก์ชั่นหลาย ๆ ครั้งข้ามการรวบรวมเวลาและผลลัพธ์โดยเฉลี่ย


1
นี่คือปัจจัยของการเร่งความเร็ว 2 โดยใช้ 16 คอร์ที่ดูเหมือนว่า เป็นไปได้หรือไม่ที่จะเพิ่มความเร็ว 16 ระดับให้เร็วขึ้น?
Anush

ใช้เคสขนาดใหญ่ BTW นี่เป็นวิธีที่โปรแกรมแบบมัลติเธรดที่มีประสิทธิภาพเช่น FFTW ทำงานภายใต้ประทุนเช่นกัน!
Chris Rackauckas

กรณีฐานขนาดใหญ่ไม่ได้ช่วย เคล็ดลับคือว่าfibเป็นเรื่องยากสำหรับ julia เพื่อเพิ่มประสิทธิภาพกว่าFดังนั้นเราเพียงแค่ใช้Fแทนสำหรับfib n< 23ฉันแก้ไขคำตอบของฉันพร้อมคำอธิบายและตัวอย่างเพิ่มเติม
Mason

ที่แปลกจริงผมได้ผลลัพธ์ที่ดีกว่าการใช้ตัวอย่างโพสต์บล็อก ...
tpdsantos

@tpdsantos ผลลัพธ์ของThreads.nthreads()คุณคืออะไร? ฉันสงสัยว่าคุณอาจมี julia ที่ทำงานด้วยเธรดเดียวเท่านั้น
Mason

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)

แต่ความเร็วนั้นมาจากการจำไม่ได้และการมัลติเธรดมากมาย บทเรียนที่นี่คือเราควรคิดขั้นตอนวิธีที่ดีกว่าก่อนที่จะมีหลายเธรด


คำถามไม่ได้เกี่ยวกับการคำนวณตัวเลขฟีโบนักชีอย่างรวดเร็ว ประเด็นก็คือ 'เหตุใดมัลติเธรดจึงไม่ปรับปรุงการใช้งานไร้เดียงสานี้'
Mason

สำหรับฉันคำถามเชิงตรรกะต่อไปคือ: วิธีทำให้รวดเร็ว ดังนั้นใครบางคนที่อ่านข้อความนี้จะเห็นทางออกของฉันและเรียนรู้จากมัน
xiaodai
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.