137
156

高速化のエンジニアリング。注文してから0.722秒。100倍速いぞ!Python : 75.884 C++ : 3.392   JIT Python : 0.722 JITコンパイラで高速化されたコードを自動生成するツール。

Last updated at Posted at 2024-07-21

73d14943-de58-4e6e-8184-33290c7328a0.png

ショートストーリー: 「秘密のJITプログラム」
古代のプログラミング王国には、数々の魔法のツールが存在しました。その中でも、特に伝説的な存在が「JITの魔法の本」でした。この本は、普通のPythonコードに特別な呪文をかけて、高速化する力を持っていました。しかし、その力を知る者はごくわずかでした。

ある日、若きプログラマーのアリスは、効率的なコードを書けずに悩んでいました。彼女は数時間もかけて計算を行うコードを書き、それが遅すぎると感じていました。ある夜、アリスは古びた図書館でプログラミングの本を探していると、一冊の古い本が光を放ち始めました。それが「JITの魔法の本」でした。

興味を惹かれたアリスは、その本を手に取りました。ページをめくると、そこには「普通のPythonコードを高速化する秘密のプログラム」が記されていました。本の指示に従って、アリスは自分のコードを入力しました。

アリスの書いたコードはこうでした:


def slow_counting():
    count = 0
    for i in range(1, 1000000001):
        count += i
    return count

すると、魔法の本は光を放ち、コードに特別な装飾を施しました。ページには新しいコードが浮かび上がりました:


from numba import jit

@jit(nopython=True)
def fast_counting():
    count = 0
    for i in range(1, 1000000001):
        count += i
    return count

アリスは驚きと興奮を抑えきれませんでした。彼女はすぐに新しいコードを試し、その速さに目を見張りました。今まで数時間かかっていた計算が、ほんの数分で終わったのです。

翌日、アリスはこの発見を友人たちに話しました。友人たちも同じように魔法の本を使い、彼らのコードを高速化しました。こうして、プログラミング王国全体で「JITの魔法の本」が広まりました。

やがて、アリスは王国のプログラミング大会で優勝し、JITの魔法の本の力をさらに広めることになりました。彼女は「JITの守護者」として称えられ、プログラミング王国はかつてない繁栄を迎えました。

アリスはいつも心に誓いました。どんなに強力なツールも、それを使う人々の努力と情熱があってこそ、本当の力を発揮するのだと。彼女の言葉は次世代のプログラマーたちに伝わり、JITの魔法の本は永遠に受け継がれていくのでした。

前回のあらすじ。

Python counting time: 75.884644 seconds
C++ counting time: 3.392050 seconds
Numba JIT Python counting time: 0.722584 seconds

ユーザーが入力したPythonコードにJITコンパイラを追加し、高速化したコードを出力するPythonスクリプトです。numbaライブラリのインストールコマンドもコード内に含まれています。このスクリプトでは、サンプルコードを自動的にJITコンパイル対応の高速化したコードに変換します。

実行結果。

元のコード:

def compute_sum(n):
total = 0
for i in range(n):
total += i
return total

JITコンパイル後のコード:

@jit(nopython=True)
def compute_sum(n):
total = 0
for i in range(n):
total += i
return total

JITコンパイラを追加し、高速化したコードを出力するコード。


# 必要なライブラリのインストール
!pip install numba

import re

# サンプルコード
sample_code = '''
def compute_sum(n):
    total = 0
    for i in range(n):
        total += i
    return total
'''

# JITコンパイルを追加する関数
def add_jit_to_code(code):
    # 関数の定義を見つけるための正規表現パターン
    function_pattern = re.compile(r'def (\w+)\((.*?)\):')
    
    # JITデコレータを追加するためのコード
    jit_code = '@jit(nopython=True)\n'
    
    # 関数名と引数を取得
    def replace_function_definition(match):
        function_name = match.group(1)
        args = match.group(2)
        return f'{jit_code}def {function_name}({args}):'
    
    # コードにJITデコレータを追加
    new_code = function_pattern.sub(replace_function_definition, code)
    
    return new_code

