1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

この記事はZennに投稿した内容をアドベントカレンダー用に転記したものです。

概要

Pythonプログラムの性能を向上させるための手段として並行化があります。

並行化の手法は次の3つに大別されます。

  • マルチスレッド
  • マルチプロセス
  • マルチインタープリター(with マルチスレッド)

これらをひとつずつ学び、実際に試すことで理解を深めようと思います。

並行化の手法

マルチスレッド

マルチスレッドとは単一プロセス内で複数のスレッドを立てて並行化することです。
私のプログラマーとしての経験はJavaがベースにあるため、マルチスレッドと聞くとCPUバウンドな処理の性能向上に役立つと考えてしまいますが、Pythonでは事情が異なります。

PythonはGIL(Global Interpreter Lock)という仕組みがあり、これは「一度にPythonのバイトコードを実行するスレッドはひとつだけであることを保証する」というものです。
このため、単純にマルチスレッド化してもCPUバウンドな処理の性能向上には繋がりません。

ではマルチスレッドは性能向上の文脈で役立たないのかというと、そうではありません。
あるスレッドがIOでブロックされているときは別のスレッドに切り替わって処理が進むため、IOバウンドな処理の性能向上に役立ちます。

ファイル操作の非同期IOサポートを提供しているaiofilesというOSSも、ファイル操作を別スレッドに委譲しており、それをasync/awaitで透過的に扱えるようにしてあります。
これはマルチスレッドでIOバウンドな処理の性能向上を図っている好例です。

とはいえ理想は標準でファイルIOの真の非同期APIが提供されることです。

マルチプロセス

マルチプロセスとは主となるプロセスから複数のサブプロセスを立てて並行化することです。
前述のGILによる制限が問題にならず、CPUバウンドな処理の性能向上に役立ちます。

マルチインタープリター(with マルチスレッド)

マルチインタープリターとは単一プロセス内で複数のインタープリターを立てることです。

Python 3.13までは複数のインタープリターを実行するにはC言語のAPIを使用する必要があったようですが、Python 3.14からはPythonのAPIでもそれが可能になったとのことです。

GILはインタープリターに対するロックなので、マルチスレッドを併用してスレッド毎に異なるインタープリターを割り当てればGILによる制限を問題とせず、CPUバウンドな処理の性能向上に役立ちます。

単一スレッドで複数インタープリターを扱う用途は本記事のスコープ外とし、マルチインタープリターを語る際はマルチスレッドとの併用を前提としています。

ここまでのまとめ

並行化の手法ごとにCPUバウンドな処理およびIOバウンドな処理の性能向上に役立つかどうかを表にしました。

並行化の手法 CPUバウンド IOバウンド
マルチスレッド ×
マルチプロセス
マルチインタープリター(with マルチスレッド)

なお、マルチプロセスの項では特に言及していませんでしたが特定のプロセスがIOでブロックされているときでも別のプロセスは処理を進められるため、IOバウンドな処理の性能向上にも役立つと言えます。

実験

CPUバウンドな処理の性能向上

ある整数の範囲内にいくつ素数が含まれているか算出するプログラムを用意しました。

def is_prime(n: int) -> bool:
    if n < 2:
        return False
    if n % 2 == 0:
        return n == 2
    limit = int(math.sqrt(n)) + 1
    for i in range(3, limit, 2):
        if n % i == 0:
            return False
    return True


def count_primes_range(args: tuple[int, int]) -> int:
    start, end = args
    count = 0
    for n in range(start, end):
        if is_prime(n):
            count += 1
    return count

このプログラムを各手法で並行に動かして実行時間を計測します。

条件は次の通りです。

  • 整数の範囲: 0以上、10,000,000未満
  • 並行度: 8
  • 計測回数: 5回

並行度はマルチスレッドにおいてはスレッド数、マルチプロセスにおいてはプロセス数、マルチインタープリター(with マルチスレッド)においてはインタープリター数(各インタープリターに1スレッドを割り当て)を指します。

計測結果は次の通りです。

並行化の手法 1回目 2回目 3回目 4回目 5回目
デフォルト(※) 14.587秒 14.588秒 14.593秒 14.648秒 14.719秒
マルチスレッド 14.279秒 14.319秒 14.406秒 14.404秒 14.507秒
マルチプロセス 2.673秒 2.652秒 2.654秒 2.654秒 2.664秒
マルチインタープリター
(with マルチスレッド)
2.637秒 2.657秒 2.645秒 2.648秒 2.694秒

※「デフォルト」は並行化していない単一プロセス単一スレッドを指します

前述のようにマルチスレッドだけはGILの制限で顕著な性能向上が見られず、マルチインタープリターとの併用が必要であることが結果にも現れました。

IOバウンドな処理の性能向上

ローカルにDockerで立てたHTTPBinを利用して遅いHTTPリクエストをシミュレートするプログラムを用意しました。

def do_http_request(_: int) -> str:
    url = "http://localhost:8080/delay/3"
    with urlopen(url) as res:
        data = res.read().decode("utf-8")
    return data

このHTTPリクエストを各手法で並行に動かして実行時間を計測します。

条件は次の通りです。

  • 並行度(リクエスト数): 4
  • 1リクエストの遅延時間: 3秒
  • 計測回数: 5回

計測結果は次の通りです。

並行化の手法 1回目 2回目 3回目 4回目 5回目
デフォルト 12.049秒 12.056秒 12.057秒 12.053秒 12.042秒
マルチスレッド 3.027秒 3.028秒 3.030秒 3.031秒 3.033秒
マルチプロセス 3.068秒 3.067秒 3.070秒 3.069秒 3.065秒
マルチインタープリター
(with マルチスレッド)
3.061秒 3.063秒 3.058秒 3.061秒 3.063秒

こちらも前述のようにマルチスレッドだけでも性能向上が見られました。

まとめ

PythonにGILという概念があり、それがためにマルチスレッドだけではCPUバウンドな処理の性能を向上させられないことが理解できました。
マルチスレッドにマルチインタープリターを組み合わせることでCPUバウンドな処理でも性能を向上させられることも理解できました。

マルチインタープリター(with マルチスレッド)とマルチプロセスはどちらもCPUバウンドな処理の性能向上に効果的ですが、それぞれのメリット・デメリットや使い分けに関しては整理できておらず、今後の宿題だと思いました。

また、IOバウンドな処理の性能向上にも言及しましたが、これに関しては前回の記事で言及した非同期IOを使用するのが素直かもしれないと考えています。

実験に使用したコードの全量は次の場所に置いてあります。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?