背景

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を追加してくれました!

https://github.com/nurse/strptime#strftime-for-time-formatting

使い方

インストールは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') {
    strftime2.exec(now)
  }
end

まとめ

ということで,もしTime#strftimeを結構な頻度で呼び出すアプリケーションを書いてる人がいれば,strptime gemを使うと,パフォーマンスが改善すると思います.まぁTime#strftimeのパフォーマンスで困るようなRubyアプリケーションがそんなにあるとは思いませんが…