Ruby で 10^7 ループくらいの処理をする時に気をつける事


※ ただしオンラインジャッジに限る

## Ruby と競技プログラミング

Ruby は必ずしも競技プログラミングに向いた言語とは言えないかもしれませんが、 `combination` メソッドや `transpose` メソッドなど便利に使えるメソッドも多く、楽しく問題を解く事ができます。

ただし、比較的大きな数のループを回す時は注意が必要です。

$N = 10^7$ の時の $O(N)$ な処理を行う、みたいな場合ですね。

## 大きい方を取る、小さい方を取る

$10^7$ のサイズの数値の配列の、最大値を取る素朴なコードを書いてみましょう。

```ruby:loop.rb
arr = [*1..(10**7)]
max = -(1.0/0)
arr.each do |e|
  max = [max, e].max
end
```

単純ですね。
では動かしてみましょう。

```
$ ruby -v
ruby 2.3.3p222 (2016-11-21 revision 56859) [x86_64-darwin15]
$ time ruby loop.rb
ruby loop.rb  3.84s user 0.15s system 97% cpu 4.094 total
```

うーん、遅い……。

## 最適化

単純な処理なのにこの遅さはいけません。
しかしこのコードは、少し工夫するだけで劇的にパフォーマンスが改善します。

```ruby:loop2.rb
arr = [*1..(10**7)]
max = -(1.0/0)
arr.each do |e|
  max = max > e ? max : e
end
```

```
$ time ruby loop2.rb
ruby loop2.rb  0.98s user 0.13s system 95% cpu 1.167 total
```

`[max, e].max` という配列の生成とメソッドの呼び出しを、 `max > e ? max : e` という三項演算子に置き換えるだけで、なんと速度が4倍になりました。

この理由は、おそらく以下の2点ですね。

- オブジェクト生成のコストがかかっていた
- `max` メソッドが重かった

### オブジェクト生成のコスト

Ruby のオブジェクト生成は、凄く重い処理というわけではないですが、 $10^7$ 回のループの中で軽々しく行っていい処理というわけでもありません。

```ruby:loop3.rb
arr = [*1..(10**7)]
arr.each do |e|
  Array.new
end
```

```
$ time ruby loop3.rb
ruby loop.rb  1.64s user 0.11s system 97% cpu 1.803 total
```

なので、ループの中でオブジェクトが生成されないようにする事で、速度の改善ができたのですね。

### `max` メソッドの重さ

ここでいう `max` メソッドとは、正確には `Enumerable#max` メソッドの事です。
Ruby 2.3 系だと、 Array クラスに対する `max` メソッドはこの Enumerable モジュールの `max` メソッドを呼び出します。

さて、このメソッドの実体は C で書かれた `enum_max` 関数なのですが、この関数を見てみると、決して冗長なわけではありませんが、新しいオブジェクトを生成したりソートしたりといった、「 `a` と `b` のどちらが大きいか」という事だけを知りたい場合には、重すぎる処理である事がわかります。

```ruby:loop4.rb
arr = [*1..(10**7)]
ab = [1, 2]
arr.each do |e|
  ab.max
end
```

```
$ time ruby loop4.rb
ruby loop.rb  2.66s user 0.12s system 95% cpu 2.897 total
```

### まとめ

そういうわけで、この、

1. オブジェクトを作る
2. そのオブジェクトの `max` メソッドを呼び出す

という重い処理を、単なる三項演算子の呼び出しに置き換える事で、大幅な速度改善になりました。

でも、 `[a, b].max` や `[a, b].min` なんてよく使うイディオムですよね？

## Ruby 2.4 での改善

この問題、 **Ruby 2.4 では改善されています** 。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">2.4 から Array インスタンス作らないっぽいですね<a href="https://t.co/p2mKON1rhe">https://t.co/p2mKON1rhe</a><a href="https://t.co/FmEWr9k5Nz">https://t.co/FmEWr9k5Nz</a></p>&mdash; Fumiaki MATSUSHIMA (@mtsmfm) <a href="https://twitter.com/mtsmfm/status/993728254321217537?ref_src=twsrc%5Etfw">2018年5月8日</a></blockquote>

