先に結論だけ(13 章末の 3.15 追記を除き、すべて自宅の検証機・6 コア / 12 スレッドでの実測値。数字の出どころは各章にあります)

  • 効く: 純 Python の CPU 処理を threading で並列化したとき(8 スレッドで ×4.06・4 章)
  • 一番効く: 重い計算の裏で GUI を止めたくないとき(tkinter の再描画が 27/210 回 → 200/200 回・7 章)
  • 効かない: デバイスの応答待ちが支配する PLC ポーリング(4 ランタイム横並び・差ゼロ・6 章)
  • コスト: 単一スレッドは同じ 3.14 の既定ビルドより 8.0% 遅い(公式は「5〜10%」・5 章)
  • 罠: Smart App Control が On の機械で、cp314t の numpy / pandas / matplotlib が import 時に弾かれた(8 章)

▶ 当サイトの推奨
① 現場 PC の標準ランタイムを 3.14t にするのは、まだ勧めません(9 章)。
② 検証機で試す価値があるのは「重い計算の裏で画面が固まる」症状を抱えている場合です(7 章)。
③ その前に 3.12 から 3.14 の既定ビルドへ上げてください。この純 CPU ワークロードはそれだけで 14.1% 速くなりました(5 章)。

この記事の目次

読了の目安: 結論だけなら約 3 分(上のボックスと図 1)/使うかどうかの判断まで約 15 分(1・6・7・9 章)/全文とスクリプトまで約 45 分。実測の本体は 4〜8 章です。通読は前提にしていません。上の結論ボックス → 図 1 → 9 章の表の 3 点だけで、使うかどうかは判断できます。

1. 監視アプリが 1 コアしか使っていない — この記事で分かること

設備からデータを読んで画面に出すアプリを threading で書き、タスクマネージャーを開いたとき、こんな見え方をしていないでしょうか。

  • スレッドを 4 本立てたのに、CPU 使用率は 12 コアの機械で 10% 前後から動かない
  • 集計処理が走った瞬間だけ画面の数値が固まり、数秒後にまとめて追いつく
  • ワーカーを増やしても、1 分あたりに処理できる件数が増えない

原因として必ず名前が挙がるのが GIL(Global Interpreter Lock)です。CPython には「同時に Python のバイトコードを実行できるスレッドは 1 本だけ」という排他ロックがあり、スレッドを何本立てても Python のコードは順番待ちになります。そして 2026 年、Python 3.14 でこの GIL を持たないビルドが正式サポートになりました。

ここで期待が先走ります。「GIL が無くなるなら、うちの監視アプリも速くなるはずだ」——当サイトもそう思って測りはじめました。結果は、速さと応答を比べた 3 系統(純 CPU・ポーリング・GUI)のうち、効いたのは 2 つ。しかも現場アプリで一番効いたのは、速さではなく「画面が止まらないこと」でした。

あなたの症状はどれか — 読む前に当たりを付ける

本記事の結論は「Python の計算が支配しているか、待ちが支配しているか」でまっぷたつに分かれます。症状から先に当たりを付けてください。

いま起きている症状free-threading は参照
集計や計算が走った瞬間だけ画面が固まる効く(一番大きい)。ただし純 Python のコードで計算している場合に限ります7 章
自前ループの集計・文字列処理・フィルタ計算に数秒かかる効く。こちらも純 Python のコードに限ります4 章
PLC のポーリング件数が、スレッドを増やしても増えない効かない(待ちが支配)6 章
CPU 使用率が上がらないまま処理が遅い(通信待ちが主)効かない6 章
numpy / pandas の重い処理を速くしたい当サイトでは測れていない(import が弾かれた)。ライブラリ内部に元から GIL を手放す区間があるため、既定ビルドのまま使うのが基本です10 章・8 章

自分がどれに当たるか分からないときは、遅い処理を 3 行で切り分けます。

  1. 遅い処理の中で、time.perf_counter() を「通信やファイルを待っている区間」と「Python が計算している区間」の前後に置き、それぞれの合計を出す
  2. 計算している区間が全体の半分以上で、その計算を書いているのが自分の Python コードなら、4 章と 7 章が当てはまります。計算を numpy / pandas が担っているなら当てはまりません(10 章)
  3. 待っている区間が大半なら 6 章です。ランタイムを替えても直りません
遅い原因が計算か待ちかで free-threading が効くかどうかを判定するフロー図 遅い処理の時間を計算区間と待ち区間に分けて測り、計算が半分以上で、かつその計算が純 Python のコードなら free-threading は効く(4 章の 8 スレッド 4.06 倍、7 章の再描画 27/210 回から 200/200 回)。待ちが大半なら効かない(6 章で 4 ランタイム横並び・差ゼロ)。計算を numpy や pandas が担っている場合はこの左枝は当てはまらず、既定ビルドのまま使うのが基本(10 章)。どちらの場合も単一スレッドは 8.0% 遅くなり、numpy や pandas を使うなら 8 章の import の罠を先に確認する必要がある。 あなたのアプリの「遅い」は、どちらが支配しているか 遅い処理の時間を time.perf_counter() で 「待っている区間」と「計算している区間」に割る 計算している区間が半分以上 待っている区間が大半 効く = free-threading の出番 純 Python のコードで計算している場合に限る ・threading で並列化すると 8 スレッドで ×4.06(4 章) ・裏で計算しても画面が止まらない 再描画 27/210 回 → 200/200 回(7 章) ・計算を numpy / pandas が担うなら別 既定ビルドのままが有利(10 章) 効かない = 直すのは別の場所 ・4 ランタイムが横並び・差ゼロ(6 章) ・待ち時間は GIL ありでも手放される ・スレッド数・ポーリング周期・通信 設計を見直すほうが速い ・すでに asyncio なら結果は同じ(6 章) どちらの場合も、単一スレッドで動く部分は 3.14 の既定ビルドより 8.0% 遅くなりました(検証機・5 章)。 計算を numpy / pandas が担っているなら、この図の左枝は当てはまりません(10 章)。 既定ビルドのままが有利で、当サイトの検証機では 3.14t での import 自体が弾かれました(8 章)。
図 1: 症状から「効く/効かない」を判定するフロー。数値はいずれも本記事の実測値です。スマートフォンでは図を横にスクロールできます。

→ 効く枝だった方は 4 章と 7 章へ、効かない枝だった方は 6 章へ、numpy / pandas に計算させている方は 10 章(当サイトの結論は「既定ビルドのまま使う」)へ進んでください。

このあとは、効く条件(4・7 章)、効かない条件(6 章)、払うコスト(5 章)、Windows への入れ方(3 章)、ライブラリが動くか(8 章)の順に示します。数値は 13 章末の 3.15 追記を除き、すべて同じ 1 台での実測です。

数値は(13 章末の 3.15 追記を除き)すべて自宅の検証機(Windows 11 Pro・デスクトップ向け x86-64 CPU・物理 6 コア / 論理 12 スレッド)での実測です。会社支給の端末は使っていません。 1 台の機械の結果なので、コア数の違う機械では倍率が変わります。

2. free-threaded ビルドとは何か — 3.14 で「実験的」が外れた

2.1 GIL があるときと、無いときの違い

GIL は「Python のコードを実行する権利」を表す 1 本の札です。スレッドが 4 本あっても、札を持っている 1 本だけが進み、残りは待ちます。free-threaded ビルドはこの札を廃止したビルドで、4 本が同時に Python のコードを実行します。

