1

I have written a very basic algorithm in Julia Studio (Julia 0.2.0, OSX 10.8.2) that calculates the average mana left on each turn for a given mana curve in Hearthstone. When done with the algorithm I added type declarations to all the variables, thinking that this would help increase the overall speed. Suprise! The added type declarations made the code run more than 4x slower (from ~7s up to ~28s). What is causing this weird behaviour, and how can I fix it? It feels like adding types should help the compiler produce faster code, or at the very least make no difference.

Here is the code without type declarations (run time 6.76s):

function all_combinations(n)
    result = Array{Int64}[]
    for x in [1:n]
        append!(result, collect(combinations(1:n,x)))
    end
    return result
end

curve = [2, 3, 4, 5, 5, 4, 3, 2, 1, 1]

games = Array{Int64}[]

function execute()
    for game_n in [1:5000]

        deck = mapreduce(
            (x) -> fill(x[1], x[2]),
            append!,
            enumerate(curve))

        function drawcard()
            card = splice!(deck, rand(1:length(deck)))
        end

        hand = [drawcard() for n in [1:3]]

        turn_leftovers = Int64[]

        for mana in [1:10]

            push!(hand, drawcard())

            possible_plays = all_combinations(length(hand))
            map!(
                play -> map(i -> hand[i], play),
                possible_plays)
            filter!(x -> sum(x) <= mana, possible_plays)

            if  !isempty(possible_plays)

                play = reduce(
                    (a, b) -> sum(a) > sum(b) ? a : b,
                    possible_plays)
                for card in play
                    splice!(hand, findfirst(hand, card))
                end
                push!(turn_leftovers, mana - sum(play))
            else
                push!(turn_leftovers, mana)
            end

        end

        push!(games, turn_leftovers)

    end
end

println(@elapsed execute())

println("Averaging over $(length(games)) games")
for turn in [1:length(games[1])]
    avrg = mean(map(game -> game[turn], games))
    println("Left on turn $turn: $avrg")
end
println("Average mana leftover: $(mean(reduce(vcat, games)))")
println("Done")

And here is the code with type declarations (run time 28.48s):

function all_combinations(n)
    result = Array{Int64}[]
    for x in [1:n]
        append!(result, collect(combinations(1:n,x)))
    end
    return result
end

curve::Array{Int64} = [2, 3, 4, 5, 5, 4, 3, 2, 1, 1]

games = Array{Int64}[]

function execute()
    for game_n::Int64 in [1:5000]

        deck::Array{Int64}
        deck = mapreduce(
            (x) -> fill(x[1], x[2]),
            append!,
            enumerate(curve))

        function drawcard()
            card::Int64 = splice!(deck, rand(1:length(deck)))
        end

        hand::Array{Int64}
        hand = [drawcard() for n in [1:3]]

        turn_leftovers::Array{Int64}
        turn_leftovers = Int64[]

        for mana::Int64 in [1:10]

            push!(hand, drawcard())

            possible_plays::Array{Array{Int64}} = all_combinations(length(hand))
            map!(
                play -> map(i::Int64 -> hand[i], play),
                possible_plays)
            filter!(x::Array{Int64} -> sum(x) <= mana, possible_plays)

            if  !isempty(possible_plays)

                play::Array{Int64} = reduce(
                    (a::Array{Int64}, b::Array{Int64}) -> sum(a) > sum(b) ? a : b,
                    possible_plays)
                for card::Int64 in play
                    splice!(hand, findfirst(hand, card))
                end
                push!(turn_leftovers, mana - sum(play))
            else
                push!(turn_leftovers, mana)
            end

        end

        push!(games, turn_leftovers)

    end
end

println(@elapsed execute())

println("Averaging over $(length(games)) games")
for turn in [1:length(games[1])]
    avrg = mean(map(game -> game[turn], games))
    println("Left on turn $turn: $avrg")
end
println("Average mana leftover: $(mean(reduce(vcat, games)))")
println("Done")

It could be worth noting that even the fastest version is a bit slower than the equivalent code written in JavaScript. This is probably only because of the lousy implementation, though. I have no doubt a better algorithm would outshine JS any day of the week.

haspaker
  • 33
  • 4
  • 1
    First thing: Julia guess when you say nothing and says okay when you insist. ``Array{Int64}`` is a incomplete array declaration that can hold any N dimensional array. ``Array{Int,1}`` is the 1d vector you actually wants. – ivarne Mar 02 '14 at 15:27
  • You are right, changing the types to Array{Int,1} increased the speed to around 5s. Thanks! – haspaker Mar 02 '14 at 15:40
  • If you want more quick gain, you can declare curve and games const – ivarne Mar 02 '14 at 15:49

1 Answers1

1

One source of slowdown: you're using a lot of anonymous functions combined with higher-order functions in things like,

map!( play -> map(i::Int64 -> hand[i], play), possible_plays ) filter!(x::Array{Int64} -> sum(x) <= mana, possible_plays)

In current Julia, both of these constructions are not easily optimized by the compiler. Replacing them with things like list comprehensions or for loops will improve things.

John Myles White
  • 2,889
  • 20
  • 14