I have done some more thinking/coding/profiling (mostly on the very nice JuliaBox platform), and the one line summary is:
Julia is not LISP
which means that programming in functional style will only get you so far.
Now, let's get to the code (which is what we want to see anyhow). First, here is the reference "stupid" implementation:
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]
On JuliaBox (also on my laptop) it takes about 40 seconds for k=0, n=10000000. An astute reader will note that this will break for largish k because of overflow. The way I get around this is by replacing the function by:
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
This takes 131 seconds for the same computation (95 seconds on my desktop) so a little more than a factor of 3 slower. By contrast, the same computation in Mathematica done as:
foo = DivisorSigma[0, Range[10000000]] // Timing
takes 27 seconds (on the desktop), which indicates that, most likely, Mathematica first checks that the computation can be done in fixnums, and then does it in the obvious way.
Now we move on to the "intelligent" implementation. The working hypothesis here was that Julia is not at functional language level for operating with data, so, with that in mind, I avoided the creation and destruction of dictionaries for the following code:
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
now, dosigma3(100000, 0) takes 0.5 seconds, for a factor of 1500 speedup over my original code, and a factor of 150 speedup over the other answer.
Also dosigma3(10000000, 0 takes too long to run on JuliaBox, but on the aforementioned desktop it takes 130 seconds, so within a factor of two of the dumb implementation.
An examination of the code shows that the makeprod routine suffers from not having the various inputs sorted, which could result in faster termination. To make them faster, we can introduce a merge step, thus:
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
However, this takes 15 seconds already for 100000, so I did not try it for 10000000, and indicates either my own incompetence in Julia (I am a novice, after all) or Julia's incompetence in managing memory. I very much look forward to insightful comments.
UPDATE a simpler sieving implementation speeds up the code by another factor of five, and introducing some more caching buys us another factor of 10(!), so computing the sigma_k for the first 10000000 (same machine as above, no parallelism used) takes between 28 (when done the first time) to 2 seconds (when done the second time. So, I guess intelligence is not entirely useless.
Number, and you should use a structure with less indirection. In general, the less strict the data type the less performant. FWIW Python classes are essentially non-strictly typed dictionaries, so that should explain a lot. - Chris RackauckasBigInts; this implementation is pretty fast if you useInts, but still needs 3.5 min and 50 GiB for n = 100000 withBigInts. The downside is that you can only use really small k then, of course... - phipsgabler@code_warntype). If it's still slow then you may just be running into a bottleneck due to arbitrary sized integers, which is just a fundamental computing issue since any arbitrary-sized number representation inherently has overhead. But make sure that's the case! - Chris Rackauckas