しょんぼり技術メモ

まいにちがしょんぼり

Rubyで配列に対して空かも知れない配列を追加する時は、事前にempty?でチェックした方が早いが、まとめて追加してから一気にflattenした方が圧倒的に早いが、そんなことするよりconcatした方が早い。

タイトルで完結シリーズ。(ご指摘頂いたArray#concatについて追記しました)

ある配列に対して、「空かもしれない配列」を追加していく処理が必要になった。

fruits1 = [:apple, :orange]
fruits2 = []
fruits3 = [:grape]

fruits_array = [fruits1, fruits2, fruits3]

こんな配列から、"[ :apple, :orange, :grape ]"こんな結果が欲しいときについて。普通はfruits_array.flattenで終わるけど。


配列に対して配列を"<<"で追加すると、配列には配列が格納される。(わかりにくい表現だ)

ruby-1.9.2-p0 > a1=Array.new
 => [] 
ruby-1.9.2-p0 > a2=[:apple, :orange]
 => [:apple, :orange] 
ruby-1.9.2-p0 > a1 << a2
 => [[:apple, :orange]] 
ruby-1.9.2-p0 > a3=[:grape]
 => [:grape] 
ruby-1.9.2-p0 > a1 << a3
 => [[:apple, :orange], [:grape]] 

それが嫌な場合には、"+="で追加すれば良い。

ruby-1.9.2-p0 > a1=Array.new
 => [] 
ruby-1.9.2-p0 > a2=[:apple, :orange]
 => [:apple, :orange] 
ruby-1.9.2-p0 > a1 += a2
 => [:apple, :orange] 
ruby-1.9.2-p0 > a3=[:grape]
 => [:grape] 
ruby-1.9.2-p0 > a1 += a3
 => [:apple, :orange, :grape] 

もし追加する対象に空配列"[]"があった場合、empty?で判断してから追加するのが良いのか、問答無用で+=を呼んでも大差ないのか、という疑問が浮かんだので、実際に試してみた。
10000個、ランダムに空配列or空じゃない配列を作成して、それをフラットな配列にする関数についてベンチマーク
ついでに、<<で追加して、最後にflattenでフラットな配列にする場合についても比較してみた。
さらに、concatで繋げていく場合についても比較してみた。(追記)


結果から:

$ ruby --version
ruby 1.9.2p0 (2010-08-18 revision 29036) [x86_64-linux]
$ ruby ./arraytest.rb
                   user     system      total        real
Rehearsal --------------------------------------------------
without  check   0.210000   0.010000   0.220000 (  0.225342)
with     check   0.090000   0.010000   0.100000 (  0.104588)
flat w/  check   0.010000   0.000000   0.010000 (  0.003227)
flat w/o check   0.000000   0.000000   0.000000 (  0.003293)
concat each      0.000000   0.000000   0.000000 (  0.001726)
----------------------------------------- total: 0.330000sec

                     user     system      total        real
without  check   0.200000   0.030000   0.230000 (  0.218026)
with     check   0.100000   0.000000   0.100000 (  0.104849)
flat w/  check   0.000000   0.000000   0.000000 (  0.003143)
flat w/o check   0.010000   0.000000   0.010000 (  0.003293)
concat each      0.000000   0.000000   0.000000 (  0.001738)
okay.

concatがブッチギリに速い。flattenはその次に速い。empty?で比較する場合は比較なしで問答無用で+=する場合に比べて倍ぐらい速い。
flattenを使う場合、empty?で比較することの効果はあまり大きくないが、確かに差は生じる。でもconcatの方がはやーい。

結論:+=じゃなくてconcat使え。


以下ベンチマークコード。

require 'benchmark'

def with_check(source_array)
  ary = Array.new

  source_array.each do |s|
    if s.empty? == false
      ary += s
    end
  end

  return ary
end

def without_check(source_array)
  ary = Array.new

  source_array.each do |s|
    ary += s
  end

  return ary
end

def do_flat_with_check(source_array)
  ary = Array.new

  source_array.each do |s|
    if s.empty? == false
      ary << s
    end
  end

  return ary.flatten
end

def do_flat_with_check_dest(source_array)
  ary = Array.new

  source_array.each do |s|
    if s.empty? == false
      ary << s
    end
  end

  ary.flatten!
  return ary
end

def do_flat_without_check(source_array)
  ary = Array.new

  source_array.each do |s|
    ary << s
  end

  return ary.flatten
end

def do_concat_whole(source_array)
  ary = Array.new

  ary.concat(source_array)

  return ary
end

def do_concat_each(source_array)
  ary = Array.new

  source_array.each do |s|
    ary.concat(s)
  end

  return ary
end

ary_size = 10_000

# prepare
src_array = Array.new
ary_size.times do
  if (rand(2)==1)
    src_array << [1,2]
  else
    src_array << []
  end
end

puts "             " + Benchmark::CAPTION
ret1 = ret2 = ret3 = ret4 = ret5 = ret6 = nil
Benchmark.bmbm do |x|
  x.report("without  check") {
    ret1 = without_check(src_array)
  }
  x.report("with     check") {
    ret2 = with_check(src_array)
  }
  x.report("flat w/  check") {
    ret3 = do_flat_with_check(src_array)
  }
  x.report("flat w/o check") {
    ret4 = do_flat_without_check(src_array)
  }
  x.report("concat each  ") {
    ret5 = do_concat_each(src_array)
  }
  # x.report("concat whole   ") {
  #   ret6 = do_concat_whole(src_array)
  # } 全体にconcatしてもフラットな配列にはならない!
end

if (ret1 != nil) and (ret1 == ret2) and (ret2 == ret3) and (ret3 == ret4) and (ret4 == ret5) # and (ret5 == ret6)
  puts "okay."
end