Edited at

Python3のdatetimeはタイムゾーンを指定するだけで高速になる

More than 1 year has passed since last update.


はじめに

Pythonで時間を扱う定番の標準ライブラリといえばdatetimeですね.

適当に使っても全く問題なく使えるライブラリですし,ちょろっと呼び出すだけならパフォーマンスを気にする必要すらありません.

しかし,数万回,数千万回とdatetimeを生成する必要に迫られた時,ボトルネックは顕在化してきます.

そこで,ちょっとしたことに気をつけるだけでパフォーマンスが若干向上する事を発見したので,そのご紹介です.

結論を書くと標準ライブラリのtimezoneを使ってdatetimeを生成しようというお話です.

※ちなみに,タイトルの通りPython3限定のお話です.


結論

いきなり結論だけ書きます.それ以降は興味があればご覧ください.

datetimeは次のように生成するのが良いと思います.

ポイントはtimezoneを指定するかどうか...それだけです.

from datetime import datetime, timedelta, timezone

# タイムゾーンの生成
JST = timezone(timedelta(hours=+9), 'JST')

# GOOD, タイムゾーンを指定している.早い
datetime.now(JST)
datetime.fromtimestamp(UNIX時間, JST)

# NG, 環境に依存した時刻を使用している.タイムゾーンを指定しない場合と比較して遅い
datetime.now()
datetime.fromtimestamp(UNIX時間)


パフォーマンス比較

早速ですが,色々な方法でdatetimeを生成してみます.

datetimeを1000万回生成した際の処理時間を測定します.

タイムゾーンの指定の有無でどれくらいパフォーマンスが変わるのか.など参考にして頂ければと思います.


  • 実行環境:


    • OS: Mac

    • CPU: Core i5 1.6Ghz

    • メモリ: 4GB DDR3



  • 言語: Python3.6.2


タイムゾーンを指定した場合

(僕の知る限り)最も高速に動作させられるパターンです.


zikan1.py

from datetime import datetime, timedelta, timezone

JST = timezone(timedelta(hours=+9), 'JST')

for _ in range(10000000):
datetime.now(JST)


$ time python zikan1.py

real 0m7.581s
user 0m7.167s
sys 0m0.114s

1000万回ループして結果は7秒.


タイムゾーンを指定しなかった場合

タイムゾーンを指定しないと若干遅くなります.若干..


zikan2.py

from datetime import datetime

for _ in range(10000000):
datetime.now()


$ time python zikan2.py

real 0m9.609s
user 0m9.149s
sys 0m0.111s

9秒くらいですね.若干遅くなりました.


pytzでタイムゾーンを指定した場合

pytzとはPython2系でタイムゾーンを指定するために良く使われているライブラリです.というのも,Python2にはtimezoneクラスがまだ実装されていなかったのです...


zikan3.py

import pytz

from datetime import datetime

# third party
JST = pytz.timezone('Asia/Tokyo')

# performance testing
for _ in range(10000000):
datetime.now(JST)


$ time python zikan3.py

real 1m9.173s
user 1m6.999s
sys 0m0.584s

思った以上にめっちゃ遅かったです.これについては後で考察します.


Python2系でタイムゾーンを指定した場合

折角なので,Python2系でもベンチマークしてみました.

Python2系だと,tzinfoというタイムゾーン用のインタフェースクラスが提供されているだけなので,自分で実装しないといけません.面倒です.面倒ゆえにpytzが流行ったのでしょう.


zikan4.py

from datetime import datetime, timedelta, tzinfo

class JST(tzinfo):
def utcoffset(self, dt):
return timedelta(hours=9)

def dst(self, dt):
return timedelta(0)

def tzname(self, dt):
return 'JST'

for _ in range(10000000):
datetime.now(JST())


$ time python zikan3.py

real 0m55.416s
user 0m51.131s
sys 0m1.532s

遅い....


結果

タイムゾーン指定 (7s) < タイムゾーン未指定 (9s) < python2 (51s) < pytz (66s)

という感じになりました.


細かい話


pytzはなんで遅いのか

標準ライブラリのタイムゾーンクラスもpytzのタイムゾーンクラスもどちらもtzinfoの実装クラスです.なのにパフォーマンスは天と地の差があります.

なぜなのか?

明確な答えに辿り着いた訳ではないですが,プロファイリングをすると両者の違いは一目瞭然です.

$ python -m cProfile -s cumtime zikan1.py

10001072 function calls (10001061 primitive calls) in 9.107 seconds

Ordered by: cumulative time

ncalls tottime percall cumtime percall filename:lineno(function)
2/1 0.000 0.000 9.107 9.107 {built-in method builtins.exec}
1 3.149 3.149 9.107 9.107 zikan1.py:1(<module>)
10000000 5.950 0.000 5.950 0.000 {built-in method now}
3/1 0.000 0.000 0.009 0.009 <frozen importlib._bootstrap>:958(_find_and_load)
3/1 0.000 0.000 0.009 0.009 <frozen importlib._bootstrap>:931(_find_and_load_unlocked)

$ python -m cProfile -s cumtime zikan3.py

70022021 function calls (70021903 primitive calls) in 83.138 seconds

Ordered by: cumulative time

ncalls tottime percall cumtime percall filename:lineno(function)
14/1 0.001 0.000 83.138 83.138 {built-in method builtins.exec}
1 3.185 3.185 83.138 83.138 zikan5.py:1(<module>)
10000000 9.225 0.000 79.868 0.000 {built-in method now}
10000000 17.973 0.000 70.643 0.000 tzinfo.py:179(fromutc)
20000000 43.347 0.000 43.347 0.000 {method 'replace' of 'datetime.datetime' objects}

注目したい部分はzikan3.pyを実行した際の

10000000   17.973    0.000   70.643    0.000 tzinfo.py:179(fromutc)

20000000 43.347 0.000 43.347 0.000 {method 'replace' of 'datetime.datetime' objects}

の部分です.

pytzはdatetime.replace()が呼び出されており,かつ2000万回も実行されています.それだけでなく,fromutc関数が呼び出されていますね.

つまり,tz.fromutc(datetime.now().replace(tzinfo=tz))という処理が走っているということです.

UTCで時間のdatetimeを生成 => タイムゾーンを付与したdatetimeを生成 => タイムゾーンのdatetimeに変換..してそうです.

一方で標準ライブラリのタイムゾーンを引数に与えた場合は,その辺を{built-in method now}が処理するようで,内部の仕様はわかりませんが効率的に処理しているっぽいことが伺えます.


おわりに

と,いうことで・・

つまり標準ライブラリのtimezoneを皆さん使っていきましょう!

以上です!

誤字脱字を始め不正確な内容がございましたら遠慮なくご指摘頂けると幸いです.