Browse Source

Performance: rewrite empty_coordinates, use :timer.tc to calculate elapsed time during simulation, save the result of precomputing stuff to check for valid move instead of doing it twice.

master
Guillaume Grenet 5 years ago
parent
commit
8ea4b94e45
8 changed files with 85 additions and 30 deletions
  1. 1
    1
      .gitignore
  2. 2
    2
      README.md
  3. 8
    9
      lib/board.ex
  4. 13
    3
      lib/board/state.ex
  5. 1
    1
      lib/gtp_commands.ex
  6. 16
    12
      lib/player/mc_rave.ex
  7. 3
    2
      test/player/mc_rave_test.exs
  8. 41
    0
      test/profile/ips.exs

+ 1
- 1
.gitignore View File

@@ -6,5 +6,5 @@ erl_crash.dump
*.log*
*.trace
profile.*
run.sh
*.sh


+ 2
- 2
README.md View File

@@ -25,9 +25,9 @@ For profiling, you'll need to run `mix deps.get`, there are no other dependencie
Many GTP front-end seem particularity finicky with the path to the binary, so I ended-up just writing a simple bash script (chmod +x) and use the path to that script:

#!/bin/bash
cd FULL_PATH/TO/PROJECT && FULL_PATH/TO/MIX/BINARY run -e "WeiqiDMC.start_gtp_server"`
cd FULL_PATH/TO/PROJECT && MIX_ENV=prod FULL_PATH/TO/MIX/BINARY run -e "WeiqiDMC.start_gtp_server"`

I used *Quarry* to test it out.
I used *Quarry* to test it out. Don't forget to run `MIX_ENV=prod mix compile` first :)

**The tests**


+ 8
- 9
lib/board.ex View File

@@ -62,7 +62,7 @@ defmodule WeiqiDMC.Board do
end

def valid_move?(state, coordinate) do
case pre_compute_valide_move(state, coordinate, false) do
case pre_compute_valid_move(state, coordinate, false) do
{:ko, _} -> false
{:ok, _} -> true
end
@@ -94,29 +94,29 @@ defmodule WeiqiDMC.Board do
if State.board_value(state, coordinate) != :empty do
{:ko, state}
else
case pre_compute_valide_move(state, coordinate, true) do
case pre_compute_valid_move(state, coordinate, true) do
{:ko, _} -> {:ko, state}
{:ok, precomputed} -> compute_valid_move state, coordinate, precomputed
end
end
end

def pre_compute_valide_move(state, coordinate, return_pre_computed) do
def pre_compute_valid_move(state, coordinate, return_pre_computed) do
color = state.next_player
coordinate_set = Set.put(HashSet.new, coordinate)
surroundings = surroundings coordinate, state.size
empty = surroundings |> Enum.filter(fn (surrounding) -> State.board_value(state, surrounding) == :empty end)

other_player = surroundings |> Enum.filter(fn (surrounding) -> State.board_value(state, surrounding) == Helpers.opposite_color(color) end)
|> Enum.map(&group_containing(&1, Helpers.opposite_color(color), state.groups))
other_player = surroundings |> Enum.filter_map(&State.board_has_value?(state, &1, Helpers.opposite_color(color)),
&group_containing(&1, Helpers.opposite_color(color), state.groups))

if length(empty) == 0 do
capturing = Enum.filter(other_player, fn({_, _, liberties}) ->
liberties == coordinate_set
end)
if length(capturing) == 0 do
same_player = surroundings |> Enum.filter(fn (surrounding) -> State.board_value(state, surrounding) == color end)
|> Enum.map(&group_containing(&1, color, state.groups))
same_player = surroundings |> Enum.filter_map(&State.board_has_value?(state, &1, color),
&group_containing(&1, color, state.groups))

