esm アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

Rails / OSS パッチ会オンライン 2021年7月のお知らせ

2021年7月の Rails / OSS パッチ会を 7月29日(木)にオンライン開催します。

この会をひとことでいうと、日頃のお仕事で使っている Rails をはじめとする OSS への upstream にパッチを送る会です。

会には Ruby と Rails のコミッターである顧問の a_matsuda もいますので、例えば Rails に送るパッチのネタがあるけれど、パッチを送るに適しているかの判断やパッチを送る流れが悩ましいときなど a_matsuda に相談して足がかりにするなどできます。

開催時間は 17:00-19:00 となりますがご都合のあう方はぜひご参加下さい。Zoom あたりのテレビ会議システムを使います。

当日の招待 URL は Idobata の esminc/rails ルームで共有する予定です。

idobata.io

特に募集ページなど設けませんが、上記理由から Idobata のアカウントが必要になると思います。

翌 7月30日(金) 締め切りの Kaigi on Rails 2021 の CFP や、最近の Ruby / Rails まわりの動向に関する話題などあるかもしれません。

その他の開催方針については以下の Gist に記していますので、ご参照ください。

Reboot Rails/OSS meetup online · GitHub

Mac から Windows マシンに移行して 9 ヶ月がたちました

こんにちは、 @yucao24hours です。ことし 3 月ごろから本格的にボディメイクにハマっていて、3 ヶ月間で筋肉量をほぼ変えず体脂肪率を 24% から 19% まで減らせました!今は筋肉量を増やすべく、もりもり食べてがんがんトレーニングしているところです。


さて、タイトルの件。

2013 年の入社以来ずっと Mac を使っていた私が Windows マシンに変えて 9 ヶ月が経過しましたので、ここに至るまでに私が実際に感じてきたことなどをまとめてみようと思います。(以下、特筆しない限り "Mac" は "macOS のマシン" を指します。)

過去の私がそうだったように、「今は Mac を使っているし、どれくらい影響があるかわからないので、なかなかふんぎりがつかないな...」と思われている方もいらっしゃるのではと思います。そのような方々の判断材料のひとつとしてお役に立てれば嬉しいです。

※ 対象読者としては、現在 Mac を使っていてお悩みのかたを想定しています。現在 Linux を使っている方は、Windows を消して Arch Linux なぞを入れれば問題ないでしょう。(ジョークだよ!)1

前提1: PC を移行する時点での私の開発環境

私がふだんのお仕事でどんなツールを使って何をしているか、ここ最近で参加した数プロジェクトの実績から代表的なものをまとめておきます。

  • コードを書くのは Vim
  • ターミナルの管理は Tmux
  • シェルは Zsh
  • 開発環境は Docker (on WSL2)
  • メインのブラウザは Chrome
  • 資料の確認のために Microsoft Excel, PowerPoint
  • チーム内、社内での連絡には Slack, Idobata
  • プロジェクト管理は Pivotal Tracker, Trello, Notion
  • ドキュメント管理は DocBase, esa, Notion, Google Docs
  • ソースコード管理は GitHub(Git の操作は CLI で)

前提2: 購入したマシン

参考までに、購入したマシンとスペックについても記載しておきます。

当時、社内の複数の先輩が既に業務で使っていたこと、私自身も某プロジェクトの検証機として使った経験があったことから、Dell XPS 13 を選びました。

  • マシン: Dell XPS 13 (9300)
  • OS: Windows 10 Pro (64ビット)
  • メモリ: 32GB
  • CPU: 第10世代 インテル® Core™ i7

で、どうだった?ファーストインプレッション

改めてこのことについて書いてみようと思ってここ一年弱の業務体験を思い返してみると、「Mac のころからあまり変わらない」というのが最初の印象です。

前述した、頻繁に利用するツールのリストをご覧いただいてもわかるとおり圧倒的にブラウザベースのツールが多いですし、ネイティブアプリがしっかり用意されているサービスばかりだというのが大きな要因かもしれません。

また、開発で使っているツールについては、WSL2 の Ubuntu 上で使っているというのもあって "このマシンは Windows である" ということを殊更意識するような場面はあまりなかったように思います。(私が VSCode 使いとかだったら、もしかしたらもう少しなにか違ったのかな...いや、そうでもないか...?)

