Python
Go
WebAPI
ベンチマーク
Falcon

PythonでGoのパフォーマンスに勝てるかを検証する

最近ではWebAPIの開発の際にGo言語が採用されるケースが増えてきたと感じています。学習コストも低く、実行速度も非常に速い。

Python言語もデーターサイエンスや機械学習の分野で使われるケースが非常に増えており、GoとPythonはどちらも人気上昇中の言語の一つです。
今回はとある方法でPythonを高速化させ、それを用いてAPIを作成し、Go言語で作成したAPIにパフォーマンスで勝るかどうかを検証してみました

この検証のきっかけ

自分はPythonが好きであり、珍しくPythonでAPIの開発を行ってきた経験があります。
Pythonの良さとして、シンプルな構文で書きやすく、学習コストも低め、サードパーティ製のライブラリも豊富にあります。
「どうせ開発すらなら好きな言語を使いたい」という、そんな願望からこの検証を実際にやってみました。決してGoが嫌いなわけではない。むしろGoも好きです。
Python界隈では定番の方法で、Pythonを高速化させる手段があります。それもスクリプト言語では叩き出せない速度を引き出すことが可能です。

どうやってPythonを高速化させるか

ごく単純に考えて、インタプリタ言語であるPythonが、コンパイラ言語であるGoに速度で勝ることは不可能です。またPythonはスクリプト言語の中でも遅い部類に入ります。
普通に両者を使って作成したAPIでは、パフォーマンスは断然Goの方が速いです。

そこでPythonでは、Pythonで書いたコードを一度C/C++のコードに変換してから、ネイティブコードに変換するCythonというものがあります。

Cythonとは

Cythonは、Pythonのコードに型付けなどを行い、コンパイル時に一度C/C++に変換を行い、ネイティブコードを生成します。
一応Pythonとは別言語扱いではあるものの、ほぼ同じような構文で書くことが可能です。

主な使い道としては、Pythonでコーディングをしたボトルネックとなる部分を、Cythonのモジュールとして分離し、Pythonから呼び出すことで使用します。

検証方法

Cythonで実装したAPIは、Goで実装されたAPIのパフォーマンスに勝るかを検証します。以下の4つの条件で実装したAPIを元に比較を行いました。

  • 処理時間のかかるアルゴリズムを使用して比較を行う
  • GETリクエストを100回行い、両者のレスポンスが返ってくる時間の平均を速度とする
  • Pythonでは最速とされるFalconを使用
  • Goでは速い方とされるechoを使用

ベンチマークで使用するアルゴリズム

使用するのはベンチマークでは定番な「モンテカルロ法で円周率の近似値を出す」アルゴリズムを使用しました。
N(試行回数)を徐々に増やして比較していきます。

検証 (その1)

以下の2つを比較する

  • Python(Falcon)
  • Go(echo)

echoを使用したGoのコード

api.go
package main

import (
    "math/rand"
    "net/http"
    "strconv"

    "github.com/labstack/echo"
)

func MonteCarlo() string {
    var num, count int
    num = 10000000

    count = 0

    for i := 0; i < num; i++ {
        x := rand.Float64()
        y := rand.Float64()

        if x*x+y*y <= 1.0 {
            count++
        }
    }

    var p float64
    p = 4.0 * float64(count) / float64(num)

    res := strconv.FormatFloat(p, 'f', 4, 64)
    return res
}

func main() {
    e := echo.New()

    e.GET("/montecarlo", func(c echo.Context) error {
        return c.String(http.StatusOK, MonteCarlo())
    })
    e.Start(":8000")
}

Falconを使用したPythonのコード

api.py
import json
import falcon
import random

NUM = 10000000

def monte():
    num = NUM
    c = 0

    for i in range(num):
        x = random.random()
        y = random.random()

        if x * x + y * y <= 1.0:
            c += 1
    ans = (4.0 * c / num)


class MonteResource(object):
    def on_get(self, req, resp):
        msg = {"ans": monte()}
        resp.body = json.dumps(msg)

app = falcon.API()
app.add_route("/monte", MonteResource())
app.add_route("/tak", TakResource())

if __name__ == "__main__":
    from wsgiref import simple_server
    httpd = simple_server.make_server("127.0.0.1", 8000, app)
    httpd.serve_forever()

比較結果

試行回数(N) Go Python
10000 0.00628(s) 0.01375(s)
100000 0.01911(s) 0.05097(s)
1000000 0.06964(s) 0.38912(s)
10000000 0.77344(s) 3.88514(s)

python と go.png

当然ながら通常のPythonとGoでは圧倒的な差があります。
1000000から速度差が一気に出ています。

検証 (その2)

以下の2つを比較する

  • Cython(Falcon)
  • Go(echo)

Goのコードは上記と同じものを使用

Falconを使用したCythonのコード

main.py
import api

if __name__ == "__main__":
    from wsgiref import simple_server
    app = api.app
    print(api)
    print(app)
    httpd = simple_server.make_server("127.0.0.1", 8000, app)
    httpd.serve_forever()

API部分のCythonコード

api.pyx
import json
import falcon
from libc.stdlib cimport rand, RAND_MAX


cdef int NUM = 10000000

def monte():
    cdef :
        int counter = 0
        int i=0
        double x
        double y
    for i in range(NUM):
        x = (rand()+1.0)/(RAND_MAX+2.0)
        y = (rand()+1.0)/(RAND_MAX+2.0)
        if x*x + y*y < 1.0:
            counter += 1

    pi = 4.0*counter/NUM
    return pi

class MonteResource(object):
    def on_get(self, req, resp):
        msg = {"ans": monte()}
        resp.body = json.dumps(msg)

app = falcon.API()
app.add_route("/monte", MonteResource())

比較結果

試行回数 Go Cython
10000 0.00628 0.00616
100000 0.01911 0.01031
1000000 0.06964 0.03197
10000000 0.77344 0.2058

go と cython.png

なんとパフォーマンス面でGoに勝利しました。Nが10000〜100000回まではそこまで差がありませんでしたが、GoはNが10000000から一気にパフォーマンスが落ちています。

Cython,Python,Goのグラフ

python、go、cython.png

まとめ

Cythonを使えば一応Goにパフォーマンス面で勝ることは可能です。ちなみに冒頭でも述べましたが、決してGo言語のことは嫌いなわけではありません。

ただし、APIでよくボトルネックとなって影響が出る部分はI/O部分の処理がほとんどです。
今回はCPUバウンドな処理(CPUに負荷をかける処理)で比較を行いましたが、わざわざCythonを使わなければいけないケースはそこまでないかもしれません。

主にCythonを使うケースは、Pythonを使っているアプリケーションの中で、CPUバウンドな処理で、ごく一部の関数の処理時間が遅い場合にみ使用することが良いそうです。

また少しではありますが、Cythonを使えば記述量が増え、C/C++の知識も多少は必要になります。
Goでも十分すぎるパフォーマンスは出せるため、わざわざ学習コストや時間を割いてCythonで書く必要もないのかもしれません。(ここまでやっておいてなんですが・・・)

参考資料

深入りしないCython入門
GoとPythonとGrumpyの速度ベンチマーク ~Googleのトランスパイラはどれくらい速い?~