LoginSignup
0
0

`length` `size` `count`の使い分け【前編:Ruby】

Last updated at Posted at 2024-01-19

はじめに

みなさん、Rubyの length size count の使い分け、気になりますよね??🤗
そう思って!公式リファレンスも含め調べたんですが、どれもいまいちしっくりこない。
以下、しっくりこなかったリスト

  • sizelengthは全く同じ挙動をする(ほんとに?根拠は?)
  • 条件付きならcountでそれ以外はsize length(なぜ?)
  • countは重い(なぜ?)

なぜ ↑ のように言われるかがわかるようになっています。

なお、Railsの場合(ActiveRecord)は挙動が違うので後編で書きます。
(2024 / 01時点の言語仕様に基づいた情報です)

この記事のまとめです

Ruby のlength size count の挙動

特に言及のない場合、Arrayクラスについて話しています。ただし、「Enumerableモジュールのcount」を除き、どのクラスでも同様の挙動だろうと思います(Stringクラスなど)

  • Array.length Array.size は完全に同じ挙動をするメソッド
  • Array.countは何を与えるかによって挙動が変わる
    • 引数なし・ブロックなし:length sizeメソッドとほぼ同じ処理(分岐処理を2度挟むだけ)
    • 引数なし・ブロックあり:要素ひとつひとつに対して、反復処理でカウントしていく
    • 引数あり:要素ひとつひとつに対し、引数と等しいか反復処理を行い、カウントする

本編

Array.length Array.size は完全に同じ挙動

いきなりですがRubyそのもののコードです。C言語です。
とはいえ、C言語を理解する必要はありません。

論拠:Ruby Github array.c@2671 ~ 2676

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

@8650, 8651

rb_define_method(rb_cArray, "length", rb_ary_length, 0);
rb_define_method(rb_cArray, "size", rb_ary_length, 0);

ここで行っているのは、

  1. rb_ary_lengthを定義する。
  2. それを「lengthメソッド」「sizeメソッド」という2つのメソッド名で定義する

というだけです。つまり、lengthメソッドとsizeメソッドは完全に同じものだと言えるわけです。

Array.countは何を与えるかによる

さっきよりは複雑なコードですが、そこまで難しくはないです。

論拠:Ruby Github array.c@6279 ~ 6307

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);
}

@8710

rb_define_method(rb_cArray, "count", rb_ary_count, -1);
  1. 最初に引数が与えられているかで条件分岐しています。
  2. 引数なし、ブロックもなしであればlength / sizeと同じ処理(LONG2NUM(RARRAY_LEN(ary));)を行い、長さを返します。
  3. 次に、「引数なし」かつ「ブロックなし」ではない場合=「引数なし」かつ「ブロックあり」の場合です。
    1. forを回して、要素ひとつひとつに対して、反復処理でカウントします。
  4. 次に、「引数なし」ではない=「引数あり」の場合。
    1. もし、引数としてブロックが与えられている場合は警告を出します。
    2. 要素ひとつひとつに対し、引数と等しいか反復処理を行い、カウントします。

つまり、何も与えていない状態では、size lengthとほぼ同等の処理を行います。
逆に、何かしらを与えた場合は反復処理(イテレート)を行います。これが重いと言われる理由。

※なお、Enumerableモジュールのcountでは引数の数に関わらず反復処理を行いカウントします。重い。(長くなるので割愛しますが、Ruby Github enum.cに書いてあります。)

RARRAY_LENは反復処理よりも軽い

さて、ここからは不必要なくらい掘った内容になります。

さっき、
「何かしらを与えた場合は反復処理(イテレート)を行います。これが重いと言われる理由。」
と言いました。
しかし、ここで疑問が沸きます。

size length 何も与えないcount」の処理
RARRAY_LENは、反復処理(イテレート)ではないのか?反復処理よりも軽いのか?
それを確かめてきました。

結論から言うと、「反復処理は行われておらず、反復処理よりも軽い処理である」です。

RARRAY_LENは反復処理ではない

※ここから先は、今までにも増して未知のC言語世界なので、確度は多少下がります。
論拠:Ruby Github rarray.h@254 ~ 265

static inline long
rb_array_len(VALUE a)
{
    RBIMPL_ASSERT_TYPE(a, RUBY_T_ARRAY); //配列型であることを確認

    if (RB_FL_ANY_RAW(a, RARRAY_EMBED_FLAG)) { //配列のフラグを確認
        return RARRAY_EMBED_LEN(a); //埋め込まれた配列ならそのままの長さを読み取る
    }
    else {
        return RARRAY(a)->as.heap.len; //埋め込まれていない(ヒープに割り当てられた)配列の場合、aをRARRAY構造体へのポインタに変換し、そのヒープ配列の長さを取得する
    }
}

@51

#define RARRAY_LEN      rb_array_len     /**< @alias{rb_array_len} */

いまさらですが、RARRAYとはRubyのArrayのことです(たぶん)。

さて、ここではrb_array_lenRARRAY_LENとして定義していますね。
埋め込み配列かどうかを見て、その長さを返しています。
最後に、埋め込み配列であった場合の処理「RARRAY_EMBED_LEN」を見て終わりにしましょう。

補足:EMBED・ヒープ