なお、WSL2 のセットアップについては当ブログの以下の記事でも解説しています!が、最新の情報ではないので今からトライする方は必ず公式情報もあたってみてください。 blog.agile.esm.co.jp

WSL2 に関しての留意事項

私が作業している中においては、以下の WSL2 特有のふるまいに遭遇し影響を受けました。WSL2 を使っているシステム開発者はほぼみな関係してくることだと思うので、予め知っておいて損はないんじゃないかなぁと思います。

systemd がデフォルトでは動いていない

timedatectl を使おうとしたら動かなかったため「はて?」と思い、調べたところで知りました。

こちらの現象に対する対応は https://github.com/arkane-systems/genie というライブラリを導入するのが定石になっているようです。

VPN に接続するとネットワークが切断され、インターネットにアクセスできなくなる

私が関わる業務ではセキュリティ上の理由から VPN に接続した状態でないとアクセスできないように制限されている操作などがあるため、よく VPN に接続するのですが、そのたびになぜか git pullcap deploy ができなくなるという現象が起きていて大変困っていました。上記に書いたとおり、VPN に接続するときというのはたいてい「失敗するとちょっとこわい」操作なので、原因がわかっていない頃に接続できなかったときは焦ったものです。。

こちらは Microsoft のページで対応方法が解説されています。他にも現実的に運用しやすいワークアラウンドを考案している方もいるようなので、参考にしてみるとよいかもしれません。

Windows にしてよかったこと

さて、それではそろそろ「Windows にしてよかった!」と思ったことについてもふれておきましょう... それは、他でもなく「(WSL2 上で)Docker を使った開発が快適にできるようになったこと」です。実際、これを一番の目当てにしてマシンを変えたので、期待通りのメリットを享受できた点にはとても満足しています。

ここ数年で参画したプロジェクトの中で、開発環境に Docker を使っていないというプロジェクトはひとつもありませんでした(本番環境でもコンテナを使う、というプロジェクトも珍しくなくなりましたね)。

それくらい Docker 上で開発することが普通になった今、Mac の Docker for Mac の処理のスローぶりは私にとって致命的でした。

私は TDD が大好きなので、コードを書くときにはまずテストを書いて作業にエンジンをかけ、そこからドライヴしていくのがお決まりのスタイルです。ところが、Mac の Docker においてはそれを満足にできず、長いこと「なんとかしたいなぁ... 」と感じていました。

しかし、Windows (WSL2)に変えてからというもの、そういったストレスは一切なくなったのです!これは本当に感動的で、当時の日報にも

Docker 上でテストを流したり、ブラウザ操作をするたびに、お笑い芸人並のオーバーリアクションで「いや処理はやっ!!!!」と声が出るくらいである。

と書いていました。

これだけでも、フォントが Mac に比べて好みじゃないなどといった微妙なネガティブポイントを補って余りある利点だと感じています。WSL2 さまさまです。

気に留めておいたほうがいいこと

最後に、乗り換えるにあたって認識しておくと後悔が少ないであろうことを挙げてみます。(当たり前のことだと思われるかもしれませんが、そう思えた方はこちらを気にせず迷わず WSL に乗り換えてしまいましょう!)

(日本語の)情報が少ない

まず、WSL2 については利用者は徐々に増えているものの、Mac に比べたら圧倒的にマイナー側なので公に出ている情報は少ないです。

何かあったときには発生事象から自力で原因を突き止めたり、WSL の公式 repo の issues 等を見て状況を把握したりできる必要はあると思います...当ブログのような技術専門リソースにご興味のあるような方ならそんなの当たり前だと思われるでしょうが。

あ、あるいは弊社のように、なにかあったときにすぐに聞ける人たちが近くにいてくれるような環境だとよりよいですね!(宣伝)

気にすることが多くなる

学習をはじめて間もない方にとっては、WSL2 というものに加えて、ホスト OS / WSL2 上の OS / Docker 等とさまざまな要素が絡み合っている状態はトラブルシューティングに苦慮するところかもしれません。

なにかあったときの問題の切り分け作業が多岐に渡る可能性が高くなるため、これらの要素に不慣れな方は解決までの道のりがちょっとハードかもしれないということは認識しておくとよいでしょう。もしも Linux( や Docker )にまったくさわったことがないという方なら、それらについてを学びたいというのが作業の主眼でない限りはまずは Mac をお使いになったほうが、やりたいことにフォーカスし続けられてよいのではと思います。