- [ruby で最大値を求める方法の速度比較](https://qiita.com/Nabetani/items/9b30738cd826f60b4137)
- [Feature #12172](https://bugs.ruby-lang.org/issues/12172)

はい、私が今挙げた

- オブジェクト生成が重い
- `max` メソッドが遅い

という両方の問題が、 Ruby 2.4 より新しいバージョンでは見事に改善されています。

```
$ ruby -v
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin16]
$ time ruby loop.rb
ruby loop.rb  0.94s user 0.10s system 86% cpu 1.202 total
```

高速化されています！

Ruby は素晴らしいですね。

## では `[a, b].max` が重い問題は過去の話なのか？

残念ながらそうとは言えないのです。

最初に Ruby と競技プログラミングとの関係について触れましたが、短い時間の中で $10^7$ 回のループを処理しないといけない、といった事態は、競技プログラミングに於いて頻出する事です。

問題を解くだけでも競技プログラミングは楽しめますが、競技プログラミングをやっている方の中にはオンラインジャッジのコンテストに参加していらっしゃる人も多いでしょう。
オンラインジャッジとは、手元でコードを実行するのではなく、コンテスト主催者の用意した環境でコードを実行し、競う形式です。

ではここで、 Ruby が利用できる幾つかのオンラインジャッジで、そのバージョンを見てみましょう。
（2018年5月17日に調査しました。）

#### AtCoder

https://language-test-201603.contest.atcoder.jp/

> Ruby (2.3.3)

#### Codeforces

http://codeforces.com/

> Ruby 2.0.0p645

サイト中には書いてないのですが、提出するフォームにありました。

#### Paiza

https://paiza.jp/guide/language

> ruby 2.3.3

#### AIZU ONLINE JUDGE

http://judge.u-aizu.ac.jp/onlinejudge/AOJ_tutorial.pdf

> Ruby 2.2.2

#### Google Code Jam

去年までは手元で実行して結果を送信する形式だった GCJ も、今年はオンラインジャッジになりました。

https://code.google.com/codejam/resources/faq

> 2.3.3p222 (package: ruby-full)

#### yukicoder

※ 2018年5月18日追記 @raccy さん、ありがとうございます！

https://yukicoder.me/help/environments

> ruby 2.5.0p0

### まとめ

おわかりいただけたでしょうか……？

上に挙げた幾つかのオンラインジャッジだと、 Ruby 2.4 以降が使える環境は、なんと ~~1つもありません~~ 1つしかありません。

これらのバージョン 2.4 未満の Ruby しか使えないサイトでコンテストに参加する場合、 Ruby 2.4 で入った `[a, b].max` みたいな書き方の最適化の恩恵を享ける事は当然できません。

なので、大きめのループの中で軽々しくオブジェクトの生成や重いメソッドの呼び出しが起きないように、注意してコーディングをしてあげる必要があるのですね。

### 何でオンラインジャッジで Ruby 2.4 以降が使えないの？

色々な理由があると思いますが、長く続いているオンラインジャッジであればあるほど、言語の新しいバージョンが出る度にアップデートをしなければならないという事になります。

皆さんもエンジニアなら、ああいうシステムの利用言語をアップデートするのがどれだけ大変か、何となく解りますよね。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">言語アップデートしろって要求がしょっちゅうメールで来るようになってきたけど、待って・・・そんな簡単なものじゃないから色々まって・・・。</p>&mdash; chokudai(高橋 直大) (@chokudai) <a href="https://twitter.com/chokudai/status/981139017587503105?ref_src=twsrc%5Etfw">2018年4月3日</a></blockquote>

利用できる環境の中で最善を尽くしつつ、気長に待ちましょう。