Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

RSpecを使用したオブジェクト割り当てのテスト

最近、誰もがRubyのパフォーマンスについて話し合っていますが、それには十分な理由があります。コードを少し調整するだけで、パフォーマンスを最大99.9%向上させることができます。

方法に関する記事はたくさんあります コードを最適化するためですが、コードが残っていることをどのように確認できますか 最適化されていますか?

定期的に呼び出されるメソッドにフリーズ定数ではなく文字列リテラルを埋め込む場合、必ずしも結果を考慮するとは限りません。将来コードを保守するときに、最適化の節約を失うのは簡単です。

HoneybadgerのRubygemで2回目(または3回目)にコードを最適化したときの最近の考えは次のとおりです。「これらの最適化が回帰しないようにする方法があれば素晴らしいと思いません / em> ?"

回帰は、名前ではなくても、ソフトウェア開発で私たちのほとんどが精通しているものです。同じコードへの将来の変更により、過去に解決されたバグまたは問題が再発した場合、リグレッションが発生します。同じ仕事を2回以上行うのが好きな人は誰もいません。退行は、床が掃除された直後に床の汚れを追跡するようなものです。

幸いなことに、私たちは秘密兵器を持っています:テスト。独断的なTDDを実践するかどうかにかかわらず、テストは素晴らしい プログラムで問題と解決策を示しているため、バグを修正してください。テストにより、変更が行われたときに回帰が発生しないという確信が得られます。

おなじみですか?私もそう思ったので、「パフォーマンスの最適化が低下する可能性があるのに、なぜそれらの低下をテストでキャッチできないのか」と疑問に思いました。

オブジェクトの割り当て、メモリ、CPU、ガベージコレクションなど、Rubyのさまざまなパフォーマンスの側面をプロファイリングするための優れたツールがたくさんあります。これらには、ruby-prof、stackprof、allocation_tracerなどがあります。

最近、allocation_statsを使用してオブジェクトの割り当てをプロファイリングしています。割り当てを減らすことは、達成するのがかなり簡単な作業であり、メモリ消費と速度を調整するための多くの手間のかからない成果をもたらします。

たとえば、デフォルトで「foo」になっている5つの文字列の配列を格納する基本的なRubyクラスは次のとおりです。

class MyClass
  def initialize
    @values = Array.new(5)
    5.times { @values << 'foo' }
  end
end

AllocationStatsAPIはシンプルです。プロファイルにブロックを付けると、ほとんどのオブジェクトが割り当てられている場所が印刷されます。

$ ruby -r allocation_stats -r ./lib/my_class
stats = AllocationStats.trace { MyClass.new } 
puts stats.allocations(alias_paths: true).group_by(:sourcefile, :sourceline, :class).to_text
^D
     sourcefile        sourceline   class   count
---------------------  ----------  -------  -----
/lib/my_class.rb           4       String       5
/lib/my_class.rb           3       Array        1
-                          1       MyClass      1

#to_text メソッド(割り当てのグループで呼び出される)は、要求する基準によってグループ化された、人間が読める形式の優れたテーブルを出力するだけです。

この出力は手動でプロファイリングする場合に最適ですが、私の目標は、通常のユニットテストスイート(RSpecで記述されている)と一緒に実行できるテストを作成することでした。my_class.rbの4行目に、5つの文字列が割り当てられていることがわかります。 、すべて同じ値が含まれていることがわかっているので、これは不要のようです。シナリオに「MyClassを初期化するときに6つのオブジェクトの下に割り当てる」のようなものを読みたいと思いました。 RSpecでは、これは次のようになります。

describe MyClass do
  context "when initializing" do
    specify { expect { MyClass.new }.to allocate_under(6).objects }
  end
end

この構文を使用して、オブジェクトの割り当てが、記述されたコードブロック(expect内)の指定された数より少ないことをテストするために必要なすべてを持っています ブロック)カスタムRSpecマッチャーを使用します。

トレース結果の出力に加えて、AllocationStatsは、#allocationsなど、Rubyを介して割り当てにアクセスするためのいくつかのメソッドを提供します。 および#new_allocations 。マッチャーを作成するために使用したものは次のとおりです。

begin
  require 'allocation_stats'
rescue LoadError
  puts 'Skipping AllocationStats.'
end

RSpec::Matchers.define :allocate_under do |expected|
  match do |actual|
    return skip('AllocationStats is not available: skipping.') unless defined?(AllocationStats)
    @trace = actual.is_a?(Proc) ? AllocationStats.trace(&actual) : actual
    @trace.new_allocations.size < expected
  end

  def objects
    self
  end

  def supports_block_expectations?
    true
  end

  def output_trace_info(trace)
    trace.allocations(alias_paths: true).group_by(:sourcefile, :sourceline, :class).to_text
  end

  failure_message do |actual|
    "expected under #{ expected } objects to be allocated; got #{ @trace.new_allocations.size }:\n\n" << output_trace_info(@trace)
  end

  description do
    "allocates under #{ expected } objects"
  end
end

