繁体   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