簡體   English   中英

Julia 功能性能混亂

[英]Julia functional performance confusion

我是 Julia 的新手,當我第一次嘗試一個溫和的程序時,我寫了下面的內容,它計算 1 到 N 范圍內所有數字的除數 sigma 函數 - 除數 sigma 函數 sigma(k, n) 計算總和n 的除數的 k 次冪(如果 k 為 0,則它只是除數的數量)。 由於 sigma(k, n) 是 n 的乘法函數,我們將其寫成如下(原則上,這應該比對我們范圍內的每個整數進行因式分解更有效):

using Primes

thefunc(i) = (a::Int, b::Int)-> sum(BigInt(a)^(k*i) for k in 0:b)

function powsfunc(x, n, func)
    bound = convert(Int, floor(log(n)/log(x)))
    return Dict{Int, Number}(x^i => func(x, i) for i in 0:bound)
end

dictprod2(d1, d2, bd)=Dict(i*j=> d1[i]*d2[j] for i in keys(d1), j in keys(d2) if i*j<bd)

function dosigma(k, n)
    primefunc = thefunc(k)
    combfunc = (a, b) -> dictprod2(a, b, n)
    allprimes = [powsfunc(p, n, primefunc) for p in primes(n)]
    trivdict = Dict{Int, Number}(1=>1)
    theresult = reduce(combfunc, trivdict, allprimes)
    return theresult
end

好消息是上述方法有效。 壞消息是它的速度非常慢, dosigma(0, 100000)占用了 10 分鍾的 CPU 時間,並占用了 150GB(!)。 問題是:為什么?

這是我的做法。 第一個powsfunc

function powsfunc(x, n, func)
    bound = Int(floor(log(n)/log(x)))
    final_dict = Dict{Int, Int}()

    for i in 0:bound
        final_dict[x^i] = func(x,i)
    end

    return final_dict
end

我將字典更改為{Int, Int}類型。 這比{Int, Number}更精確{Int, Number}因此它的性能應該稍微提高一些 - 正如 Chris 在評論中指出的那樣。 但實際測試並沒有顯示出任何性能差異。

另一個變化是,我沒有使用理解來定義Dict ,而是逐個分配字典元素。 這減少了內存估計 - 由@benchmark powsfunc(10, 1000, thefunc(0))從 1.68KiB 測量到 1.58KiB。 不是 100% 確定這是為什么。 我想我在某處讀到Dict定義還沒有完全完善,但我找不到那個參考。 也許知識淵博的人可以解釋為什么會這樣?

dictprod2很大的收益。 再次感謝 Chris 指出這一點。 Int類型定義Dict會有很大的不同。

function dictprod2(d1, d2, bd)
    res = Dict{Int, Int}()

    for (key1, val1) in d1
        for (key2, val2) in d2
            key1*key2>bd ? continue : true
            res[key1*key2] = val1 * val2
        end
    end 

    return res
end

我對此進行了基准測試:

primefunc = thefunc(0)
b1 = powsfunc(2, 1000, primefunc)
b2 = powsfunc(5, 1000, primefunc)

@benchmark dictprod2(b1, b2, 1000)

您的代碼的結果是 9.20KiB 分配,平均運行時間為 5.513 微秒。 新代碼僅使用 1.97KiB,中位運行時間為 1.68μs。

最后的功能,我幾乎保持不變,因為我沒有找到任何改進它的方法。

function dosigma(k, n)
    primefunc = thefunc(k)
    combfunc = (a, b) -> dictprod2(a, b, n)
    allprimes = [powsfunc(p, n, primefunc) for p in primes(n)]
    trivdict = Dict{Int, Int}(1=>1)
    theresult = reduce(combfunc, trivdict, allprimes)
    return theresult
end

使用@benchmark檢查所有這些都需要dosigma(0, 1000)從 81 毫秒和 28 MB 到僅 16 毫秒和 13 MB。

我還運行了dosigma(0, 100000)並獲得了 85s 和 52GiB 的分配。

我會將其留給專家以添加到此答案中。 我敢肯定,知識淵博的人可以使這更快。

我做了更多的思考/編碼/分析(主要是在非常好的JuliaBox平台上),其中一行摘要是:

Julia 不是 LISP

這意味着函數式編程只會讓你走到這一步。

現在,讓我們進入代碼(無論如何,這是我們想要看到的)。 首先,這是參考“愚蠢”的實現:

using Primes

function sigma(k, x)
   fac = factor( x)
   return prod(sum(i^(j*k) for j in 0:fac[i]) for i in keys(fac))
   end

allsigma(k, n) = [sigma(k, x) for x in 2::n]

