目次
はじめまして!エンジニアのUemaです。
Pythonコードを書いていて処理が遅いなと感じることがあると思います。
そこで、処理が遅いPythonコードに対して高速化するには、下記の手法があり、場合に応じて対応すると良いでしょう。
- リファクタリングを行い最適化
- 目的にあった高速化できるライブラリを使う
- マルチスレッド/マルチプロセス
- マシンコードへコンパイル
最近、CodonというPythonコンパイラが出てきました。そこで、Codonを使ってAPIを作ってみたいと思います。
Codonとは
Python コードをネイティブ マシン コードにコンパイルする高性能 Python コンパイラです。(公式から引用)
CythonだとCythonようにコードを書かないといけないが、CodonはPythonコードと互換性があるため、Pythonコードに対してコンパイルを行えるそうです。
しかし、完全な互換性があるわけではなく、文字列がUnicodeではなくASCIIなど注意が必要です。異なるところについては下記のURLを参照してください。
https://docs.exaloop.io/codon/general/differences
実装してみる
処理内容
ランダムに整数を生成し、バブルソートを行うAPIを作ってみます。
Pythonで考えられる重い処理処理として、ループをたくさん行う処理を高速化していきます。
こちらを参考に、処理を実装しました。
実装の流れ
私が考えた今回の処理内容に対してのCodonを使ったAPIの実装の流れとしては
- 遅い処理をCodonでコンパイルを行い、実行ファイルを作成する。
- subprocessライブラリのを使って実行ファイルを実行する。
のような流れで実装していきます。
ディレクトリ構成
project-dir
├ Dockerfile
├ main.py # FastAPIの処理
├ random_nums.py # コンパイルするPythonファイル
├ pyproject.toml
└ poetry.lock
実装したコード類
Codonを使ってコンパイルするコードは、下記のように実装しました。
生成する整数の数を渡して、ランダムに整数を生成し、バブルソートを行う処理です。
import random
import sys
def create_sorted_random_nums(count: int) -> None:
numbers = [random.randint(0, count) for _ in range(count)]
# バブルソート
for _ in range(len(numbers)):
for j in range(len(numbers)-1):
if numbers[j] > numbers[j+1]:
numbers[j], numbers[j+1] = numbers[j+1], numbers[j]
print(numbers)
args = sys.argv
try:
count = int(args[1])
except IndexError:
count = 100
create_sorted_random_nums(count)
FastAPIを使ってAPIを下記のように実装しました。
速度を比較するため、Pythonのみの処理も記載しました。
import subprocess
import json
import random
import time
import logging
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
logger = logging.getLogger('uvicorn')
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
""" 処理時間をヘッダーに付与する """
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
@app.get("/run_codon")
async def get_random_nums_by_codon(count: int = 100) -> JSONResponse:
""" Codonを使った処理を実行 """
result = subprocess.run(["./random_nums", str(count)], capture_output=True)
json_data = []
if result.stdout:
json_data = json.loads(result.stdout.strip(b"\n"))
return JSONResponse(content=json_data)
@app.get("/run_python")
async def get_random_nums(count: int = 100) -> JSONResponse:
""" Pythonのみを使った処理を実行 """
numbers = [random.randint(0, count) for _ in range(count)]
# バブルソート(Codonと同様の処理)
for _ in range(len(numbers)):
for j in range(len(numbers)-1):
if numbers[j] > numbers[j+1]:
numbers[j], numbers[j+1] = numbers[j+1], numbers[j]
return JSONResponse(content=numbers)
Dockerを使って構築してみました。
# poetryでrequirements.txtを作成
FROM python:3.10-slim-buster AS poetry
WORKDIR /tmp
RUN pip install poetry
COPY pyproject.toml poetry.lock ./
RUN poetry export -f requirements.txt --output requirements.txt
# APIを構築
FROM python:3.10-buster
ENV PYTHONUNBUFFERED 1
# Codonをインストール
RUN /bin/bash -c "$(curl -fsSL https://exaloop.io/install.sh)"
ENV PATH $PATH:/root/.codon/bin
WORKDIR /app
COPY --from=poetry /tmp/requirements.txt ./
RUN pip install -r requirements.txt
COPY main.py ./
COPY random_nums.py ./
RUN codon build -release -exe random_nums.py # コンパイルする
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
動作確認
Pythonのみの結果
最初にCodonを使用していないAPIを実行してみます。
10000件の乱数をバブルソートすると18秒かかっていることがわかります。
curl -i http://127.0.0.1:8080/run_python?count=10000
HTTP/1.1 200 OK
date: Fri, 02 Jun 2023 01:38:55 GMT
server: uvicorn
content-length: 48932
content-type: application/json
x-process-time: 18.40340304374695
[0,1,1,1,2,2,3,3,3,4,
...
,9994,9995,9995,9996,9996,9996,9996,9997,9998,9999]
Codonを使用した結果
次にCodonを使用したAPIを実行してみます。
10000件の乱数をバブルソートすると0.16秒となっていました。
100倍処理が早くなっていることがわかります。
すごいですね!!
curl -i http://127.0.0.1:8080/run_codon?count=10000
HTTP/1.1 200 OK
date: Fri, 02 Jun 2023 01:43:40 GMT
server: uvicorn
content-length: 48835
content-type: application/json
x-process-time: 0.16369128227233887
[0,0,2,2,2,4,5,5,6,7,
...
,9994,9995,9995,9996,9997,9997,9998,9998,9998,10000]
まとめ
今回の実装でCodonを使用して、高速化することができました。
PythonとCodonは完全な互換性があるわけではないので、使い所が限られてきますが、用途に合った使い方すれば高速化することができると思います。
ぜひ使ってみてください。
参考
https://dev.classmethod.jp/articles/python-compiler-codon-trial-use/
https://zenn.dev/turing_motors/articles/e23973714c3ecf
https://qiita.com/sotasato/items/cc36a532ba6487dd3dba