Ruby2.7.0から追加された、Enumerable#filter_mapとEnumerable#tallyの可読性の比較と速度検証
TL;DR
Ruby2.7.0のEnumerable#filter_map
とEnumerable#tally
はどちらも従来より速い上に、可読性も高く、積極的に取り入れていきたいメソッドだった。
はじめに
Ruby2.7.0のリリースノートを読んで、Enumerable#filter_map
とEnumerable#tally
の2つのメソッドが良さそうと感じたので、従来の書き方と比較しつつ、速度も検証してみました。
Rubyのバージョン
$ ruby -v ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-linux-musl]
filter_mapの検証
使用したコード
require 'benchmark' N = 10_000 range = 1.upto(10_000) Benchmark.bmbm do |x| x.report('select + map') do N.times { range.select(&:even?).map { |i| i + 1 } } end x.report('select + map!') do N.times { range.select(&:even?).map! { |i| i + 1 } } end x.report('map + compact') do N.times { range.map { |i| i + 1 if i.even? }.compact } end x.report('map + compact!') do N.times { range.map { |i| i + 1 if i.even? }.compact! } end x.report('filter_map') do N.times { range.filter_map { |i| i + 1 if i.even? } } end end
結果
user system total real select + map 16.550000 0.420000 16.970000 ( 16.982197) select + map! 16.880000 0.190000 17.070000 ( 17.085424) map + compact 14.560000 0.570000 15.130000 ( 15.145185) map + compact! 14.900000 0.320000 15.220000 ( 15.245962) filter_map 13.870000 0.130000 14.000000 ( 14.027301)
破壊的メソッドを使っている2つは、途中に生成されるArrayを減らせるから速くなる気がしたのですが、むしろ遅くなり謎でした。
とりあえずこの2つは無視します。
途中に生成されるArrayが小さいmap + compactのほうがselect + mapよりも性能は良かったです。
それ以上にArrayを途中で作らないfilter_mapが1~2割ほど高速でした。
可読性に関しても、filter_mapが一番スッキリしていますね。
tallyの検証
使用したコード
# frozen_string_literal: true require 'benchmark' N = 10_000 rands = 10_000.times.map { rand(10) } Benchmark.bmbm do |x| x.report('group_by + to_h + count') do N.times { rands.group_by { _1 }.to_h { |k, v| [k, v.count] } } end x.report('each_with_object') do N.times { rands.each_with_object({}) { |rand, result| result[rand] = result[rand] ? result[rand] + 1 : 1 } } end x.report('each_with_object 2') do N.times { rands.each_with_object((0..9).to_h { |i| [i, 0] }) { |rand, result| result[rand] += 1 } } end x.report('tally') do N.times { rands.tally } end end
結果
user system total real group_by + to_h + count 12.040000 0.010000 12.050000 ( 12.053547) each_with_object 25.830000 0.000000 25.830000 ( 25.855872) each_with_object 21.800000 0.010000 21.810000 ( 21.825286) tally 7.590000 0.070000 7.660000 ( 7.675483)
今まではgroup_by + to_h + countで書いていました。
一応愚直なeach_with_objectでも試しましたが、読みにくい上に遅かったです。
each_with_object 2は少しズルをして場合分けを減らしていますが、それでも遅いです。Hashの順番も他と異なります。
tallyめっちゃ速い上に、めっちゃ読みやすい!!!
Ruby2.7.0すごい!!!!!!
おまけ
一応、中の実装がCであることも確かめました。(自分のRubyの書き方が悪いということもあり得るので)
irb(main):001:0> [].method(:filter_map).source_location => nil irb(main):002:0> [].method(:tally).source_location => nil
とりあえず、どちらもCで実装されているっぽいです。
おわりに
簡単な検証なので、要素数や要素のcardinalityを固定にしているので、常に有効かどうかは確かめられていません。
しかし、どちらのメソッドも速そうな上に、可読性も高く、積極的に取り入れていきたいメソッドでした。
すべてのコードが記事中に載っていますが、githubも公開しておきます。 https://github.com/tabuchi0919/ruby2_7_sandbox