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