GIL があるビルドと free-threaded ビルドで 4 スレッドの走り方がどう違うかの概念図 左は GIL ありのビルドで、4 本のスレッドが時間軸上で 1 本ずつ交代で実行され、残りは GIL 待ちになる。右は free-threaded ビルドで、4 本が時間軸上すべて同時に実行される。下段の注記は、通信やファイルの待ち時間は GIL ありでも手放されるため、待ちが大半の処理には効かないことを示す。 同じ「4 スレッド」でも、走り方がこれだけ違う GIL あり(既定ビルド・3.12 / 3.14) Python のコードを実行できるのは同時に 1 本だけ 時間 → スレッド1 スレッド2 スレッド3 スレッド4 実行中 GIL 待ち 4 スレッドにしても所要時間は 1 スレッドと変わらない (4 章の実測で ×1.00〜×1.02) GIL なし(free-threaded ビルド・3.14t) 4 本とも同時に Python のコードを実行する 時間 → スレッド1 スレッド2 スレッド3 スレッド4 実行中(待ちなし) 8 スレッドで ×4.06(6 コア / 12 スレッド機の実測) ただし 1 スレッドあたりは約 8% 遅い(5 章) この図が効くのは「Python のコードを実行している時間」だけ 通信やファイルの待ち時間は GIL ありのビルドでも手放される。待ちが大半のポーリング処理は、左の図でも並列に進む(6 章) だから「GIL が無くなれば現場アプリが速くなる」は、そのままでは成り立たない
図 2: GIL あり/なしでスレッドがどう動くかの概念図(時間軸の刻みは説明用で、実際のスケジューリング単位ではありません)。スマートフォンでは図を横にスクロールできます。

2.2 3.14 で「実験的」が外れた — ただし既定ではなく、3.15 でも変わらない

経緯を 3 行で押さえます。

  • PEP 703 が GIL をビルドオプション化する仕組みを定め、Python 3.13 で実験的(experimental)機能として入りました(ビルドフラグ --disable-gil)
  • PEP 779(2025 年 6 月 16 日採択)が昇格の基準——pyperformance で GIL ありビルド比のオーバーヘッドが 15% 以内、メモリは幾何平均で 20% 以内——を定め、Python 3.14 で「実験的」が外れました
  • ただし既定ビルドではありません。 通常の python.exe は GIL ありのままで、free-threaded ビルドは別の実行ファイル python3.14t.exe として同居します

「GIL なしを既定にする」フェーズ(Phase III)の時期は決まっていません。 PEP 779 本文にも、既定化の判断は別の性質の決定であると明記されています。「次のバージョンで既定になるのでは」という期待もよく聞きますが、2026 年 10 月 9 日にリリースされた Python 3.15 の What's New(正式版・2026 年 10 月 26 日に確認)にも GIL 既定化の記述はありません。free-threading 関連で目立つ変更は PEP 803(abi3t)で、C 拡張が free-threaded ビルド向けにも安定 ABI を使える道が新設されました。ほかに、free-threaded ビルドの性能向上を理由に、生のメモリ割り当て(raw allocation)の既定アロケータが mimalloc になったという記載もあります。

初版(2026 年 9 月 17 日)の時点では 3.15.0rc2 のドラフトしか読めませんでした。正式版で読み直しても、この節の内容は変わっていません。3.15.1 で測り直した結果は 13 章の末尾にあります。

2.3 単一スレッドは遅くなる — 公式が明記しているコスト

GIL を外すと、参照カウントやオブジェクト操作を別の方式で守る必要が出るため、1 スレッドで動かしたときの速度は落ちます。Python 3.14 の What's New は、free-threaded ビルドの単一スレッドオーバーヘッドを "roughly 5-10%, depending on the platform and C compiler used"(プラットフォームと使用する C コンパイラによっておおむね 5〜10%)と記載しています。free-threading の HOWTO ページには pyperformance ベースの数値として、macOS aarch64 で約 1%、x86-64 Linux で約 8% という記述があります。自宅実測との突き合わせは 5 章で行います。

2.4 覚えておく API は 2 つ

計測でも運用でも必ず使うのがこの 2 つです。混同すると結論を丸ごと間違えます。

知りたいこと使う API返る値
いま GIL が動いているか(プロセスの状態)sys._is_gil_enabled()True / False
ビルド自体が free-threaded かsysconfig.get_config_var("Py_GIL_DISABLED")1 / 0(3.12 以前は None)

free-threaded ビルドでも GIL が戻っていることがあります。環境変数 PYTHON_GIL=1、コマンドラインの -X gil=1、そしてfree-threading 対応を宣言していない C 拡張を import したときです。公式ドキュメントによれば、非対応の C 拡張を import すると警告が出てプロセス全体で GIL が再有効化されます(拡張側は Py_mod_gil スロットなどで対応を宣言します)。この挙動は公式ドキュメントの記述として紹介するもので、当サイトの検証機では再現できていません(理由は 8 章と 13 章)。

3. Windows に 3.14t をインストールする — 管理者権限なしで、既存環境を壊さずに

現場 PC に新しい Python を入れるときの制約は、たいてい次の 3 つです。管理者権限が無い/既存の日次バッチを止められない/セキュリティ製品が新しい実行ファイルを警戒する。この 3 つを踏まえた手順を、実際に実行したログのまま書きます。

⚠️ なお per-user インストールで管理者権限が要らない場合でも、会社の端末に入れるときは情報システム部門の承認を先に取ってください。ここで実行しているのは自宅の検証機です。

3.1 入れる前に py -0 を控える

最初にやるべきことは記録です。インストール後に「何が既定になったか」を比較できるようにします。

py -0

検証機ではこの時点で -V:3.12 * の 1 行だけでした。末尾の * が「バージョン指定なしで py を呼んだときに選ばれるもの」です。この行が後で動きます(3.3)。

3.2 公式インストーラを per-user で入れる

⚠️ 先に断っておきます。python.org の .exe フルインストーラと py ランチャーは、Python 3.14 で公式に非推奨(deprecated)になりました(フルインストーラは 3.16 以降は提供されません)。置き換え先は下のコラムに書いた Python install manager です。当サイトの計測で非推奨側を使ったのは既存の 3.12 環境と条件を揃えるためで、3.14 の時点では問題なく動きます。導入方式の世代交代そのものは Python 学習ロードマップの 3.1 にまとめています。

python.org から python-3.14.7-amd64.exe(31.7MB)を取得しました。閉域ネットワークの PC に入れる場合は、この exe を持ち込む形になります。まず署名を確認します。

Get-AuthenticodeSignature .\python-3.14.7-amd64.exe | Format-List Status, SignerCertificate

検証機での結果は Status = Valid、Signer = CN=Python Software Foundation、Issuer = CN=Microsoft ID Verified CS EOC CA 04 でした。次にサイレントインストールします。free-threaded ビルドを入れる指定が Include_freethreaded=1 です。

.\python-3.14.7-amd64.exe /quiet InstallAllUsers=0 PrependPath=0 `
    Include_launcher=0 Include_freethreaded=1 Include_test=0 `
    AssociateFiles=0 Shortcuts=0 SimpleInstall=1

引数の意図は次のとおりです。

引数意図
InstallAllUsers=0per-user インストール=管理者権限が不要。=1 にすると管理者権限が要る
PrependPath=0PATH を書き換えない。既存の python コマンドの向き先を変えないため
Include_launcher=0既存の py.exe を上書きしない
Include_freethreaded=1本題。python3.14t.exe が入る
AssociateFiles=0 / Shortcuts=0.py の関連付けとスタートメニューを触らない

検証機では終了コード 0 で完了しました。Smart App Control は On の状態でしたが(HKLM\SYSTEM\CurrentControlSet\Control\CI\Policy の VerifiedAndReputablePolicyState が 1)、インストーラ本体がブロックされることはありませんでした。PSF 署名付きの実行ファイルだからと考えられますが、これは 1 台での観測であり、SAC の判定を断定するものではありません。

コラム: 置き換え先の Python install manager(PyManager)

3.14 からの公式な導入方法はこちらです。Microsoft Store、python.org の MSIX、winget install 9NQ7512CXL7T のいずれでも入り、py install 3.14t で free-threaded ビルドを取得できます。PEP 773 によれば、PyManager 自身は利用者と同じ権限レベルでのみ動作し、per-user インストールは原則として管理者権限が不要です(C ランタイムのシステム更新が必要な場合を除く、と公式に注記があります)。ただし当サイトの計測は 3.2 のインストーラ方式で行ったため、PyManager 経由は未検証です。

3.3 事後確認でぶつかった落とし穴 — py の既定が 3.12 から 3.14t に動く

