猫の魔法

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

Array#lengthと引数・ブロックなしArray#countの違い

自分はArrayで使える#lengthと#countの違いを生まれてこの方意識した事がなかったのですが、配列の長さを数えるだけであれば、lengthの方がcountより早いという話を聞いたので、自分なりに根拠を調べて見ました。

調査

そもそも両者のドキュメントを比べてみると、lengthは純粋に長さを求めるだけなのに対して、countは引数やブロックに数える対象の条件を加えることが出来ます。

MRIの場合countはArray用にメソッドがあるのですが、るりま的にはEnumerableがそれを代理しているようだったので、一旦はそちらのリンクを貼っています。

docs.ruby-lang.org

docs.ruby-lang.org

この時点ですでに何か違うのかなという気はするのですが、今回は「lengthの方がcountより早い」の根拠が知りたいのでもう少し深掘りしてみます。

まずlengthの方を見ていきます。

https://ruby-doc.org/core-2.6.3/Array.html#method-i-length

こちらの方はRARRAY_LEN というマクロに配列を渡して、その長さを返しています。

               static VALUE
rb_ary_length(VALUE ary)
{
    long len = RARRAY_LEN(ary);
    return LONG2NUM(len);
}

ここで気になるのは RARRAY_LEN マクロの実装なので、それを追ってみます。

#define RARRAY_LEN(a) rb_array_len(a)

 

static inline long
rb_array_len(VALUE a)
{
    return (RBASIC(a)->flags & RARRAY_EMBED_FLAG) ?
    RARRAY_EMBED_LEN(a) : RARRAY(a)->as.heap.len;
}

RARRAY_EMBED_LENは短い配列のときだけ使用する物らしいので一旦読み飛ばすと、RARRAY(a)->as.heap.lenRARRAY_LEN は返しているようです。 RARRAY(a)->as.heap.len は構造体メンバの静的な値を返しているだけな為、O(1)で配列の長さを返せるようになっています。

次にcountを見ていきます。

https://ruby-doc.org/core-2.6.3/Array.html#method-i-count

委細は省くのですが、countが引数なし、ブロックなしで呼ばれる場合の動作はreturn LONG2NUM(RARRAY_LEN(ary) になる為、lengthと変わらないように見えます。 違うところと言えば、rb_check_arityrb_block_given_p を呼び出しているところ位なので、これのオーバーヘッド分lengthが有利になるかなという気はしました。

   static VALUE
rb_ary_count(int argc, VALUE *argv, VALUE ary)
{
    long i, n = 0;

    if (rb_check_arity(argc, 0, 1) == 0) {
        VALUE v;

        if (!rb_block_given_p())
            return LONG2NUM(RARRAY_LEN(ary));

        for (i = 0; i < RARRAY_LEN(ary); i++) {
            v = RARRAY_AREF(ary, i);
            if (RTEST(rb_yield(v))) n++;
        }
    }
    else {
        VALUE obj = argv[0];

        if (rb_block_given_p()) {
            rb_warn("given block not used");
        }
        for (i = 0; i < RARRAY_LEN(ary); i++) {
            if (rb_equal(RARRAY_AREF(ary, i), obj)) n++;
        }
    }

    return LONG2NUM(n);
}

手元のPCで検証してみると、わずかではありますがlengthの方が早かったです。

検証ソース

start = Time.now
(0...10_000_000).each do
  a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  a.length
end
finish = Time.now
puts "length:#{finish - start}"

start = Time.now
(0...10_000_000).each do
  a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  a.count
end
finish = Time.now
puts "count:#{finish - start}"

結果

ruby test.rb
length:1.01813
count:1.258869