背景
Rubyで時間表現を文字列に変換するにはTime#strftime
をよく使います.以下は公式リファレンスの例です.
t = Time.now #=> 2010-09-05 15:41:17 0900
t.strftime("Printed on %m/%d/%Y") #=> "Printed on 09/05/2010"
これは楽でとても便利なんですが,Time#strftime
には遅いという問題があります.例えばFluentdのような秒間数千数万とかのログの時間を任意の文字列に変換しないと行けないミドルウェアの場合,Time#strftime
がパフォーマンス低下の一要因になります.
なので,定数倍でもここが高速化してくれると嬉しいなぁと,オフィスにいたRuby 2.5のリリースマネージャの方に相談したところ,strptime
gemにStrftime
を追加してくれました!
使い方
インストールはgem install strptime
するだけです.使い方はStrptime
と同じで,Strftime
クラスを変換したいフォーマットで生成し,その後exec
メソッドにTime
オブジェクトを渡すだけです.処理結果はTime#strftime
と同じになるように作られています.全てのフォーマットをサポートしているわけではありませんが(例えば%F
などのショートカット),プロダクションで利用されるようなフォーマットはほぼサポートされています.
require 'strptime'
now = Time.now
formatter = Strftime.new('%Y-%m-%dT%H:%M:%S.%L %z')
formatter.exec(now) # 2017-12-29T07:24:31.505 +0900
formatter.execi(now.to_i) # 2017-12-28T22:24:31.000 +0000
Strftime
はあらかじめフォーマットをパースして専用の命令セットを構築し,変換する時にはその命令セットをなぞるだけになっています.なのでTime#strftime
で行われるような毎回のフォーマットのパースをスキップでき,その分高速化されています.
ベンチマーク
Strftime
を使えば,よく使われるフォーマットへの変換が高速化されます.以下が簡単なベンチマークスクリプトと手元のMBPでの結果になりますが,色々なケースで数倍高速されていることが確認出来ます.
- 結果
Time#strftime:%d/%b/%Y:%H:%M:%S %z 667.146k (± 7.5%) i/s - 3.340M in 5.035575s
Strftime#exec:%d/%b/%Y:%H:%M:%S %z 1.853M (± 8.3%) i/s - 9.242M in 5.024461s
Time#strftime:/path/to/log/file.%Y%m%d.log 683.880k (± 7.3%) i/s - 3.443M in 5.062916s
Strftime#exec:/path/to/log/file.%Y%m%d.log 1.965M (± 6.8%) i/s - 9.818M in 5.021328s
Time#iso8601 427.001k (± 3.7%) i/s - 2.143M in 5.025909s
Strftime#exec:%Y-%m-%dT%H:%M:%S.%LZ 2.058M (± 3.6%) i/s - 10.287M in 5.006869s
- スクリプト
require 'benchmark/ips'
require 'time'
require 'strptime'
now = Time.now
Format1 = '%d/%b/%Y:%H:%M:%S %z'
strftime1 = Strftime.new(Format1)
Format2 = "/path/to/log/file.%Y%m%d.log"
strftime2 = Strftime.new(Format2)
strftime3 = Strftime.new('%Y-%m-%dT%H:%M:%S.%LZ')
Benchmark.ips do |x|
x.report('Time#strftime:%d/%b/%Y:%H:%M:%S %z') {
now.strftime(Format1)
}
x.report('Strftime#exec:%d/%b/%Y:%H:%M:%S %z') {
strftime1.exec(now)
}
x.report('Time#strftime:/path/to/log/file.%Y%m%d.log') {
now.strftime(Format2)
}
x.report('Strftime#exec:/path/to/log/file.%Y%m%d.log') {
strftime2.exec(now)
}
x.report('Time#iso8601') {
now.iso8601(3)
}
x.report('Strftime#exec:%Y-%m-%dT%H:%M:%S.%LZ') {
strftime3.exec(now)
}
end
まとめ
ということで,もしTime#strftime
を結構な頻度で呼び出すアプリケーションを書いてる人がいれば,strptime
gemを使うと,パフォーマンスが改善すると思います.まぁTime#strftime
のパフォーマンスで困るようなRubyアプリケーションがそんなにあるとは思いませんが…