3

I finished writing the following program and began to do some cleanup after the debugging stage:

using BenchmarkTools

function main()
    global solution = 0
    global a = big"1"
    global b = big"1"
    global c = big"0"
    global total = 0

    while a < 100
        while b < 100
            c = a^b
            s = string(c)
            total = 0
            for i in 1:length(s)
                total = total + Int(s[i]) - 48 
            end
            if total > solution
                global solution = total
            end
            global b = b + 1
        end
        global b = 1
        global a = a + 1
    end
end

@elapsed begin
main()
end

#run @elapsed twice to ignore compilation overhead
t = @elapsed main()
print("Solution: ", solution)
t = t * 1000;
print("\n\nProgram completed in ", round.(t; sigdigits=5), " milliseconds.")

The runtime for my machine was around 150ms.

I decided to rearrange the globals to better match the typical layout of program, where globals are defined at the top:

using BenchmarkTools

global solution = 0
global a = big"1"
global b = big"1"
global c = big"0"
global total = 0

function main()

    while a < 100
        while b < 100
            c = a^b
            s = string(c)
            total = 0
            for i in 1:length(s)
                total = total + Int(s[i]) - 48 
            end
            if total > solution
                global solution = total
            end
            global b = b + 1
        end
        global b = 1
        global a = a + 1
    end
end

@elapsed begin
main()
end

#run @elapsed twice to ignore compilation overhead
t = @elapsed main()
print("Solution: ", solution)
t = t * 1000;
print("\n\nProgram completed in ", round.(t; sigdigits=5), " milliseconds.")

Making that one change for where the globals were defined reduced the runtime on my machine to 0.0042ms.

Why is the runtime so drastically reduced?

MFerguson
  • 1,739
  • 9
  • 17
  • 30
  • 0.0042ms sounds like timed region didn't include any work at run-time. 4.2 microseconds is still many thousands of clock cycles, but I wouldn't be surprised if it's just timing overhead. I don't know Julia well enough to have any idea about what might actually be going on, but does the faster time still scale with problem size? [Idiomatic way of performance evaluation?](https://stackoverflow.com/q/60291987) mentions several ways you can sanity-check a benchmark, like changing the timed code in a way that you expect would make it run much longer. – Peter Cordes Jun 17 '22 at 01:19
  • 1
    Won't `while a < 100` be false on the first iteration for the 2nd run of your program, so the 2nd call to the function doesn't do any work? Have you single-stepped with a debugger to see if that happens? – Peter Cordes Jun 17 '22 at 01:21
  • Why are you using `globals`? That is virtually always a terrible idea. Pass needed values as input and output arguments to your function. If there are too many of them, wrap them in a struct, a tuple or a dictionary. But don't use globals, unless you are using them as proper constants, and mark them as `const`. – DNF Jun 17 '22 at 06:45
  • Also, I suggest googling "why are globals bad". – DNF Jun 17 '22 at 06:47
  • 1
    To sum up the various replies you got, you are observing that setting global variables outside the function body reduces computation time. This is however due simply to the variable having already a certain value and the second run of the code does basically nothing. Globals are discouraged, but they can be useful for some init settings that don't change in the rest of your program. In such cases just add the keyword `const` to the global to retrieve back good performances. – Antonello Jun 17 '22 at 12:23

2 Answers2

4

In the first case, you are always assigning the globals and possibly changing types. Hence compiler needs to do extra work. I assume that the two programs generate different answers after the 2nd run because of the failure to reset globals…

Globals are discouraged in Julia for performance reasons because of potential type instability.

4

Don't use globals.

Don't. Use. Globals. They are bad.

When you define your globals outside the main function, then the second time you run your function, a already equals 100, and main() bails out before doing anything at all.

Global variables are a bad idea, not just in Julia, but in programming in general. You can use them when defining proper constants, like π, and maybe some other specialized cases, but not for things like this.

Let me rewrite your function without globals:

function main_locals()
    solution = 0
    a = 1
    while a < 100
        b = 1
        c = big(1)
        while b < 100
            c *= a
            s = string(c)
            total = sum(Int, s) - 48 * length(s)
            solution = max(solution, total)
            b += 1
        end
        a += 1
    end
    return solution
end

On my laptop this is >20x faster than your version with globals defined inside the function, that is, the version that actually works. The other one doesn't work as it should, so the comparison is not relevant.

Edit: I have even complicated this too much. The only thing you need to do is to remove all the globals from your first function, and return the solution, then it will work fine, and be almost as fast as the code I wrote:

function main_with_globals_removed()
    solution = 0
    a = big"1"
    b = big"1"
    c = big"0"
    total = 0

    while a < 100
        while b < 100
            c = a^b
            s = string(c)
            total = 0
            for i in 1:length(s)
                total = total + Int(s[i]) - 48 
            end
            if total > solution
                solution = total
            end
            b = b + 1
        end
        b = 1
        a = a + 1
    end
    return solution # remember return!
end

Don't use globals.

DNF
  • 11,584
  • 1
  • 26
  • 40