RevComm Tech Blog

コミュニケーションを再発明し 人が人を想う社会を創る

GILを無効化したPythonを早速試してみた (2024/06 更新)

バックエンドエンジニアの小門です。

この記事ではグローバルインタプリタロック (GIL) が解消されたPythonを動かしてみた検証の方法と結果について書きます。

なおGIL自体の説明や詳しい仕組みについてこの記事ではほとんど説明しないのでご了承ください。

準備として開発バージョンを取得してソースコードからビルドし、ビルド成果物のPythonランタイムを使って検証します。

追記: 2024/6/14 時点で最新の 3.13.0 beta2 を使ってベンチマークを再疎検証しました。また、一部の内容の訂正を行いました。

準備(ビルド)

Pythonにおける「GIL廃止」の第一歩として、CPython本家のリポジトリにおいてGILを無効化できるようにするための修正が2024年3月12日mainブランチへマージされました。

gh-116167: Allow disabling the GIL with PYTHON_GIL=0 or -X gil=0 #116338

また同日、上記の変更を取り込んだ開発バージョンが v3.13.0a5 としてリリースされました。

https://www.python.org/downloads/release/python-3130a5/
https://github.com/python/cpython/releases/tag/v3.13.0a5

まだ開発途中のアルファ版ですが、今回はこのバージョンを使って検証していきます。

なお筆者の動作環境は以下の通りです。

  • CPU: AMD Ryzen 7 3700X(8コア)
  • OS: Ubuntu 22.04 (on WSL2 / Windows10)
    • gcc: 11.4.0

また、本記事の手順ではソースコードをビルドするためのツール群が必要になります。
お使いの環境に応じて必要な準備をしてください。

参考: Python Developer's Guide - Setup and building

ビルド/インストール手順

GILを無効化するにはビルド時にオプションを指定しておく必要があります。

オプションは--disable-gilとのこと。分かりやすいですね。
https://github.com/python/cpython/blob/076d169ebbe59f7035eaa28d33d517bcb375f342/configure#L1815-L1816

一連のコマンド手順は以下のようになります。

$ pwd
/home/skokado/playground-py313

$ # インストール用ディレクトリを作成
$ mkdir -p local/python-3.13

$ # ソースコードを取得
$ wget https://www.python.org/ftp/python/3.13.0/Python-3.13.0a5.tgz
$ tar xf Python-3.13.0a5.tgz
$ cd Python-3.13.0a5/

$ # オプションの確認
$ ./configure --help | grep gil
  --disable-gil           enable experimental support for running without the

$ # ビルド、インストール
$ ./configure --disable-gil --prefix $(pwd)/../local/python-3.13 && make install
checking build system type... x86_64-pc-linux-gnu
checking host system type... x86_64-pc-linux-gnu
checking for Python interpreter freezing... ./_bootstrap_python
...
()
Successfully installed pip-24.0

$ # インストールできたことを確認
$ cd ../local/python-3.13
$ ./bin/python3.13 -VV
Python 3.13.0a5 (main, Mar 14 2024, 18:37:25) [GCC 11.4.0]

ベンチマーク検証

GILはCPUバウンドなマルチスレッド処理において実行可能なスレッドが制限されるものです。
したがってマルチスレッド処理を行うスクリプトで実行結果を比較してみます。

検証スクリプトは以下です。

# test_gil.py

from concurrent.futures import ThreadPoolExecutor
import time
import math


def get_primes(max: int) -> list[int]:
  # (あえて低速な) n以下の素数一覧を返す関数
  if max < 2:
    raise ValueError()

  primes = [2]
  for n in range(3, max + 1):
    is_prime = True
    for i in range(2, int(math.sqrt(n)) + 1):
      if n % i == 0:
        is_prime = False
        break

    if is_prime:
      primes.append(n)

  return primes


if __name__ == "__main__":
  print("concurrency,time")
  for concurrency in range(1, 10 + 1):

    start = time.monotonic()

    with ThreadPoolExecutor(max_workers=concurrency) as executor:
      futures = [executor.submit(get_primes, 500000) for _ in range(concurrency)]

    for f in futures:
      f.result()

    end = time.monotonic()
    duration = end - start

    print(f"{concurrency},{duration:.2f}")
  • 「CPUバウンド処理」として素数判定をメインにした関数をThreadPoolExecutorでマルチスレッド処理する
    • ※引数maxは筆者の環境で1、2秒程度かかる値を選択
  • concurrencyで指定されたスレッド数分だけ get_primes を並列に起動する
  • concurrencyを1から10まで変化させて所要時間を計測する
    • ※それぞれ3回ずつ実行し、平均時間を取得

ちなみに、上記でビルドしたv3.13.0a5において実際の処理でGILを無効にするには環境変数PYTHON_GIL=0とともに実行する必要があります。