在 JuliaBox(也在我的筆記本電腦上)上,k=0, n=10000000 大約需要 40 秒。 精明的讀者會注意到,由於溢出,這對於較大的 k 會中斷。 我解決這個問題的方法是將函數替換為:

function sigma(k, x)
   fac = factor( x)
   return prod(sum(BigInt(i)^(j*k) for j in 0:fac[i]) for i in keys(fac))
   end

對於相同的計算(在我的桌面上為 95 秒),這需要 131 秒,所以慢了 3 倍多。 相比之下, Mathematica 中的相同計算如下:

foo = DivisorSigma[0, Range[10000000]] // Timing

需要 27 秒(在桌面上),這表明Mathematica很可能首先檢查計算是否可以在 fixnums 中完成,然后以明顯的方式進行。

現在我們繼續進行“智能”實現。 這里的工作假設是 Julia 不是用於操作數據的函數式語言級別,因此,考慮到這一點,我避免了為以下代碼創建和銷毀字典:

using Primes

thefunc(i) = (a::Int, b::Int)-> sum(BigInt(a)^(k*i) for k in 0:b)

function biglist(n, func, thedict)
bot = Int(ceil(sqrt(n)))
theprimes = primes(bot, n)
    for i in theprimes
        thedict[i] = func(i, 1)
    end
    return theprimes
end

function powsfunc(x, n, func, motherdict)
    bound = convert(Int, floor(log(n)/log(x)))
    res = Int[]
    for i in 1:bound
        tmp = x^i
        push!(res, tmp)
        motherdict[tmp] = func(x, i)
    end
    return res
end

function makeprod(l1, l2, nn, dd)
    res = []
    for i in l1
        for j in l2 
            if i*j <= nn
                dd[i*j] = dd[i] * dd[j]
                push!(res, i*j)
            end
        end
    end
    return vcat(l1, l2, res)
end

function dosigma3(n, k)
    basedict = Dict{Int, BigInt}(1=>1)
    ff = thefunc(k)
    f2(a, b) = makeprod(a, b, n, basedict)
    smallprimes = reverse(primes(Int(ceil(sqrt(n))) -1))
    bl = biglist(n, ff, basedict)
    for i in smallprimes
        tmp = powsfunc(i, n, ff, basedict)
        bl = makeprod(bl, tmp, n, basedict)
    end
    return basedict
end

現在, dosigma3(100000, 0)需要 0.5 秒,比我的原始代碼加速 1500 倍,比另一個答案加速 150 倍。

同樣dosigma3(10000000, 0在 JuliaBox 上運行需要太長時間,但在上述桌面上需要 130 秒,因此在愚蠢實現的兩倍之內。

對代碼的檢查表明makeprod例程沒有對各種輸入進行排序,這可能會導致更快的終止。 為了使它們更快,我們可以引入一個合並步驟,因此:

function domerge(l1, l2)
   newl = Int[]
   while true
        if isempty(l1)
            return vcat(l2, reverse(newl))
        elseif isempty(l2)
            return vcat(l1, reverse(newl))
        elseif l1[end]>l2[end]
                tmp = pop!(l1)
                push!(newl, tmp)
        else
                tmp = pop!(l2)
                push!(newl, tmp)
        end
    end
end

function makeprod2(l1, l2, nn, dd)
    res = Int[]
    for i in l1
         restmp = Int[]
         for j in l2 
            if i*j > nn
                break
            end
            dd[i*j] = dd[i] * dd[j]
            push!(restmp, i*j)
        end
        res = domerge(res, restmp)
    end
    return domerge(l1, domerge(l2, res))
end

function dosigma4(n, k)
    basedict = Dict{Int, BigInt}(1=>1)
    ff = thefunc(k)
    f2(a, b) = makeprod(a, b, n, basedict)
    smallprimes = reverse(primes(Int(ceil(sqrt(n))) -1))
    bl = biglist(n, ff, basedict)
    for i in smallprimes
        tmp = powsfunc(i, n, ff, basedict)
        bl = makeprod2(bl, tmp, n, basedict)
    end
    return basedict
end

然而,這對於 100000 已經需要 15 秒,所以我沒有嘗試 10000000,這表明要么是我自己在 Julia 方面的無能(畢竟我是一個新手),要么是 Julia 在管理內存方面的無能。 我非常期待有見地的評論。

更新一個更簡單的篩分實現,將代碼的速度提高了 5 倍,引入更多的緩存使我們又獲得了 10 倍(!) 28(第一次完成時)到2秒(第二次完成時。所以,我想智力並不是完全沒用的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM