PEP 703 グローバル・インタプリタ・ロックを除去可能に の解説

GILとは?Pythonの並列実行参照カウントメモリ管理コレクション今後について

先日、Pythonの仕様を決定する Steering CouncilPEP 703 – Making the Global Interpreter Lock Optional in CPython に関するコメントを発表し、大きな話題を呼びました。 最終的にこのPEPがどのようになるのか、まだ結論は出ていませんが、Pythonの将来に大きな影響を与えることになるでしょう。

ここでは、PEP-703がどのようにPythonを改善しようとしているのか、概略を説明します。

.. target:: whatgil

GILとは?

まず、グローバル・インタプリタ・ロック(GIL) とはなんでしょう?

Pythonでは、スレッドを利用して複数の処理を同時に実行できます。次の処理は、100万個の整数の二乗和を計算する関数を、2つのスレッドで同時に実行しています。

In [6]:
from concurrent.futures import ThreadPoolExecutor
def sum_of_squares(n):
    ret = 0
    for i in range(n):
        ret += i**2
    return ret

import time
f = time.time()
with ThreadPoolExecutor() as e:
    e.submit(sum_of_squares, 1_000_000)
    e.submit(sum_of_squares, 1_000_000)

print("実行時間:", time.time()-f, "秒")
実行時間: 0.5871181488037109 秒

このマシンでは、0.59秒ほどで処理が終了しました。

さて、ここで質問です。

この関数を、スレッドを使わずに2回連続で実行すると何秒かかるでしょう?

実際にやってみると、

In [8]:
import time
f = time.time()
sum_of_squares(1_000_000)
sum_of_squares(1_000_000)
print("実行時間:", time.time()-f, "秒")
実行時間: 0.57623291015625 秒

こちらは0.58秒でした。スレッドを使って複数の処理を同時に実行した場合と比べて、誤差程度の違いしかありません。スレッドを使って同時に実行した場合には、半分程度の時間で終わるはずではないでしょうか?

実は、Pythonのプログラムを複数のスレッドで実行しても、複数の処理が同時に実行されることはありません。Pythonプログラムを実行するスレッドは常に一つだけで、決まった期間だけ実行すると、次のスレッドに実行権限を渡します。あるスレッドが実行中は、その他のスレッドではなにもせず、実行の順番が回ってくるのを待っています。

このため、スレッドを使っても使わなくても、トータルの実行時間は変わらない、ということになってしまいます。

Pythonは複数のスレッドで同時に処理が実行されないように制御していますが、この制御に使う仕組みを 「グローバル・インタプリタ・ロック(GIL)」 と呼びます。

スレッドは無意味?

それでは、Pythonのスレッドは処理時間の改善の役には立たないのでしょうか?

GILで同時実行が禁止されるのは、主にPython言語で書かれた処理です。Numpy による行列演算などはC言語などで書かれており、GILによる制限を受けずに、同時に実行できます。

次の例では、Pythonで書かれたsum_of_squares() と、C言語で書かれた行列同士の加算処理を行う add_matrix() を実行しています。

In [42]:
import time
import numpy as np


def sum_of_squares(n):
    ret = 0
    for i in range(n):
        ret += i**2
    return ret

def add_matrix(m):
    m+m


m = np.random.rand(10000,10000)
f1 = time.time()
sum_of_squares(1_000_000)
print("sum_of_squaresの実行時間:", time.time()-f1, "秒")

f2 = time.time()
add_matrix(m)
print("add_matrixの実行時間:", time.time()-f2, "秒")

print("実行時間:", time.time()-f1, "秒")
sum_of_squaresの実行時間: 0.2849080562591553 秒
add_matrixの実行時間: 0.4313819408416748 秒
実行時間: 0.7165729999542236 秒

Pythonで書かれた sum_of_squares() が約0.28秒、C言語で書かれた add_matrix() が約0.43秒かかっています。これをスレッドを使って実行してみましょう。

In [38]:
from concurrent.futures import ThreadPoolExecutor
import time
import numpy as np

def sum_of_squares(n):
    ret = 0
    for i in range(n):
        ret += i**2
    return ret

def add_matrix(m):
    m+m

m = np.random.rand(10000,10000)
f = time.time()
with ThreadPoolExecutor() as e:
    e.submit(sum_of_squares, 1_000_000)
    e.submit(add_matrix, m)
print("実行時間:", time.time()-f, "秒")
実行時間: 0.44359302520751953 秒

スレッドで同時実行した場合、トータルの処理時間は 0.72秒から0.44秒と大きく高速化しています。これは、m+m で行われる行列同士の足し算は、GILによる制限を受けずに sum_of_squares() と同時に実行できるためです。

Numpy だけではなく、他にもこのように同時に実行できる処理はたくさんあります。このように、GILの働きはCPythonで効率的な処理を行うために重要な知識です。しかし、GILの制御については仕様として決まっているのではないので、実装のソースコードや経験則に頼るしかなく、習得はなかなか面倒です。

Copyright © 2001-2023 python.jp Privacy Policy python_japan
Amazon.co.jpアソシエイト
Amazonで他のPython書籍を検索