$ PYTHON_GIL=0 ./bin/python3.13 test_gil.py

結果

比較対象は以下の通りです。

  1. v3.12.2: 執筆時点の最新リリースバージョン
  2. v3.13.0a5: "--disable-gil" オプション無しでビルドしたランタイム
  3. v3.13.0a5 & --disable-gil: "--disable-gil" オプション付きでビルドかつ "PYTHON_GIL=0" 無しで実行
  4. v3.13.0a5 & --disable-gil & PYTHON_GIL=0: GILを無効化して実行

(単位: 秒)

cocurrency v.3.12.2 v3.13.0a5 v3.13.0a5 & --disable-gil v3.13.0a5 & --disable-gil & PYTHON_GIL=0
1 1.12 1.01 1.47 1.46
2 2.29 2.07 3.00 1.56
3 3.46 3.13 4.56 1.74
4 4.62 4.17 6.02 1.78
5 5.74 5.22 7.37 1.98
6 6.91 6.35 8.82 2.06
7 8.08 7.40 10.29 2.23
8 9.22 8.42 11.84 2.40
9 10.38 9.47 13.26 2.56
10 11.53 10.52 14.80 2.71

GIL無効化(--disable-gilオプションでビルトかつPYTHON_GIL=0)の場合のみ所要時間が並列度に単純比例せず、期待した結果となりました。

また、GILが有効なv3.12.2v3.13.0a5では単純に約10%程度高速になりました。 バージョンアップに伴って性能が改善されるのは嬉しいですね。
(6/14 訂正) 上記の3.12.2 と 3.13.0a5 の比較は正確なものではありませんでした。
筆者の環境で用意したランタイムが同じ条件でビルドされたものではありませんでした。

一方非マルチスレッド処理(concurrency=1)においては性能が悪化しました。
上記の検証スクリプトの場合、データ構造の安全性に関するオーバーヘッドの影響が考えられます。

コレクション - python.jp

PythonでGILの排除が難しい理由として、参照カウントの存在に加えて、Pythonインタープリタが辞書やリストなどの複雑なコレクションに依存している、という点もよく挙げられます。

残念ながら、手放しに喜べる検証結果とはなりませんでした。
今後のベータ版やrc版でも引き続き検証してみたいです。

(6/14追記) 追加検証: 3.13.0b2

2024/6/14 時点で最新バージョンである 3.13.0b2 を使って追加の検証を行いました。
また検証環境と手順を見直しました。

検証環境

専用環境として AWS Amazon EC2 インスタンス (仮想マシン) を用意しました。

  • リージョン: us-west-2
  • OS: Debian 12 (ami-0c2644caf041bb6de)
  • インスタンスタイプ: c7a.2xlarge (8vCPU / 16GB)

手順

Docker Official Image のランタイムと同じ手順を再現しました。
python/3.13-rc/bookworm/Dockerfile at 748d6e9b44c0ee63e766a7c601d471e0763383d6 · docker-library/python

上記のベースイメージを辿っていくと必要パッケージのインストール、ビルド手順は以下の通りとなります。

検証手順

# 1. Prepare Build dependencies

## https://github.com/docker-library/buildpack-deps/blob/d0ecd4b7313e9bc6b00d9a4fe62ad5787bc197ae/debian/bookworm/curl/Dockerfile
sudo apt-get install -y --no-install-recommends \
  ca-certificates \
  curl \
  gnupg \
  netbase \
  sq \
  wget

## https://github.com/docker-library/buildpack-deps/blob/d0ecd4b7313e9bc6b00d9a4fe62ad5787bc197ae/debian/bookworm/scm/Dockerfile
sudo apt-get install -y --no-install-recommends \
  git \
  mercurial \
  openssh-client \
  subversion \
  procps

## https://github.com/docker-library/buildpack-deps/blob/d0ecd4b7313e9bc6b00d9a4fe62ad5787bc197ae/debian/bookworm/Dockerfile
sudo apt-get install -y --no-install-recommends \
  autoconf \
  automake \
  bzip2 \
  default-libmysqlclient-dev \
  dpkg-dev \
  file \
  g++ \
  gcc \
  imagemagick \
  libbz2-dev \
  libc6-dev \
  libcurl4-openssl-dev \
  libdb-dev \
  libevent-dev \
  libffi-dev \
  libgdbm-dev \
  libglib2.0-dev \
  libgmp-dev \
  libjpeg-dev \
  libkrb5-dev \
  liblzma-dev \
  libmagickcore-dev \
  libmagickwand-dev \
  libmaxminddb-dev \
  libncurses5-dev \
  libncursesw5-dev \
  libpng-dev \
  libpq-dev \
  libreadline-dev \
  libsqlite3-dev \
  libssl-dev \
  libtool \
  libwebp-dev \
  libxml2-dev \
  libxslt-dev \
  libyaml-dev \
  make \
  patch \
  unzip \
  xz-utils \
  zlib1g-dev

