258
195

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-07-29

はじめに

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を皆さん使っていきましょう!
以上です!

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

258
195
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
258
195

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?