アイルランドで開催 ―EuroPython 2022レポート

#02Faster CPythonやGILの除去によるPythonの高速化 ―EuroPython Day 2、Day 3セッションレポート

EuroPythonはヨーロッパ地域で開催されるPythonに関するカンファレンスです。EuroPython 2022レポートの第2回目は、カンファレンス2、3日目の様子を中心にお伝えします。

カンファレンス2日目はOrganisers' Lunch、日本から参加したwhitphxさんのトークや、Social Eventという名前のパーティーなどがありました。

Writing Faster Python 3

スピーカーのSebastian氏は2019年にPyCon JPでも発表していたので、見かけたことがある人もいると思います。氏のWebサイトswitowski.comのトップページの画像はPyCon JPでの発表時のものが使われています。

Sebastian氏の発表
Sebastian氏の発表

最初に「Pythonは実行速度の最適化はされておらず、開発速度のために最適化されてきた」という言葉を紹介しました。Pythonは動的言語なため、aという変数の中身が文字列かも知れませんし、数値、リストまたはDataFrameかも知れません。

Python高速化のアプローチ

そしてPythonを早くするためのアプローチとしていくつかの手法が紹介されました。当然ですが、より速いハードウェアで実行すればPythonの実行速度は速くなります。

別のアプローチとして、CPythonとは別のインタープリターとして、PyPyPystonPyjionCinderGraalPythonが紹介されました。PyjionとCinderは筆者が初めて聞く名前でした。さまざまな高速化されたPython実装が開発されているようです。

NumPyにNumbaを組み合わせると、大量データ処理などをより速く実行してくれます。

また、Pythonのバージョンを更新すると高速化が見込めます。Python 3.9から3.10に変えるだけでも数パーセントの高速化が見込めるそうです。Python 2系やPython 3.6などからはバージョンアップは大変ですが、最新のPythonにすることで高速化できます

そして最後に「よりよいアルゴリズムやデータ構造」というアプローチが紹介されました。

よりよいアルゴリズムやデータ構造

以下のような「0から999,999までの2乗の合計を出力する」プログラムを例に説明が進みます。このコードはSebastian氏の環境では実行に72ミリ秒かかります。

total = 0
def compute_sum_of_powers():
    global total
    for x in range(1_000_000):
        total = total + x*x

compute_sum_of_powers()
print(total)

次にグローバル変数をローカル変数に変えると63ミリ秒に短縮します。これは変数をグローバルから検索するために前のコードの方が遅くなっています。

def compute_sum_of_powers():
    total = 0
    for x in range(1_000_000):
        total = total + x*x
    return total

total = compute_sum_of_powers()
print(total)

次に全体を加算する部分をsum()関数で行い、リスト内包表記で2乗のリストを作成します。処理時間は59ミリ秒になりました。

def compute_sum_of_powers():
    return sum([n * n for n in range(1_000_000)])

total = compute_sum_of_powers()
print(total)

リスト内包表記をジェネレーター式に変更します。ジェネレーター式はリストを一度に作成しないため使用するメモリは非常に少ないですが、処理速度は63ミリ秒とリスト内包表記より遅くなります。

def compute_sum_of_powers():
    return sum(n * n for n in range(1_000_000))

total = compute_sum_of_powers()
print(total)

ここでさきほど紹介したnumbaを導入します。pip install numbaでインストールしてimportし、関数に@jitデコレーターを付けます。これだけで実行時間が34ミリ秒に短縮されます。

当然ですが、numbaのJITがうまく効く処理もあれば、そうでない処理もあるので注意が必要です。

from numba import jit  # pip install numba

@jit
def compute_sum_of_powers():
    total = 0
    for x in range(1_000_000):
        total = total + x*x
    return total

total = compute_sum_of_powers()
print(total)

次に、このコードでは整数しか扱っていないのでNumPyを使用します。NumPyの関数やデータ型を使用すると57ミリ秒となりました。ただし、実行時間の大半はNumPyのimportにかかっており、計算処理自体は10ミリ秒とかなり高速です。

import numpy

def compute_sum_of_powers():
    numbers = numpy.arange(1_000_000)
    powers = numpy.power(numbers, 2)
    return numpy.sum(powers)

total = compute_sum_of_powers()
print(total)

このようにして、72ミリ秒の処理を10ミリ秒にまで短縮することができました。

