猫の魔法

主にruby系の技術メモを記載

Array#include?の速度

rubyを書いているとよくArray#include? を使う事が多いのですが、これのパフォーマンスが気になったの調査して見ました。 パフォーマンスの調査としては最悪のケースを考えたいので、要素が引っかからない場合(線形探索と仮定した場合、Arrayのすべての要素を舐める必要がある)を対象とします。

def make_array_number_of(n)
  array = []
  n.times do |i|
    array << "hoge#{i}"
  end
  array
end

def include_performance(array)
  a=Time.now
  pp array.include?("gaga")
  b=Time.now

  pp b-a
end

[1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000].each do |n|
  pp "#{n}s array"
  make_array_number_of(n).tap do |array|
    include_performance(array)
  end
end

結果

someone $ ruby performance.rb
"1s array"
false
2.7e-05
"10s array"
false
3.1e-05
"100s array"
false
3.7e-05
"1000s array"
false
9.3e-05
"10000s array"
false
0.000857
"100000s array"
false
0.001764
"1000000s array"
false
0.016368
"10000000s array"
false
0.167093
"100000000s array"
false
1.78809

要素が増えると線形的に探索時間が増加している(要素が10倍になると、探索時間も10倍)になるように見えます。 つまりArrayに対する include? はArrayの要素が増えて来ると、それだけボトルネックになるようです。

ちなみにHashを使うと、要素に対してO(1)でアクセスする事が出来るようになります。

def make_array_number_of(n)
  array = []
  n.times do |i|
    array << "hoge#{i}"
  end
  array
end

def include_performance(array)
  a=Time.now
  pp array.include?("gaga")
  b=Time.now

  pp b-a
end

def make_hash_number_of(n)
  hash = {}
  n.times do |i|
    hash["hoge#{i}"] = true
  end
  hash
end

def hash_performance(hash)
  a=Time.now
  pp hash.include?("gaga")
  b=Time.now

  pp b-a
end

[1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000].each do |n|
  pp "#{n}s array"
  make_array_number_of(n).tap do |array|
    include_performance(array)
  end

  make_hash_number_of(n).tap do |hash|
    hash_performance(hash)
  end
end

結果

someone $ ruby performance.rb
"1s array"
false
2.7e-05
false
2.1e-05
"10s array"
false
3.1e-05
false
2.9e-05
"100s array"
false
6.6e-05
false
3.6e-05
"1000s array"
false
6.8e-05
false
0.000114
"10000s array"
false
0.000376
false
9.1e-05
"100000s array"
false
0.006702
false
7.7e-05
"1000000s array"
false
0.01912
false
0.000101
"10000000s array"
false
0.171654
false
7.1e-05
"100000000s array"
false
1.888531
false
0.000158

こちらについてはHashの要素数がどれだけ増えても、探索時間に変化はありませんでした。 つまり配列の要素にするよりはハッシュのキーにした方が早くアクセス出来るということになります。

おまけ

Cのソースを見てみると、同じ include? でもかなり実装に差異があることが分かります。

Array

/*
 *  call-seq:
 *     ary.include?(object)   -> true or false
 *
 *  Returns +true+ if the given +object+ is present in +self+ (that is, if any
 *  element <code>==</code> +object+), otherwise returns +false+.
 *
 *     a = [ "a", "b", "c" ]
 *     a.include?("b")   #=> true
 *     a.include?("z")   #=> false
 */

VALUE
rb_ary_includes(VALUE ary, VALUE item)
{
    long i;
    VALUE e;

    for (i=0; i<RARRAY_LEN(ary); i++) {
        e = RARRAY_AREF(ary, i);
        if (rb_equal(e, item)) {
            return Qtrue;
        }
    }
    return Qfalse;
}

Hash

/*
 *  call-seq:
 *     hsh.has_key?(key)    -> true or false
 *     hsh.include?(key)    -> true or false
 *     hsh.key?(key)        -> true or false
 *     hsh.member?(key)     -> true or false
 *
 *  Returns <code>true</code> if the given key is present in <i>hsh</i>.
 *
 *     h = { "a" => 100, "b" => 200 }
 *     h.has_key?("a")   #=> true
 *     h.has_key?("z")   #=> false
 *
 *  Note that <code>include?</code> and <code>member?</code> do not test member
 *  equality using <code>==</code> as do other Enumerables.
 *
 *  See also Enumerable#include?
 */

MJIT_FUNC_EXPORTED VALUE
rb_hash_has_key(VALUE hash, VALUE key)
{
    if (RHASH_AR_TABLE_P(hash) && ar_lookup(hash, key, 0)) {
        return Qtrue;
    }
    else if (RHASH_ST_TABLE_P(hash) && st_lookup(RHASH_ST_TABLE(hash), key, 0)) {
        return Qtrue;
    }
    return Qfalse;
}
static int
ar_lookup(VALUE hash, st_data_t key, st_data_t *value)
{
    st_hash_t hash_value = do_hash(key);
    unsigned bin = find_entry(hash, hash_value, key);

    if (bin == RHASH_AR_TABLE_MAX_BOUND) {
        return 0;
    }
    else {
        HASH_ASSERT(bin < RHASH_AR_TABLE_MAX_BOUND);
        if (value != NULL) {
            *value = RHASH_AR_TABLE_REF(hash, bin)->record;
        }
        return 1;
    }
}
static inline st_hash_t
do_hash(st_data_t key)
{
    st_hash_t hash = (st_hash_t)(*objhash.hash)(key);
    return (RESERVED_HASH_VAL == hash) ? RESERVED_HASH_SUBSTITUTION_VAL : hash;
}

ハッシュの仕組みが分かっていないと難しいですね。(st_hash_t)(*objhash.hash)(key) はおそらく hash[key] の事でしょう。 正直これ以上追うのはしんどかったので追っていないのですが、仕組みとしては「Rubyソースコード完全解説」に記載されているシステム上で使用しているハッシュテーブルと同じ仕組みを使っているという認識です。 詳しくは下記のリンク先をご覧下さい。

i.loveruby.net