これは、世界で最も人気のあるプログラミング言語の開発における重要なポイントです。 Python はその読みやすさと大規模なエコシステムで長い間認められてきましたが、その実行速度は「部屋の中の象」であることがよくありました。
3.14 の登場により、CPython コア開発チームは、最近最も待ち望まれていた機能を 1 つではなく 2 つ提供しました。
ギルの終わり
これについては以前にも書きました。必要に応じて、真の同時実行性を Python で利用できるようになりました。 GIL フリー Python についてさらに詳しく知りたい場合は、それに関する私の記事へのリンクを最後に残しておきます。
ジャストインタイム (JIT) コンパイラー
この実験的な機能は現在、公式インストーラーに直接バンドルされており、ここではこれに焦点を当てます。これは、データ サイエンスから Web バックエンドまであらゆるものを強化する C 拡張エコシステムを壊すことなく、Python を「デフォルトで高速」にすることを目的とした、Python コア チームと他のチームによる長年にわたるアーキテクチャの準備の成果です。
この記事では、新しい JIT の概要を説明し、これまでの最適化の取り組みとどのように異なるかを検討し、ワークロードで JIT を試す時期であるかどうかを判断するのに役立つベンチマーク手法をいくつか取り上げます。
Python の新しい Just-in-Time (JIT) コンパイラとは何ですか?
3.14 JIT を理解するには、Python が従来どのように動作するかを知る必要があります。標準 Python (CPython) は、 説明 言語。スクリプトを実行すると、コードがコンパイルされます バイトコード、 これは、CPython 仮想マシンが実行する一連の命令です。
JIT はこの流れを変えます。 JIT は、単純にバイトコードを 1 行ずつ解釈するのではなく、コードのどの部分が最も頻繁に実行されるかを監視します (「ホット」パス)。関数またはループが「ホット」に解釈されると、JIT はバイトコードをネイティブ マシン コード (CPU が理解する命令) に変換します。そうすれば、次回コードが呼び出されるとき、解釈は必要ありません。代わりに、そのまま動作します。後で説明するように、これは時間を大幅に節約できます。
JIT は CPython にどのように適合しますか
Python 3.14 JIT は完全な書き直しではありません。これは、既存のインタープリターと連携するオプトイン コンポーネントとして設計されています。これは、「コピー アンド パッチ」と呼ばれる手法を使用しており、LLVM のような巨大で複雑なコンパイラ バックエンドを必要とせずに、JIT を軽量にし、さまざまな CPU アーキテクチャ間で移植できるようにします。
Python 3.14 では何が変わりましたか?
Python 3.13 には基本的な実験的な JIT がありましたが、デフォルトでは無効になっていました。これをテストしたい場合は、CPython ソース ツリーのクローンを作成し、次のような特定の実験フラグを使用してコンパイルする必要があります。 - - enable-experimental-jit。
Python 3.14 ではすべてが変わりました。公式の .msi (Windows) および .pkg (macOS) インストーラーで JIT を提供しました。これは、JIT のメリットを享受するためにマシンに C コンパイラが必要なくなることも意味します。まだ「実験的」ではありますが、公式バイナリに含まれていることは、コア チームが JIT が広範なコミュニティ テストに十分安定していると信じていることを示しています。
Python 3.14 の入手
https://www.python.org/downloads/ に移動すると、3.14 のダウンロード オプションが表示されます。それをクリックして、指示に従ってください。
あるいは、持っている場合は、 紫外線 ツールがインストールされたら、次のように入力できます。
PS C:\ > uv python install 3.14
JIT の有効化
デフォルトでは、JIT は 無効。これはセキュリティ対策です。これは実験的なものであるため、Python Steering Council は、ユーザーが明示的に選択せずに安定性やメモリ使用量の予期せぬ低下に悩まされないようにしたいと考えています。
JIT をアクティブにするには、環境変数を使用します。これにより、CPython ランタイムが起動時に JIT エンジンを初期化するように指示されます。
Windows の場合 (PowerShell):
$env:PYTHON_JIT=1
python my_script.py
MacOS/Linux (Bash/Zsh) の場合:
PYTHON_JIT=1
python my_script.py
CPython を有効にすると、すぐにすべてが JIT コンパイルされるわけではありません。それはを使用します 階層化システム。 基本的に、最初はできるだけ安価にコードを実行しようとし、ホットであることが判明した部分にのみコンパイル/最適化の労力を費やします。
- 階層 0: 標準的な解釈。
- 階層 1: 特殊なバイトコード (3.11 で導入)。
- 階層 2 (JIT): 最も一般的に使用されるパスのマシンコード生成。
JIT の影響の測定
JIT をテストする場合、単純に使用することはできません 時間.時間() 関数の周り。 JITは必須です ウォームアップ期間。 JIT がコードをプロファイリングするため、ループの最初の数回の反復は通常よりも遅くなる可能性がありますが、その後の反復は大幅に高速になる可能性があります。
ベンチマークスイート
以下は、複雑な数学から複雑なオブジェクト操作まで、JIT のさまざまな側面を実践するために設計された包括的なテスト スイートです。
ファイル 1:workloads.py
このファイルには、3 つの異なる CPU バウンド タスクが含まれています。
1/ マンデルブロ関数は、ピクセル グリッド上でマンデルブロ式を反復し、ピクセルごとの反復回数のチェックサムを返します。
2/ ダイクストラ関数は、決定論的ランダム加重グラフを作成し、ノード 0 からダイクストラを実行して、完了/訪問されたノードの数を示します。
3/ レーベンシュタイン関数は、n 個の決定論的なランダム文字列ペアを生成し、それらのレーベンシュタイン距離の合計を返します。
from __future__ import annotations
import random
import heapq
# Workload 1: Mandelbrot (CPU + math loops)
def mandelbrot(width: int = 1000, height: int = 1000, iters: int = 500) -> int:
checksum = 0
for y in range(height):
cy = (y / height) * 2.4 - 1.2
for x in range(width):
cx = (x / width) * 3.2 - 2.2
zx, zy, count = 0.0, 0.0, 0
while zx * zx + zy * zy <= 4.0 and count < iters:
zx, zy = zx * zx - zy * zy + cx, 2.0 * zx * zy + cy
count += 1
checksum += count
return checksum
# Workload 2: Dijkstra (heap + list + logic)
def dijkstra(n: int = 10000, edges_per_node: int = 50, seed: int = 123) -> int:
rng = random.Random(seed)
graph = [[] for _ in range(n)]
for u in range(n):
for _ in range(edges_per_node):
v = rng.randrange(n)
if v != u:
graph[u].append((v, rng.randrange(1, 30)))
dist = [10**12] * n
dist[0] = 0
pq = [(0, 0)]
visited = 0
while pq:
d, u = heapq.heappop(pq)
if d != dist[u]:
continue
visited += 1
for v, w in graph[u]:
nd = d + w
if nd < dist[v]:
dist[v] = nd
heapq.heappush(pq, (nd, v))
return visited
# Workload 3: Levenshtein distance (dynamic programming)
def levenshtein(a: str, b: str) -> int:
prev = list(range(len(b) + 1))
for i, ca in enumerate(a, 1):
cur = [i]
for j, cb in enumerate(b, 1):
cur.append(min(cur[j - 1] + 1, prev[j] + 1, prev[j - 1] + (ca != cb)))
prev = cur
return prev[-1]
def levenshtein_batch(n: int = 10000, seed: int = 7, k: int = 50) -> int:
"""
Deterministic batch: fixed RNG seed, fixed alphabet, fixed string length.
Returns the sum of distances.
"""
rng = random.Random(seed)
alphabet = "abc"
total = 0
for _ in range(n):
a = "".join(rng.choices(alphabet, k=k))
b = "".join(rng.choices(alphabet, k=k))
total += levenshtein(a, b)
return total
ファイル 2: ベンチマーク.py
このスクリプトは、JIT が有効な場合と無効な場合のさまざまなワークロードの比較を自動化します。
import os
import time
import json
import subprocess
from pathlib import Path
PYTHON_EXE = r"C:\Users\thoma\AppData\Local\Programs\Python\Python314\python.exe"
PROJECT_DIR = Path(__file__).resolve().parent
# Original workloads (statement prints a result for sanity)
WORKLOADS = [
("mandelbrot", 'from workloads import mandelbrot; print(mandelbrot())'),
("dijkstra", 'from workloads import dijkstra; print(dijkstra())'),
("levenshtein_batch", 'from workloads import levenshtein_batch; print(levenshtein_batch())'),
]
N_RUNS = 10 # average of ALL runs (set to 6/10/20 as you like)
OUTFILE = PROJECT_DIR / "results_avg.json"
def run_once(stmt: str, jit_val: int) -> tuple[float, str]:
env = os.environ.copy()
env["PYTHON_JIT"] = str(jit_val)
# Ensure local workloads.py is importable in subprocess
env["PYTHONPATH"] = str(PROJECT_DIR) + (os.pathsep + env.get("PYTHONPATH", ""))
t0 = time.perf_counter()
p = subprocess.run(
[PYTHON_EXE, "-c", stmt],
env=env,
cwd=str(PROJECT_DIR),
capture_output=True,
text=True,
)
t1 = time.perf_counter()
if p.returncode != 0:
raise RuntimeError(
f"Run failed (PYTHON_JIT={jit_val})\n\n"
f"Statement:\n{stmt}\n\n"
f"STDOUT:\n{p.stdout}\n\nSTDERR:\n{p.stderr}"
)
return (t1 - t0, p.stdout.strip())
def summarize(times: list[float]) -> dict:
return {
"avg": sum(times) / len(times),
"min": min(times),
"max": max(times),
"runs": times,
}
def bench_workload(name: str, stmt: str) -> dict:
results = {}
outputs = {}
for jit_val in (0, 1):
times = []
outs = []
print(f" PYTHON_JIT={jit_val}: running {N_RUNS} times...")
for i in range(1, N_RUNS + 1):
dt, out = run_once(stmt, jit_val)
times.append(dt)
outs.append(out)
print(f" run {i}/{N_RUNS}: {dt:.6f}s")
results[jit_val] = summarize(times)
outputs[jit_val] = outs
avg0 = results[0]["avg"]
avg1 = results[1]["avg"]
speedup = avg0 / avg1 if avg1 else float("inf")
delta_pct = (avg1 - avg0) / avg0 * 100.0 if avg0 else 0.0
return {
"workload": name,
"jit0": results[0],
"jit1": results[1],
"speedup_jit0_over_jit1": speedup,
"delta_pct_jit1_vs_jit0": delta_pct,
"outputs": outputs, # sanity: should be stable
}
def main() -> int:
all_results = []
print(f"Using Python: {PYTHON_EXE}")
print(f"Project dir: {PROJECT_DIR}")
print(f"Runs per setting (avg of all runs): {N_RUNS}\n")
for name, stmt in WORKLOADS:
print(f"=== {name} ===")
r = bench_workload(name, stmt)
all_results.append(r)
print(f"\n Averages:")
print(f" JIT=0 avg: {r['jit0']['avg']:.6f}s (min {r['jit0']['min']:.6f}, max {r['jit0']['max']:.6f})")
print(f" JIT=1 avg: {r['jit1']['avg']:.6f}s (min {r['jit1']['min']:.6f}, max {r['jit1']['max']:.6f})")
print(f" Speedup (JIT=0 / JIT=1): {r['speedup_jit0_over_jit1']:.3f}× (Δ={r['delta_pct_jit1_vs_jit0']:+.2f}%)\n")
# Optional: warn if outputs vary across runs (nondeterminism)
if len(set(r["outputs"][0])) != 1:
print(" !! WARNING: JIT=0 output differs across runs (nondeterministic workload?)")
if len(set(r["outputs"][1])) != 1:
print(" !! WARNING: JIT=1 output differs across runs (nondeterministic workload?)")
OUTFILE.write_text(json.dumps(all_results, indent=2), encoding="utf-8")
print(f"Wrote: {OUTFILE}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
これが私の結果です。
C:\Users\thoma\projects\python_jit>C:\Users\thoma\AppData\Local\Programs\Python\Python314\python.exe benchmark.py
Using Python: C:\Users\thoma\AppData\Local\Programs\Python\Python314\python.exe
Project dir: C:\Users\thoma\projects\python_jit
Runs per setting (avg of all runs): 10
=== mandelbrot ===
PYTHON_JIT=0: running 10 times...
run 1/10: 6.890924s
run 2/10: 6.950737s
run 3/10: 7.265357s
run 4/10: 6.947150s
run 5/10: 6.932333s
run 6/10: 6.939378s
run 7/10: 7.194705s
run 8/10: 6.995550s
run 9/10: 6.902696s
run 10/10: 7.256164s
PYTHON_JIT=1: running 10 times...
run 1/10: 5.216740s
run 2/10: 5.241888s
run 3/10: 5.350822s
run 4/10: 5.246767s
run 5/10: 5.294771s
run 6/10: 5.273295s
run 7/10: 5.272135s
run 8/10: 5.617062s
run 9/10: 5.251656s
run 10/10: 5.239060s
Averages:
JIT=0 avg: 7.027499s (min 6.890924, max 7.265357)
JIT=1 avg: 5.300420s (min 5.216740, max 5.617062)
Speedup (JIT=0 / JIT=1): 1.326× (Δ=-24.58%)
=== dijkstra ===
PYTHON_JIT=0: running 10 times...
run 1/10: 0.235401s
run 2/10: 0.227603s
run 3/10: 0.244492s
run 4/10: 0.232971s
run 5/10: 0.249589s
run 6/10: 0.232229s
run 7/10: 0.229422s
run 8/10: 0.238399s
run 9/10: 0.230657s
run 10/10: 0.235772s
PYTHON_JIT=1: running 10 times...
run 1/10: 0.238862s
run 2/10: 0.239266s
run 3/10: 0.240312s
run 4/10: 0.231413s
run 5/10: 0.232692s
run 6/10: 0.233783s
run 7/10: 0.230016s
run 8/10: 0.237760s
run 9/10: 0.240895s
run 10/10: 0.246033s
Averages:
JIT=0 avg: 0.235653s (min 0.227603, max 0.249589)
JIT=1 avg: 0.237103s (min 0.230016, max 0.246033)
Speedup (JIT=0 / JIT=1): 0.994× (Δ=+0.62%)
=== levenshtein_batch ===
PYTHON_JIT=0: running 10 times...
run 1/10: 2.176256s
run 2/10: 2.171253s
run 3/10: 2.171834s
run 4/10: 2.170444s
run 5/10: 2.149874s
run 6/10: 2.162820s
run 7/10: 2.171975s
run 8/10: 2.199151s
run 9/10: 2.168398s
run 10/10: 2.167821s
PYTHON_JIT=1: running 10 times...
run 1/10: 1.575666s
run 2/10: 1.612615s
run 3/10: 1.571106s
run 4/10: 1.584650s
run 5/10: 1.579948s
run 6/10: 1.582633s
run 7/10: 1.593924s
run 8/10: 1.573608s
run 9/10: 1.581427s
run 10/10: 1.578553s
Averages:
JIT=0 avg: 2.170983s (min 2.149874, max 2.199151)
JIT=1 avg: 1.583413s (min 1.571106, max 1.612615)
Speedup (JIT=0 / JIT=1): 1.371× (Δ=-27.06%)
結果の解釈
ご覧のとおり、結果はまちまちです。これは実験的な JIT では正常です。
- 10 ~ 30% のスピードアップ: JIT がバイトコード ディスパッチ ループのオーバーヘッドを回避できる「純粋な Python」ループ (マンデルブロ テストやレーベンシュタイン テストなど) で一般的です。
- 0% 改善: I/O バウンドのタスクや、C 拡張機能を多用するコードでよく見られます。 Dijkstra のコードは高速化されませんでした。そのランタイムは、現在の CPython JIT が大幅に最適化していないヒープ/タプル操作とメモリを大量に使用する割り当て主導の作業によって支配されているためです。そのため、インタープリタで節約した効果はノイズの中に消えてしまいます。
Python 3.14 JIT を使用する場合
JIT は強力なツールですが、「魔法のボタン」ではありません。私の経験から言えば、次のような場合には JIT を試してみてください。
- CPU バウンドのロジック: アプリケーションは、複雑な計算、データ処理、または複雑なロジックを純粋な Python で実行します。
- 長時間実行されるプロセス: Web サーバー (Gunicorn/Uvicorn) またはバックグラウンド ワーカー (Celery) は何時間も実行され、JIT にホット パスのウォームアップと最適化に十分な時間を与えます。
- 実技試験: JIT がより積極的に使用される可能性がある Python の将来のバージョン (3.15 以降) に向けてコードベースを準備したいと考えています。
そして、次のような場合は避けてください…
- I/Oバウンドのアプリ: アプリがデータベース クエリまたは API 応答のみを待機する場合、JIT は役に立ちません。
- メモリに制約のある環境: 小さな Lambda 関数または小さなコンテナは、JIT キャッシュのメモリ フットプリントの増加の影響を受ける可能性があります。
- 一時的な CLI ツール: 1 秒未満で実行されるスクリプトには JIT は必要ありません。
今後の方向性:3.14以降
CPython コア チームは 3.14 を「創立の年」とみなしています。将来のイテレーション (Python 3.15 および 3.16) には以下が含まれる予定です。
- 詳細な最適化パス: 実行時に収集された型情報を使用して、さらに積極的なマシンコード生成を実行します。
- 推測したほうがよいでしょう: より良い決断が続く いつ? コンパイルするには、「ウォームアップ」ペナルティを最小限に抑えます。
- オーバーヘッドの削減: コピーとパッチのメカニズムを改良してメモリ消費を削減します。
まとめ
Python 3.14 の JIT は単なるパフォーマンス パッチではありません。これは意思表明です。これは、Python が、有名になった「バッテリーを必要とする」シンプルさを維持しながら、Java や Go などの言語とのパフォーマンスの差を埋めることに真剣に取り組んでいることを示しています。
ほとんどの開発者にとって、JIT は注目に値するもう 1 つのツールです。プロジェクトでパフォーマンスが重要な場合は、既存のワークロードに対して Python 3.14 をテストする価値があります。最も重要なコード パスの一部のベンチマークでは、予想外のパフォーマンスの向上が示される場合があります。
冒頭で触れた GIL Fee Python に関する以前の記事へのリンクはこちらです。









Leave a Reply