このあともコード例を示しながら、同じ目的でどう書く方が速いのかという説明がされました。紹介されたのは以下のような例です。

  • 権限の事前チェックとtry/exceptではどちらが速いか
  • コレクションの中から要素を探す
  • リストをフィルタリングする場合filter()よりリスト内包表記が速い
  • リストに要素が存在するか調べる場合setが速いが、setへの変換処理は時間がかかる
  • 空の辞書生成のdict(){}では{}の方が速い
  • リストから重複の削除はset()がとても速いが順番が変わる

最後にここまでに挙げたコードをさまざまなバージョンのPythonで実行してみると、ほとんどのコードでPython 3.11.0b3がもっとも速い結果を出していました。これはFaster CPythonの成果であるという説明がありました。Faster CPythonは3.10から3.13までで約50%CPythonを速くするというプロジェクトです(Day 1のCPythonパネルに参加していたMark Shannon氏がリードしています⁠⁠。

さらなるサンプルコードなどが、GitHubやBlogで公開されていることを紹介していました。詳細が気になる方は、ぜひ参照してみてください。

最後に「少しのパフォーマンスのためにコードの読みやすさを失わないでように」というコメントがありました。

How we are making Python 3.11 faster

スピーカーのMark Shannon氏は、Day 1のCPython Developer Panelにも登壇していたCPythonのコアデベロッパーです。Marc氏はPyCon US 2021で発表されたFaster CPythonの提唱者であり、現在はCPythonの高速化チームの技術リーダーとしてGuido氏らとともに開発を進めています。

ちなみに、Mark氏は10月に開催されるPyCon JP 2022のキーノートスピーカーでもあります。日本でCPythonの最前線の話が聞けそうで、非常に楽しみです。

Mark Shannon氏の発表
Mark Shannon氏の発表

このトークでは、どのようにしてPython 3.11を高速化しているのかについて、内部構造なども交えて開発の中心人物から語られました。

最初に、CPythonはCで書かれたコンピュータープログラムであるため、高速化をするにはコンピューターがどのように動作しているかを理解する必要があると述べられました。筆者も理解が追いついていないですが、CPUはスーパースカラーというアーキテクチャーにより、複数の命令を同時に取り込んで並列で実行しているそうです。

また、メモリアクセスも重要で、CPUからメモリにアクセスする速度は物理的な制約があるため、CPUのキャッシュメモリを搭載して効率化しているとの説明がありました。

データ構造

ここでは、CPythonを高速化するためにデータ構造をどのように変更したかについて解説されました。

最初の例はリスト型です。リストをリンクリストで実装すると、毎回メモリを確保することと、データを探すためにリンクを辿らないといけないため効率がわるくなります。そこで、Python 3.11ではあらかじめメモリを確保したスタック上に書き込むことと、最後に追加したデータを参照することが多いため、効率が良くなったそうです。

また、例外発生時に使用するframe objectをあとから生成する(Lazy creating frame object)ようにしたそうです。これは最適化の一般的な考え方の「通常の処理は速くなるように設計するため、一般的でないケース(例外発生時など)は少し遅くなる場合がある」に合致しています。

Python 3.10以前のリスト(発表スライドより)
Python 3.10以前のリスト(発表スライドより)
Python 3.11のリスト(発表スライドより)
Python 3.11のリスト(発表スライドより)

もう1つは以下のような一般的なPythonのオブジェクトについてです。

class C:
    def __init__(self, a, b):
        self.a = a
        self.b = b

Pythonのオブジェクトは__dict__を持っており、この中にオブジェクトの属性を持っています。しかし通常のコードでは__dict__には直接アクセスしません。

Python 3.10以前のオブジェクトは以下のような形式です。オブジェクトの属性にアクセスするには、class__dict__dictionaryvaluesとアクセスする必要があり効率が悪いです。

Python 3.10以前のオブジェクト(発表スライドより)
Python 3.10以前のオブジェクト(発表スライドより)

Python 3.11では以下のように変更されました。__dict__を先頭に移動しました。こうすることでclassdict_offsetを参照する必要がなくなりました。

Python 3.11のオブジェクト(__dict__あり:発表スライドより)
Python 3.11のオブジェクト(__dict__あり:発表スライドより)

そして、Python 3.11のオブジェクトはvaluesにアクセスできるため、作成したばかりのオブジェクトでは__dict__は存在しないように改良されたそうです。このようにPython 3.11ではオブジェクトがコンパクトになりました。

Python 3.11のオブジェクト(デフォルト:発表スライドより)
Python 3.11のオブジェクト(デフォルト:発表スライドより)

アルゴリズム

アルゴリズムの高速化として、PEP 659で提案されPython 3.11に採用された「Specializing Adaptive Interpreter」が紹介されました。この機能は、バイトコードの中の命令が以下の2つのいずれかの状態を持つというものです。

  • General:ウォームアップカウンターを持つ。カウンターが0になるとSpecialized(最適化される)になる
  • Specialized:ミスカウンターを持つ。カウンターが0になると最適化が外れる

Specialized(最適化)がどういうことかというと、一般的な命令のバイトコードが特殊なものに置き換わります。例として、BINARY_OPBINARY_OP_ADD_INTになったりします。

たとえば、+演算子BINARY_OPの場合、左右が数値同士、文字列同士、リスト同士などで動作します。プログラム中で、連続して整数の加算のみが行われている場合は、今後の整数の加算が連続して使用されると見なして、特殊なバイトコードBINARY_OP_ADD_INTを使用して処理を高速化するということです。この「連続して実行されているか」をカウンターで確認しています。

Specializing Adaptive Interpreterについては、以下の資料も参考にしてください。

最後に、3.12で予定している高速化のための施策を紹介して、トークは終わりました。

筆者にとっては難しい内容でしたが、図なども交えてていねいにCPythonの高速化について説明してくれたトークでした。細かい地道な努力の積み重ねでCPythonが徐々に高速化していくのだなと感じました。

Mark Shannon氏と筆者
Mark Shannon氏と筆者

Organisers' Lunch

カンファレンス2日目のランチはOrganisers' Lunchがあったので参加してきました。このイベントは、各国でPython関連のイベントやコミュニティを運営しているメンバーで、一緒にランチをとりながら交流しましょう、という企画のようです。筆者はPyCon JP Associationの副代表理事を務めていることもあり、イクバルさんにも声をかけて、Organisers' Lunchに参加しました。

ランチ会場に行ってみると、発表会場前のホワイエ的なところに丸テーブルがいくつかセッティングされていました。

Organisers' Lunchの様子
Organisers' Lunchの様子

筆者はイクバルさんと同じテーブルに座りました。同じテーブルには、たびたびこのレポートにも出てくるMarc氏、現在のEuroPython SocietyのChairであるRaquel氏などがいました。

筆者の右隣に座った3名はKiwi.comという会社のメンバーで、10月にバルセロナでPython Weekendという3日間のイベントを無料で開催するという話をしてくれました。それに対して「日本でも似たアイデアで、Python Boot Campという半日のチュートリアルイベントを各地でやっています。」という情報を共有しました。Python Boot Campのテキストを英語に機械翻訳したものをスマートフォン見せたところ興味深く見ており、URLなどの情報を共有しました。

左隣に座っているGajendra Deshpande氏@gcdeshpandeはインドからの参加者で、10月のPyCon JP 2022でも発表予定だそうです。DjangoCongress JP 2021でもリモートで発表していたそうで、名前を見たことがある方もいるのではなでいしょうか。⁠日本でまた会いましょう」と約束して、連絡先を交換しました。ちなみに連絡先の交換はLinkedInが一般的な感じでした(以前はFacebookが主流だったイメージです⁠⁠。

筆者のいたテーブル
筆者のいたテーブル

Kiwi.comのVolodymyr Piskun氏@VolodymyrPがとてもかわいいTシャツを着ていたので、写真を撮らせてもらいました。本人もお気に入りのTシャツらしく「これは漢字でなんて書いてあるの?」と質問されたので「Ninja Catと書いてあるよ(そのままですが⁠⁠」と教えてあげました。ちなみに、次の日Volodymyr氏は「ニシキゴイ対猫」Tシャツを着てきました。いったいどこでそんなTシャツを見つけてきたんでしょう?

忍者猫Tシャツがお気に入りのVolodymyr氏
忍者猫Tシャツがお気に入りのVolodymyr氏

ライトニングトーク

2日目も最後にライトニングトークがありました。いくつか紹介します。

ライトニングトーク
ライトニングトーク
  • EuroPython Society <3: 昨日のライトニングトークにも登場したVB氏による「EuroPythonのボランティアになってね」というトーク
  • Hugging Face Gradio Hackathonの紹介
  • Django-reversion:Djangoのモデルインスタンスのバージョン管理をする拡張の紹介
  • EuroSciPy 2022というカンファレンスの紹介
  • Reverse-Engineering Keynote: MacのKeynoteファイルをリバースエンジニアリングするトーク。keynote-parserというライブラリを紹介
  • latest.cat:ソフトウェアの最新バージョンを取得するコマンドの紹介

Day 3 ―ライトニングトークにサインアップ

ここからはカンファレンス3日目の様子をお伝えします。

前日はSocial Eventでビールをけっこう飲みましたが、朝早く起きることができました。この日はカンファレンス後にそのまま移動だったので、ホテルで荷物をまとめて会場に来ました。

9時からオープニングですが、朝8時前には会場に到着しました。筆者が早起きして会場に来た理由は、ライトニングトークにサインアップするためです。EuroPythonのライトニングトークは朝にボードが貼り出され、早いもの勝ちで埋まっていきます。無事、4番目に自分の発表タイトルを書き込むことができました。

ライトニングトークのサインアップボード
ライトニングトークのサインアップボード

Keynote 3:Multithreaded Python without the GIL

カンファレンス最後のキーノートです。キーノートスピーカーを紹介するためにLarry Hastings氏が壇上に上りました。

Larry氏はPython 3.4と3.5のリリースマネージャーでもあるPythonのコア開発者で、自身もPythonからGIL(Global Interpreter Lock)を取り除くことに以前チャレンジしたそうです。そんなLarry氏が現在GILを取り除くことにチャレンジしているSam氏を紹介するところからキーノートが開始しました。

Larry Hasting氏(左)とSam Gross氏
Larry Hasting氏(左)とSam Gross氏

このトークではSam氏が過去数年取り組んでいるプロジェクト、PythonのマルチスレッドプログラムをGILなしで実行させること、について語られます。

まず前提として、プログラムをマルチスレッド化すると、マルチプロセッサやマルチコアコンピューターで並列の動作して効率的です。シングルプロセッサでもスレッドによって並行処理ができます。このプロジェクトのゴールは、Pythonでマルチスレッドのコードをマルチコアコンピューターで効率的に動作させることですが、その障害の1つがGILです。GILによって、一度に1つのスレッドのみがPythonコードを実行することが保証されます。

1991年にPython 0.9.9がリリースされたときにはGILは存在せず、1992年にスレッドのサポートと同時にGILが導入されました。GILは実装が簡単で、シングルスレッドでは効率的に動作します。また、マルチスレッドでデッドロックを回避するため、理にかなっているとのことです。

Pythonのリリースから30年が経ち、マルチコアのプロセッサーはどこにでもあるようになりました。しかし、Pythonで並行処理にはGILによって制限があります。制限を回避するために、たとえばscikit-learnなどでは内部でOpenMPを使用しています。他にはマルチプロセスで実行する方法もありますが、プロセス間の通信が問題となります。

GILを取り除くには以下のような対応が必要です。

  • オブジェクトの参照カウンター
  • リスト、辞書などのコレクションをスレッドセーフにする
  • メモリアロケーター
  • ガーベッジコレクター
  • シングルスレッドでのパフォーマンスを維持しながら、効率的にマルチスレッドでスケールする

GILの削除には過去3回チャレンジしたことがあるそうです。"free threading" patchの一部は現在のPythonにも取り込まれているそうです。また、GilectomyがもっともSam氏のプロジェクトに影響を与えているそうです。

  • 1996:"free threading" patch (Greg Stein)
  • 2007:python-safethread (Adam Olsen)
  • 2016:Gilectomy (Larry Hastings)

参照カウントの改善

参照カウントをスレッドセーフにするもっとも簡単な実装方法として、アトミック操作にしてみます。pyperformanceでベンチマークしてみると、平均60%遅くなるそうです。そこで2018年に発表されたBiased reference countingというアルゴリズムを採用したところ、10%遅くなる程度となったそうです。

次にImmortal objects(不滅オブジェクト)を導入しました。これはTrueFalseNone01といったオブジェクトは常に存在するオブジェクトにして、参照カウントの対象外とする物です。この状態でフィボナッチ関数をマルチスレッドで実行してみたところ、8CPUでのスループットはシングルスレッドの25%となったそうです(本当は8倍になってほしい⁠⁠。

この原因は関数の参照カウント処理の問題であり、この問題を解決するためにDeferred reference counting(遅延参照カウント)を導入しました。これにより、8CPUでほぼ8倍のスループットが実現されました。

Biased reference counting、Immortal objects、Deferred reference countingの3つを使い分けることで、マルチスレッドでの参照カウントのパフォーマンスが改善しました。

コレクションのスレッドセーフ対応

次に、辞書、リスト、セットなどのコレクションに並行でアクセスするために、Lightweight locks(軽量ロック)を導入します。これは、WebKitのWTF::Lockが元になっているそうです。コレクションにこれらの軽量ロックを導入し、スレッドセーフに値を書き換えることができるようになります。また、以下のような処理はこのロックの対象外となります。

value = dict["key"]
value = object.attr
item = list[idx]

メモリーアロケーターとガーベッジコレクター

Pythonのpymallocはスレッドセーフではありません。代わりにスレッドセーフなmimalocを使います。

また、ガーベッジコレクターはグローバルな二重のリンクリストでオブジェクトを追跡するため、スレッドセーフではありません。mimallocを使用することでこの問題も解決します。

まとめ

このプロジェクトの成果物はhttps://github.com/colesbury/nogilで公開されており、ぜひインストールして使ってみてほしいとのことです。pyenvを使っている場合はpyenv install nogil-3.9.10で簡単にインストールできます。また、この内容を元にPythonからGILを取り除くPEP(Python Enhancement Proposal、Pythonの拡張提案)を書く予定とのことです。

とても難しい内容でしたが、数年後にGILを取り除いたPythonが普通に使えるようになり、Pythonのマルチスレッドプログラムがマルチコアで効率的に動くことに期待したいと思います。

筆者のライトニングトーク:Spread the community after COVID-19 in Japan

ライトニングトークのサインアップに成功したので、発表資料の準備に時間を使い、トークはほとんど聞きませんでした。ライトニングトークは「Spread the community after COVID-19 in Japan」と題して、PyCon JP Associationとしてコロナ前後でどのようにPythonコミュニティを広げる活動をしたか、というものです。このトークは、PyCon JP Associationの寺田さん@terapyonがPyCon US 2022で発表した内容をベースに、私なりにアレンジしたものです(寺田さんのPyCon USでの発表はPyCon US 2022のYouTubeで参照できます⁠⁠。

ライトニングトーク中の筆者
ライトニングトーク中の筆者

トークの中では、コロナ禍となりPython Boot Campのようなリアルイベントができなくなったこと、その代わりにPython Charity TalksPyCon JP TVといったリモートでの活動にチャレンジしていることを紹介しました。

このPyCon JP TVのところで「EuroPython 2022をPyCon JP TVで紹介するので、そのためにみなさんとセルフィーを撮ります!」と宣言して、舞台上で記念撮影をしました。我ながら「無謀なことをやるな」と思いながらも、チャレンジしました。下の写真をよく見ると手前の人もピースサインを出してくれています。

舞台上から記念撮影
舞台上から記念撮影

撮影した画像をトーク終了後に確認してみると、たくさんの人が手を上げたり笑ったりしてくれていて、とてもいい写真が撮れたなと思いました。トークの後にこの画像をツイートしました。ぜひ拡大画像で確認してみてください。

持ち時間5分の中にネタを入れまくったトークでしたが、なんとか時間ギリギリで終了しました。伝えたいことは伝わり、ウケてほしいところはウケていたような気がするので、自分としては「やりきった!」と感じました。

クロージング

カンファレンスの最後クロージングです。Day 1のオープニングと同様にRaquel氏の進行で進みます。会場は、2年振りのオンサイトカンファレンスが無事終了できた喜びに満ちていました。参加者数は現地参加1194名、オンライン143名とのことです。また、参加者の男女比、国別比較なども報告されました。他にはトークのプロポーザルの数や、遠方支援者の数などについても述べられました。最後にEuroPython 2022を支えたオンサイトとオンラインのボランティアが紹介され、2023年のスタッフとして参加することを呼びかけていました。2023年の開催場所は未定のようです。

EuroPython 2022ボランティアのみなさん
EuroPython 2022ボランティアのみなさん

クロージングの一番最後に「ノートPCを持っている人はpipx run saghettiを実行してみて」というコメントがありました。なにが起こるかはぜひみなさんの手元で確認してみてください。

まとめ

久しぶりの現地開催での海外カンファレンス「EuroPython 2022」のレポートは以上で終了です。筆者にとってとても楽しく、刺激的な3日間でした。

筆者がPyCon JP Associationとして行っているYouTubeライブ、PyCon JP TVでもEuroPython 2022について紹介しました。よりたくさんの写真とともに現地の様子を伝えているので、もしよかったらそちらもご覧になってください。

また、オフィシャルの写真はFlickrの以下のURLで共有されています。筆者が、会場内のソファに寝っ転がりながら、ライトニングトークの資料作成をしている写真もあります。ぜひ、探してみてください。

カンファンレンス自体はこのあとの土日に開発スプリントが開催されていましたが、筆者はDay 3のライトニングトークを終えると飛行機でデンマークのビルンへと移動しました。ビルンではLEGO HouseLEGOLANDを堪能したのですが、それはまた別のお話し。

LEGO House / LEGOLAND Billund
EGO House / LEGOLAND Billund

おすすめ記事

記事・ニュース一覧