インストール後の確認結果です。

  • ユーザー環境変数の PATH は Python312 のまま変化なし
  • py.exe のタイムスタンプも 2025-04-08 のまま=既存のランチャーは上書きされていない
  • しかし py -0 の既定マーク * が 3.12 から 3.14t に移った

3 番目が落とし穴です。py はバージョン指定が無いと最も新しいものを選ぶため、ランチャー本体を上書きしていなくても既定が動きます。当サイトの検証機では既存の定期実行が python.exe を絶対パスで呼ぶ書き方だったため影響はありませんでした。しかしバッチやタスクスケジューラの起動行が py script.py や python script.py になっている環境では、実行されるランタイムが黙って変わります。これは free-threaded ビルドを入れるときに限った話ではなく、新しい Python を追加インストールするとき共通の確認ポイントです。

3.4 3.14t の起動と venv の作り方(py -V:3.14t)

入れたあとの呼び出し方です。起動構文は次の 2 つがどちらも通りました。

py -V:3.14t -c "import sys, sysconfig; print(sys.version); print(sysconfig.get_config_var('Py_GIL_DISABLED'), sys._is_gil_enabled())"
py -3.14t   -c "print('short form works')"

公式ドキュメントは -x.y を -V:Company/Tag の短縮形と説明しており、free-threaded ビルドの節でも py.exe -3.14t に触れています。当サイトが確認できていなかったのはこの短縮形が 3.12 世代の旧ランチャー(py.exe は上書きしていない状態)でも通るかのほうで、実際にそのまま通りました。venv も通常どおり作れます。

py -V:3.14t -m venv .venv314t

こちらも終了コード 0 で成功しました。使う Python のバージョンをどう選ぶかという話は Python 学習ロードマップにまとめています。

4. 実測 A: 純 Python の CPU 処理は、たしかに速くなる

ここからが本題です。まずは free-threading が最も効くはずの条件から測ります(A〜E は計測系統の記号で、D は A の 1 スレッド列を取り出したものです)。

ワークロード: 3 から 1,000,000 までの奇数を試し割りで素数判定する(純 Python・外部ライブラリなし)。総仕事量は固定で、候補の数を飛び飛びにスレッドへ配り、軽い数と重い数が各スレッドに均等に入るようにしています。各条件 5 回実行の中央値で、計測前にウォームアップを 3 回捨てています。さらにランタイムを回す順番を逆にしたキャンペーンをもう 1 巡し、表の値は順方向と逆方向、2 巡分の中央値を平均したものです(順序効果を疑ったためで、理由は 11 章に書きます)。

比較するランタイムは 4 つです。3.14t の 2 行は同じ実行ファイルで、環境変数 PYTHON_GIL=1 の有無だけが違います。こうすると「free-threaded ビルドであること」のコストと「GIL を外したこと」の効果を分けて読めます。

ランタイム1 スレッド2 スレッド4 スレッド8 スレッド
3.121.91s(×1.00)1.88s(×1.02)1.87s(×1.02)1.89s(×1.01)
3.141.64s(×1.00)1.64s(×1.00)1.64s(×1.00)1.65s(×1.00)
3.14t(PYTHON_GIL=1)1.80s(×1.00)1.79s(×1.01)1.81s(×1.00)1.80s(×1.00)
3.14t(GIL なし)1.78s(×1.00)1.02s(×1.74)0.63s(×2.83)0.44s(×4.06)
4 つのランタイムで純 CPU 処理を 1・2・4・8 スレッドで実行したときの所要時間の棒グラフ 3.12・3.14・3.14t(GIL あり)はスレッド数を増やしても所要時間がほぼ変わらず、1.6 秒から 1.9 秒の間で横ばい。3.14t(GIL なし)だけが 1 スレッド 1.78 秒から 2 スレッド 1.02 秒、4 スレッド 0.63 秒、8 スレッド 0.44 秒へ短くなる。 同じ総仕事量を 1 / 2 / 4 / 8 スレッドで処理した所要時間(秒・5 回の中央値を 2 巡ならした値) 0 0.5 1.0 1.5 2.0 秒 1.91 1.88 1.87 1.89 1.64 1.64 1.64 1.65 1.80 1.79 1.81 1.80 1.78 1.02 0.63 0.44 この列だけスレッド数に応じて短くなる スレッド数 1 2 4 8 1 2 4 8 1 2 4 8 1 2 4 8 3.12 3.14 3.14t(GIL あり) 3.14t(GIL なし) 自宅の検証機(Windows 11 Pro・6 コア / 12 スレッド)での実測。2026 年 9 月 17 日計測。
図 3: 実測 A のスケーリング。棒が低いほど速い。スマートフォンでは図を横にスクロールできます。

読み取れることを順に書きます。

  1. GIL がある 3 条件は、スレッドを増やしても所要時間が動きません(×1.00〜×1.02)。同じ実行ファイルでも PYTHON_GIL=1 を付けた瞬間にスケーリングが消えるので、効いているのが GIL の有無であることがはっきりします
  2. GIL なしは 8 スレッドで ×4.06。ただし線形ではありません。4 スレッドで ×2.83(理想の 71%)、8 スレッドで ×4.06(理想の 51%)です。物理 6 コアの機械なので、8 スレッドでは 6 コアを超えた分が論理スレッドでの分け合いになります
  3. 1 スレッドの値は 3.14t が 3.14 より遅い(1.78s 対 1.64s)。これが free-threading の入場料です(5 章)

ランタイムをまたいで比べると、3.14t の 8 スレッド 0.44 秒は、既定ビルド 3.14 の最速値 1.64 秒に対して 3.7 倍、いま現場でよく使われている 3.12 の 1.87 秒に対して 4.3 倍です(表の値からの割り算)。純 Python の CPU 処理が数百ミリ秒以上続くなら、これは無視できない差です。

⚠️ 逆に言えば、この結果が当てはまるのは「純 Python の計算」に限られます。numpy の行列演算のように処理の中身が C の世界で動いているものは、もともと GIL を手放している区間が長く、この表とは違う挙動になります(そして当サイトはそれを測れていません。8 章)。

5. 実測 D: 単一スレッドの入場料は 8.0% だった

4 章の 1 スレッド列だけを取り出し、3.14 の既定ビルドを基準に並べ直します。

ランタイム1 スレッドの中央値3.14 既定ビルド比
3.121.913s+16.4%
3.14(既定ビルド)1.644s基準
3.14t(PYTHON_GIL=1)1.802s+9.6%
3.14t(GIL なし)1.775s+8.0%

