Windows版Python 3.15はなぜ高速化するのか? その技術的根拠を読み解くHPかわさきの研究ノート

2025年12月に突如として「Windows版のPython 3.15は15%の高速化」という話題が出てビックリ、そして喜んだ方もいらっしゃるでしょう。その発火地点であるKen Jin氏のブログ記事を基に高速化できる理由を解き明かしてみました。

» 2026年01月09日 05時00分 公開
[かわさきしんじDeep Insider編集部]
「HPかわさきの研究ノート」のインデックス

連載目次

 先日、『Python 3.15's interpreter for Windows x86-64 should hopefully be 15% faster』というブログ記事が公開されていました。これは「Windows用のPythonインタプリターが15%くらい高速化する(予定だ)よ」という内容の記事です。『HPかわさきの研究ノート』の第1弾では、ちょっとどういうこと? って少しだけ深掘りしてみることにしましょう。


かわさき

 どうもHPかわさきです。

 明けましておめでとうございます。ホントはこの記事、ザックリとご紹介するだけのつもりだったのですが、冬休みの自由研究みたくなってしまったので、研究ノートで紹介することにしました。といっても、WindowsとVisual Studio(!=Visual Studio Code)を用意して実際にビルドしてみよう、なんてことはしていません。

 ここではブログ記事の内容を補足しながら、「へー」と思っていただければ十分です。

 編集部でも「お前はこだわり過ぎている」「そこまでやらんでもいいと思います」などの声があるのですが、つい気になっちゃったんですよん。ごめんなさい。


ざっくりとまとめると?

 元の記事『Python 3.15's interpreter for Windows x86-64 should hopefully be 15% faster』は、PythonのコアチームのメンバーであるKen Jin氏によるものです。そして、これは2025年3月に同氏が公開した『I'm Sorry for Python's tail-calling Interpreter's Results』を受けた記事になっています。

 2025年3月の記事には、次のようなことが書かれていました。

  • Python 3.14では末尾呼び出しと呼ばれる手法を使うことでインタプリターの速度が9%から15%向上すると報告した
  • が、実は比較対象のPythonはClang 19のバグにより性能が低く抑えられていた(ので末尾呼び出し版の速度がものすごく向上したように見えた)
  • 実際の性能向上は3%から5%程度だった
  • ごめんね

 なお、『What's new in Python 3.14』によれば比較対象はx86-64とAArch64アーキテクチャ上でClang 19を用いてビルドされたPython 3.14で、末尾呼び出し手法を使っていないものだそうです。

 そして、今回の記事ではこの謝罪を2つのプラットフォームに関して撤回できることになったそうです。

  • 対象となるプラットフォーム:AArch64なmacOS(XCode Clangを使用)とWindows x86-64(Visual Studio 2026 MSVC 18を使用)
  • macOS環境ではcomputed-gotoと呼ばれる手法を採用しているPythonと比較して5%の性能向上
  • WindowsではMSVCの実験的なバージョンを使うことでswitch-caseでバイトコードの処理をループしているバージョンよりも約15%の性能向上

 「筆者にも間違いはあるので、早めにこのことを発表して、多くの人に検証してもらうことで、バグがあればそれも直せるね」という理由で2025年12月にこのことがブログ記事として発表されたというわけです。

 このキーポイントとなるのが以下の要素です。

  • 末尾呼び出し手法の採用
  • Visual Studio 2026(MSVC 18)での[[msvc::musttail]]属性のサポート

 特に後者はWindows版(X86-64)のPythonインタプリターの速度向上に欠かせない要素となっています(一方、Clangを使ってビルドするのであれば、同様に__attribute__((musttail))という属性がサポートされています)。これらについては後で触れます。

 それから末尾呼び出し手法を採用したことで得られたもう1つの大きなメリットについてもブログ記事では触れられています。これについても最後で触れましょう。

 でも、その前に従来のswitch-caseによるインストラクションの処理から見てみましょう。

古いやり方:switch-case

 switch-caseでPythonのバイトコードを処理するというのは次のようなコードを実行するということです(コードの体裁はHPかわさきが適当に調整しています)。

