.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "auto-examples/performance.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr_auto-examples_performance.py: Performance Comparisons ======================== ``bulletchess``'s creation was motivated by my frustration with `python-chess `_'s slow performance, especially for areas such as machine learning and engine development. ``python-chess`` is a fantastic, feature-rich library, but is inherently limited in its speed by being implemented in python. ``bulletchess``, however, is implemented as a pure C-extension, allowing it to be significantly faster. To demonstrate this, we can write equivalent functions in both libraries, and compare the runtimes. .. note:: ``bulletchess`` is neither an extension nor a port of ``python-chess``, and has a distinct and independent implementation. Let's start by implementing a `Perft `_ function. In ``bulletchess``: .. GENERATED FROM PYTHON SOURCE LINES 16-34 .. code-block:: Python import bulletchess from bulletchess.utils import count_moves def bullet_perft(board : bulletchess.Board, depth : int) -> int: if depth == 0: return 1 elif depth == 1: return count_moves(board) else: nodes = 0 moves = board.legal_moves() for move in moves: board.apply(move) nodes += bullet_perft(board, depth - 1) board.undo() return nodes .. GENERATED FROM PYTHON SOURCE LINES 35-36 And in ``python-chess`` .. GENERATED FROM PYTHON SOURCE LINES 36-51 .. code-block:: Python import chess def chess_perft(board : chess.Board, depth : int) -> int: if depth == 0: return 1 elif depth == 1: return board.legal_moves.count() else: nodes = 0 for move in board.legal_moves: board.push(move) nodes += chess_perft(board, depth - 1) board.pop() return nodes .. GENERATED FROM PYTHON SOURCE LINES 52-53 Notice how the code we write is nearly identical. However, when we test their run times: .. GENERATED FROM PYTHON SOURCE LINES 53-69 .. code-block:: Python from time import time start = time() result = chess_perft(chess.Board(), 6) chess_time = time() - start print(f"`chess_perft` returned {result} in {chess_time:.4f}s") start = time() bullet_perft(bulletchess.Board(), 6) bullet_time = time() - start print(f"`bullet_perft` returned {result} in {bullet_time:.4f}s") print(f"bulletchess is {chess_time/bullet_time:.4f}x faster") .. rst-class:: sphx-glr-script-out .. code-block:: none `chess_perft` returned 119060324 in 109.9025s `bullet_perft` returned 119060324 in 1.6925s bulletchess is 64.9358x faster .. GENERATED FROM PYTHON SOURCE LINES 70-72 We see a massive difference in ``bulletchess``'s move generation and application speed. ``bulletchess`` is also very fast at writing and parsing FEN strings. .. GENERATED FROM PYTHON SOURCE LINES 72-78 .. code-block:: Python import json # JSON file with a list of 1 million FENs with open("../data/fens.json", "r") as f: fens = json.load(f) .. GENERATED FROM PYTHON SOURCE LINES 79-84 We can define FEN "roundtrip" functions in ``bulletchess`` and ``python-chess``, which will take in a list of FEN strings. Each FEN will be parsed to create a object representation for the position it describes. Then, each object will write a new FEN string describing itself, which should match the original. Neither library stores the given FEN when a board object is created, so both ``bulletchess`` and ``python-chess`` will fully parse and rewrite the input FENs. .. GENERATED FROM PYTHON SOURCE LINES 84-96 .. code-block:: Python def bullet_roundtrip(fens : list[str]): boards = [bulletchess.Board.from_fen(fen) for fen in fens] return [board.fen() for board in boards] def chess_roundtrip(fens : list[str]): boards = [chess.Board(fen) for fen in fens] return [board.fen(en_passant = "fen") for board in boards] .. GENERATED FROM PYTHON SOURCE LINES 97-98 Like before, we'll compare the runtimes of each version. .. GENERATED FROM PYTHON SOURCE LINES 98-113 .. code-block:: Python start = time() chess_fens = chess_roundtrip(fens) chess_time = time() - start print(f"`chess_roundtrip` took {chess_time:.4}s") start = time() bullet_fens = bullet_roundtrip(fens) bullet_time = time() - start print(f"`bullet_roundtrip` took {bullet_time:.4}s") print(f"bulletchess is {chess_time/bullet_time:.4f}x faster") assert(chess_fens == bullet_fens) .. rst-class:: sphx-glr-script-out .. code-block:: none `chess_roundtrip` took 46.58s `bullet_roundtrip` took 0.9744s bulletchess is 47.8052x faster .. GENERATED FROM PYTHON SOURCE LINES 114-116 Once again, ``bulletchess`` is much faster. Using the same dataset of FENs, lets compare checking if positions are checkmate, a draw, or ongoing. .. GENERATED FROM PYTHON SOURCE LINES 116-143 .. code-block:: Python def chess_statuses(boards : list[chess.Board]) -> dict: outcomes = {"ongoing": 0, "checkmate": 0, "draw": 0} for board in boards: outcome = board.outcome(claim_draw = True) if outcome == None: outcomes["ongoing"] += 1 elif outcome.winner != None: outcomes["checkmate"] += 1 else: outcomes["draw"] += 1 return outcomes from bulletchess import CHECKMATE, DRAW def bullet_statuses(boards : list[bulletchess.Board]) -> dict: outcomes = {"ongoing": 0, "checkmate": 0, "draw": 0} for board in boards: if board in CHECKMATE: outcomes["checkmate"] += 1 elif board in DRAW: outcomes["draw"] += 1 else: outcomes["ongoing"] += 1 return outcomes .. GENERATED FROM PYTHON SOURCE LINES 144-146 The syntax of ``bulletchess`` and ``python-chess`` diverges more here, but the structure is still the same. Running the comparison: .. GENERATED FROM PYTHON SOURCE LINES 146-164 .. code-block:: Python chess_boards = [chess.Board(fen) for fen in fens] bullet_boards = [bulletchess.Board.from_fen(fen) for fen in fens] start = time() chess_res = chess_statuses(chess_boards) chess_time = time() - start print(f"`chess_statuses` took {chess_time:.4}s") print(chess_res) start = time() bullet_res = bullet_statuses(bullet_boards) bullet_time = time() - start print(f"`bullet_statuses` took {bullet_time:.4}s") print(bullet_res) print(f"bulletchess is {chess_time/bullet_time:.4f}x faster") .. rst-class:: sphx-glr-script-out .. code-block:: none `chess_statuses` took 117.8s {'ongoing': 933861, 'checkmate': 40147, 'draw': 25992} `bullet_statuses` took 0.3026s {'ongoing': 933861, 'checkmate': 40147, 'draw': 25992} bulletchess is 389.3435x faster .. GENERATED FROM PYTHON SOURCE LINES 165-167 The speed up is even larger. Like ``python-chess``, ``bulletchess`` provides a PGN reader. Let's do a simple task reading a PGN file, we'll go through every position in each game, and check how many have a pawn of any color on E4. .. GENERATED FROM PYTHON SOURCE LINES 167-201 .. code-block:: Python import chess.pgn import bulletchess.pgn # a large PGN file PATH = "../data/pgn/modern.pgn" def chess_check_games(): count = 0 with open(PATH, "r") as f: game = chess.pgn.read_game(f) while game: board = chess.Board() for move in game.mainline_moves(): board.push(move) if board.piece_type_at(chess.E4) == chess.PAWN: count += 1 game = chess.pgn.read_game(f) return count def bullet_check_games(): count = 0 with bulletchess.pgn.PGNFile.open(PATH) as f: game = f.next_game() while game: board = game.starting_board for move in game.moves: board.apply(move) piece = board[bulletchess.E4] if piece and piece.piece_type == bulletchess.PAWN: count += 1 game = f.next_game() return count .. GENERATED FROM PYTHON SOURCE LINES 202-204 We've kept the operation on each position simple on purpose, so we can more directly compare reading through games. .. GENERATED FROM PYTHON SOURCE LINES 204-220 .. code-block:: Python start = time() chess_res = chess_check_games() chess_time = time() - start print(f"`chess_check_games` took {chess_time:.4}s") print(f"python-chess found {chess_res} positions with a pawn on E4") start = time() bullet_res = bullet_check_games() bullet_time = time() - start print(f"`bullet_check_games` took {bullet_time:.4}s") print(f"bulletchess found {bullet_res} positions with a pawn on E4") print(f"bulletchess is {chess_time/bullet_time:.4f}x faster") .. rst-class:: sphx-glr-script-out .. code-block:: none `chess_check_games` took 18.31s python-chess found 824592 positions with a pawn on E4 `bullet_check_games` took 1.415s bulletchess found 824592 positions with a pawn on E4 bulletchess is 12.9408x faster .. rst-class:: sphx-glr-timing **Total running time of the script:** (5 minutes 24.630 seconds) .. _sphx_glr_download_auto-examples_performance.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: performance.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: performance.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: performance.zip ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_