61385


タイトルのとおりです。  
Rubyで連番の配列を作る最も効率のよい方法はどれでしょうか？

[code]

    [*1..100]
    Array.new(100){|i| i} # 0 … 99
    (1..100).to_a
    
[/code]

パッと思いつくのは上のような感じですが、ほかにもっと良い方法とかありますでしょうか。  
配列が大きくなってきた時のパフォーマンスの違いはどうでしょう？

自分でベンチマークとればいいのかも知れませんが、やっぱり質問すると知見が集まることが多いのでみなさまよろしくおねがいします。


CC BY-SA 4.0
2019-12-07T13:25:19.687
61221
2019-12-12T12:12:58.940
2019-12-07T23:58:51.447
32772
32772
question
ruby
Rubyで連番の配列を作る最も効率のよい方法はどれでしょうか？


恐ろしいことに、ここでRubyの質問をすると、ruby-jpというslackに転送される仕組みになっていたのです。

そこでRubyのコミッターの@n0kadaさんからベンチマークと回答頂きました。ips=instruction/secondsなので数値が大きいほうが速いとのことです。to_aの方がちょっと速いということになりそうです。

[code]

    $ ruby -rbenchmark/ips -e 'Benchmark.ips{|x|x.report("splat") {[*1..100]}; x.report("to_a") {(1..100).to_a}}'
    Warming up --------------------------------------
                   splat    44.105k i/100ms
                    to_a    47.079k i/100ms
    Calculating -------------------------------------
                   splat    493.775k (± 0.7%) i/s -      2.470M in   5.002272s
                    to_a    518.966k (± 1.2%) i/s -      2.636M in   5.080851s
    
[/code]

私も大きい配列でやってみました。

[code]

    $ ruby -rbenchmark/ips -e 'Benchmark.ips{|x|x.report("splat") {[*1..1000000000]}; x.report("to_a") {(1..1000000000).to_a}}'
    Warming up --------------------------------------
                   splat     1.000  i/100ms
                    to_a     1.000  i/100ms
    Calculating -------------------------------------
                   splat      0.035  (± 0.0%) i/s -      1.000  in  28.416589s
                    to_a      0.041  (± 0.0%) i/s -      1.000  in  24.492379s
    
[/code]


CC BY-SA 4.0
2019-12-12T05:23:31.150
61363
2019-12-12T05:28:33.287
2019-12-12T05:28:33.287
32772
32772
61221
answer


考えられる限りの0からnまで(含まないパターンと含むパターン)の配列を作るメソッドを用意し、ベンチマークを取る(公平にするために要素数が同じとし、含むパターンは初めから-1した数を渡すようにする)ようにしてみました。

integer_sequence.rb

[code]

    # frozen_string_literal: true
    
    module IntegerSequence
      module_function
    
      # from 0 to n exclude n
    
      def arr_new_itself(n)
        Array.new(n, &:itself)
      end
    
      def arr_new_block(n)
        Array.new(n) { |i| i }
      end
    
      def rng_l_ho_to_a(n)
        (0...n).to_a
      end
    
      def arr_l_exp_rng_l_ho(n)
        [*0...n]
      end
    
      def arr_m_rng_l_ho(n)
        Array(0...n)
      end
    
      def rng_new_ho_to_a(n)
        Range.new(0, n, true).to_a
      end
    
      def arr_l_exp_rng_new_ho(n)
        [*Range.new(0, n, true)]
      end
    
      def arr_m_rng_new_ho(n)
        Array(Range.new(0, n, true))
      end
    
      def times_to_a(n)
        n.times.to_a
      end
    
      def rng_l_inf_take(n)
        (0..).take(n)
      end
    
      def rng_l_inf_ho_take(n)
        (0...).take(n)
      end
    
      # from 0 to m include m
    
      def rng_l_to_a(m)
        (0..m).to_a
      end
    
      def arr_l_exp_rng_lit(m)
        [*0..m]
      end
    
      def arr_m_rng_lit(m)
        Array(0..m)
      end
    
      def rng_new_to_a(m)
        Range.new(0, m).to_a
      end
    
      def arr_l_exp_rng_new(m)
        [*Range.new(0, m)]
      end
    
      def arr_m_rng_new(m)
        Array(Range.new(0, m))
      end
    
      def upto_to_a(m)
        0.upto(m).to_a
      end
    
      def step_to_a(m)
        0.step(m).to_a
      end
    end
    
    if $0 == __FILE__
      require 'minitest/autorun'
      include IntegerSequence
      describe IntegerSequence do
        before do
          @n = 100
          @m = @n - 1
          @a = (0...@n).to_a
        end
    
        it 'arr_new_itself' do
          _(arr_new_itself(@n)).must_equal @a
        end
    
        it 'arr_new_block' do
          _(arr_new_block(@n)).must_equal @a
        end
    
        it 'rng_l_ho_to_a' do
          _(rng_l_ho_to_a(@n)).must_equal @a
        end
    
        it 'arr_l_exp_rng_l_ho' do
          _(arr_l_exp_rng_l_ho(@n)).must_equal @a
        end
    
        it 'arr_m_rng_l_ho' do
          _(arr_m_rng_l_ho(@n)).must_equal @a
        end
    
        it 'rng_new_ho_to_a' do
          _(rng_new_ho_to_a(@n)).must_equal @a
        end
    
        it 'arr_l_exp_rng_new_ho' do
          _(arr_l_exp_rng_new_ho(@n)).must_equal @a
        end
    
        it 'arr_m_rng_new_ho' do
          _(arr_m_rng_new_ho(@n)).must_equal @a
        end
    
        it 'times_to_a' do
          _(times_to_a(@n)).must_equal @a
        end
    
        it 'rng_l_inf_take' do
          _(rng_l_inf_take(@n)).must_equal @a
        end
    
        it 'rng_l_inf_ho_take' do
          _(rng_l_inf_ho_take(@n)).must_equal @a
        end
    
        it 'rng_l_to_a' do
          _(rng_l_to_a(@m)).must_equal @a
        end
    
        it 'arr_l_exp_rng_lit' do
          _(arr_l_exp_rng_lit(@m)).must_equal @a
        end
    
        it 'arr_m_rng_lit' do
          _(arr_m_rng_lit(@m)).must_equal @a
        end
    
        it 'rng_new_to_a' do
          _(rng_new_to_a(@m)).must_equal @a
        end
    
        it 'arr_l_exp_rng_new' do
          _(arr_l_exp_rng_new(@m)).must_equal @a
        end
    
        it 'arr_m_rng_new' do
          _(arr_m_rng_new(@m)).must_equal @a
        end
    
        it 'upto_to_a' do
          _(upto_to_a(@m)).must_equal @a
        end
    
        it 'step_to_a' do
          _(step_to_a(@m)).must_equal @a
        end
      end
    end
    
