[英]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.