公式ドキュメント(Python 3.14 What's New)の記載は "roughly 5-10%, depending on the platform and C compiler used"。検証機の +8.0% はその範囲に収まりました。HOWTO ページにある pyperformance ベースの x86-64 Linux 約 8% ともほぼ同じ位置です。公式値と自宅実測が食い違わなかった、という確認です。

この表には、もう 1 つ見落とされやすい事実が写っています。3.12 から 3.14 の既定ビルドへ上げるだけで、同じ処理が 1.913 秒から 1.644 秒に縮んでいることです(所要時間で 14.1% 減。3.14 を基準に取ると 3.12 が 16.4% 遅い、という上の表の読み方と同じことです)。free-threading のことを一度忘れて素直に読むと、「3.12 のまま free-threading を検討する」より「まず 3.14 の既定ビルドへ上げる」ほうが、この純 CPU ワークロードでは効いています。しかも既定ビルドなら C 拡張の互換性の心配がありません。free-threaded ビルドの導入を検討する前に、この 1 段を済ませたかどうかを確認する価値があります。

なお PYTHON_GIL=1 付きの 3.14t は GIL なしの 3.14t よりわずかに遅いのですが(1.802s 対 1.775s)、この差が誤差かどうかを判定できるだけの試行回数を取っていません。ここを根拠に何かを主張しないでください。

6. 実測 B: 疑似 PLC のポーリングは速くなるか — 答えはノー

現場アプリの中心は通信です。ここが速くなるかどうかが実務上の分かれ目なので、条件を 2 つに分けて測りました。

ワークロード: SLMP 3E バイナリで D0 から 64 ワードを読み出し → struct で解析 → 最小値・最大値・移動平均を集計 → logging でファイルに 1 行。各ワーカースレッドが自分のソケットを 1 本ずつ持ちます。相手は公開仕様に基づいて自作した SLMP ダミーサーバーで、条件を揃えるためサーバー側は常に 3.12 で起動しています。総ポーリング回数は固定です。

再現するときの注意が 1 つあります。計測に使ったサーバーは公開版そのままではありません。 公開版は受信フレームを INFO で 16 進ダンプするため計測を支配してしまうので、ログレベルを WARNING に落とし、6.2 用に 1 要求あたりの待ち時間を足すノブだけを追加しています。公開版のまま追試すると、下の B-1 の数字には届きません。

6.1 B-1: 応答待ちゼロ(ループバック全開)

まず 127.0.0.1 のダミーサーバーを全力で叩く条件です。デバイス側の応答待ちがゼロという、実機の PLC では起きない理論上限にあたります。

ランタイム1 スレッド2 スレッド4 スレッド8 スレッド
3.1211,768/s(×1.00)15,979/s(×1.36)16,202/s(×1.38)15,626/s(×1.33)
3.1411,447/s(×1.00)19,860/s(×1.74)16,169/s(×1.41)15,776/s(×1.38)
3.14t(GIL あり)10,613/s(×1.00)16,906/s(×1.59)14,195/s(×1.34)14,026/s(×1.32)
3.14t(GIL なし)10,654/s(×1.00)20,241/s(×1.90)26,983/s(×2.53)27,436/s(×2.57)

※ 応答待ちゼロの理論上限です。実機の PLC ではこの条件は起きません。現実に近いのは 6.2 の表のほうです。

この条件では差が出ます。GIL ありの 3 条件が 4 スレッド以降で頭打ちになる(×1.3〜×1.4)のに対し、GIL なしは 4 スレッドで ×2.53 まで伸びました。実数でも 26,983/s 対 16,169/s です。応答待ちが無い分、struct 解析・集計・ログ整形という純 Python の処理が支配的になり、4 章と同じ理屈が効いたと読めます。実行順を逆にした再計測でも同じ傾向でした。

ただし代償もあります。GIL なしの 8 スレッドでは 1 回あたりの応答時間の p95 が 0.50ms と、他の条件(0.23〜0.34ms)より悪化しました。スループットは上がるが、個々の応答は遅い方向にばらつくという形です。

6.2 B-2: デバイス応答 10 ミリ秒を模擬(こちらが現実)

実機の PLC は、要求を受けてからスキャンして返すまでに時間がかかります。ダミーサーバー側に 10 ミリ秒の応答待ちを入れて測り直しました。

ランタイム1 スレッド2 スレッド4 スレッド8 スレッド
3.1292/s(×1.00)185/s(×2.01)373/s(×4.04)749/s(×8.12)
3.1492/s(×1.00)185/s(×2.00)371/s(×4.02)746/s(×8.08)
3.14t(GIL あり)92/s(×1.00)185/s(×2.02)370/s(×4.03)743/s(×8.08)
3.14t(GIL なし)92/s(×1.00)185/s(×2.01)370/s(×4.01)749/s(×8.13)

4 ランタイムが完全に横並びになりました。 縦に読んでも横に読んでも、free-threading の効果はゼロです。p95 もすべて 10.88〜11.21ms の範囲に収まりました。

理由は単純で、スループットを決めているのがデバイスの待ち時間だからです。1 スレッドで 92 回/秒= 1 回あたり約 10.8 ミリ秒。10 ミリ秒の応答待ちに Python 側の処理が 1 ミリ秒弱乗っただけの値です。GIL の取り合いは、この 1 ミリ秒弱の中でしか起きません。

そしてスレッドを増やせば 8 スレッドで 8.1 倍まで素直に伸びています。これは free-threading のおかげではありません。通信の待ち時間は GIL がある既定ビルドでも手放されるので、GIL ありのままでも待ち時間は重ねられるからです。図 2 の下段の注記は、これを指しています。

ここが本記事で一番言いたいところです。 現場の監視アプリが「遅い」とき、その時間の大半はデバイスの応答待ち・再接続待ち・タイムアウト待ちです。この部分に free-threading は効きません。効くのは、既存の threading のままスレッド数とポーリング周期を見直すこと、そして通信の設計を直すことです。再接続とタイムアウトの設計は Python socket の実戦ガイド、メーカー別のライブラリ選定は PLC と Python で通信する方法にまとめています。

なお asyncio を使えば、スレッドを増やさずに同じ「待ちを重ねる」効果が得られます(10 章)。すでに asyncio で待ちを畳んでいるアプリに free-threaded ビルドを持ち込んでも、この表と同じ理由で効果は期待できません。

7. 実測 C: tkinter の画面が止まらなくなった — ここが一番大きい

最後に GUI です。本記事の実測で一番はっきり差が出たのはここでした。

ワークロード: after で 50 ミリ秒ごとに値を再描画する最小構成の監視画面を動かしながら、背景で純 Python の CPU 計算スレッドを 4 本回します。測るのは「予定時刻からどれだけ遅れたか(ドリフト)」と「10 秒間に何回再描画できたか」です。10 秒 ×5 回の中央値。

ランタイム背景スレッド再描画回数 / 予定ドリフト p50p95最大背景処理の回数
3.120200 / 2006.6ms13.8ms16.6ms0
3.12427 / 2104,609.1ms8,569.1ms8,980.2ms2,020,686
3.140200 / 2006.8ms13.6ms14.9ms0
3.14429 / 2034,350.2ms8,348.0ms8,704.2ms2,250,894
3.14t(GIL あり)0200 / 2006.7ms13.4ms14.8ms0
3.14t(GIL あり)430 / 2024,582.4ms8,454.6ms8,653.0ms2,116,680
3.14t(GIL なし)0200 / 2006.9ms13.7ms15.0ms0
3.14t(GIL なし)4200 / 2006.5ms13.5ms14.8ms4,443,674
背景で 4 スレッドが計算しているときの tkinter 再描画達成率とドリフトの比較 3.12 は予定 210 回に対し 27 回、3.14 は 203 回に対し 29 回、3.14t の GIL ありは 202 回に対し 30 回しか再描画できず、ドリフトの中央値は 4.3 秒から 4.6 秒。3.14t の GIL なしだけが 200 回中 200 回を再描画し、ドリフト中央値は 6.5 ミリ秒。 背景で 4 スレッドが計算している間に、50 ミリ秒周期の画面はどれだけ更新できたか 棒=予定どおり再描画できた割合(10 秒 ×5 回の中央値) 0% 50% 100% 3.12 27 / 210 回 ドリフト p50 4,609ms 3.14 29 / 203 回 ドリフト p50 4,350ms 3.14t(GIL あり) 30 / 202 回 ドリフト p50 4,582ms 3.14t(GIL なし) 200 / 200 回 ドリフト p50 6.5ms 背景スレッドが 0 本のときは 4 ランタイムとも 200 / 200 回・ドリフト p50 6.6〜6.9ms で差はありませんでした。
図 4: 実測 C のドリフト比較。棒が長いほど画面が予定どおり動いている。スマートフォンでは図を横にスクロールできます。

背景スレッドが 0 本のときは 4 ランタイムとも 200/200 回・p50 6.6〜6.9ms で、差はありません。free-threaded ビルドにしただけで画面が軽くなるわけではない、ということです。

予定回数が 200 を超えている行があるのは、計測の作りによるものです。この計測は実際に経過した秒数を 50 ミリ秒で割った値を「予定」としているため、最後の再描画が大きく遅れて計測が 10.5 秒まで伸びた回は、予定も 210 回に増えます(実経過は 10.01〜10.55 秒)。分母が動いても、再描画できた実数が 27〜30 回であることは変わりません。

差が出るのは背景に 4 スレッド積んだときです。GIL のある 3 条件は予定 200 回超に対して 27〜30 回しか再描画できず、ドリフトの中央値が 4.3〜4.6 秒になりました。1 回の更新に平均 4 秒以上遅れる画面は、現場から見れば「フリーズしている」と同じです。 最大では 8.6〜9.0 秒です。

3.14t(GIL なし)だけが 200/200 回・p50 6.5ms。背景が 0 本のときと変わりません。しかも背景処理の回数は他の条件の約 2 倍(4,443,674 回 対 2,250,894 回)こなしています。画面を犠牲にして背景を減らしたのではなく、背景を倍こなしながら画面も落とさなかったということです。

もう 1 つ、事前に確認が必要だった点があります。tkinter は C 拡張なので、import した時点で GIL が再有効化されてしまえば、この結果は出ません。実際に測った結果は次のとおりです。

import tkinter 直後  : sys._is_gil_enabled() -> False
tk.Tk() 生成直後     : sys._is_gil_enabled() -> False

tkinter は GIL を再有効化しませんでした。 標準ライブラリとしてビルドに同梱されるため、free-threaded ビルド側の tkinter は free-threaded ビルドとして作られているからと考えられます。

補足: Tk のバージョンは 3.14 系(既定ビルド・3.14t とも)が 9.0.4、3.12 が 8.6.15 でした。3.14 と 3.14t は同じ Tk 9.0.4 なので、この 2 つの比較は同じ GUI ツールキット上の比較として成立しています。 3.12 だけ Tk が古いため、見た目や DPI の挙動は別途の確認が必要です。

⚠️ ただし、これは「GUI を別スレッドから触ってよい」という話ではありません。ウィジェットに触るのはメインスレッドだけ、という原則は free-threaded ビルドでも変わりません。GUI とスレッドの境界の作り方は tkinter で監視アプリを作るの 5 章、PyQt6 / PySide6 側の作法は PyQt6 / PySide6 で本格的なデスクトップアプリを作るにあります。本記事はその基礎を前提にしています。

⚠️ もう 1 つ、この結果を見て載せ替えを考える前に。「ランタイムを差し替えるだけ」で済むのは、背景スレッドとメインスレッドが状態を共有していない場合です。 共有カウンタや共有 dict を持つ既存コードは、GIL が偶然隠していた競合が表に出ます(10 章にチェックリストを置きました)。

8. 実測 E: pip は全部通り、import で止まった

ここまでの結果を見て「使ってみよう」と思った方に、最後の関門を先に伝えます。

先に確認: あなたの PC の Smart App Control は On か

この章の話が自分に当てはまるかは、ここだけで決まります。公式に案内されている確認方法は画面からの確認です。

  • 設定 → Windows セキュリティ → アプリとブラウザー制御 → スマート アプリ コントロール。この項目自体が無ければ、その PC に SAC は入っていません。オン=強制モード、評価=評価モード、オフ=動いていません
  • スクリプトから見るなら次の 1 行です。当サイトの検証機は画面が「オン」の状態で 1 を返しました(値と表示の対応は公式ドキュメントに記載が見つからないため、判断は画面の表示で行ってください)
(Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\CI\Policy').VerifiedAndReputablePolicyState

前提として、SAC はすべての Windows 11 で動いている機能ではありません。Microsoft Learn は有効化の要件を "it can only be enabled on a clean install of a version of Windows that contains the Smart App Control feature"(その機能を含む Windows のクリーンインストールでのみ有効にできる)と説明し(PC の初期化もクリーンインストール扱い)、さらに "If we detect that you're one of those users, we automatically turn Smart App Control off"=企業ユーザーや開発者のような使い方を検出した場合は自動的にオフにするとも書いています。

一方で Microsoft Support の FAQ には "Recent Windows updates allow Smart App Control to be enabled without requiring a clean installation."(最近の Windows 更新により、クリーンインストールを必要とせずに有効にできる)とあり、公式の 2 ページで説明が一致していません(Learn 側のページは 2025-11-18 更新と表示。いずれも 2026 年 9 月 17 日に参照。どちらが新しい仕様なのかを当サイトは判断できません)。だからこそ推測せず、上の画面で確認してください。オフなら、この章のブロックはあなたの PC では起きません。

8.1 wheel は揃っていた

3.14t の venv で、現場で使うものを中心に入れてみました。

py -V:3.14t -m pip install numpy pandas matplotlib pyserial pymodbus pymcprotocol paho-mqtt

すべて cp314t の wheel で解決し、ソースビルドは一度も走りませんでした(2026 年 9 月 17 日時点・numpy 2.5.3 / pandas 3.0.5 / matplotlib 3.11.2、依存の contourpy・fonttools・kiwisolver・pillow も cp314t)。pyserial / pymodbus / pymcprotocol / paho-mqtt は純 Python の wheel なので、そもそも ABI タグを持ちません。「free-threaded ビルドで動くライブラリが無い」という段階は、少なくとも Windows では終わっていました。

8.2 ところが import が通らなかった

同じ機械・同じフォルダ・同じライブラリ版で、通常ビルド(3.14)と free-threaded ビルド(3.14t)だけを入れ替えて import した結果です。

モジュール3.14t(free-threaded)3.14(通常ビルド)
numpyブロックOK
pandasブロックOK
matplotlibブロックOK
matplotlib.pyplotブロックOK
kiwisolverブロックOK
contourpy._contourpyブロックOK
PIL._imagingOKOK
fontToolsOKOK
serial(pyserial)OKOK
pymodbusOKOK
pymcprotocolOKOK
paho.mqtt.clientOKOK
tkinterOKOK
sqlite3OKOK

「ブロック」の中身はこの例外です。

ImportError: DLL load failed while importing _multiarray_umath:
    アプリケーション制御ポリシーによってこのファイルがブロックされました。

free-threading の非対応警告ではなく、Windows 側がファイルの読み込みを拒否していました。イベントログ Microsoft-Windows-CodeIntegrity/Operational に対応する記録(イベント ID 3077 / 3033)が残っており、4 分ほどの計測時間の間に cp314t を対象とするものが 83 件ありました。cp314(通常ビルド)の .pyd では 1 件も出ていません。

再現性も確認しました。import numpy を 30 回連続で試した結果は 3.14t が 0 成功 / 30 ブロック、3.14 が 30 成功 / 0 ブロックです。たまたまではありませんでした。

8.3 署名の有無では説明がつかなかった

原因を切り分けるため、両ビルドの .pyd に署名検証をかけました。

Get-AuthenticodeSignature .\numpy\_core\_multiarray_umath*.pyd | Format-List Status

結果は cp314t の .pyd も cp314 の .pyd も等しく NotSigned。つまり署名の有無では、片方だけがブロックされる理由を説明できません。

Microsoft の公式 FAQ は、Smart App Control がまずクラウドのサービスで安全かどうかを予測すると説明しています。予測に確信が持てないときの扱いはこうです——"If the security service is unable to make a confident prediction about the app, then Smart App Control checks to see if the app has a valid signature"(確信のある予測ができないときは、有効な署名があるかを確認する)。続けて "If the app is unsigned, or the signature is invalid, Smart App Control will consider it untrusted and block it for your protection."(署名が無い、または署名が無効なら、信頼できないものと見なして保護のためにブロックする)とあります。

今回ブロックされた .pyd は cp314t 側も cp314 側も等しく未署名ですから、この説明に当てはめると、どちらも「署名で通る」経路には乗っていません。差が出たとすれば、残るのはクラウド側の予測です——ただし、当サイトは SAC の内部判定を確認できる立場になく、これが原因だと断定はしません。1 台の機械での、2026 年 9 月時点の観測です。

同じ cp314t でも PIL._imaging(C 拡張そのもの)は通っています。C 拡張なら一律に弾かれる、という単純な線引きではありませんでした(fontTools も通りましたが、import しただけでは同梱の C 拡張を読み込まないため、ここでは根拠にしていません)。

8.4 現場アプリにとっての意味

この結果には、悪い面と悪くない面の両方があります。

  • 悪い面: 当サイトの検証機と同じ条件(SAC が On・2026 年 9 月時点の cp314t wheel)では、3.14t で numpy / pandas / matplotlib を使うアプリが起動しませんでした。同じ構成の PC では同じことが起きる可能性があります(逆に、クラウド側の評価が変われば通るようになる可能性もあります)。しかも失敗するのは pip ではなく、動かした瞬間です。検証機で pip が通ったことを「導入できた」と判断すると、現場 PC で初めて気づくことになります
  • 悪くない面: 通信系は無傷でした。pyserial・pymodbus・pymcprotocol・paho-mqtt はいずれも純 Python なので、C 拡張の問題を踏みません。tkinter・sqlite3・logging も動きます。つまり「通信 + GUI + ログ」だけで組んだ監視アプリは、3.14t で動かせる範囲に入っています

import に成功したモジュールについては、import 後の sys._is_gil_enabled() がすべて False のままでした(tkinter・serial・pymodbus・pymcprotocol・paho.mqtt.client・sqlite3・logging・concurrent.futures、およびまとめて import した場合)。GIL を再有効化させたライブラリは、当サイトで import できた範囲では 1 つもありません。 集計やログの設計は Python logging 設計|ログが出ない・ローテーションが壊れるを実測で潰すを参照してください(free-threaded 環境でのログ集約の挙動は本記事では扱っていません)。

9. 結局、現場アプリで使うべきか — 条件付きの結論

ここまでの実測を、判断できる形に畳みます。

区分条件根拠
効く 純 Python の CPU 処理を threading で並列化する。集計・文字列処理・自前のフィルタ計算など 8 スレッドで ×4.06(4 章)
効く 重い計算の裏で GUI を止めたくない。画面の応答が業務上の要件になっている 再描画 27/210 回 → 200/200 回(7 章)
効かない デバイスの応答待ち・タイムアウト待ちが支配するポーリング処理 4 ランタイム横並び・差ゼロ(6.2)
効かない すでに asyncio で待ちを畳んである処理 同上(GIL が律速でないため)
罠 numpy / pandas / matplotlib を使うアプリ + Smart App Control が On の PC(当サイトの検証機での観測・2026 年 9 月時点) import numpy が 30 回中 30 回ブロック(8 章)
コスト 単一スレッドで動く部分は遅くなる 3.14 既定ビルド比 +8.0%(5 章)

当サイトの立場を書きます。2026 年 10 月時点で、現場 PC の標準ランタイムを 3.14t にすることは勧めません。 理由は 3 つです。既定ビルドではないこと、単一スレッドで 8% の税金がかかること、そして C 拡張まわりの不確実性(8 章)が残っていることです。

一方で「まだ関係ない技術」でもありません。7 章の差は、設計を変えずにランタイムを差し替えるだけで得られたものです。重い計算の裏で画面が固まるという症状を抱えているなら、検証機で 3.14t を試す価値は十分にあります。 ただし順番があります。安いものから試してください。

  1. 3.12 から 3.14 の既定ビルドへ上げる。 純 CPU ワークロードはそれだけで所要時間が 14.1% 縮みました(5 章)。既定ビルドのままなので C 拡張の互換性リスクがありません
  2. 重い計算を numpy や multiprocessing に逃がす。 「純 Python で数百ミリ秒」の処理は、配列演算に置き換えるだけで消えることが珍しくありません(10 章)
  3. それでも純 Python の計算が数百ミリ秒以上残り、その間 GUI や通信を止めたくない——ここまで来て初めて、検証機で 3.14t を評価する番です

ランタイムの差し替えは、この 3 段の一番上ではなく一番下にあります。

10. threading / multiprocessing / asyncio の使い分け

free-threading は選択肢を 1 つ増やしただけで、既存の 3 つを置き換えるものではありません。実測から見えた住み分けを表にします。

やりたいことまず選ぶもの理由
PLC・シリアル・MQTT の待ちを重ねたい threading(既定ビルドのまま) 待ち時間は GIL ありでも手放される。8 スレッドで ×8.1(6.2)
接続数が数十〜数百に増える asyncio スレッドを増やさずに待ちを畳める。実装は socket の実戦ガイド
純 Python の重い計算を速くしたい multiprocessing、または 3.14t + threading 既定ビルドでは threading は効かない(×1.00)
計算の裏でGUI を止めたくない multiprocessing、または 3.14t + threading 既定ビルドで threading に置くと画面が止まる(7 章)
numpy / pandas の重い処理 既定ビルドのまま ライブラリ内部で GIL を手放す区間があり、SAC の問題も避けられる

multiprocessing と 3.14t + threading の比較には、実測していない論点が残っています。プロセス分割はデータの受け渡しにコスト(pickle 化とプロセス間通信)がかかり、Windows では起動も重い一方、既定ビルドのまま使えて C 拡張の心配がありません。3.14t + threading はデータ共有が単純になりますが、共有オブジェクトの競合は自分で守る必要があります。GIL が無くなってもデータ競合は無くなりません——むしろ、これまで GIL が偶然隠していた競合が表に出ます。

count += 1 のような「読み出す → 足す → 書き戻す」は、GIL があるビルドでも本来はアトミックではありません。GIL のもとではスレッドの切り替わり方が粗く、たまたま壊れずに済んでいた場面が多かっただけです。GIL を外すと同じコードが本当に同時に走るので、その「たまたま」が消えます。既存コードを 3.14t に載せ替えるなら、先にこの 4 点を確認してください。

  • 複数のスレッドが書き込む int・dict・list・set をすべて列挙したか
  • その更新を threading.Lock で囲んだか、queue.Queue に渡す形へ付け替えたか
  • 「1 行だから安全」と思っている箇所を見落としていないか(count += 1/d[k] = d.get(k, 0) + 1/最大値の比較更新)
  • 集計途中の値を GUI スレッドや別スレッドから読んでいないか。読むならスナップショットを渡す

この 4 点は公式仕様と一般的な並行処理の原則から書いたもので、当サイトが実測で壊してみた結果ではありません。壊れる・壊れないの境目は、自分のコードで確認してください。

現場 PC のログ出力まわりで文字コードが絡む場合は cp932 と UTF-8 の完全対策を参照してください(本記事は文字コードの話題には踏み込みません)。

11. 手元で再現するスクリプト(111 行・標準ライブラリのみ)

4 章(実測 A)と 5 章(実測 D)を、自分の機械で再現するための完成スクリプトです。断片ではなく、これ 1 本をそのまま保存して実行できます。 外部ライブラリを使っていないので、pip の通らない PC でも動きます。bench_cpu.py という名前で保存し、py -V:3.14t bench_cpu.py のように呼び出す形です。本記事の実測値から逆算すると、既定の設定で 1 ランタイムあたり 20〜40 秒、4 ランタイムを順に回して約 2 分です。

コア数の違う機械では倍率が変わるので、本記事の数値を信じずに自分の機械で測ってください。それがこの記事で一番役に立つ使い方です。

"""free-threaded ビルドの効き目を自分の機械で測る(本記事の実測 A と D)

実行例(Windows・py ランチャー):
    py -V:3.12  bench_cpu.py
    py -V:3.14  bench_cpu.py
    py -V:3.14t bench_cpu.py                  # GIL なし(free-threaded の既定)
    py -V:3.14t -X gil=1 bench_cpu.py         # 同じビルドで GIL だけ戻す

固定の総仕事量(3 〜 limit の奇数を試し割りで素数判定)をスレッドに配り、
1 / 2 / 4 / 8 スレッドの所要時間を比べる。1 スレッドの値が実測 D にあたる。
標準ライブラリだけで動くので、pip の通らない PC でもそのまま実行できる。
"""

import argparse
import json
import os
import statistics
import sys
import sysconfig
import threading
import time


def count_odd_primes(start, stop, step):
    """start は奇数・step は偶数。奇数だけを試し割りで判定し、素数の個数を返す。"""
    hits = 0
    for n in range(start, stop, step):
        i = 3
        prime = True
        while i * i <= n:
            if n % i == 0:
                prime = False
                break
            i += 2
        if prime:
            hits += 1
    return hits


def one_run(nthreads, limit):
    """nthreads 本で 1 回計測する。戻り値は (所要秒, 見つけた素数の個数)。"""
    out = [0] * nthreads
    barrier = threading.Barrier(nthreads)      # 全スレッドが揃ってから走り出す

    def worker(tid):
        barrier.wait()
        # 候補 3, 5, 7, ... を飛び飛びに配る(軽い数と重い数が各スレッドに均等に入る)
        out[tid] = count_odd_primes(3 + 2 * tid, limit, 2 * nthreads)

    threads = [threading.Thread(target=worker, args=(i,)) for i in range(nthreads)]
    t0 = time.perf_counter()
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    return time.perf_counter() - t0, sum(out) + 1      # +1 は素数 2 の分


def describe_runtime():
    """どのランタイムで測ったのかを必ず一緒に残す(取り違えの事故が一番多い)。"""
    return {
        "version": sys.version.split()[0],
        "gil_enabled": sys._is_gil_enabled() if hasattr(sys, "_is_gil_enabled") else None,
        "py_gil_disabled": sysconfig.get_config_var("Py_GIL_DISABLED"),
        "PYTHON_GIL": os.environ.get("PYTHON_GIL"),
        "cpu_count": os.cpu_count(),
    }


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--limit", type=int, default=1_000_000, help="素数判定の上限")
    ap.add_argument("--reps", type=int, default=5, help="各スレッド数での試行回数")
    ap.add_argument("--threads", default="1,2,4,8")
    ap.add_argument("--json", action="store_true", help="生データを JSON でも出す")
    args = ap.parse_args()

    counts = [int(x) for x in args.threads.split(",")]
    env = describe_runtime()
    env.update(limit=args.limit, reps=args.reps)

    print(f"Python {env['version']}  "
          f"free-threaded build={bool(env['py_gil_disabled'])}  "
          f"GIL={env['gil_enabled']}  CPU={env['cpu_count']}")
    if env["py_gil_disabled"] and env["gil_enabled"]:
        print("  * GIL が有効なままです(PYTHON_GIL / -X gil / C 拡張のどれかで戻っています)")

    # ウォームアップ。ここを省くと 1 スレッド目だけ極端に遅い値が出る
    for _ in range(3):
        one_run(4, args.limit // 2)

    results, base = {}, None
    print(f"{'threads':>8}{'median[s]':>12}{'min[s]':>10}{'speedup':>10}")
    for n in counts:
        samples, primes = [], None
        for _ in range(args.reps):
            elapsed, primes = one_run(n, args.limit)
            samples.append(round(elapsed, 4))
        med = round(statistics.median(samples), 4)
        if base is None:
            base = med
        results[str(n)] = {"samples": samples, "median": med,
                           "min": round(min(samples), 4), "primes": primes}
        print(f"{n:>8}{med:>12.3f}{min(samples):>10.3f}{base / med:>9.2f}x")

    if args.json:
        print(json.dumps({"env": env, "results": results}, ensure_ascii=False))


if __name__ == "__main__":
    main()

実行すると、まずどのランタイムで走っているかが 1 行目に出ます。次は検証機で 3.14t を走らせたときの実測値を、このスクリプトの書式で並べたものです(数値は実測 A の順方向 1 巡分)。

Python 3.14.7  free-threaded build=True  GIL=False  CPU=12
 threads   median[s]    min[s]   speedup
       1       1.770     1.763     1.00x
       2       1.021     0.983     1.73x
       4       0.623     0.588     2.84x
       8       0.447     0.405     3.96x

これは 1 巡分の値です。4 章の表は実行順を逆にした 2 回分をならした値なので、末尾の桁は一致しません(表の 8 スレッドは ×4.06、この回単独では ×3.96)。1 回測って終わりにしないでください——順序効果でこの程度は動きます。

押さえておくポイントを 3 つ書きます。

  • 1 行目を必ず確認してください。 free-threaded build=True なのに GIL=True なら、環境変数か -X gil か C 拡張のどれかで GIL が戻っています。この状態で測った数値は「free-threaded の性能」ではありません
  • ウォームアップの 3 回は省かないでください。 当サイトも初回の計測でここが不足し、1 スレッドの値が最大 60% ずれました。実行順を逆にした再計測で気づいた、実際に踏んだ間違いです
  • 他の作業をしながら測らないでください。 ブラウザやウイルス対策のスキャンが走っていると、数値は簡単に 2 割動きます

12. よくある質問(FAQ)

3.14t を入れると既存の Python 3.12 環境は壊れますか

当サイトの検証機では、3.2 の per-user インストール後もユーザー環境変数の PATH は Python312 のままで変化せず、py.exe のタイムスタンプも更新されませんでした。ただし py -0 の既定マーク(*)は 3.12 から 3.14t に移りました。py はバージョン指定が無いと最も新しいものを選ぶためです。バッチやタスクスケジューラの起動行が py script.py や python script.py になっていると、実行されるランタイムが黙って変わります。導入前に py -0 の出力を控え、日次処理の起動行が絶対パス指定か py -V:3.12 指定になっているかを確認してください(3.3)。

numpy や pandas は free-threaded ビルドで使えますか

2026 年 9 月 17 日時点で、numpy 2.5.3 / pandas 3.0.5 / matplotlib 3.11.2 はいずれも cp314t の Windows 向け wheel を配布しており、pip install はソースビルドを一度も走らせずに完了。ところが当サイトの検証機(Smart App Control が On)では、その後の import が numpy / pandas / matplotlib / kiwisolver / contourpy で失敗しました。同じ機械・同じライブラリ版で通常ビルド(3.14)に入れ替えると、import numpy の 30 回連続試行が 30 回とも成功しました。SAC が Off の機械での挙動は当サイトでは確認できていません(8 章)。

PLC のポーリングを速くしたいのですが free-threading は効きますか

当サイトの実測では効きませんでした。デバイス応答 10 ミリ秒を模擬した条件で、4 ランタイムはすべて 1 スレッド 92 回/秒・8 スレッド 743〜749 回/秒で横並びです。スループットを決めているのは待ち時間であって GIL ではありません。スレッドを増やすだけで 8 スレッド 8.1 倍まで伸びるので、既定ビルドのままスレッド数とポーリング周期を見直すのが先です(6.2)。

GIL が本当に無効になっているかはどう確認しますか

プロセスの状態は sys._is_gil_enabled()、ビルドの種別は sysconfig.get_config_var("Py_GIL_DISABLED") で判定します。この 2 つは別物です。 free-threaded ビルドでも、PYTHON_GIL=1・-X gil=1・free-threading 非対応の C 拡張の import によって GIL が戻っていることがあります。ベンチマークを取る前に必ず両方を出力してください(11 章のスクリプトは 1 行目に出します)。

いま現場の PC に 3.14t を入れるべきですか

当サイトはまだ既定ビルドを使い続けることを勧めます。既定ビルドではないこと、単一スレッドが 8.0% 遅いこと、C 拡張まわりの不確実性が残ることが理由です。効果が見込めるのは「純 Python の CPU 処理が数百ミリ秒以上続き、その間に GUI や通信を止めたくない」場合に限られます。まずは既定ビルドのまま重い処理を multiprocessing に逃がす設計を試し、それでも足りないときに検証機で評価してください(9 章)。

13. 検証環境と、当サイトが測れなかったこと

右の列は「当サイトでは確認できていない」という意味で、間違いという意味ではありません。 結果が本記事と食い違ったら、それはあなたの環境での事実です。

実測した(検証機で実行)実測していない・扱っていない
純 CPU の threading スケーリング(1 / 2 / 4 / 8 スレッド ×4 ランタイム ×5 回)と単一スレッドのオーバーヘッド コア数の違う機械での倍率。 本記事は(13 章末の 3.15 追記を除き)単一の機械・単一の CPU(6 コア / 12 スレッド)の結果です。コア数が変われば 4 章の倍率は変わります
3.14t 上での numpy / pandas / matplotlib の pip install(cp314t wheel で解決)と import 可否(30 回連続) 3.14t 上で numpy / pandas / matplotlib を実際に動かした性能。 SAC にブロックされたため、import できるかどうかまでしか分かっていません(8 章)
import 後の sys._is_gil_enabled()(tkinter・serial・pymodbus・pymcprotocol・paho.mqtt.client・sqlite3・logging・concurrent.futures) 「free-threading 非対応の C 拡張が GIL を再有効化する」公式仕様の挙動。 import できた範囲では GIL は False のままで、当サイトの環境では再現できていません。2.4 の記述は公式ドキュメントの紹介であって、当サイトの実測ではありません
Smart App Control が On のままでのインストールと import(イベントログの記録まで確認) SAC を切る/例外登録したときの挙動。 機械の設定は一切変えていません。また SAC の内部判定は確認できないため、8.3 の説明は公表情報との整合の指摘であって原因の断定ではありません
疑似 PLC への SLMP ポーリング(応答待ちゼロ/10 ミリ秒模擬の 2 条件) 実機の PLC。 当サイトは実機 PLC を所有しておらず、すべてダミーサーバーです。B-2 の 10 ミリ秒は模擬値で、実際の機種・スキャン時間・ネットワークによって変わります
tkinter 監視画面のドリフト(背景 0 本/4 本 ×4 ランタイム ×5 回)と Tk のバージョン確認 PyQt6 / PySide6 での同じ計測と、24 時間以上の連続稼働。10 秒 ×5 回の短時間計測です。長時間でのメモリ挙動は測っていません
Python 3.14.7(通常ビルド/free-threading ビルド)と 3.12.10 Windows の検証機での Python 3.15。3.15.1 は別の機械(Linux の仮想マシン)でだけ測りました(この章の末尾)。3.13 の free-threaded ビルドも測っていません
電源プラン「バランス」・AC 接続での計測 「高パフォーマンス」電源プランでの再計測。 設定は変えていません。バッテリー駆動も試していません
— 他サイト・他社が公表しているベンチマークとの突き合わせ。 本記事は他所の倍率を引用していません。13 章末の 3.15 追記を除き、すべて自宅検証機の実測値です

検証環境は次のとおりです。

  • 機械: 自宅の検証機(Windows 11 Pro・ビルド 26200・デスクトップ向け x86-64 CPU・物理 6 コア / 論理 12 スレッド・RAM 16GB・AC 接続・電源プラン「バランス」)。会社支給の端末は使っていません。
  • ランタイム: CPython 3.12.10 / 3.14.7(通常ビルド)/ 3.14.7 free-threading ビルド(python3.14t.exe)。3.14t は PYTHON_GIL=1 で GIL を戻した状態も計測しています
  • 計測条件: 各条件 5 回実行の中央値。ウォームアップ 3 回は破棄。実測 A・D は実行順を逆にした 2 巡分の中央値を平均した値です
  • 計測日: 2026 年 9 月 17 日。処理時間は同じ機械でも実行するたびに数 % 前後します

本記事の数値・手順は当サイトの検証機で確認したもので、他の環境で同じ結果になることを保証するものではありません。とくに 8 章の SAC の挙動は、Windows の構成・ライブラリのバージョン・時期によって変わりえます。

追記(2026-10-26): Python 3.15.1 で測り直した結果 — 4・6・7 章の結論は変わらなかった

先に注意です。 この追記の数値は、本文の Windows 検証機ではなく、Linux の仮想マシン(Linux Mint 22.3・vCPU 4)で測りました。コア数も OS も違うので、本文の数値とこの追記の数値を並べて比べることはできません。そのため同じ仮想マシンで 3.14.7 も測り直し、3.14 と 3.15 の比較はすべて同じ機械の中で行っています。ランタイムは python-build-standalone の CPython 3.14.7 / 3.14.7t / 3.15.1 / 3.15.1t(uv で導入)です。スクリプトは、A・D が 11 章のもの、B・C・E が 9 月の計測と同じもの(B のダミーサーバーは 3.14 通常ビルド)で、ランタイムの一覧だけを差し替えました。各条件 5 回の中央値です。

計測3.14.7 系3.15.1 系読み方
A: 純 CPU・GIL なし・4 スレッド(1 スレッド比)0.89 秒(×2.31)0.68 秒(×2.81)伸びた。GIL ありの 2 ランタイムは、どちらの版でもスレッドを増やすと逆に遅くなった(×0.65〜0.71)
D: 1 スレッドの速さ(通常ビルド)1.991 秒1.675 秒(15.9% 速い)3.15 の通常ビルドが速くなった。理由はこの計測からは分からない
D: 入場料(GIL なし版 ÷ 同じ版の通常ビルド)+3.0%+14.6%GIL なし版も 2.050 秒 → 1.919 秒と速くなったが、通常ビルドの伸びのほうが大きく、差が開いた。3.15t は正順・逆順で 1.862 / 1.975 秒と 6% 揺れた
B-2: PLC ポーリング・応答 10 ミリ秒・8 スレッド719〜734 回/秒717〜731 回/秒6 条件とも横並び。6 章の「答えはノー」は 3.15 でも同じ
C: tkinter・背景 4 スレッド(再描画回数/予定)GIL あり 18〜19 回/205〜219 回、GIL なし 201/201GIL あり 22〜33 回/205〜210 回、GIL なし 201/2017 章の結論は 3.15 でも同じ。GIL なしのドリフト p95 は 1.9 ミリ秒(3.14t)と 2.1 ミリ秒(3.15t)
E: 12 モジュールの import 後の GIL全部 False全部 Falsenumpy 2.5.4・pandas 3.1.0・matplotlib 3.11.2・pymodbus 3.16.1 を含む。tkinter も tk.Tk() の後まで False

読み方の注意が 2 つあります。 ①E は Linux での結果です。Smart App Control は Windows の機能なので、8 章で numpy が弾かれた件を 3.15t で測り直したわけではありません。言えるのは「Linux では 3.14t・3.15t とも numpy・pandas・matplotlib を import しても GIL は無効のままだった」までです。②A の「GIL ありだとスレッドを増やすほど遅くなる」という結果は、この仮想マシンに特有の可能性があります。本文の Windows 検証機では、GIL ありはほぼ ×1.00 で横ばいでした。

Windows の検証機ではまだ測っていない項目は 3 つです。3.15 の公式インストーラ(Include_freethreaded=1)と py -V:3.15t の動き(3 章)、Smart App Control 下での import(8 章)、そして本文と同じ 6 コアの機械での A〜C の倍率です。測ったらこの欄に追記します。

⚠️ 実機適用時の注意: ランタイムの差し替えは設備の安全を担う仕組みの代替にはなりません。稼働中のラインで試すことは避けてください。実際の現場 PC に適用するときは、①対象の PC とネットワークについて設備保全部門・情報システム部門の承認を得る、②24 時間稼働している PLC や設備には接続せず、計画停止中か、読み出し専用の検証環境で確認する、③安全インターロック・安全 PLC・法定の警報装置は Python アプリで置き換えない、④既存の日次バッチが使うランタイムが変わっていないことを 3.3 の手順で確認する——この 4 点が前提です。適用による設備の停止・データ欠損・機会損失について、筆者・GenbaPy は責任を負いません。

Python は Python Software Foundation(PSF)の商標です。本記事は PSF およびその関連団体とは無関係の第三者による解説です。本記事で触れたその他の製品名・会社名(Microsoft・Windows・Smart App Control・Intel・SLMP など)は、各社の商標または登録商標です(利用規約の商標に関する条項)。本サイトの AI 利用方針はサイトについてに記載しています。

当サイトが何者で、どうやって測っているのかは サイトについてにまとめています。あなたの機械で本記事と違う結果が出たなら、それは当サイトが一番知りたい情報です。 お問い合わせから教えてください。数値の誤りの指摘も歓迎します。

関連記事

参考文献・一次情報

いずれも 2026-09-17 に参照しました(What's New In Python 3.15 は 2026-10-26 に読み直し)。