以上、気がついたことをまとめてみました。

他にもご意見などがあればぜひ聞かせてもらえると嬉しいです!それでは、ごきげんよう~。


  1. 最近の弊社では Arch Linux を入れている人が多いようなのです。

Input object を使ってリクエストパラメータを検証する

はじめに

Bonjour! 近ごろ Duolingo のフランス語コースにハマっている @ima1zumi です。

さて、最近リクエストパラメータにバリデーションをかけたいことがありました。そのために、ユーザの入力を検証する専用のクラスを作ると便利だったので紹介します。

動作確認環境

  • Ruby 3.0.1
  • Ruby on Rails 6.1.4

実現したいこと

ここでは例として User の一覧を表示する UsersController#index に、表示順を制御する order_keyorder_direction というリクエストパラメータを追加します。

order_keyUser モデルのカラム名を、 order_directionascdesc のみ受けつけます。これらのパラメータにバリデーションをかけて、規定の値のみ受け付けるようにします。

class UserController < ApplicationController
  def index
    # order に渡すクエリパラメータのバリデーションを行いたい
    @users = User.all.order(user_params[:order_key] => user_params[:order_direction])
  end

  private

  def user_params
    params.permit(:order_key, :order_direction)
  end
end

Input object の責務

ここではバリデーションをかけるクラスを Input object と呼ぶことにします。

これは Shopify の Upgrow (現在は非公開) の Input object の考え方を参考にしています。

このオブジェクトの責務は ユーザーの入力を検証しオブジェクト化する ことです。

参考:Upgrow: Railsアプリの保守性を高めるためのShopifyのアプローチ / Upgrow - Speaker Deck

Input object の実装

まず、 ActiveModel::ModelActiveModel::Attributesinclude したクラスを作成します。

app/inputs/user_order_input.rb

class UserOrderInput
  include ActiveModel::Model
  include ActiveModel::Attributes
end

ActiveModel::Attributes

ActiveModel::Attributesinclude すると attribute クラスメソッドを使えるようになります。 attribute で属性を宣言するとアクセサが定義されたり、 initialize でその値を渡すことができるようになります。attribute に属性名と型を渡すと型をキャストします。

class UserOrderInput
  include ActiveModel::Model
  include ActiveModel::Attributes
  # 属性の設定を宣言的に記述する
  attribute :order_key, :string
  attribute :order_direction, :string, default: 'desc'
end

ActiveModel::Attributes のドキュメントはありませんので rails のコードを直接ご覧ください。

github.com

キャストできる型はこちらに書いてあります。

github.com

サンプルコード

attribute で宣言した属性にアクセスすることができます。

user_order_input = UserOrderInput.new(order_key: 'name', order_direction: 'asc')
p user_order_input.order_key
# => name

いろいろな型にキャストしてみました。Boolean などキャストできて便利です。

class SampleInput
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :number, :integer
  attribute :flag, :boolean
  attribute :birthday, :date
end

sample = SampleInput.new(number: '1', flag: 'true', birthday: '1993-02-24')

p sample.number
# => 1
p sample.number.class
# => Integer

p sample.flag
# => true
p sample.flag.class
# => TrueClass

p sample.birthday
# => Wed, 24 Feb 1993
p sample.birthday.class
# => Date

ちなみに、ActiveModel::Attributes -> ActiveModel::Model の順番で include すると initialize メソッドの評価順がおかしくなり動かなくなるので注意してください。

class SampleInput
  include ActiveModel::Attributes
  include ActiveModel::Model

  attribute :name, :string
end
# => #<Concurrent::Map:0x00007f8ab03225e8 entries=0 default_proc=nil>
SampleInput.new(name: 'hoge')
# => /Users/mi/.asdf/installs/ruby/3.0.1/lib/ruby/gems/3.0.0/gems/activemodel-6.1.4/lib/active_model/attributes.rb:124:in `_write_attribute': undefined method `write_from_user' for nil:NilClass (NoMethodError)

ActiveModel::Model

ActiveModel::Modelinclude すると validates クラスメソッドなどのバリデーションメソッドを使えるようになります。