EMBEDとは埋め込み配列のことです。メモリにおいて、配列の内容がヘッダーの直後に続いている(別途割り当てられたバッファではなく)という意味です。数個の配列にしか適用できませんが、最適化され高速な状態です。
また、ヒープとは「実行時に動的に割り当てられるメモリ領域」です。「埋め込み」はサイズが限られており、埋め込めないもの、動的なサイズ変更があるものに関してはこちらの処理になるようです。
ここらへんはTHE・C言語ですねぇ・・・・・・😇😇

RARRAY_EMBED_LENは反復処理ではない

論拠:@234 ~ 244

RARRAY_EMBED_LEN(VALUE ary)
{
    RBIMPL_ASSERT_TYPE(ary, RUBY_T_ARRAY); //型チェック
    RBIMPL_ASSERT_OR_ASSUME(RB_FL_ANY_RAW(ary, RARRAY_EMBED_FLAG)); //埋め込みフラグチェック

    VALUE f = RBASIC(ary)->flags; //配列のフラグ取得
    f &= RARRAY_EMBED_LEN_MASK; //埋め込み長フラグの抽出
    f >>= RARRAY_EMBED_LEN_SHIFT; //フラグのシフト(埋め込み長を正しい位置にシフト)
    return RBIMPL_CAST((long)f); //フラグから取得した長さを `long` 型にキャストして返す
}

やはり、反復処理は行っていないですね。
見るからに反復処理より軽そうです(主観)。
不安なので、いちおうChatGPT先生に聞いてみましょう。

このコードはRubyの内部実装で、Arrayオブジェクトの長さを効率的に取得する方法を示しています。この処理は、配列の各要素にアクセスしてカウントするよりもはるかに軽量で効率的です。具体的な理由を説明します。

  1. 直接的な長さの取得: rb_array_len 関数は、Arrayオブジェクトの長さを直接取得します。これは、メモリに格納されている配列の長さの情報を直接参照することで、配列の各要素を個別に調べる必要がなくなります。
  1. 埋め込み配列の最適化: Rubyは小さな配列に対して「埋め込み配列」の最適化を行います。これは、小さな配列をオブジェクト自体に直接埋め込むことで、メモリ割り当てのオーバーヘッドを削減します。RB_FL_ANY_RAW(a, RARRAY_EMBED_FLAG) によって埋め込み配列かどうかをチェックし、RARRAY_EMBED_LEN(a) で直接その長さを取得します。
  1. ヒープ配列の効率的な処理: 埋め込み配列でない場合、つまり通常のヒープ配列の場合、RARRAY(a)->as.heap.len を使って配列の長さを直接取得します。これにより、配列の長さを知るために配列の各要素にアクセスする必要がありません。

このように、Rubyは配列の長さを効率的に取得するために、配列の内部表現に基づいた最適化を行っています。これにより、配列の長さを取得する際のパフォーマンスが向上し、特に大きな配列を扱う際に顕著な効果を発揮します。

終わりに

最初の、しっくりこなかったリストを振り返りましょう。

  • sizelengthは全く同じ挙動をする(ほんとに?根拠は?)
  • 条件付きならcountでそれ以外はsize length(なぜ?)
  • countは重い(なぜ?)
    sizelengthは全く同じ処理を別名で割り当てているだけでしたね。
    条件付きならcount、と言われるのは単に条件指定できるのがcountだけだからですね。
    countが重いと言われるのは、引数やブロックを与えたときの挙動を指しているわけです。

最も汎用的なのはcount

array.countの形(引数・ブロックなし)で使えば、size / length と変わりありません。

よって、最も汎用的なメソッドはcountと言えるでしょう。

この記事のまとめです ※再掲

Ruby のlength size count の挙動

特に言及のない場合、Arrayクラスについて話しています。ただし、「Enumerableモジュールのcount」を除き、どのクラスでも同様の挙動だろうと思います(Stringクラスなど)

  • Array.length Array.size は完全に同じ挙動をするメソッド
  • Array.countは引数によって挙動が変わる
    • 引数なし:length sizeメソッドとほぼ同じ処理(分岐処理を2度挟むだけ)
    • 引数なし・ブロックあり:要素ひとつひとつに対して、反復処理でカウントしていく
    • 引数あり:要素ひとつひとつに対し、引数と等しいか反復処理を行い、カウントする

雑記

とりあえずの疑問は解消しましたが、疑問は少し残ります。
まず、

なんで同じ挙動に別の名前を割り当てたんだ・・・・・・?

異なるバックグラウンドのエンジニアでも気持ちよく使えるように、でしょうか。

 Ruby の存在価値は「楽しさ」です。(意訳)

ですからね。
そしてもうひとつ

誰かベンチマーク回してください🥹

これについて調べてるとき、とある記事に気になるコメントがついてたんですよね。
内容は、countを引数なしで回すとlengthより大幅に遅いということ。

gem "benchmark_driver"
require "benchmark_driver"

Benchmark.driver do |r|
 r.prelude "a = Array.new(1000){ rand }"
 r.report "a.count"
 r.report "a.length"
end

結果:

Comparison:
           a.length: 161043915.9 i/s
            a.count:  33329644.2 i/s - 4.83x  slower

countを引数なしで呼び出した場合、lengthと同じRARRAY_LENを実行するので、差がつくはずないんですよね。
ただ、Rubyの環境構築もままならない初心者プログラマで・・・やはりgem周りがうまくいかなくてですね。誰かベンチマークを回していただけるとありがたき🙏

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0