dispatch_opcode:
    switch (dispatch_code)
    {
        /* BEGIN INSTRUCTIONS */

        case BINARY_OP:  // TARGETマクロを展開後はcase節とラベルになる
        {
            // ...
            // BINARY_OPインストラクションを処理するコード
            // ...

            // 次のバイトコードの処理
            {
                NEXTOPARG();  // opcodeなどを取得する(内容は省略)
                PRE_DISPATCH_GOTO(); // 省略
                dispatch_code = opcode | tracing_mode;
                goto dispatch_opcode;
            }
        }

        case BINARY_OP_ADD_FLOAT:
        {
            // ...
            // BINARY_OP_ADD_FLOATインストラクションを処理するコード
            // ...

            // 次のバイトコードの処理
            {
                NEXTOPARG();  // opcodeなどを取得する(内容は省略)
                PRE_DISPATCH_GOTO(); // 省略
                dispatch_code = opcode | tracing_mode;
                goto dispatch_opcode;
            }
        }

        // こんな感じのコードがたくさん

        /* END INSTRUCTIONS */
    }

    Py_UNREACHABLE();


switch-caseによるバイトコードのインストラクションの処理

 swich文の前に「dispatch_opcode:」というラベルがあります。そして、各case節の最後では「goto dispatch_opcode;」としてswitch文の先頭にジャンプしています(このとき、次に処理するインストラクションを取得するのはNEXTOPARGマクロの役割になっています。これは次に紹介する末尾呼び出し手法のコードでも同じです)。


かわさき

 上のコードは紹介している以外のラベルや処理を省略して、かなり簡単なものにしているので、実際のコードとは異なる点には注意してください。実際のところ、このコードはPythonのバイトコード一覧(bytecodes.c)から自動的に生成されるgenerated_cases.c.hというファイルを基に、いろいろなマクロを展開した上で、HPかわさきが適宜調整したものです。


 で、このコード、実は2026年1月6日の時点で1万2000行以上あります。1万2000行のswitch文ですよ。なんか大変そうですよね。

末尾呼び出し手法だとどうなるの?

 一方、同じgenerated_cases.c.hファイルを末尾呼び出し用にマクロ展開すると次のようなコードになります(これも体裁はHPかわさきが適当に調整しています)。

    /* BEGIN INSTRUCTIONS */

    Py_NO_INLINE PyObject *Py_PRESERVE_NONE_CC _TAIL_CALL_BINARY_OP(
                _PyInterpreterFrame *frame, _PyStackRef *stack_pointer,
                    // ……省略……
                int oparg)
    {
        // ...
        // BINARY_OPインストラクションを処理するコード
        // ...

        NEXTOPARG();  // opcodeなどを取得する
        PRE_DISPATCH_GOTO();  // 省略
        Py_MUSTTAIL return
            (((py_tail_call_funcptr *)instruction_funcptr_table)[opcode])(
                frame, stack_pointer, tstate, next_instr,
                instruction_funcptr_table, oparg);
    }

    Py_NO_INLINE PyObject *Py_PRESERVE_NONE_CC _TAIL_CALL_BINARY_OP_ADD_FLOAT(
                _PyInterpreterFrame *frame, _PyStackRef *stack_pointer,
                    // ……省略……
                int oparg)
    {
        // ...
        // BINARY_OP_ADD_FLOATインストラクションを処理するコード
        // ...

        NEXTOPARG();  // opcodeなどを取得する
        PRE_DISPATCH_GOTO(); // 省略
        Py_MUSTTAIL return
            (((py_tail_call_funcptr *)instruction_funcptr_table)[opcode])(
                frame, stack_pointer, tstate, next_instr,
                instruction_funcptr_table, oparg);
    }

        // こんな感じのコードがたくさん

    /* END INSTRUCTIONS */


末尾呼び出し手法でのインストラクションを処理するコード

 1万2000行のswitch文がCの関数定義に変わっているところに注目してください。それも関数名が「_TAIL_CALL_」で始まり、switch文のcase節にあったインストラクションがそれに続く形になっています。いかにも末尾呼び出しを念頭に置いた関数名ですね。そして、関数の末尾は「Py_MUSTTAIL return (((py_tail_call_funcptr *)instruction_funcptr_table)[opcode])(...)」のようになっています。

 しっかりとソースを追い切れていないのですが、instruction_funcptr_tableは各インストラクションを処理するコードのアドレスを含むテーブルだと思われます。そこからopcodeで示される次に処理するインストラクションに対応するアドレスを取得して、それをあたかも関数かのように呼び出しを行っているのが分かります。

 ここまでに分かったことはswitch-caseの手法では1万2000行のswitch文が実行されること。末尾呼び出しの手法では、それが関数定義に分割されて、各関数の末尾では次のインストラクションを処理する関数を呼び出すようになっているということです。

 でも、幾つものバイトコードを処理していくとしたら、関数を呼び出すたびにスタックにデータが積まれていくわけで、そのうちにスタックが溢れてしまいそうです。ですが、そうはならないのです。それが末尾呼び出し手法のよいところ。そして、キーになるのが「Py_MUSTTAIL」です。

