2014/02/20

いろんな型の範囲に対する範囲内チェックメソッドのパフォーマンスチェックをやってみた

先日書いた pinzolo/tekido を開発していた時、どうも一部時間のかかる処理があった。
割合系の spec を確認するために、誤差1%で合格する spec を書いていたんだけど、各sampleで試行回数10000回だとどうしても、ちょくちょく誤差を超えてしまう。
試行回数を10倍の100000回で行うと安定して、誤差内でおさまるんだけどテストの実行に結構な時間がかかっていた。
調べてみると、どうやら日付の Range に対しての include マッチャが時間がかかっている。
実際はマッチャは関係なく、include? メソッドの実行が結構重い。
ドキュメントによると、cover? だと、両端の <=> 比較によるチェックを行うということなので置き換えてみるとずいぶんと高速になったので解決はした。

そんなこともあって、せっかくなのでいろんな Range に対して include?, member?, ===, cover? のパフォーマンスを測ってみた。

環境

MacBook Air 13-inch, Mid 2012
プロセッサ : 2GHz Intel Core i7
メモリ : 8GB 1600 MHz DDR3
ソフトウェア : OS X 10.9.1 (Mavericks)
Ruby : ruby 2.0.0p353

数値

ほとんど変わらないしそもそも高速なので何使おうがどうでもいいレベル。

文字列

相対的にみると圧倒的に cover? だけど、10000回で4秒ちょい、1回あたり0.4ミリ秒なら include? が使われててもまあ別にいいかなと思うレベル。(状況にもよるけど)

日付

さすがに差があり過ぎでコストも高いので無視できない(したくない)レベル。

ということで

範囲内チェックは一律 cover? を使うのが無難そうです。
RSpec にも cover マッチャがあるので単純に置き換えできます。
ちなみに、Windows7 32bit環境(MBAよりもスペックは低い)で試してみたら、数値と文字列は変わらなかったけど日付の上三つの処理時間は13秒ぐらいでした。
MBAよりも倍近く高速なのが興味深い。

2014/02/17

ランダムデータを作るときにちょっとは役に立つかもしれない gem を作った

先日お仕事で、ランダムなデモデータを作成する必要がありまして、そのとき感じたことをいくつか。

  • rand(0..2) と値ぐらいならいいが、 rand(0..2).zero? ? "foo" : "bar" みたいなコードを結構使うが、意味が汲み取りにくい。
  • 直接関係ない値はついつい固定値にしてしまいがち。おかげで、ほとんどのデータが同じメールアドレスとか誕生日とか。そして、あとで他の機能からそのデータを使うときに、またランダム生成する。
  • そもそもランダムなメールアドレスや日付などは一手間かかる。この手のデータ作成は一時的な使い捨てであることも多々あるので、ちょくちょく同じような処理を書くことになる。

というわけで、ランダムデータ作成用に gem を作ってみた。

pinzolo/tekido
tekido | RubyGems.org

適度にデータを作るようにということで tekido。
使い方は、 Tekido.integer とか Tekido.date とか Tekido.string でよく使うデータ型のランダムなデータを作成出来ます。
Tekido.percentTekido.emailTekido.birthday みたいなちょっといい感じににしてくれるメソッドもついてます。
Kernel.rand のように引数で色々作成するデータが変わるので詳しくは README.mdUSAGE_ja.md を見てください。

rand を多用するよりはメソッド名で意味を汲み取りやすいし、普段なら一手間かかるデータも一発で作れます。
これで、ランダムデータの作成も少しは楽になるかもしれません。

2014/02/09

自分流 Rails での区分値

Railsでの区分値の扱いについて考える - TIM Labs
ActiveHashを使ってRailsで区分値を扱う方法 - TIM Labs

このあたりを読んでの話。

自分の場合は、もっぱらDBに保存する派です。
まあ、理由はいくつかあって

  • SQLで使いやすい
    過去のしごとで「アプリケーションの機能を作る程ではないけどこういう統計情報がほしい」という依頼を受けることがわりとあって、その時に画面表示と同じ文言を JOIN だけで出せる。
    区分が増えるほど CASE が増えていく SQL は書くのも保守するのもしんどい。
    Rubyを通さないと区分と文言の定義を正しく取れないのは使いづらい。
  • 区分を増やしてもアプリケーションの更新が必要ない場合がある
    区分値というのはドロップダウンリスト、チェックボックスのリスト、ラジオボタンのリストなどで画面から選択させることがよくあります。
    そんな場合、マスタ管理の機能を組み込むなり、INSERT実行するなりでアプリケーションの更新なしで対応できます。
    プログラム内で区分値を特定して使用する場合に初めてアプリケーションの更新が必要というのは理にかなってると思う。
  • 区分の保存先は統一したい
    数が少ない区分はコードで定義したほうが使いやすいが、付随する情報が多い区分はDBに保存したくなりません?
    でも保存先が分散されるのは保守しづらいのでどこかに統一したい。そんな場合、大は小を兼ねるでDBを選択してます。
というメリットが大きいな、使いやすいなと感じています。

でもDBに保存した場合、二重管理というのは確かに頭を悩ませる問題ですね。

def self.male
  find(id: 1)
end
みたいなコードを過去には量産してしまったりしてました。
それはやっぱりよろしくないので、自分の場合はちょっとした gem を作って対応してます。

pinzolo/mastar という gem で、以下その紹介。

countries
id name code
1 日本 japan
2 アメリカ合衆国 usa

こんなテーブルに対して、

class Country
  include Mastar
  mastar.key :code
end

とすれば、Country.japanfind(1) の結果にアクセスできます。
code のような、一意なコードを扱う列を作らなければいけないですが、二重管理からは開放されるので個人的には重宝してますね。
機能だけ作って README.md 書くのがめんどくさくなって v0.9.0 としてひっそりリリースして放置してました。
せっかくなので Country クラスのオブジェクトに対して japan? で判別できるメソッドが生える機能を足して v1.0.0 としてリリースしました。 もし使えそうなら使ってみてください。

あ、あと view でドロップダウンリストのために f.select :country_id, Country.all.map { |c| [c.name, c.id] } やら f.collection_select :country_id, Country.all, :id, :name と書いていたのを f.select :country_id, Country.pairs と書けるようにもなります。地味に気に入ってます。