Hi,
I used Julia for a “computing” project recently and it was really nice. Now, I’m tempted to use Julia for a more “classic” project (involving tree-like data-structures) so I’ve implemented a test project (the tictactoe game with a monte-carlo bot… ) but my Julia code is 3x slower than my C++ code (see Julia code below and the github repo here). According to the profiler, a lot of time is spent to copy the game board so I’ve implemented a “mutable” version making less copies but it is not faster.
Is this a limitation of Julia or did I make mistakes in my code ?
# tictactoe_cmp.jl
const NB_GAMES = 1000
struct Tictactoe
board::Vector{Int}
nbFreeMoves::Int
currentPlayer::Int
nextPlayer::Int
finished::Bool
winner::Int
end
function Tictactoe()
board = [0, 0, 0, 0, 0, 0, 0, 0, 0]
nbFreeMoves = 9
currentPlayer = 1
nextPlayer = 2
finished = false
winner = 0
Tictactoe(board, nbFreeMoves, currentPlayer, nextPlayer, finished, winner)
end
# check if the game is finished and find the winner
function computeFinish(board::Vector{Int}, nbFreeMoves::Int)
b11, b12, b13, b21, b22, b23, b31, b32, b33 = board
if b11 != 0 && b11 == b12 == b13
true, b11
elseif b21 != 0 && b21 == b22 == b23
true, b21
elseif b31 != 0 && b31 == b32 == b33
true, b31
elseif b11 != 0 && b11 == b21 == b31
true, b11
elseif b12 != 0 && b12 == b22 == b32
true, b12
elseif b13 != 0 && b13 == b23 == b33
true, b13
elseif b11 != 0 && b11 == b22 == b33
true, b11
elseif b13 != 0 && b13 == b22 == b31
true, b11
elseif nbFreeMoves == 0 # no winner, no empty cell -> draw
true, 0
else # no winner, empty cell -> game not finished
false, 0
end
end
# find the moveIndex'th free cell in the board
function findIndex(board::Vector{Int}, moveIndex::Int)
nbFree = 0
k = 1
while k<=9
if board[k] == 0
nbFree += 1
if nbFree == moveIndex
break
end
end
k += 1
end
k
end
function playIndex(game::Tictactoe, moveIndex::Int)
k = findIndex(game.board, moveIndex)
if k <= 9
board = copy(game.board) # this line seems to take some time
board[k] = game.currentPlayer
nbFreeMoves = game.nbFreeMoves - 1
currentPlayer = game.nextPlayer
nextPlayer = game.currentPlayer
finished, winner = computeFinish(board, nbFreeMoves)
Tictactoe(board, nbFreeMoves, currentPlayer, nextPlayer, finished, winner)
else
game
end
end
abstract type Bot end
function runGame(game::Tictactoe, bot1::Bot, bot2::Bot)
while !game.finished
bot = game.currentPlayer == 1 ? bot1 : bot2
moveIndex = genmove(game, bot)
game = playIndex(game, moveIndex)
end
game.winner
end
struct RandomBot <: Bot end
function genmove(game::Tictactoe, bot::RandomBot)
nbMoves = game.nbFreeMoves
rand(1:nbMoves)
end
struct McBot <: Bot
maxSims::Int
end
# evaluate a move by running many random simulations (monte-carlo)
function evalMove(game::Tictactoe, moveIndex::Int, nbSims::Int)
bot = RandomBot()
player = game.currentPlayer
game1 = playIndex(game, moveIndex)
evalMove1(unused) = runGame(game1, bot, bot) == player ? 1 : 0
mapreduce(evalMove1, (+), 0, 1:nbSims)
end
function genmove(game::Tictactoe, bot::McBot)
nbMoves = game.nbFreeMoves
nbMoveSims = div(bot.maxSims, nbMoves)
simulate(moveIndex) = evalMove(game, moveIndex, nbMoveSims)
wins = map(simulate, [moveIndex for moveIndex in 1:nbMoves])
indmax(wins)
end
# run many games for reducing the variance of the results
function runGames(game::Tictactoe, bot1::Bot, bot2::Bot, nbGames::Int)
res = [0, 0, 0]
for _ in 1:nbGames
winner = 1 + runGame(game, bot1, bot2)
res[winner] += 1
end
res
end
# compare RandomBot and McBot for a given number of simulations
function runXp(nbSims::Int)
game = Tictactoe()
botA = RandomBot()
botB = McBot(nbSims)
resAB = runGames(game, botA, botB, NB_GAMES)
resBA = runGames(game, botB, botA, NB_GAMES)
println("RandomBot McBot ", nbSims, " ", NB_GAMES, " ",
resAB[1], " ", resAB[2], " ", resAB[3], " ",
resBA[1], " ", resBA[2], " ", resBA[3], " ",
resAB[1]+resBA[1], " ", resAB[2]+resBA[3], " ", resAB[3]+resBA[2])
end
println("botA botB nbSims nbGames drawsA1B2 winsA1 winsB2 drawsB1A2 winsB1 winsA2 draws winsA winsB")
vecNbSims = [1, 10, 100, 1000, 10000]
map(runXp, vecNbSims)