末尾呼び出しとは?

 今も見たように末尾呼び出しとは、関数の末尾で別の関数を呼び出すことのように聞こえます。でも、ただ単にそうすればよいというわけでもありません。

 「末尾再帰」という言葉は聞いたことがあると思います。これは関数を再帰的に呼び出すときに、現在実行中の関数のスタックを再利用して、そこに次に呼び出す関数に必要なデータを置き、自分自身を呼び出すのではなく、自分自身の関数のコードの先頭にジャンプするというものです。こうすることで、スタックを無駄に使うことなく、再帰的に自分自身を実行できます。スタックが溢れることもありません(Pythonでは関数の末尾再帰の最適化が標準ではサポートされていないことをご存じの方もいるはずですよね)。

 ここでいう「末尾呼び出し」はこれを他の関数の呼び出しについても適用しようというものです。こうすれば、たくさんのバイトコードのストリームを処理していく中で、スタックを再利用しながら、連続的にインストラクションを処理できるようになります。

 ただし、末尾呼び出しの最適化には問題があります。これはあくまでも「最適化」です。最適化をするかしないかはコンパイラ次第です。最適化してくれないとせっかくの末尾呼び出しが役に立ちません(というか、おそらくはバイトコードの処理が進んでいくうちにスタックが溢れてしまうかもしれません)。

 そこで重要になるのが先も挙げた「Py_MUSTTAIL」なのです。原稿執筆時点ではこのマクロはceval_macros.hファイルで次のように定義されています。

#   if defined(_MSC_VER) && !defined(__clang__)
#      define Py_MUSTTAIL [[msvc::musttail]]
#   else
#       define Py_MUSTTAIL __attribute__((musttail))
#   endif


Py_MUSTTAILはコンパイラのmusttail属性をセットする

 MSVCにおける[[msvc::musttail]]属性とClangにおける__attribute__((musttail))属性はどちらもその関数の末尾呼び出し(つまり、スタックを再利用して、次の関数の先頭アドレスにジャンプすること)を強制するものです。末尾呼び出しできないと、コンパイルも失敗します。

 この結果、末尾呼び出し手法のコードでは次に呼び出すインストラクション処理関数が末尾呼び出しとなります。

 ブログ記事でAArch64なmacOS(XCode Clang)とWindows(MSVC 18)だけが、謝罪を撤回する対象となった理由はここにあるのでしょう。ですが、この属性が他のコンパイラでもサポートされるようになれば、多くの環境で末尾呼び出し手法を採用したインタプリターが使われるようになるはずです。

 switch-caseでのインストラクションの処理と、末尾呼び出しでのインストラクションの処理の差を図にすると次のようになるでしょう。例えば、LOAD_NAME→PUSH_NULL→LOAD_SMALL_INT→LOAD_SMALL_INT→CALL→……のような順序でインストラクションを処理していくとします。

 switch-caseなら以下の図のようになります。

switch-caseによるインストラクションの処理 switch-caseによるインストラクションの処理

 switch文でのインストラクションの処理は分かりやすいのですが、全体で1万2000行を超える巨大なコードになってしまいます。

 一方、末尾呼び出しでのインストラクションの処理は次のようになります。

末尾呼び出しによるインストラクションの処理 末尾呼び出しによるインストラクションの処理

 こちらはインストラクションを処理する関数が多数存在し、それらが末尾呼び出しの形で(スタックあふれの心配や、いわゆる不要なハウスキーピングジョブの必要なしに)インストラクションの処理が連鎖していきます。

