ニュース24 (Nyūsu 24)

最新ニュース (Saishin Nyūsu) – 世界と日本の最新情報

データ サイエンスに向けた Python 3.14 とその新しい JIT コンパイラー

データ サイエンスに向けた Python 3.14 とその新しい JIT コンパイラー


これは、世界で最も人気のあるプログラミング言語の開発における重要なポイントです。 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

Your email address will not be published. Required fields are marked *

ಕ್ಯಾಲಿಫೋರ್ನಿಯಾ ಚುನಾವಣಾ ಫಲಿತಾಂಶಗಳು ಸಿಎ ಚುನಾವಣಾ ಫಲಿತಾಂಶಗಳು ಚುನಾವಣಾ ಫಲಿತಾಂಶಗಳು ಕ್ಯಾಲಿಫೋರ್ನಿಯಾ ಚುನಾವಣೆ ಚುನಾವಣಾ ಫಲಿತಾಂಶಗಳು ಕ್ಯಾಲಿಫೋರ್ನಿಯಾ ಕ್ಯಾಲಿಫೋರ್ನಿಯಾ ಪ್ರಾಥಮಿಕ ಕ್ಯಾಲಿಫೋರ್ನಿಯಾ ಪ್ರಾಥಮಿಕ ಫಲಿತಾಂಶಗಳು ಇಂದು ಪ್ರಾಥಮಿಕ ಚುನಾವಣಾ ಫಲಿತಾಂಶಗಳು ಸಿಎ ಗವರ್ನರ್ ರೇಸ್ ಫಲಿತಾಂಶಗಳು ಕ್ಯಾಲಿಫೋರ್ನಿಯಾ ಗವರ್ನರ್ ಸ್ಟೀವ್ ಹಿಲ್ಟನ್ ಗವರ್ನರ್ ಕ್ಯಾಲಿಫೋರ್ನಿಯಾ ಚುನಾವಣೆಗಳು ಕ್ಯಾಲಿಫೋರ್ನಿಯಾ ಚುನಾವಣಾ ಫಲಿತಾಂಶಗಳು 2026 ಕ್ಯಾಲಿಫೋರ್ನಿಯಾ ಗವರ್ನರ್ ರೇಸ್ ಫಲಿತಾಂಶಗಳು ಕ್ಯಾಲಿಫೋರ್ನಿಯಾ ಸಿಎ ಪ್ರಾಥಮಿಕ ಫಲಿತಾಂಶಗಳು ಕ್ಯಾಲಿಫೋರ್ನಿಯಾ ಪ್ರಾಥಮಿಕ 2026 ಕ್ಯಾಲಿಫೋರ್ನಿಯಾ ಗವರ್ನರ್ ರೇಸ್ ಪೋಲ್ಸ್ ಕ್ಯಾಲಿಫೋರ್ನಿಯಾದ ಗವರ್ನರ್ ಅನ್ನು ಯಾರು ಗೆದ್ದರು ಲಾ ಗವರ್ನರ್ ರೇಸ್ ಕ್ಯಾಲಿಫೋರ್ನಿಯಾ ಗವರ್ನರ್ ರೇಸ್ ಅನ್ನು ಯಾರು ಗೆದ್ದರು ಕ್ಯಾಲಿಫೋರ್ನಿಯಾ ಪ್ರಾಥಮಿಕ ಫಲಿತಾಂಶಗಳು 2026 ಪ್ರಾಥಮಿಕ ಫಲಿತಾಂಶಗಳು ಸಿಎ ಗವರ್ನರ್ ರೇಸ್ ಸಿಎ ಚುನಾವಣಾ ಫಲಿತಾಂಶಗಳು 2026 ಗ್ಯಾವಿನ್ ನ್ಯೂಸಮ್ ಕ್ಯಾಲಿಫೋರ್ನಿಯಾ ಫಲಿತಾಂಶಗಳು ಕ್ಯಾಲಿಫೋರ್ನಿಯಾ ಗವರ್ನರ್ 2026 ಅನ್ನು ಯಾರು ಗೆದ್ದರು ಕ್ಯಾಲಿಫೋರ್ನಿಯಾ ಗವರ್ನರ್ ಪ್ರೈಮರಿ ಕ್ಯಾಲಿಫೋರ್ನಿಯಾದಲ್ಲಿ ಗವರ್ನರ್ ರೇಸ್ ಅನ್ನು ಯಾರು ಗೆದ್ದರು ಸಿಎ ಪ್ರೈಮರಿ ಚುನಾವಣಾ ದಿನ ಕ್ಯಾಲಿಫೋರ್ನಿಯಾ ಕ್ಯಾಲಿಫೋರ್ನಿಯಾ ಗವರ್ನರ್ ಫಲಿತಾಂಶಗಳು