## https://github.com/docker-library/python/blob/748d6e9b44c0ee63e766a7c601d471e0763383d6/3.13-rc/bookworm/Dockerfile
sudo apt-get install -y --no-install-recommends \
  libbluetooth-dev \
  tk-dev \
  uuid-dev

# 2. Build / Install
mkdir sandbox
cd sandbox/

## https://github.com/docker-library/python/blob/748d6e9b44c0ee63e766a7c601d471e0763383d6/3.13-rc/bookworm/Dockerfile#L22-L87
## and `--disable-gil`
export GPG_KEY=7169605F62C751356D054A26A821E680E5FA6305 && \
export PYTHON_VERSION=3.13.0b2 && \
wget -O python.tar.xz "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz" && \
wget -O python.tar.xz.asc "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc" && \
GNUPGHOME="$(mktemp -d)"; export GNUPGHOME && \
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$GPG_KEY" && \
gpg --batch --verify python.tar.xz.asc python.tar.xz && \
gpgconf --kill all && \
rm -rf "$GNUPGHOME" python.tar.xz.asc && \
mkdir -p ./python && \
tar --extract --directory ./python --strip-components=1 --file python.tar.xz && \
rm python.tar.xz && \
cd ./python && \
gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" && \
./configure \
  --build="$gnuArch" \
  --disable-gil \
  --enable-loadable-sqlite-extensions \
  --enable-optimizations \
  --enable-option-checking=fatal \
  --enable-shared \
  --with-lto \
  --with-system-expat \
  --without-ensurepip \
 && \
nproc="$(nproc)" && \
EXTRA_CFLAGS="$(dpkg-buildflags --get CFLAGS)" && \
LDFLAGS="$(dpkg-buildflags --get LDFLAGS)" && \
make -j "$nproc" \
  "EXTRA_CFLAGS=${EXTRA_CFLAGS:-}" \
  "LDFLAGS=${LDFLAGS:-}" \
  "PROFILE_TASK=${PROFILE_TASK:-}" \
 && \
rm python && \
make -j "$nproc" \
  "EXTRA_CFLAGS=${EXTRA_CFLAGS:-}" \
  "LDFLAGS=${LDFLAGS:--Wl},-rpath='\$\$ORIGIN/../lib'" \
  "PROFILE_TASK=${PROFILE_TASK:-}" \
  python \
 && \
make install && \
cd ../ && ./local/python3.13 && ./local/python-3.13/bin/python3 -VV

結果

以下の4通りで比較しました。

  1. Python 3.12.4
    docker run python:3.12.4 python -c "$(< test_gil.py)"
  2. Python 3.13.0b2
    docker run python:3.13.0b2 python -c "$(< test_gil.py)"
  3. Python 3.13.0b2 (GIL diabled): 環境変数 PYTHON_GIL=1
  4. Python 3.13.0b2 (GIL diabled): 環境変数 PYTHON_GIL=0

リリース版である 3.12.4 および「GIL 有効化の 3.13.0b2」はDocker コンテナで実行しました (ランタイムの条件をなるべく揃えるため)。
また実行したスクリプト test_gil.py は前述のものとほぼ同じですが、最大並列度はコア数の2倍 (= 16) まで上げてみました。

cocurrency v.3.12.4 v3.13.0b2 v3.13.0b2 & --disable-gil & PYTHON_GIL=1 v3.13.0b2 & --disable-gil & PYTHON_GIL=0
1 0.96 0.88 1.16 1.13
2 1.55 1.72 2.17 1.13
3 2.33 2.57 3.23 1.13
4 3.10 3.43 4.27 1.14
5 3.87 4.28 5.31 1.14
6 4.72 5.14 6.36 1.15
7 5.45 6.01 7.41 1.16
8 6.26 6.87 8.44 1.19
9 7.06 7.73 9.52 1.47
10 7.90 8.58 10.65 1.87
11 8.61 9.43 11.65 1.84
12 9.43 10.29 12.67 1.93
13 10.28 11.13 13.85 1.93
14 11.16 11.99 14.90 2.13
15 11.97 12.86 15.91 2.22
16 12.68 13.70 17.13 2.36

概ね同様の結果になりました。
GIL 無効化の場合では CPU コア数 (= 8) を超えたあたりで所要時間のベースラインが上がっていることが分かります。

また、今回の結果では僅かですが 3.12.4 が 3.13.0b2 の結果を上回りました。

まとめ

Python3.13の開発バージョンを用いてのGILの解消を確認しました。

プロセスあたりのマルチスレッド処理の性能向上が期待できますね。

GILの解消を提案したPEP 703によるとターゲットバージョンはPython3.13であり、順調に開発が進めば2024年10月にリリースされることになりそうです。

参考