違う、そうじゃない……

 「末尾呼び出しで完璧」と思っていたら、そうではないような気がするというのが今回紹介した記事の重要なポイントです。ブログ記事の「Where exactly do the speedups come from?」には「My main guess now is that tail calling resets compiler heuristics to sane levels, so that compilers can do their jobs.」とあります。

 適当訳をすると「今思うと、末尾呼び出し手法だと、コンパイラのヒューリスティックが健全な状態にリセットされるので、コンパイラが自分の仕事をできるんじゃないかな」くらいでしょうか。ここでいう「ヒューリスティック」とはCコードの最適化に関する試行錯誤や検討のような意味でしょう。

 つまり、1万2000行のswitch文の最適化はできないけど、数百の関数ならそれらを個別に最適化することは可能だということです。ブログ記事の後半では末尾呼び出しの話ではなく、最適化の話にフォーカスが移っています。

 例えば、先ほども出てきたgenerated_cases.c.hファイルには次のような記述があります。

        TARGET(BINARY_OP_ADD_INT) {
            // ...
            // 省略
            // ...
            // _POP_TOP_INT
            {
                value = r;
                assert(PyLong_CheckExact(PyStackRef_AsPyObjectBorrow(value)));
                PyStackRef_CLOSE_SPECIALIZED(value, _PyLong_ExactDealloc);
            }
            // ...
            // 省略
            // ...
        }


TARGETマクロ

 これはswitch-case手法のインタプリターではcase節に、末尾呼び出しのインタプリターでは末尾呼び出しを念頭に置いた関数定義へと展開されるマクロです。まあ、それは置いておいて、このBINARY_OP_ADD_INTインストラクションを処理するコードの中では、PyStackRef_CLOSE_SPECIALIZEDという関数が呼び出されています。

 この関数は大ざっぱに言うと、次のようにインライン化可能な関数として定義されています。

static inline void
PyStackRef_CLOSE_SPECIALIZED(_PyStackRef ref, destructor destruct)
{
    // ……省略……

    assert(!PyStackRef_IsNull(ref));

    // ……省略……

    if (PyStackRef_RefcountOnObject(ref)) {
        _Py_DECREF_SPECIALIZED(obj, destruct);
    }
}


PyStackRef_CLOSE_SPECIALIZED関数はインライン化可能

 ところが、switch-case手法のインタプリターでは最適化がうまくいっていないのを示したのがken jin氏のブログ記事にある以下の画像です。

インライン化が可能なPyStackRef_CLOSE_SPECIALIZED関数がcallされていることが分かる インライン化が可能なPyStackRef_CLOSE_SPECIALIZED関数がcallされていることが分かる

 ブログ記事内でHPかわさきが「PyStackRef_CLOSE_SPECIALIZED」を検索しているので該当箇所が黄色いマーカーで示されていますが、一番下でこの関数がcallされているのが分かります。

 一方、末尾呼び出し手法のインタプリターでは次の画像のように、この関数が呼び出されずにインライン化されました。

インライン化された インライン化された

 巨大なswitch文では、コンパイラが全てを把握した上で最適化を進めることが難しいけれども、関数に分割することで個々の関数にだけ目を向ければよいため、このような最適化(ここでは関数のインライン化)が可能になり、全体としてはかなりの高速化が見込めるという一例を示したものだと思います。「末尾呼び出しという手法を採用したことで、巨大なswitch文が個別の関数へと分割でき、それがコンパイラによる最適化を可能にした」というのが高速化のキモだったというわけですね。



かわさき

 というのが、この冬休みの自由研究の結果です。Pythonで書かれたソースコードはいろいろな記事を書く中でさんざん読みましたが、Cのコードを読むのはホントに久しぶりでした。注意点としては、上記の最適化の画像を見ると「_TAIL_CALL_……」みたいなところがありますが、これは別に末尾呼び出しをしているのではなく、その近辺にジャンプしているだけのようです。最初に見たときには、「末尾呼び出ししてるじゃん!」と思ったこともありますが、そうではなく、あれはあくまでも最適化のお話なのでお間違えなく。

 こんな感じで「HPかわさきの研究ノート」が始まっちゃいました。今回は前後編の記事に分割できるかな? と思ったのですが、ちょっと無理そうで、長くなるのを覚悟で1回の記事とさせてもらいました。今後は、なるべく手短にチャチャッとした記事をお届けしたいと思っていますが、そうでもないこともあるでしょう。そうでないときには苦笑いしながらお付き合いくださいませ。


「HPかわさきの研究ノート」のインデックス

HPかわさきの研究ノート

Copyright© Digital Advantage Corp. All Rights Reserved.

アイティメディアからのお知らせ

スポンサーからのお知らせPR

注目のテーマ

Microsoft & Windows最前線2026
人に頼れない今こそ、本音で語るセキュリティ「モダナイズ」
4AI by @IT - AIを作り、動かし、守り、生かす
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。