# ジェネレートされたJITコードを表示
print("元のコード:")
print(sample_code)

jit_code = add_jit_to_code(sample_code)
print("\nJITコンパイル後のコード:")
print(jit_code)
説明

サンプルコード: sample_code 変数にJITコンパイルを追加するサンプルのPythonコードを含めています。

add_jit_to_code 関数: この関数は、入力されたPythonコードにJITデコレータを追加します。正規表現を使って関数定義を探し、JITデコレータを追加します。

コードの表示: 元のコードとJITコンパイル後のコードを表示します。

このスクリプトを実行すると、元のPythonコードと、JITコンパイラが追加された高速化されたコードの両方が表示されます。

JIT(Just-In-Time)コンパイラは、プログラムの実行時にコードをコンパイルして最適化する仕組みを持っています。JITコンパイラは、以下のような仕組みでコードの高速化を実現します。

JITコンパイラの仕組み
ランタイムコンパイル:
JITコンパイラは、プログラムの実行中にソースコードをバイトコードからネイティブコードに変換します。これにより、プログラムが実行される際に最適化されたコードが実行されるため、パフォーマンスが向上します。

最適化:
JITコンパイラは、実行時にコードの動作を分析し、性能を向上させるための最適化を行います。例えば、ループの展開、インライン化、デッドコードの削除などが行われます。

プロファイリング:
JITコンパイラは、コードがどの部分で最も多く実行されるかをプロファイリングし、頻繁に実行されるコードパスに対して特別な最適化を施します。このプロファイリングデータを基に、最も効果的な最適化を施すことができます。

高速化できない場合と有効な場合の例
高速化できない場合
I/O操作が多い場合:
JITコンパイラは主に計算集約型の処理に効果的です。ファイルの読み書きやネットワーク通信などのI/O操作が多いプログラムでは、JITコンパイラの最適化の効果が限定的です。

例: 大量のデータをファイルに書き込む処理や、データベースからのクエリ結果を取得して処理するコード。

短時間で終了する処理:
JITコンパイラのコンパイルオーバーヘッドが大きい場合、短時間で終了する処理ではJITの効果が相殺されることがあります。JITコンパイルには準備時間がかかるため、短い処理ではオーバーヘッドの方が大きくなることがあります。

例: 数回しか呼ばれない小さな関数や、一度だけ実行されるスクリプト。

高速化できる場合
計算集約型の処理:
数値計算やデータ処理など、計算がメインの処理では、JITコンパイラの最適化が非常に効果的です。特に、同じコードパスが繰り返し実行される場合には、JITコンパイラがその部分を最適化して大幅にパフォーマンスを向上させることができます。

例: 数値シミュレーション、データの集計やフィルタリング、科学技術計算。

ループや再帰が多い処理:
繰り返し処理や再帰呼び出しが多いプログラムでは、JITコンパイラがこれらのパターンを最適化して実行速度を向上させることができます。

例: 数字の合計を計算するループ、フィボナッチ数列の計算、データのソートアルゴリズム。

JITコンパイラを使うことで、特に計算集約型の処理や繰り返しの多い処理において大幅なパフォーマンス向上が期待できますが、I/O操作や短時間の処理ではその効果が限定的なことがあります。

ジッドコンパイラーは複雑な処理を行っているということですね。 また、ジッドコンパイラーはC言語にして実行しているわけではないのですか。
ChatGPT
はい、JITコンパイラーは複雑な処理を行っており、その仕組みや効果を理解することは非常に重要です。JITコンパイラーの具体的な処理について、以下の点を説明します。

JITコンパイラーの複雑な処理
ランタイムコンパイル:
JITコンパイラーはプログラムが実行される際に、ソースコードや中間バイトコードをネイティブマシンコードに変換します。これにより、最適化されたコードが実行され、パフォーマンスが向上します。

最適化:
実行時にコードをプロファイリングし、ボトルネックを特定します。その結果に基づき、ループ展開、インライン化、デッドコードの削除、メモリアクセスの最適化など、さまざまな最適化を施します。