LoadErrorを救助しています すべてのテスト実行にAllocationStatsを含めたくない場合があるため、最初のrequireステートメントに(テストの速度が低下する傾向があります)。次に、:allocate_underを定義します match内でトレースを実行するマッチャー ブロック。 failure_message ブロックにはto_textが含まれているため、ブロックも重要です。 AllocationStatsトレースからの出力失敗メッセージのすぐ内側 !マッチャーの残りの部分は、ほとんどが標準のRSpec構成です。

マッチャーがロードされたので、以前からシナリオを実行して、失敗するのを見ることができます:

$ rspec spec/my_class_spec.rb 

MyClass
  when initializing
    should allocates under 6 objects (FAILED - 1)

Failures:

  1) MyClass when initializing should allocates under 6 objects
     Failure/Error: expect { MyClass.new }.to allocate_under(6).objects
       expected under 6 objects to be allocated; got 7:

               sourcefile           sourceline   class   count
       ---------------------------  ----------  -------  -----
       <PWD>/spec/my_class_spec.rb           6  MyClass      1
       <PWD>/lib/my_class.rb                 3  Array        1
       <PWD>/lib/my_class.rb                 4  String       5
     # ./spec/my_class_spec.rb:6:in `block (3 levels) in <top (required)>'

Finished in 0.15352 seconds (files took 0.22293 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/my_class_spec.rb:5 # MyClass when initializing should allocates under 6 objects

OK、それで私はプログラムでパフォーマンスの問題を示しました。それはMyClassが同じ値で余分な文字列オブジェクトを割り当てるということです。これらの値を固定定数にスローすることで、この問題を修正しましょう:

class MyClass
  DEFAULT = 'foo'.freeze

  def initialize
    @values = Array.new(5)
    5.times { @values << DEFAULT }
  end
end

問題を修正したので、もう一度テストを実行して、合格するのを確認します。

$ rspec spec/my_class_spec.rb

MyClass
  when initializing
    should allocates under 6 objects

Finished in 0.14952 seconds (files took 0.22056 seconds to load)
1 example, 0 failures

次回MyClass#initializeを変更するとき メソッド、私はあまり多くのオブジェクトを割り当てていないことを確信できます。

プロファイリングの割り当ては比較的遅い可能性があるため、これらを常にではなくオンデマンドで実行することが理想的です。欠落しているallocation_statsをすでに適切に処理しているため、Bundlerを使用して複数のgemfileを作成し、BUNDLE_GEMFILE環境変数で使用するgemfileを指定できます。

$ BUNDLE_GEMFILE=with_performance.gemfile bundle exec rspec spec/
$ BUNDLE_GEMFILE=without_performance.gemfile bundle exec rspec spec/

もう1つのオプションは、評価宝石のようなライブラリを使用することです。これは、これと同じアプローチを採用し、いくつかのBundlerの落とし穴を解決します。 Jason Clarkは、2015年3月にRubyonAlesでこれを行う方法について優れたプレゼンテーションを行いました。詳細については、彼のスライドをご覧ください。

また、これらのタイプのテストを通常の単体テストとは別に維持することも良い考えだと思うので、新しい「パフォーマンス」ディレクトリを作成して、ユニットテストスイートがspec / unit /にあり、パフォーマンススイートがspecにあるようにします。 / performance /:

spec/
|-- spec_helper.rb
|-- unit/
|-- features/
|-- performance/

パフォーマンスのためにRubyコードをプロファイリングするためのアプローチをまだ改良しています。パフォーマンステストスイートを維持することで、現在のコードの速度を改善し、将来的に高速に保ち、自分自身や他の人のためにドキュメントを作成するのに役立つことを願っています。


  1. QRGenを使用した悪意のあるQRコード

    QRコードは、自動的にスキャンする必要があるものすべてに使用される機械可読データ形式です。製品のパッケージングから航空会社の搭乗券など、あらゆる場所でカスタムQRコードにパックされたエクスプロイトを使用して、一般的な脆弱性を悪用することができます。ハッカーは、脆弱なデバイスを標的とする悪意のあるQRコードを作成するツールQRGenを使用しました。人間はQRコードに含まれる情報をスキャンせずに読み取ったり理解したりすることができないため、QRコード攻撃は強力であり、コードの解読を試みるために使用されるデバイスが、QRコードに含まれるエクスプロイトにさらされる可能性があります。人間は実際にスキャン

  2. RuboCopを使用したRubyコードのリンティングと自動フォーマット

    リンティングは、プログラムおよびスタイルのエラーについてソースコードを自動チェックすることです。このチェックは、リンターと呼ばれる静的コード分析ツールによって実行されます。ただし、コードフォーマッタは、事前に構成された一連のルールに厳密に準拠するようにソースコードをフォーマットするためのツールです。リンターは通常違反を報告しますが、問題を修正するのは通常プログラマー次第ですが、コードフォーマッターはそのルールをソースコードに直接適用する傾向があるため、フォーマットの間違いを自動的に修正します。 プロジェクトでより一貫性のあるコードスタイルを作成するタスクでは、通常、個別のリンティングツールと