liberties_same_player_group = Enum.map(same_player, fn ({_, _, liberties}) ->
Set.size(liberties) - (if Set.member?(liberties, coordinate) do 1 else 0 end)
@@ -267,8 +267,7 @@ defmodule WeiqiDMC.Board do

def is_eyeish_for?(color, state, coordinate) do
surroundings = surroundings(coordinate, state.size)
values = surroundings |> Enum.map(fn surrounding -> State.board_value(state, surrounding) end)
length(surroundings) == length(Enum.filter(values, fn (value) -> value == color end))
length(surroundings) == length(Enum.filter(surroundings, &State.board_has_value?(state, &1, color)))
end

def game_over?(state) do

+ 13
- 3
lib/board/state.ex View File

@@ -30,9 +30,15 @@ defmodule WeiqiDMC.Board.State do
end

def empty_coordinates(state) do
(0..state.size*state.size-1)
|> Enum.filter(fn index -> elem(state.board, index) == :empty end)
|> Enum.map(fn index -> {trunc(index/state.size)+1, rem(index, state.size)+1} end)
empty_coordinates state.board, state.size, 0, state.size*state.size, []
end

def empty_coordinates(_, _, max_index, max_index, coordinates) do coordinates end
def empty_coordinates(board, size, index, max_index, coordinates) do
if elem(board, index) == :empty do
coordinates = [{trunc(index/size)+1, rem(index, size)+1}|coordinates]
end
empty_coordinates board, size, index+1, max_index, coordinates
end

def update_board(state, coordinate, value) when is_bitstring(coordinate) do
@@ -44,6 +50,10 @@ defmodule WeiqiDMC.Board.State do
%{ state | board: (state.board |> Tuple.delete_at(position) |> Tuple.insert_at(position, value)) }
end

def board_has_value?(state, {row, column}, value) do
elem(state.board, (row-1)*state.size+(column-1)) == value
end

def board_value(state, coordinate) when is_bitstring(coordinate) do
board_value state, Helpers.coordinate_string_to_tuple(coordinate)
end

+ 1
- 1
lib/gtp_commands.ex View File

@@ -100,7 +100,7 @@ defmodule WeiqiDMC.GTPCommands do

def process(["genmove", color], state_agent) do
state = state(state_agent) |> Board.force_next_player(color)
case WeiqiDMC.Player.MCRave.generate_move(state, 20000) do
case WeiqiDMC.Player.MCRave.generate_move(state, 10000) do
:ko -> {:ko, "illegal state"}
:resign -> {:ok, "resign"}
:pass ->

+ 16
- 12
lib/player/mc_rave.ex View File

@@ -27,23 +27,21 @@ defmodule WeiqiDMC.Player.MCRave do

def generate_move(state, think_time_ms) do
:random.seed(:os.timestamp)
{mega, secs, micro} = :os.timestamp
{mc_rave_state, stats} = mc_rave state,
{mega, secs, micro+think_time_ms*1000}, think_time_ms*1000,
%WeiqiDMC.Player.MCRave.State{},
0

# IO.puts "Simulation: #{stats}"
{mc_rave_state, _} = mc_rave state, think_time_ms*1000,
%WeiqiDMC.Player.MCRave.State{}, 0


select_move state, mc_rave_state
end

def mc_rave(_, _, remaining_time, mc_rave_state, stats) when remaining_time < 0 do
def mc_rave(_, remaining_time, mc_rave_state, stats) when remaining_time < 0 do
{mc_rave_state, stats}
end

def mc_rave(state, target_time, _, mc_rave_state, stats) do
mc_rave state, target_time, :timer.now_diff(target_time, :os.timestamp), simulate(state, mc_rave_state), stats + 1
def mc_rave(state, remaining_time, mc_rave_state, stats) do
{elapsed, mc_rave_state} = :timer.tc &simulate/2, [state, mc_rave_state]
mc_rave state, remaining_time - elapsed, mc_rave_state, stats + 1
end

def simulate(state, mc_rave_state) do
@@ -111,7 +109,10 @@ defmodule WeiqiDMC.Player.MCRave do
{moves, outcome?(from_state)}
else
new_action = default_policy(from_state)
{:ok, new_state} = Board.compute_move(from_state, new_action, from_state.next_player)
{:ok, new_state} = case new_action do
:pass -> Board.compute_move(from_state, :pass)
{coordinate, precomputed} -> Board.compute_valid_move(from_state, coordinate, precomputed)
end
sim_default new_state, moves ++ [new_action]
end
end
@@ -139,8 +140,11 @@ defmodule WeiqiDMC.Player.MCRave do
def default_policy(_, []) do :pass end
def default_policy(state, candidates) do
move = Enum.at candidates, :random.uniform(length(candidates)) - 1
if Board.valid_move?(state, move) and !ruin_perfectly_good_eye?(state, move) do
move
if !ruin_perfectly_good_eye?(state, move) do
case Board.pre_compute_valid_move(state, move, true) do
{:ok, computed} -> {move, computed}
{:ko, nil} -> default_policy(state, candidates -- [move])
end
else
default_policy state, candidates -- [move]
end

+ 3
- 2
test/player/mc_rave_test.exs View File

@@ -24,7 +24,8 @@ defmodule WeiqiDMC.Player.McRaveTest do
"A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2", "J2",
"A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1", "J1" ], "White"

assert Player.default_policy(Board.force_next_player(state, :black)) == {9,1}
{move, _} = Player.default_policy(Board.force_next_player(state, :black))
assert move == {9,1}
assert Player.default_policy(Board.force_next_player(state, :white)) == :pass
end

@@ -247,6 +248,6 @@ defmodule WeiqiDMC.Player.McRaveTest do
state = %{state | moves: 0}

assert Player.outcome?(state) == 1
assert Player.generate_move(Board.force_next_player(state, :white), 200) == {9,2}
assert Player.generate_move(Board.force_next_player(state, :white), 100) == {9,2}
end
end

+ 41
- 0
test/profile/ips.exs View File

@@ -0,0 +1,41 @@
defmodule WeiqiDMC.Profile.IPS do
def run do
size = 9
board = Tuple.duplicate(:empty, size*size)
Task.async(fn -> IO.puts "method_1: #{ips(&method_1/2, [board, size], 2)}/seconds" end) |> Task.await
Task.async(fn -> IO.puts "method_2: #{ips(&method_2/2, [board, size], 2)}/seconds" end) |> Task.await
end

def ips(method, arguments, seconds) do
ips(method, arguments, 0, seconds*1000000) / seconds
end

def ips(_, _, iterations, remaining_time) when remaining_time < 0 do iterations end
def ips(method, arguments, iterations, remaining_time) do
{elapsed_time, _} = :timer.tc method, arguments
ips method, arguments, iterations+1, remaining_time - elapsed_time
end

def method_1(board, size) do
(0..size*size-1)
|> Enum.filter_map(&method_1_predicate(&1), &method_1_map(&1))
end

def method_1_predicate?(board, index) do elem(board, index) == :empty end
def method_1_map(index) do {trunc(index/size)+1, rem(index, size)+1} end

def method_2(board, size) do
method_2 board, size, 0, size*size, []
end

def method_2(_, size, max_index, max_index, coordinates) do coordinates end

def method_2(board, size, index, max_index, coordinates) do
if elem(board, index) == :empty do
coordinates = [{trunc(index/size)+1, rem(index, size)+1}|coordinates]
end
method_2 board, size, index+1, max_index, coordinates
end
end

WeiqiDMC.Profile.IPS.run

Loading…
Cancel
Save