はじめに
みなさん、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@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);
ここで行っているのは、
-
rb_ary_length
を定義する。 - それを「
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);
}
rb_define_method(rb_cArray, "count", rb_ary_count, -1);
- 最初に引数が与えられているかで条件分岐しています。
- 引数なし、ブロックもなしであれば
length
/size
と同じ処理(LONG2NUM(RARRAY_LEN(ary));
)を行い、長さを返します。 - 次に、「引数なし」かつ「ブロックなし」ではない場合=「引数なし」かつ「ブロックあり」の場合です。
-
for
を回して、要素ひとつひとつに対して、反復処理でカウントします。
-
- 次に、「引数なし」ではない=「引数あり」の場合。
- もし、引数としてブロックが与えられている場合は警告を出します。
- 要素ひとつひとつに対し、引数と等しいか反復処理を行い、カウントします。
つまり、何も与えていない状態では、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構造体へのポインタに変換し、そのヒープ配列の長さを取得する
}
}
#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
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
オブジェクトの長さを効率的に取得する方法を示しています。この処理は、配列の各要素にアクセスしてカウントするよりもはるかに軽量で効率的です。具体的な理由を説明します。
- 直接的な長さの取得:
rb_array_len
関数は、Array
オブジェクトの長さを直接取得します。これは、メモリに格納されている配列の長さの情報を直接参照することで、配列の各要素を個別に調べる必要がなくなります。
- 埋め込み配列の最適化: Rubyは小さな配列に対して「埋め込み配列」の最適化を行います。これは、小さな配列をオブジェクト自体に直接埋め込むことで、メモリ割り当てのオーバーヘッドを削減します。
RB_FL_ANY_RAW(a, RARRAY_EMBED_FLAG)
によって埋め込み配列かどうかをチェックし、RARRAY_EMBED_LEN(a)
で直接その長さを取得します。
- ヒープ配列の効率的な処理: 埋め込み配列でない場合、つまり通常のヒープ配列の場合、
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度挟むだけ) - 引数なし・ブロックあり:要素ひとつひとつに対して、反復処理でカウントしていく
- 引数あり:要素ひとつひとつに対し、引数と等しいか反復処理を行い、カウントする
- 引数なし:
雑記
とりあえずの疑問は解消しましたが、疑問は少し残ります。
まず、
なんで同じ挙動に別の名前を割り当てたんだ・・・・・・?
異なるバックグラウンドのエンジニアでも気持ちよく使えるように、でしょうか。
ですからね。
そしてもうひとつ
誰かベンチマーク回してください🥹
これについて調べてるとき、とある記事に気になるコメントがついてたんですよね。
内容は、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周りがうまくいかなくてですね。誰かベンチマークを回していただけるとありがたき🙏