railsguides.jp

class UserOrderInput
  include ActiveModel::Model
  include ActiveModel::Attributes
  attribute :order_key, :string
  attribute :order_direction, :string, default: 'desc'
  # 各属性のバリデーションを記述する
  validates :order_key, inclusion: { in: User.column_names }, presence: true
  validates :order_direction, inclusion: { in: %w(asc desc) }
end

サンプルコード

インスタンスに対して valid? / invalid? でバリデーション結果を確認することが出来ます。

user_order_input = UserOrderInput.new(order_direction: 'asc')
p user_order_input.valid?
# => true
user_order_input = UserOrderInput.new(order_direction: 'hoge')
p user_order_input.valid?
# => false

コントローラから呼び出す

ここでは UsersControllerindex アクションでバリデーションをかけます。

class UserController < ApplicationController
  before_action :valid_params, only: [:index]

  def index
    @users = User.all.order(user_order_input.order_key => user_order_input.order_direction)
  end

  private

  def valid_params
    head :bad_request if user_order_input.invalid?
  end

  def user_order_input
    @user_order_input ||= UserOrderInput.new(user_params)
  end

  def user_params
    params.permit(:order_key, :order_direction)
  end
end

このようにコントローラとバリデーションの責務を分けることができました。

Input object と Form object の違い

Form object はいろいろな定義がありますが、ここでは willnet さんの定義を引用します。

form_withのmodelオプション*1にActive Record以外のオブジェクトを渡すデザインパターンです。form_withのmodelオプションに渡すオブジェクト自体もform objectと呼びます。

(参考:form objectを使ってみよう - メドピア開発者ブログ

Form object と Input object の一番の違いは、 Input object は入力値の検証とインスタンスの作成のみ行うということです。

Form object は名前や性質がフォームと強く結びついています。また Form object を使って save することをスコープに入れる場合があります。

今回紹介した方法では、フォームでなくてもよいです。また、 save は行いません。

ここでは、リクエストパラメータのバリデーションに使える Input object の実装例を紹介しました。

参考

プルリクエストのレビュー時に気をつけていること

こんにちは、 @yuki0920 です。

エンジニア歴が1年を超えたあたりから、プルリクエスト(以下、PR)をレビューしてもらうだけでなく、レビューをする機会が多くなりました。

本記事では、PRのレビュワーとして、業務で気をつけている点について記します。

前提

私がエンジニア1年目の時はPRを出すと、100や200以上の指摘をもらうことが多々ありました。 自分のレビューを受ける側としての経験から、「対応しやすかった」というレビューのポイントを抽出し、自身がレビュワーをする際に活かすことで、効率的なPRのレビューを行えるのではないかと考え実践しています。

ポイント1. 現時点の問題点の理由を伝える

「~の実装のほうが良いのではないでしょうか?」という指摘は、対応しづらいことが多いです。 なぜ現状のコードが良くないのか?がわからないためです。

そのため、現状のコードが抱える課題とその理由をできるだけ具体的に記すようにしています。

ポイント2. 提案内容をコードで示す

提案内容をコードで示すのは、レビュワーからすると自分の頭の中で描いていることをコードに落とし込むだけなので、そんなに時間をかけずにできる場合が多いはずです。 一方で、レビューを受ける側からすると、具体的なコードがないと対応方針がわからない場合でも、レビュワーから具体的なコードの提案があれば、対応方針を立てやすくなります。

私は GitHubのSuggestionを積極的に活用し、自分がより良いと思うコードを書いて、議論の足がかりとなるようにしています。

ポイント3. 断定するのではなく提案する

いくら合理的な指摘をしたとしても、断定口調で言葉使いが良くないと、指摘を受けた側は積極的に対応しづらい気持ちになります。

あくまでも、PRの提出者を尊重して、「~と対応するのはいかがでしょうか?」など、提案する風に、かつ柔らかく伝えられるように気をつけています。

まとめ

私がPRのレビュワーとして普段から気をつけている点を紹介しました。 みなさんの気をつけている点もぜひ教えていただけると幸いです。

Rails / OSS パッチ会オンライン 2021年6月のお知らせ

2021年6月の Rails / OSS パッチ会を 6月24日(木)にオンライン開催します。

この会をひとことでいうと、日頃のお仕事で使っている Rails をはじめとする OSS への upstream にパッチを送る会です。

会には Ruby と Rails のコミッターである顧問の a_matsuda もいますので、例えば Rails に送るパッチのネタがあるけれど、パッチを送るに適しているかの判断やパッチを送る流れが悩ましいときなど a_matsuda に相談して足がかりにするなどできます。

開催時間は 17:00-19:00 となりますがご都合のあう方はぜひご参加下さい。Zoom あたりのテレビ会議システムを使います。

当日の招待 URL は Idobata の esminc/rails ルームで共有する予定です。

idobata.io

特に募集ページなど設けませんが、上記理由から Idobata のアカウントが必要になると思います。

RubyKaigi Takeout 2021 や Kaigi on Rails 2021 の CFP や、最近の Ruby / Rails まわりの動向に関する話題などあるかもしれません。

その他の開催方針については以下の Gist に記していますので、ご参照ください。

Reboot Rails/OSS meetup online · GitHub

Amazon ECS タスクメタデータを利用して、Sentry のタグを設定する

はじめに

こんにちは。yoshino です。

私が開発に参加しているチームではECSタスクで処理を実行して、エラー監視にはSentryを利用しています。

その際、Sentry のCustomize Tagsは、Sentry からの通知をタグで絞り込むことができるので便利です。

この記事では、Amazon ECS タスクメタデータエンドポイントを利用して、「タスク定義で指定されたコンテナの名前」と「コンテナのイメージ名」を Sentry のタグに設定する方法を紹介します。

Amazon ECS タスクメタデータ

タスクメタデータエンドポイントバージョン 3 のECS_CONTAINER_METADATA_URIを利用する例です*1

エージェントはタスクの各コンテナに、環境変数 ECS_CONTAINER_METADATA_URI を挿入するため、環境変数経由でエンドポイントへ GET リクエストをするだけでタスクのメタデータを取得することができます。

require 'net/http'

response = Net::HTTP.get(URI.parse(ENV['ECS_CONTAINER_METADATA_URI']))
meta_data = JSON.parse(response.body, symbolize_names: true)
container = meta_data[:Name]   # => 'nginx-curl'
image = meta_data[:Image]      # => 'nrdlngr/nginx-curl'

Sentry のタグの設定

Railsであればconfig/initializers/sentry.rbに取得したcontainerimageを設定します。

getsentry/sentry-rubyを利用することを想定しています。

Sentry.configure_scope do |scope|
  scope.set_tags(container: container, image: image)
end

以上の設定を追加しておくことで、新しい ECS タスクを作成する時に、タグの設定を追加で書く必要がなくなります。

おわりに

タスクメタデータエンドポイントでは様々な情報を取得できるので、Sentry と組み合わせて利用することで、エラー監視のためのより良い環境を構築できるのではないかと思います。

*1: タスクメタデータエンドポイントバージョン 4が最新のバージョンになります。

Rubyで任意の文字列を削除するメソッドの速度を比較してみた

はじめに

こんにちは、wai-doiです。

Railsアプリケーションを開発していて、文字列の削除をするコードを書くことがあると思います。 例えば以下のようなコードです。

zip_code = '123-4567'
zip_code.gsub(/-/, '')
#=> 1234567

この例では String#gsub を使いましたが、他のメソッドでも同様のことを実現することができます。 私はそのたびに、どの書き方が良いのか迷っていました。そこで今回は速度の観点でどのメソッドを使うのがよいのかを測定してみました。

実行環境

速度の測定には、簡単に処理速度を計測することができる benchmark-ips を用いました。

github.com

今回の実行環境です。

  • Ruby (3.0.0)
  • Active Support (6.1.3.1)
  • benchmark-ips (2.8.4)

測定その1

郵便番号からハイフン - 1 文字を削除するときの速度を測定します。以下の観点を比較してみることにしました。

  • 比較するメソッドはString#tr, String#delete, String#sub, String#gsub, String#remove の 5 種類。
  • 非破壊的と破壊的な変更をするメソッドはどちらが速いのか。
  • 置換対象の引数に文字列でも正規表現でもとることができるメソッドはどちらが速いのか。

郵便番号を表す文字列から - 1文字を削除する Ruby のコードを実行します。String#remove を使うため Active Support を require しています。

require 'active_support/core_ext/string'
require 'benchmark/ips'

Benchmark.ips do |x|
  x.report('String#tr') { '123-4567'.tr('-', '') }
  x.report('String#tr!') { '123-4567'.tr!('-', '') }

  x.report('String#delete') { '123-4567'.delete('-') }
  x.report('String#delete!') { '123-4567'.delete!('-') }

  x.report('String#sub (string)') { '123-4567'.sub('-', '') }
  x.report('String#sub (regexp)') { '123-4567'.sub(/-/, '') }

  x.report('String#sub! (string)') { '123-4567'.sub!('-', '') }
  x.report('String#sub! (regexp)') { '123-4567'.sub!(/-/, '') }

  x.report('String#gsub (string)') { '123-4567'.gsub('-', '') }
  x.report('String#gsub (regexp)') { '123-4567'.gsub(/-/, '') }

  x.report('String#gsub! (string)') { '123-4567'.gsub!('-', '') }
  x.report('String#gsub! (regexp)') { '123-4567'.gsub!(/-/, '') }

  x.report('String#remove (string)') { '123-4567'.remove('-') }
  x.report('String#remove (regexp)') { '123-4567'.remove(/-/) }

  x.report('String#remove! (string)') { '123-4567'.remove!('-') }
  x.report('String#remove! (regexp)') { '123-4567'.remove!(/-/) }

  x.compare!
end

benchmark-ips の出力は以下になりました。

Warming up --------------------------------------
           String#tr   402.521k i/100ms
          String#tr!   434.153k i/100ms
       String#delete   453.843k i/100ms
      String#delete!   519.036k i/100ms
 String#sub (string)   245.331k i/100ms
 String#sub (regexp)   240.213k i/100ms
String#sub! (string)   262.920k i/100ms
String#sub! (regexp)   255.543k i/100ms
String#gsub (string)   120.926k i/100ms
String#gsub (regexp)    63.566k i/100ms
String#gsub! (string)
                       117.623k i/100ms
String#gsub! (regexp)
                        63.364k i/100ms
String#remove (string)
                        56.069k i/100ms
String#remove (regexp)
                        37.356k i/100ms
String#remove! (string)
                        67.638k i/100ms
String#remove! (regexp)
                        42.655k i/100ms
Calculating -------------------------------------
           String#tr      3.933M (± 2.4%) i/s -     19.724M in   5.018301s
          String#tr!      4.370M (± 1.6%) i/s -     22.142M in   5.067534s
       String#delete      4.514M (± 1.9%) i/s -     22.692M in   5.028565s
      String#delete!      5.081M (± 2.0%) i/s -     25.433M in   5.007540s
 String#sub (string)      2.419M (± 1.6%) i/s -     12.267M in   5.071963s
 String#sub (regexp)      2.385M (± 3.1%) i/s -     12.011M in   5.041188s
String#sub! (string)      2.600M (± 3.5%) i/s -     13.146M in   5.062776s
String#sub! (regexp)      2.539M (± 1.9%) i/s -     12.777M in   5.035136s
String#gsub (string)      1.186M (± 2.6%) i/s -      5.925M in   5.001154s
String#gsub (regexp)    630.950k (± 3.6%) i/s -      3.178M in   5.044408s
String#gsub! (string)
                          1.074M (± 4.8%) i/s -      5.411M in   5.050392s
String#gsub! (regexp)
                        528.834k (± 1.7%) i/s -      2.661M in   5.033851s
String#remove (string)
                        487.230k (± 2.8%) i/s -      2.467M in   5.067225s
String#remove (regexp)
                        349.516k (± 4.1%) i/s -      1.756M in   5.032162s
String#remove! (string)
                        677.874k (± 1.7%) i/s -      3.450M in   5.090182s
String#remove! (regexp)
                        427.461k (± 1.9%) i/s -      2.175M in   5.091096s

Comparison:
      String#delete!:  5080887.5 i/s
       String#delete:  4514317.0 i/s - 1.13x  (± 0.00) slower
          String#tr!:  4370478.5 i/s - 1.16x  (± 0.00) slower
           String#tr:  3932740.8 i/s - 1.29x  (± 0.00) slower
String#sub! (string):  2600367.7 i/s - 1.95x  (± 0.00) slower
String#sub! (regexp):  2538594.2 i/s - 2.00x  (± 0.00) slower
 String#sub (string):  2419134.9 i/s - 2.10x  (± 0.00) slower
 String#sub (regexp):  2385105.6 i/s - 2.13x  (± 0.00) slower
String#gsub (string):  1185649.3 i/s - 4.29x  (± 0.00) slower
String#gsub! (string):  1073853.4 i/s - 4.73x  (± 0.00) slower
String#remove! (string):   677873.7 i/s - 7.50x  (± 0.00) slower
String#gsub (regexp):   630949.8 i/s - 8.05x  (± 0.00) slower
String#gsub! (regexp):   528833.6 i/s - 9.61x  (± 0.00) slower
String#remove (string):   487230.2 i/s - 10.43x  (± 0.00) slower
String#remove! (regexp):   427461.0 i/s - 11.89x  (± 0.00) slower
String#remove (regexp):   349516.3 i/s - 14.54x  (± 0.00) slower

測定その 1 の考察

メソッドを速い順に並べると String#delete, String#tr, String#sub, String#gsub, String#remove の順番でした。用途がより限定的なメソッドは速く、より汎用的なメソッドは遅いという結果となりました。便利な String#gsub をいつも使うのではなく、用途に合わせて適切なメソッドを使うことが良いということですね。

非破壊的と破壊的メソッドについては、常にどちらの方が速いかというのは今回の測定ではわかりませんでした。破壊的メソッドの方がオブジェクトを新たに作らないため速いのではないかと予想していましたが、速度のために破壊的メソッドを使うメリットはそれほど無いのかもしれません。

引数に文字列を与えるか正規表現を与えるかですが、常に文字列を与えたときの方が少しだけ速いことがわかりました。それは正規表現のマッチに少し時間がかかるためと考えられます。置換対象の引数が文字列で十分なときは文字列を使うのが良いですね。

また、とても学びになったのは String#remove がとても遅かったということです。Rails で開発しているなら String#remove がせっかく使えるから使おうと私はいままで考えていましたが、考え直す必要があるなと思いました。実装を見たところ、中で String#gsub! を呼び出しているだけなので gsub より遅いのは納得ですね。

測定その2

次に、レシーバーが長い文字列の場合の速度を測定してみました。

先ほどの郵便番号に対して1 万字の長さの文字列をくっつけた文字列をレシーバーにして、削除のメソッドを実行してみました 。比較する観点は、測定その 1 と同じにしています。

Benchmark.ips do |x|
  x.report('String#tr') { ('a' * 10000 + '123-4567').tr('-', '') }
  x.report('String#tr!') { ('a' * 10000 + '123-4567').tr!('-', '') }

  x.report('String#delete') { ('a' * 10000 + '123-4567').delete('-') }
  x.report('String#delete!') { ('a' * 10000 + '123-4567').delete!('-') }

  x.report('String#sub (string)') { ('a' * 10000 + '123-4567').sub('-', '') }
  x.report('String#sub (regexp)') { ('a' * 10000 + '123-4567').sub(/-/, '') }

  x.report('String#sub! (string)') { ('a' * 10000 + '123-4567').sub!('-', '') }
  x.report('String#sub! (regexp)') { ('a' * 10000 + '123-4567').sub!(/-/, '') }

  x.report('String#gsub (string)') { ('a' * 10000 + '123-4567').gsub('-', '') }
  x.report('String#gsub (regexp)') { ('a' * 10000 + '123-4567').gsub(/-/, '') }

  x.report('String#gsub! (string)') { ('a' * 10000 + '123-4567').gsub!('-', '') }
  x.report('String#gsub! (regexp)') { ('a' * 10000 + '123-4567').gsub!(/-/, '') }

  x.report('String#remove (string)') { ('a' * 10000 + '123-4567').remove('-') }
  x.report('String#remove (regexp)') { ('a' * 10000 + '123-4567').remove(/-/) }

  x.report('String#remove! (string)') { ('a' * 10000 + '123-4567').remove!('-') }
  x.report('String#remove! (regexp)') { ('a' * 10000 + '123-4567').remove!(/-/) }

  x.compare!
end
Warming up --------------------------------------
           String#tr     4.700k i/100ms
          String#tr!     5.329k i/100ms
       String#delete     4.399k i/100ms
      String#delete!     5.182k i/100ms
 String#sub (string)    15.998k i/100ms
 String#sub (regexp)     8.603k i/100ms
String#sub! (string)    14.971k i/100ms
String#sub! (regexp)     8.500k i/100ms
String#gsub (string)    11.075k i/100ms
String#gsub (regexp)     5.322k i/100ms
String#gsub! (string)
                        13.001k i/100ms
String#gsub! (regexp)
                         5.132k i/100ms
String#remove (string)
                        10.418k i/100ms
String#remove (regexp)
                         4.718k i/100ms
String#remove! (string)
                        12.948k i/100ms
String#remove! (regexp)
                         5.079k i/100ms
Calculating -------------------------------------
           String#tr     47.354k (± 3.1%) i/s -    239.700k in   5.066950s
          String#tr!     50.061k (±10.0%) i/s -    250.463k in   5.078335s
       String#delete     45.918k (± 6.5%) i/s -    228.748k in   5.005509s
      String#delete!     51.088k (± 5.2%) i/s -    259.100k in   5.088284s
 String#sub (string)    160.722k (±14.8%) i/s -    799.900k in   5.071508s
 String#sub (regexp)     83.337k (± 7.9%) i/s -    421.547k in   5.090275s
String#sub! (string)    149.517k (±11.8%) i/s -    748.550k in   5.070937s
String#sub! (regexp)     83.269k (± 5.1%) i/s -    416.500k in   5.015623s
String#gsub (string)    140.134k (±10.0%) i/s -    697.725k in   5.024230s
String#gsub (regexp)     55.118k (± 3.4%) i/s -    276.744k in   5.026910s
String#gsub! (string)
                        137.273k (± 8.2%) i/s -    689.053k in   5.051651s
String#gsub! (regexp)
                         54.317k (± 6.9%) i/s -    271.996k in   5.036091s
String#remove (string)
                         99.842k (± 6.8%) i/s -    500.064k in   5.031706s
String#remove (regexp)
                         45.780k (± 9.3%) i/s -    231.182k in   5.114578s
String#remove! (string)
                        128.251k (± 8.8%) i/s -    647.400k in   5.085621s
String#remove! (regexp)
                         53.039k (± 3.1%) i/s -    269.187k in   5.080040s

Comparison:
 String#sub (string):   160722.0 i/s
String#sub! (string):   149517.0 i/s - same-ish: difference falls within error
String#gsub (string):   140134.1 i/s - same-ish: difference falls within error
String#gsub! (string):   137273.0 i/s - same-ish: difference falls within error
String#remove! (string):   128250.9 i/s - same-ish: difference falls within error
String#remove (string):    99842.5 i/s - 1.61x  (± 0.00) slower
 String#sub (regexp):    83337.3 i/s - 1.93x  (± 0.00) slower
String#sub! (regexp):    83269.4 i/s - 1.93x  (± 0.00) slower
String#gsub (regexp):    55118.3 i/s - 2.92x  (± 0.00) slower
String#gsub! (regexp):    54317.2 i/s - 2.96x  (± 0.00) slower
String#remove! (regexp):    53039.1 i/s - 3.03x  (± 0.00) slower
      String#delete!:    51087.8 i/s - 3.15x  (± 0.00) slower
          String#tr!:    50060.5 i/s - 3.21x  (± 0.00) slower
           String#tr:    47353.9 i/s - 3.39x  (± 0.00) slower
       String#delete:    45917.8 i/s - 3.50x  (± 0.00) slower
String#remove (regexp):    45779.8 i/s - 3.51x  (± 0.00) slower

測定その 2 の考察

結果は測定その 1 から大きく変わり、長い文字列がレシーバーのときは String#deleteString#tr はとても遅くなってしまうことが分かりました。逆に String#sub, String#gsub, String#remove の方が速くなっていました。

また、String#sub, String#gsub, String#remove において引数が正規表現の場合がとても遅くなってしまうことがわかりました。それは、文字列が長いと正規表現のマッチ処理のステップ数が多くなるため速度に影響してしまったと思われます。

まとめ

今回は任意の文字列を削除するメソッドの速度を測定してみました。今回の知見から速度の面と用途の面で適切なメソッドを選んでいきましょう。