最近では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のコード
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のコード
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では圧倒的な差があります。
1000000から速度差が一気に出ています。
検証 (その2)
以下の2つを比較する
- Cython(Falcon)
- Go(echo)
Goのコードは上記と同じものを使用
Falconを使用したCythonのコード
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コード
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に勝利しました。Nが10000〜100000回まではそこまで差がありませんでしたが、GoはNが10000000から一気にパフォーマンスが落ちています。
Cython,Python,Goのグラフ
まとめ
Cythonを使えば一応Goにパフォーマンス面で勝ることは可能です。ちなみに冒頭でも述べましたが、決してGo言語のことは嫌いなわけではありません。
ただし、APIでよくボトルネックとなって影響が出る部分はI/O部分の処理がほとんどです。
今回はCPUバウンドな処理(CPUに負荷をかける処理)で比較を行いましたが、わざわざCythonを使わなければいけないケースはそこまでないかもしれません。
主にCythonを使うケースは、Pythonを使っているアプリケーションの中で、CPUバウンドな処理で、ごく一部の関数の処理時間が遅い場合にみ使用することが良いそうです。
また少しではありますが、Cythonを使えば記述量が増え、C/C++の知識も多少は必要になります。
Goでも十分すぎるパフォーマンスは出せるため、わざわざ学習コストや時間を割いてCythonで書く必要もないのかもしれません。(ここまでやっておいてなんですが・・・)
参考資料
深入りしないCython入門
GoとPythonとGrumpyの速度ベンチマーク ~Googleのトランスパイラはどれくらい速い?~