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

# はじめに
みなさん、Rubyの `length` `size` `count` の使い分け、気になりますよね？？🤗
そう思って！公式リファレンスも含め調べたんですが、どれもいまいちしっくりこない。
以下、しっくりこなかったリスト
- `size`と`length`は全く同じ挙動をする（ほんとに？根拠は？）
- 条件付きなら`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](https://github.com/ruby/ruby/blob/61da90c1b8d5c9a62d429ef66f000117eca675b3/array.c)@2671 ~ 2676
```c
static VALUE
rb_ary_length(VALUE ary)
{
    long len = RARRAY_LEN(ary);
    return LONG2NUM(len);
}
```
@8650, 8651
```c
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](https://github.com/ruby/ruby/blob/61da90c1b8d5c9a62d429ef66f000117eca675b3/array.c)@6279 ~ 6307
```c
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
```c
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](https://github.com/ruby/ruby/blob/61da90c1b8d5c9a62d429ef66f000117eca675b3/enum.c)に書いてあります。）

## `RARRAY_LEN`は反復処理よりも軽い
さて、ここからは不必要なくらい掘った内容になります。

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

**「`size` `length` 何も与えない`count`」の処理**
**＝`RARRAY_LEN`は、反復処理（イテレート）ではないのか？反復処理よりも軽いのか？**
それを確かめてきました。

結論から言うと、「反復処理は行われておらず、反復処理よりも軽い処理である」です。
## `RARRAY_LEN`は反復処理ではない
※ここから先は、今までにも増して未知のＣ言語世界なので、確度は多少下がります。
論拠：[Ruby Github rarray.h](https://github.com/ruby/ruby/blob/7b253cfea4212bc97a37514b9ffa8405de04748c/include/ruby/internal/core/rarray.h#L255)@254 ~ 265
```c
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
```c
#define RARRAY_LEN      rb_array_len     /**< @alias{rb_array_len} */
```
いまさらですが、`RARRAY`とは`RubyのArray`のことです（たぶん）。

さて、ここでは`rb_array_len`を`RARRAY_LEN`として定義していますね。
埋め込み配列かどうかを見て、その長さを返しています。
最後に、埋め込み配列であった場合の処理「`RARRAY_EMBED_LEN`」を見て終わりにしましょう。
### 補足：`EMBED`・ヒープ
※`EMBED`とは`埋め込み配列`のことです。メモリにおいて、配列の内容がヘッダーの直後に続いている（別途割り当てられたバッファではなく）という意味です。数個の配列にしか適用できませんが、最適化され高速な状態です。
また、ヒープとは「実行時に動的に割り当てられるメモリ領域」です。「埋め込み」はサイズが限られており、埋め込めないもの、動的なサイズ変更があるものに関してはこちらの処理になるようです。
ここらへんはTHE・C言語ですねぇ・・・・・・😇😇
## `RARRAY_EMBED_LEN`は反復処理ではない

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

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

# 終わりに
最初の、しっくりこなかったリストを振り返りましょう。
- `size`と`length`は全く同じ挙動をする（ほんとに？根拠は？）
- 条件付きなら`count`でそれ以外は`size` `length`（なぜ？）
- `count`は重い（なぜ？）
`size`と`length`は全く同じ処理を別名で割り当てているだけでしたね。
条件付きなら`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 の存在価値は「楽しさ」です。](https://ja.wikipedia.org/wiki/Ruby)（意訳）

ですからね。
そしてもうひとつ
### 誰かベンチマーク回してください🥹
これについて調べてるとき、とある記事に気になるコメントがついてたんですよね。
内容は、`count`を引数なしで回すと`length`より大幅に遅いということ。
>```rb
>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周りがうまくいかなくてですね。誰かベンチマークを回していただけるとありがたき🙏