[/code]

bm_make_arr.rb

[code]

    # frozen_string_literal: true
    
    require 'benchmark/ips'
    require_relative 'integer_sequence'
    
    include IntegerSequence
    
    # make arr from 1 to `n` (without `n`)
    def bench_make_arr(n)
      m = n - 1
      Benchmark.ips do |x|
        # n
        x.report('arr_new_itself') { arr_new_itself(n) }
        x.report('arr_new_block') { arr_new_block(n) }
        x.report('rng_l_ho_to_a') { rng_l_ho_to_a(n) }
        x.report('arr_l_exp_rng_l_ho') { arr_l_exp_rng_l_ho(n) }
        x.report('arr_m_rng_l_ho') { arr_m_rng_l_ho(n) }
        x.report('rng_new_ho_to_a') { rng_new_ho_to_a(n) }
        x.report('arr_l_exp_rng_new_ho') { arr_l_exp_rng_new_ho(n) }
        x.report('arr_m_rng_new_ho') { arr_m_rng_new_ho(n) }
        x.report('times_to_a') { times_to_a(n) }
        x.report('rng_l_inf_take') { rng_l_inf_take(n) }
        x.report('rng_l_inf_ho_take') { rng_l_inf_ho_take(n) }
        # m
        x.report('rng_l_to_a') { rng_l_to_a(m) }
        x.report('arr_l_exp_rng_lit') { arr_l_exp_rng_lit(m) }
        x.report('arr_m_rng_lit') { arr_m_rng_lit(m) }
        x.report('rng_new_to_a') { rng_new_to_a(m) }
        x.report('arr_l_exp_rng_new') { arr_l_exp_rng_new(m) }
        x.report('arr_m_rng_new') { arr_m_rng_new(m) }
        x.report('upto_to_a') { upto_to_a(m) }
        x.report('step_to_a') { step_to_a(m) }
    
        x.compare!
      end
    end
    
    if $0 == __FILE__
      n = ARGV[0].to_i
      n = 100 if n.zero?
      puts "Ruby description: #{RUBY_DESCRIPTION}"
      puts "size: #{n}"
      bench_make_arr(n)
    end
    
[/code]

`gem install benchmark-ips`をしておいた上で、二つを同じディレクトリに置いて`ruby
bm_make_arry.rb`でベンチマークが取ることができます。引数でnを指定できます(デフォルトは100)。なお、2.6から使用できる`0..`と`0...`いうリテラルを使用しているため、2.6以上必須です。2.5以下で行う場合はコメントアウトするなり個別に対応しておいてください。

手元のUbuntu(WSL上)で2.6.5と2.7.0-preview3のJIT有り/無しでn=100で試したところ、`(0..).take(n)`と`(0...).take(n)`が同速一位で、次点は`n.times.to_a`を初めとした`to_a`系が並ぶという結果でした。JITの有無はあまり影響がありませんでした。`step`や`Array.new`はかなり遅かったです。数を増やしたりしたら、また違う結果が出るかも知れませんが、あとは自分で試して見てください。


CC BY-SA 4.0
2019-12-12T12:12:58.940
61385
2019-12-12T12:12:58.940
7347
61221
answer