プロファイリング:
JITコンパイラーは、コードの実行パターンを監視し、どの部分が最も頻繁に実行されるかを分析します。これにより、実行頻度が高いコードパスに対して重点的に最適化を行います。

JITコンパイラーとC言語の関係
JITコンパイラーは、通常、プログラムの中間言語(バイトコードや中間表現)をネイティブコードに変換するものであり、C言語自体には変換されません。具体的には以下の点があります。

中間言語からネイティブコードへの変換:
JITコンパイラーは、JavaのバイトコードやC#の中間言語(IL)などの中間表現を、実行環境に適したネイティブマシンコードに変換します。C言語のコードそのものが対象ではありません。

C言語との関係:
JITコンパイラー自体は、C言語で実装されていることもありますが、実際にJITが変換するのは中間言語であり、C言語のソースコードそのものではありません。C言語で書かれたコンパイラやランタイム環境(例えば、LLVMのようなライブラリ)は、JITコンパイラーの実装に使われることがあります。

ネイティブコードの生成:
JITコンパイラーは、ターゲットプラットフォームのネイティブアーキテクチャに適した機械語コードを生成します。このネイティブコードは、通常、C言語とは無関係に直接CPUで実行されます。

まとめ
JITコンパイラーは、複雑な処理を行い、プログラムの実行時に中間言語からネイティブコードへの変換を行います。JITコンパイラーはC言語で実装されることもありますが、実際に高速化を行うのは中間言語をターゲットにしたネイティブコード生成であり、C言語そのもののコード変換ではありません。

処理時間検証データ生成用のコード。

!pip install numba

import time
import subprocess
from numba import jit

# C++コードをファイルとして保存
cpp_code = """
#include <iostream>
#include <chrono>

int main() {
    auto start = std::chrono::high_resolution_clock::now();
    
    long long count = 0;
    for (long long i = 1; i <= 1000000000; ++i) {
        count += i;
    }
    
    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> duration = end - start;
    std::cout << "Time taken for counting in C++: " << duration.count() << " seconds" << std::endl;

    return 0;
}
"""

# C++コードをファイルに書き込む
with open('counting.cpp', 'w') as file:
    file.write(cpp_code)

# C++コードをコンパイルする関数
def compile_cpp_code():
    try:
        subprocess.run(["g++", "-o", "counting", "counting.cpp"], check=True)
    except subprocess.CalledProcessError as e:
        print("Error in compilation:", e)
        print("Output:", e.output)
        print("Error output:", e.stderr)

# C++コードを実行する関数
def run_cpp_code():
    try:
        result = subprocess.run(["./counting"], check=True, capture_output=True, text=True)
        cpp_time = float(result.stdout.split(": ")[1].split(" ")[0])
        return cpp_time
    except subprocess.CalledProcessError as e:
        print("Error in execution:", e)
        print("Output:", e.output)
        print("Error output:", e.stderr)
        return None

# Pythonでカウントを実行
def python_counting():
    start = time.time()
    
    count = 0
    for i in range(1, 1000000001):
        count += i
    
    end = time.time()
    return end - start

# NumbaでJITコンパイラを使用したPythonカウント
@jit(nopython=True)
def jit_counting():
    count = 0
    for i in range(1, 1000000001):
        count += i

# Pythonでの実行時間を計測
python_time = python_counting()
print(f"Python counting time: {python_time:.6f} seconds")

# Numba JITでの実行時間を計測
start = time.time()
jit_counting()
jit_time = time.time() - start
print(f"Numba JIT Python counting time: {jit_time:.6f} seconds")

# C++コードをコンパイル
compile_cpp_code()

# C++での実行時間を計測
cpp_time = run_cpp_code()
if cpp_time:
    print(f"C++ counting time: {cpp_time:.6f} seconds")

# 実行結果の比較
if cpp_time:
    print(f"Numba JIT Python is {python_time / jit_time:.2f} times faster than regular Python")
    print(f"C++ is {python_time / cpp_time:.2f} times faster than Python")
    print(f"C++ is {jit_time / cpp_time:.2f} times faster than Numba JIT Python")
else:
    print("C++ execution failed")

137
156
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
137
156