Performance Julia函数性能混乱

Performance Julia函数性能混乱,performance,julia,Performance,Julia,我是Julia的新手,正如我第一次尝试一个温和的非平凡程序时所写的,该程序计算1到N范围内所有数字的除数sigma函数-除数sigma函数sigma(k,N)计算N的除数的k次幂之和(如果k为0,它只是除数)。由于sigma(k,n)是n的乘法函数,我们将其写成如下(原则上,这应该比分解范围内的每个整数更有效): 使用素数 thefunc(i)=(a::Int,b::Int)->求和(BigInt(a)^(k*i)表示0:b中的k) 函数powsfunc(x,n,func) 绑定=转换(Int,

我是Julia的新手,正如我第一次尝试一个温和的非平凡程序时所写的,该程序计算1到N范围内所有数字的除数sigma函数-除数sigma函数sigma(k,N)计算N的除数的k次幂之和(如果k为0,它只是除数)。由于sigma(k,n)是n的乘法函数,我们将其写成如下(原则上,这应该比分解范围内的每个整数更有效):

使用素数
thefunc(i)=(a::Int,b::Int)->求和(BigInt(a)^(k*i)表示0:b中的k)
函数powsfunc(x,n,func)
绑定=转换(Int,floor(log(n)/log(x)))
返回Dict{Int,Number}(0:bound中i的x^i=>func(x,i)
结束
dictprod2(d1,d2,bd)=Dict(i*j=>d1[i]*d2[j]表示键(d1)中的i,如果i*j dictprod2(a,b,n)表示键(d2)中的j
所有素数=[powsfunc(p,n,primefunc)表示素数(n)中的p]
trivdict=Dict{Int,Number}(1=>1)
结果=减少(combfunc、trivdict、allprimes)
返回结果
结束
好消息是上面的方法有效。坏消息是它的速度非常慢,
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}
更精确,所以它的性能应该稍微好一点-正如Chris在评论中指出的。但是实际测试没有显示出性能上的任何差异

另一个变化是,我不是使用理解来定义Dict,而是一个接一个地分配dictionary元素。这减少了内存估计-通过
@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 allocs,平均运行时间为5.513μs。新代码仅使用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
检查所有这些都需要
剂量(0,1000)
从81ms和28MB增加到16ms和13MB

我还运行了dosigma(01000),得到了85s和52GiB的allocs


我将留给专家们来补充这个答案。我相信有更多知识的人可以更快地解决这个问题。

我做了更多的思考/编码/分析(主要是在非常好的平台上),一行总结是:

朱莉娅不是口齿不清的人

这意味着函数式编程只会让你走到这一步

现在,让我们来看看代码(这是我们无论如何都想看到的)。首先,这里是参考的“愚蠢”实现:

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
同样的计算需要131秒(在我的桌面上是95秒),所以慢了3倍多一点。相比之下,Mathematica中的相同计算完成如下:

需要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
然而,这需要15秒的时间才能完成100000次,所以我没有尝试10000000次,这表明我自己在Julia(毕竟我是新手)方面的能力不足,或者Julia在管理内存方面的能力不足。我非常期待有见解的评论


UPDATEa将代码的速度提高了5倍,引入更多的缓存又为我们带来了10倍(!),因此计算第一个10000000的sigma_k(与上面相同的机器,没有使用并行性)需要28秒(第一次完成时)到2秒(第二次完成时。因此,我猜智能并非完全无用。

您正在迭代构建一个非严格类型的字典。我无法考虑性能较低的代码。您的代码不可能是类型稳定的,因为生成的代码只能是一个
数字
,您应该使用间接性较小的结构。我一般来说,数据类型越不严格,性能就越差。FWIW Python类本质上是非严格类型的字典,这应该可以解释很多。@Igor我真的不是性能优化方面的专家,所以我写这篇文章并不是为了回答这个问题,但我认为瓶颈可能只是
BigInt
s;如果使用
s,它的速度相当快>Int
s,但使用
BigInt
s的n=100000仍然需要3.5分钟和50 GiB。缺点是,您只能使用非常小的k,当然…@ChrisRackauckas谢谢,但您有什么建议吗?使用元组列表(在本应用程序中更自然)更快,但速度不是很大。你能用元组代码列表更新它吗?如果这些元组都是相同的类型,那么它应该是类型稳定的(用
@code\u warntype
检查)。如果它仍然很慢,那么你可能会因为任意大小的整数而遇到瓶颈,这只是一个基本的计算问题
foo = DivisorSigma[0, Range[10000000]] // Timing
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
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