Vấn đề về hiệu suất song song đa luồng với chuỗi Fibonacci trong Julia (1.3)


14

Tôi đang thử chức năng đa luồng của Julia 1.3Phần cứng sau:

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

Khi chạy đoạn script sau:

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

nó cho tôi đầu ra sau

2.229305 seconds (2.00 k allocations: 103.924 KiB)
433494437

Tuy nhiên, khi chạy đoạn mã sau được sao chép từ trang Julia về đa luồng

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)

Điều gì xảy ra là việc sử dụng RAM / CPU tăng vọt từ 3,2GB / 6% lên 15GB / 25% mà không có bất kỳ đầu ra nào (trong ít nhất 1 phút, sau đó tôi đã quyết định giết phiên julia)

Tôi đang làm gì sai?

Câu trả lời:


19

Câu hỏi tuyệt vời.

Việc triển khai đa luồng này của hàm Fibonacci không nhanh hơn phiên bản luồng đơn. Chức năng đó chỉ được hiển thị trong bài đăng trên blog như một ví dụ về đồ chơi về cách các khả năng phân luồng mới hoạt động, làm nổi bật rằng nó cho phép sinh ra nhiều luồng trong các chức năng khác nhau và bộ lập lịch sẽ tìm ra một khối lượng công việc tối ưu.

Vấn đề là @spawncó một chi phí không hề nhỏ xung quanh 1µs, vì vậy nếu bạn sinh ra một chủ đề để thực hiện một nhiệm vụ ít hơn 1µs, bạn có thể làm tổn thương hiệu suất của bạn. Định nghĩa đệ quy fib(n)có độ phức tạp theo thời gian theo cấp số nhân của đơn hàng 1.6180^n[1], vì vậy khi bạn gọi fib(43), bạn sinh ra một cái gì đó của các chuỗi đơn hàng 1.6180^43. Nếu mỗi người 1µssinh sản, sẽ mất khoảng 16 phút để sinh sản và lên lịch cho các chủ đề cần thiết và thậm chí không tính đến thời gian để thực hiện các tính toán thực tế và hợp nhất lại / đồng bộ hóa các chủ đề mà thậm chí mất thêm thời gian.

Những thứ như thế này khi bạn sinh ra một luồng cho mỗi bước tính toán chỉ có ý nghĩa nếu mỗi bước tính toán mất nhiều thời gian so với @spawnchi phí chung.

Lưu ý rằng có công việc đang giảm bớt chi phí hoạt động @spawn, nhưng bằng chính tính vật lý của chip silicon đa lõi, tôi nghi ngờ nó có thể đủ nhanh để fibthực hiện ở trên .


Nếu bạn tò mò về cách chúng ta có thể sửa đổi fibhàm luồng để thực sự có ích, thì cách dễ nhất là chỉ tạo ra một fibluồng nếu chúng ta nghĩ rằng nó sẽ mất nhiều thời gian hơn 1µsđể chạy. Trên máy của tôi (chạy trên 16 lõi vật lý), tôi nhận được

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)

vì vậy đó là một đơn đặt hàng lớn hai lần so với chi phí sinh ra một chủ đề. Điều đó có vẻ như là một điểm cắt tốt để sử dụng:

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

bây giờ, nếu tôi làm theo phương pháp điểm chuẩn thích hợp với BenchmarkTools.jl [2] tôi thấy

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 hỏi trong các ý kiến: Đây là một yếu tố tăng tốc độ bằng cách sử dụng 16 lõi. Có thể có được một cái gì đó gần hơn với một yếu tố 16 tăng tốc?

Vâng, đúng vậy. Vấn đề với chức năng trên là cơ thể chức năng lớn hơn cơ thể F, với rất nhiều điều kiện, chức năng / luồng sinh sản và tất cả điều đó. Tôi mời bạn so sánh @code_llvm F(10) @code_llvm fib(10). Điều này có nghĩa fiblà khó khăn hơn nhiều cho julia để tối ưu hóa. Chi phí hoạt động thêm này tạo nên một thế giới khác biệt cho những ntrường hợp nhỏ .

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

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

Ôi không! tất cả các mã bổ sung mà không bao giờ được chạm vào n < 23đang làm chúng ta chậm lại bởi một thứ tự cường độ! Có một cách khắc phục dễ dàng: khi nào n < 23, đừng lặp lại fib, thay vào đó hãy gọi một luồng 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

mang lại kết quả gần hơn với những gì chúng ta mong đợi cho rất nhiều chủ đề.

[1] https://www.geekforgeek.org/time-complexity-recursive-fiborie-program/

[2] @btimeMacro BenchmarkTools từ BenchmarkTools.jl sẽ chạy các chức năng nhiều lần, bỏ qua thời gian biên dịch và kết quả trung bình.


1
Đây là một yếu tố của 2 tốc độ tăng lên bằng cách sử dụng 16 lõi. Có thể có được một cái gì đó gần hơn với một yếu tố 16 tăng tốc?
Anush

Sử dụng một trường hợp cơ sở lớn hơn. BTW, đây là cách các chương trình đa luồng hiệu quả như FFTW hoạt động hiệu quả!
Chris Rackauckas

Trường hợp cơ sở lớn hơn không giúp đỡ. Bí quyết là fibkhó có thể tối ưu hóa cho julia hơn F, vì vậy chúng tôi chỉ sử dụng Fthay vì fibcho n< 23. Tôi chỉnh sửa câu trả lời của mình với một lời giải thích và ví dụ sâu sắc hơn.
Mason

Thật kỳ lạ, tôi thực sự đã có kết quả tốt hơn bằng cách sử dụng ví dụ bài đăng trên blog ...
tpdsantos

@tpdsantos Đầu ra của Threads.nthreads()bạn là gì? Tôi nghi ngờ bạn có thể có julia chạy chỉ với một chủ đề duy nhất.
Mason

0

@Anush

Như một ví dụ về việc sử dụng ghi nhớ và đa luồng bằng tay

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

Nhưng tốc độ tăng lên đến từ việc ghi nhớ và không có nhiều luồng. Bài học ở đây là chúng ta nên nghĩ các thuật toán tốt hơn trước khi đa luồng.


Câu hỏi không bao giờ là về việc tính toán các số Fibonacci nhanh. Vấn đề là "tại sao không đa luồng cải thiện việc thực hiện ngây thơ này?".
Mason

Đối với tôi, câu hỏi hợp lý tiếp theo là: làm thế nào để làm cho nó nhanh. Vì vậy, ai đó đọc nó có thể thấy giải pháp của tôi và học hỏi từ nó, có lẽ.
xiaodai
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.