Edited at

Array#sample についてさっき知ったごくごく基本的なこと

More than 3 years have passed since last update.

配列の要素をランダムに選んで返してくれる Array#sample というメソッドがあって,パスワード生成したりするのにとても便利に使っているのですが,配列の要素数よりも大きい数を引数にとった場合,配列の要素数の配列が返ってくるだけということをさっき知りました.

どういうことかというと,

a = [*0..9]

a.sample(5)
=> [6, 5, 8, 0, 4]

a.sample(32)
=> [3, 9, 4, 0, 7, 2, 8, 5, 1, 6] # 10個しかない!

そんな挙動書いてあったっけ?と ri Array#sample 見返したところ,


The elements are chosen by using random and unique indices into the array in

order to ensure that an element doesn't repeat itself unless the array already

contained duplicate elements.


とあり,今読むと duplicate elements がなかったら repeat されないとか unique indices が云々とか書いてあるしここかと気付けましたが前読んだ時は見落としてました,読解力不足です.

というか,日本語版マニュアル の方も見てみたのですが,こちらは


配列の要素を1個(引数を指定した場合は自身の要素数を越えない範囲で n 個) ランダムに選んで返します。


とズバリ書いてあって,これならわかったのに!と思ったりしましたが,これはまあ単に負け惜しみです.

とまれ今回は,

a *= 2 while a.size < 32

a.sample(32)
=> [6, 5, 2, 3, 4, 2, 6, 8, 2, 3, 0, 8, 1, 9, 1, 5, 0, 7, 2, 9, 4, 7, 9, 1, 5, 8, 8, 5, 3, 4, 6, 9]

として乗り切った気分になりかけたのですが,上記例だと同じ数字が4回出たらその数字はもう出てこなくなるためパスワードでそれはよくない気がします.

ベタに,

(0..31).map {|i| a.sample }

=> [4, 1, 3, 4, 7, 2, 7, 9, 5, 3, 5, 0, 9, 4, 9, 6, 6, 0, 9, 4, 4, 9, 0, 6, 3, 1, 4, 6, 8, 3, 0, 8]

とかするとよいのかなーと思いつつも,これはこれで極端な話全部同じ文字になることもありうるわけでどうしたものか.


無駄に長い追記

コメント欄で yancya さんに SecureRandom.random_number というのがあるというのを教えてもらいました.

コメントにあるママになりますが,

SecureRandom.random_number(10**32)

=> 31722506540665720485632862149874

というように,数字だけの文字列が一瞬にして得られます.便利!

便利ついでにベンチマークを取ってみました.


bench1.rb

require 'benchmark/ips'

require 'securerandom'

numbers = [*0..9]

Benchmark.ips do |x|
x.report('SecureRandom.random_number') do
SecureRandom.random_number(10**32)
end

x.report('Array#sample1') do
numbers *= 2 while numbers.size < 32
numbers.sample(32).join
end

x.report('Array#sample2') do
32.times.map { numbers.sample }.join
end

x.compare!
end



result1

Calculating -------------------------------------

SecureRandom.random_number
10.661k i/100ms
Array#sample1 7.742k i/100ms
Array#sample2 5.357k i/100ms
-------------------------------------------------
SecureRandom.random_number
127.943k (± 3.4%) i/s - 639.660k
Array#sample1 87.266k (± 3.1%) i/s - 441.294k
Array#sample2 57.361k (± 3.4%) i/s - 289.278k

Comparison:
SecureRandom.random_number: 127943.1 i/s
Array#sample1: 87265.6 i/s - 1.47x slower
Array#sample2: 57361.4 i/s - 2.23x slower


というように,SecureRandom.random_number を使うと Array#sample を使った時よりも 1.5-2倍くらい速いということがわかりました.記述も少なくて済むし便利なので数字だけのランダム文字列が欲しい時はこれを使おうと思います!


と,ここで〆ても良かったのですが,Array#sample 使ってる方は,Array#join してる分遅くなってそうだなと気になったので,Array#join しないベンチもとってみました.


bench2.rb

# join 外しただけ

require 'benchmark/ips'
require 'securerandom'

numbers = [*0..9]

Benchmark.ips do |x|
x.report('SecureRandom.random_number') do
SecureRandom.random_number(10**32)
end

x.report('Array#sample1') do
numbers *= 2 while numbers.size < 32
numbers.sample(32)
end

x.report('Array#sample2') do
32.times.map { numbers.sample }
end

x.compare!
end



result2

Calculating -------------------------------------

SecureRandom.random_number
10.301k i/100ms
Array#sample1 39.461k i/100ms
Array#sample2 12.192k i/100ms
-------------------------------------------------
SecureRandom.random_number
124.434k (±11.0%) i/s - 618.060k
Array#sample1 618.819k (± 7.6%) i/s - 3.078M
Array#sample2 140.011k (± 7.0%) i/s - 707.136k

Comparison:
Array#sample1: 618819.3 i/s
Array#sample2: 140011.0 i/s - 4.42x slower
SecureRandom.random_number: 124434.5 i/s - 4.97x slower


というように join しない場合,SecureRandom.random_number よりも最大5倍くらい速いようです. 1


そこでもしかして join させなければもう少し速度ましになるのかも?と思ったので,numbers.sample(32) の方は直しようがないのですが,都度 numbers.sample してた方を文字列結合させるように手直して計測してみました.


bench3

