よくある☆5段階評価とかの実装と、ベンチマーク測定

先に書いた、
irbに定義した、ベンチマークの簡単な測定メソッド追加を利用した例も兼ねてます。
http://d.hatena.ne.jp/fre_oik/20110605/1307237389

例えば、以前あった前例を元にして、こんな事したり・考えたりしますって記事を1つ。


5段階評価を、
☆★★★★ を1
☆☆★★★ を2
☆☆☆★★ を3
☆☆☆☆★ を4
☆☆☆☆☆ を5
として、
★・☆とか2種類のマークで表し、
個数は合計5個確定の仕様で出力したいとする。


この時、どう実装すると、速度が速いだろう?と考えます。
色々やり方は思いつきますよね。

1〜5のrate(評価)数字を持っていて、そこから描写するなら、簡単に書くと

  rate = 1 #5段階評価
  rate_array = []
  5.times do |n|
    rate_array << (rate >= (i+1) ? "" : "")
  end
  puts rate_array.join #=> "☆★★★★"

こんな感じでしょうか。

ここで、上部でirbベンチマークを計るするメソッドの呼び出しを簡単にしてあげたので、
それと合わせてみる。

C:\>irb
irb(main):001:0>bench do
irb(main):002:1*  rate = rand(5)+1
irb(main):003:1>  rate_array = []
irb(main):004:1>  5.times do |i|
irb(main):005:2*    rate_array << (rate >= (i+1) ? "" : "")
irb(main):006:2>  end
irb(main):007:1>  rate_array.join
irb(main):007:0>end

  user    system    total     real
0.172000  0.000000  0.172000  (  0.163000)
0.156000  0.000000  0.156000  (  0.085000)
0.109000  0.000000  0.109000  (  0.094000)
0.140000  0.000000  0.149000  (  0.093000)
0.141000  0.000000  0.141000  (  0.093000)
=> 5

10000回実行を、5回やったらこんな結果になりました。
実行処理が決まっているなら、irbrcにメソッドとして定義するか、
irbで一旦メソッドとして作るのも見やすくなります。

C:\>irb
irb(main):001:0>def rate_star(rate = rand(5)+1)
irb(main):002:1>  rate_array = [] 
irb(main):003:1>  5.times do |i|
irb(main):004:2*    rate_array << (rate >= (i+1) ? "" : "")
irb(main):005:2>  end    
irb(main):006:1>  return rate_array.join
irb(main):007:1>end
=> nil
irb(main):008:0>bench do rate_star end

  user    system    total     real
0.109000  0.000000  0.109000  (  0.123000)
0.125000  0.000000  0.125000  (  0.099000)
0.078000  0.000000  0.078000  (  0.095000)
0.125000  0.000000  0.125000  (  0.085000)
0.109000  0.000000  0.109000  (  0.084000)
=> 5

こんな感じ。


irbrcにメソッドをちょいちょい色々書き換えて、同じ結果を返すメソッドを何個か書く。
bench do メソッド名 end
でそれぞれ試す。

みたいな事を私はよくやります。

このサンプルだと、
超簡単な速度の変わる書き換えイメージとして、例えば
5.timesを(1..5).eachとRangeオブジェクトにしてみたり、
三項演算子を、if文にしてみたり、
joinを、別の記述(シンタックスシュガー)でString#*で呼び出してみたり、
アルゴリズムを真剣に考えて順序こっちの方が評価減って速くなるんじゃないかと考えてみたりするわけです。
で、その中からこれが速いかもってのがあれば、それを使う。

あんまり変わらない、同じだろうと判断したら、読みやすい・書き換え・拡張しやすいのを使う。



そして、もっと速い実装できないのかなって、根本的な別の実装による書き方もないか考える。

ちなみにこの☆5段階評価の例は、もっと速い速度のコードを書ける。
さっきのirbの続きで書きます。

irb(main):009:0>def rate_star_fill(rate = rand(5)+1)
irb(main):010:1>  return Array.new(5,"").fill("",rate).join
irb(main):011:1>end
=> nil
irb(main):012:0>bench do rate_star_fill end

  user    system    total     real
0.047000  0.000000  0.047000  (  0.095000)
0.094000  0.000000  0.094000  (  0.065000)
0.046000  0.000000  0.046000  (  0.048000)
0.078000  0.000000  0.078000  (  0.048000)
0.047000  0.000000  0.047000  (  0.037000)
=> 5

大体倍近く早い。

ま、そもそもそんなに速度早くしなくても、遅くはないからアレですが、
例えばブックマークの管理ページがあったとして、
ブックマークのタイトル(リンク)と、自己評価を5段階でつけれたとする。
1ページに200件☆の表示を出すとしたら、
ちょっとでも早くしたいって事、考えたほうが幸せになるよね。

動画サイトとかだとしても、何かと☆のレーティング多いし。
1ページ内に結構必要になることも、たまにあるので、
やはり小さなとこから速度を気にするとパフォーマンスは上がるというか、
変なコードによるボトルネックがある程度だけど防げるね。



ちなみに上の最後のより速い方法もあるんじゃないかとは思いますが、
私はこの実装に行き着き落ち着いて、それ以上は考えてません。

こっちのが速くね?とかあったら教えてくださいw


fillを使った処理の流れは、
まず5個確定してるから、要素5個の配列を作って初期値を5評価で作っちゃう。
Array.new(5, "☆") #=> ["☆", "☆", "☆", "☆", "☆"]

出来た配列に対して、Array#fill(val, start)で、要素を置き換えてセットする。
配列の最初の位置は[0]なので、rateが1だと、上手い事2番目の要素からスタートして黒星★に塗り替える。
["☆", "☆", "☆", "☆", "☆"].fill("★", 1) #=> ["☆", "★", "★", "★", "★"]
["☆", "☆", "☆", "☆", "☆"].fill("★", 3) #=> ["☆", "☆", "☆", "★", "★"]
["☆", "☆", "☆", "☆", "☆"].fill("★", 5) #=> ["☆", "☆", "☆", "☆", "☆"]
#想定外の数字の動作
["☆", "☆", "☆", "☆", "☆"].fill("★", 0) #=> ["★", "★", "★", "★", "★"]
["☆", "☆", "☆", "☆", "☆"].fill("★", 7) #=> ["☆", "☆", "☆", "☆", "☆"]

で、上記で返った結果をjoinして返せばおkで、
if文や三項演算子で毎回判定して、Arrayにどちらかを格納するより速いよと。


ちょっとしたfillメソッドを使ったtipsですね。
Webアプリで実際に評価判定を入れるなら、DBに評価値は持ってるか、
評価人数・評価合計を保持して動的に出してるかって実装が多いと思われる。

前者なら、DB格納時に想定外の数字が保持されないようにvalidateかけて、
ちゃんとエラーで弾けば、表示で規格外の数字チェックして1個処理入れる必要は出ないね。

計算や処理、変数が1個減るとか、
そういうのたまーに考えるよ。

とか、そんな事を考えたり、考えなかったり。
随時考えてるわけじゃないけど、たまには考えるようになるときっと良いと思うんだ!