grasys blog

Codonを使ってAPI作ってみる

はじめまして!エンジニアの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の実装の流れとしては

  1. 遅い処理をCodonでコンパイルを行い、実行ファイルを作成する。
  2. 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://docs.exaloop.io/codon

https://dev.classmethod.jp/articles/python-compiler-codon-trial-use/

https://zenn.dev/turing_motors/articles/e23973714c3ecf

https://qiita.com/sotasato/items/cc36a532ba6487dd3dba


採用情報
お問い合わせ