require 'benchmark/ips'

require 'securerandom'

numbers = [*0..9]

Benchmark.ips do |x|
x.report('SecureRandom.random_number') do
SecureRandom.random_number(10**32)
end

x.report('Array#sample') do
a = ""
32.times { a << numbers.sample }
a
end

x.compare!
end



result3

Calculating -------------------------------------

SecureRandom.random_number
10.820k i/100ms
Array#sample 11.333k i/100ms
-------------------------------------------------
SecureRandom.random_number
132.034k (± 3.4%) i/s - 660.020k
Array#sample 128.892k (± 3.0%) i/s - 645.981k

Comparison:
SecureRandom.random_number: 132033.7 i/s
Array#sample: 128892.0 i/s - 1.02x slower


以上のように,Array#sample を使った方が若干速くなりました.数字以外を含めたランダム文字列を作る際にはこの方法を使うのも良いかもしれません. 2


と言いつつも,上記の3つのベンチでは事前にnumbers = [*0..9] を用意してたのがずるいと思ったので,x.report の後のブロックの中にそれも入れたらどれだけ遅くなるのかも計測してみました.


bench4.rb

require 'benchmark/ips'

require 'securerandom'

Benchmark.ips do |x|
x.report('SecureRandom.random_number') do
SecureRandom.random_number(10**32)
end

x.report('Array#sample1') do
numbers = [*0..9]
a = ""
32.times { a << numbers.sample }
a
end

x.compare!
end



result4

Calculating -------------------------------------

SecureRandom.random_number
11.664k i/100ms
Array#sample1 9.804k i/100ms
Array#sample2 11.716k i/100ms
-------------------------------------------------
SecureRandom.random_number
143.039k (± 3.0%) i/s - 723.168k
Array#sample1 111.852k (± 6.4%) i/s - 558.828k

Comparison:
SecureRandom.random_number: 143039.1 i/s
Array#sample1: 111852.5 i/s - 1.28x slower


バッチリ(?)遅くなりました.Array#sample に投げたい配列は事前に作って使い回すと良さそうです.


いい加減終わりかと思ったのですが,もう1つ気になったので続き.

numbers = [*0..9] というように用意してたのですが,numbers = [0,1,2,3,4,5,6,7,8,9] というようにあらかじめ用意しておいたらどうなるのか,せっかくなので事前に用意した時とブロックの中で用意した時と合わせて計測してみます.


bench5

require 'benchmark/ips'

require 'securerandom'

numbers_a = [*0..9]
numbers_b = [0,1,2,3,4,5,6,7,8,9]

Benchmark.ips do |x|
x.report('SecureRandom.random_number') do
SecureRandom.random_number(10**32)
end

x.report('Array#sample1a') do
a = ""
32.times { a << numbers_a.sample }
a
end

x.report('Array#sample1b') do
a = ""
32.times { a << numbers_b.sample }
a
end

x.report('Array#sample2a') do
nums_a = [*0..9]
a = ""
32.times { a << nums_a.sample }
a
end

x.report('Array#sample2b') do
nums_b = [0,1,2,3,4,5,6,7,8,9]
a = ""
32.times { a << nums_b.sample }
a
end

x.compare!
end


Calculating -------------------------------------

SecureRandom.random_number
11.533k i/100ms
Array#sample1a 11.963k i/100ms
Array#sample1b 12.194k i/100ms
Array#sample2a 10.060k i/100ms
Array#sample2b 11.707k i/100ms
-------------------------------------------------
SecureRandom.random_number
139.525k (± 4.9%) i/s - 703.513k
Array#sample1a 139.819k (± 3.5%) i/s - 705.817k
Array#sample1b 141.481k (± 3.1%) i/s - 707.252k
Array#sample2a 117.108k (± 3.1%) i/s - 593.540k
Array#sample2b 136.917k (± 4.0%) i/s - 690.713k

Comparison:
Array#sample1b: 141481.1 i/s
Array#sample1a: 139818.7 i/s - 1.01x slower
SecureRandom.random_number: 139524.7 i/s - 1.01x slower
Array#sample2b: 136916.5 i/s - 1.03x slower
Array#sample2a: 117107.9 i/s - 1.21x slower

事前に用意しておいた版が速いのは既に計測してわかっていたことですが,ブロックの中で都度生成した場合でも,都度RangeArrayに変換とかせずに事前に展開済の配列として用意しておけば SecureRandom.random_number とそこまで変わらない速度が出るようです.


まとめ


  • 数字だけのランダム文字列を得るには SecureRandom.random_number 使うのが記述の簡素さと速度を両立できて良さそう.

  • 英文字記号などを混ぜる場合は,使う文字を突っ込んだ配列を用意しておいて Array#sample して得た文字を結合させていくのが,記述の容易さと速度の兼ね合いで良さそう. 3


その他

繰り返しになりますが,SecureRandom.random_number を教えてもらったり,そこから join しなかったらどうなるだろう等々考えることができたし,しょぼいエントリなりに投稿したの,少なくとも自分にとってはよかったと思いました.

yancya さんありがとうございました!

おしまい.





  1. 返す型とか用途とか違うし比較になってない感あります. 



  2. もっと良い方法あると思いますが,とりあえずこのエントリで検証してるものとしては. 



  3. SecureRandom にある他のメソッドや,SecureRandom 以外のクラス,モジュールの中に良いものあると思うのですが未検証です.