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

メモ化によるレールの高速化

アプリケーションを開発するとき、実行速度が遅いメソッドがあることがよくあります。おそらく、データベースにクエリを実行するか、外部サービスにアクセスする必要があります。どちらの場合も、速度が低下する可能性があります。そのデータが必要になるたびにメソッドを呼び出してオーバーヘッドを受け入れることもできますが、パフォーマンスが懸念される場合は、いくつかのオプションがあります。

1つは、データを変数に割り当てて再利用できるため、プロセスが高速化されます。可能な解決策ではありますが、その変数を手動で管理することはすぐに面倒になる可能性があります。

しかし、代わりに、この「遅い作業」を実行するメソッドがその変数を処理できるとしたらどうでしょうか。これにより、同じ方法でメソッドを呼び出すことができますが、メソッドにデータを保存して再利用させることができます。これはまさにメモ化が行うことです。

簡単に言うと、メモ化はメソッドの戻り値を保存するため、毎回再計算する必要はありません。すべてのキャッシングと同様に、メモリを時間と効果的に交換します(つまり、値を格納するために必要なメモリを放棄しますが、メソッドの処理に必要な時間を節約します)。

値をメモ化する方法

Rubyは、or-equals演算子を使用して値をメモ化するための非常にクリーンなイディオムを提供します: || = 。これは論理OR( || )を使用します )左と右の値の間で、結果を左側の変数に割り当てます。動作中:

value ||= expensive_method(123)

#logically equivalent to:
value = (value || expensive_method(123))
メモ化はどのように機能しますか

これがどのように機能するかを理解するには、「偽の」値と遅延評価という2つの概念を把握する必要があります。まず、真偽から始めましょう。

真実と偽

Ruby(他のほとんどすべての言語と同様)には、ブール値の trueのキーワードが組み込まれています。 およびfalse 値。期待どおりに機能します:

if true
  #we always run this
end

if false
  # this will never run
end

ただし、Ruby(および他の多くの言語)にも「true」および「falsey」の値の概念があります。これは、値を trueであるかのように「あたかも」扱うことができることを意味します またはfalse 。 Rubyの場合のみ nil およびfalse 偽りです。他のすべての値(ゼロを含む)は trueとして扱われます (注:他の言語では異なる選択が行われます。たとえば、Cはゼロを falseとして扱います。 )。上記の例を再利用すると、次のように書くこともできます。

value = "abc123" # a string
if value
  # we always run this
end

value = nil
if value
  # this will never run
end
遅延評価

遅延評価は、プログラミング言語で非常に一般的な最適化の形式です。これにより、プログラムは不要な操作をスキップできます。

論理OR演算子( || )左側または右側のいずれかがtrueの場合、trueを返します。これは、左側の引数が真の場合、結果が真になることがすでにわかっているため、右側を評価する意味がないことを意味します。これを自分で実装すると、次のようになる可能性があります。

>
def logical_or (lhs, rhs)
  return lhs if lhs

  rhs
end

lhsの場合 およびrhs 関数(例:lamdas)だった場合は、 rhsが表示されます。 lhsの場合にのみ実行されます 偽りです。

または-等しい

真偽の値と遅延評価のこれら2つの概念を組み合わせると、 || =が何であるかがわかります。 オペレーターが行っていること:

value #defaults to nil
value ||= "test"
value ||= "blah"
puts value
=> test

値はnilから始めます 初期化されていないためです。次に、最初の || =に遭遇します オペレーター。 この段階では偽であるため、右側を評価します( "test" )そして結果を valueに割り当てます 。次に、2番目の || =をヒットします。 演算子ですが、今回は value 値が"test"であるため、真実です。 。右側の評価をスキップして、 valueに進みます 手つかず。

メモ化をいつ使用するかを決定する

メモ化を使用する場合、自分自身に尋ねる必要のあるいくつかの質問があります。値はどのくらいの頻度でアクセスされますか?何が原因で変化しますか?どのくらいの頻度で変更されますか?

値に一度だけアクセスする場合、値をキャッシュしてもあまり役に立ちません。値にアクセスする頻度が高いほど、値をキャッシュすることで得られるメリットが大きくなります。

何が変化するのかということになると、メソッドでどの値が使用されているかを調べる必要があります。議論は必要ですか?もしそうなら、メモ化はおそらくこれを考慮に入れる必要があります。個人的には、これにメモの宝石を使用するのが好きです。それはあなたの議論を処理するからです。

最後に、値が変化する頻度を考慮する必要があります。変更を引き起こすインスタンス変数はありますか?キャッシュされた値が変更されたときにクリアする必要がありますか?値はオブジェクトレベルまたはクラスレベルでキャッシュする必要がありますか?

これらの質問に答えるために、簡単な例を見て、決定をステップスルーしましょう:

class ProfitLossReport
  def initialize(title, expenses, invoices)
    @expenses = expenses
    @invoices = invoices
    @title = title
  end

  def title
    "#{@title} #{Time.current}"
  end

  def cost
    @expenses.sum(:amount)
  end

  def revenue
    @invoices.sum(:amount)
  end

  def profit
    revenue - cost
  end

  def average_profit(months)
    profit / months.to_f
  end
end

発信コードはここには表示されていませんが、 titleであると推測できます。 メソッドはおそらく1回だけ呼び出され、 Time.currentも使用します したがって、メモ化すると、値がすぐに古くなる可能性があります。

収益 およびcost このクラス内でもメソッドは数回ヒットします。どちらもデータベースにアクセスする必要があることを考えると、パフォーマンスが問題になった場合にメモ化するための主要な候補になります。これらをメモ化すると仮定すると、 profit メモ化する必要はありません。そうしないと、最小限の利益を得るために、キャッシュの上にキャッシュを追加するだけです。

最後に、 average_profitがあります 。ここでの値は引数に依存しているため、メモ化ではこれを考慮に入れる必要があります。 収益のような単純なケースの場合 これを行うことができます:

def revenue
  @revenue ||= @invoices.sum(:amount)
end

average_profitの場合 ただし、渡される引数ごとに異なる値が必要です。これにはmemoistを使用できますが、わかりやすくするために、ここで独自のソリューションをロールします。

def average_profit(months)
  @average_profit ||= {}
  @average_profit[months] ||= profit / months.to_f
end

ここでは、計算された値を追跡するためにハッシュを使用しています。まず、 @average_profitを確認します が初期化されたら、渡された引数をハッシュキーとして使用します。

クラスレベルまたはインスタンスレベルでのメモ化

ほとんどの場合、メモ化はインスタンスレベルで行われます。つまり、インスタンス変数を使用して計算値を保持します。これは、オブジェクトの新しいインスタンスを作成するときはいつでも、「キャッシュされた」値の恩恵を受けないことも意味します。これは非常に簡単な図です:

class MemoizedDemo
  def value
    @value ||= computed_value
  end

  def computed_value
    puts "Crunching Numbers"
    rand(100)
  end
end

このオブジェクトを使用すると、結果を確認できます:

demo = MemoizedDemo.new
=> #<MemoizedDemo:0x00007f95e5d9d398>

demo.value
Crunching Numbers
=> 19

demo.value
=> 19

MemoizedDemo.new.value
Crunching Numbers
=> 93

これは、クラスレベルの変数( @@ を使用)を使用するだけで変更できます。 )メモ化された値の場合:

  def value
    @@value ||= computed_value
  end

結果は次のようになります:

demo = MemoizedDemo.new
=> #<MemoizedDemo:0x00007f95e5d9d398>
demo.value
Crunching Numbers
=> 60
demo.value
=> 60
MemoizedDemo.new.value
=> 60

クラスレベルのメモ化はあまり必要ないかもしれませんが、オプションとしてあります。ただし、このレベルで値をキャッシュする必要がある場合は、Redisやmemcachedなどの外部ストアで値をキャッシュすることを検討する価値があります。

RubyonRailsアプリケーションでの一般的なメモ化のユースケース

Railsアプリケーションでは、メモ化で最も一般的なユースケースは、データベース呼び出しを減らすことです。特に、単一のリクエスト内で値が変更されない場合はそうです。コントローラでレコードを検索するための「Finder」メソッドは、次のようなこの種のデータベース呼び出しの良い例です。

  def current_user
    @current_user ||= User.find(params[:user_id])
  end

もう1つの一般的な場所は、ビューのレンダリングに何らかのタイプのデコレータ/プレゼンター/ビューモデルタイプのアーキテクチャを使用する場合です。これらのオブジェクトのメソッドは、リクエストの存続期間中のみ存続し、データは通常は変更されず、一部のメソッドはビューのレンダリング時に複数回ヒットする可能性があるため、メモ化の候補として適していることがよくあります。

メモ化の落とし穴

最大の落とし穴の1つは、本当に必要でないときにメモ化することです。文字列補間のようなものはメモ化の簡単な候補のように見えますが、実際には、サイトのパフォーマンスに目立った影響を与える可能性はほとんどありません(もちろん、非常に大きな文字列を使用している場合や、非常に大量の文字列操作を行っている場合を除きます)。例:

  def title
    # memoization here is not going to have much of an impact on our performance
    @title ||= "#{@object.published_at} - #{@object.title}"
  end

特に、メモ化された値がオブジェクトの状態に依存している場合は、旧友のキャッシュの無効化に注意する必要があります。これを防ぐのに役立つ1つの方法は、可能な限り低いレベルでキャッシュすることです。 a + bを計算するメソッドをキャッシュする代わりに aをキャッシュする方が良い場合があります およびb 個別の方法。

  # Instead of this
  def profit
    # anyone else calling 'revenue' or 'losses' is not benefitting from the caching here
    # and what happens if the 'revenue' or 'losses' value changes, will we remember to update profit?
    @profit ||= (revenue - losses)
  end

  # try this
  def profit
    # no longer cached, but subtraction is a fast calculation
    revenue - losses
  end

  def revenue
    @revenue ||= Invoice.all.sum(:amount)
  end

  def losses
    @losses ||= Purchase.all.sum(:amount)
  end

最後の落とし穴は、遅延評価がどのように機能するかによるものです。 || =のように、偽の値(つまり、nilまたはfalse)をメモ化する必要がある場合は、もう少しカスタムを行う必要があります。 保存した値がfalseの場合、イディオムは常に右側を実行します。私の経験では、これらの値をキャッシュする必要はあまりありませんが、キャッシュする場合は、すでに計算されていることを示すブールフラグを追加するか、別のキャッシュメカニズムを使用する必要があります。

  def last_post
    # if the user has no posts, we will hit the database every time this method is called
    @last_post ||= Post.where(user: current_user).order_by(created_at: :desc).first
  end

  # As a simple workaround we could do something like:
  def last_post
    return @last_post if @last_post_checked

    @last_post_checked = true
    @last_post ||= Post.where(user: current_user).order_by(created_at: :desc).first
  end
メモ化が十分でない場合

メモ化は、アプリケーションの一部のパフォーマンスを向上させるための安価で効果的な方法ですが、欠点がないわけではありません。大きな問題の1つは、永続性です。一般的なインスタンスレベルのメモ化の場合、値はその1つの特定のオブジェクトに対してのみ保存されます。これにより、メモ化はWebリクエストの存続期間中の値を保存するのに最適ですが、複数のリクエストで同じ値があり、毎回再計算される場合は、キャッシュのメリットを十分に享受できません。

>

クラスレベルのメモ化はこれに役立ちますが、キャッシュの無効化を管理することはより困難になります。サーバーを再起動すると、キャッシュされた値が失われ、複数のWebサーバー間で共有できないことは言うまでもありません。

キャッシングに関するこのシリーズの次の号では、これらの問題に対するRailsのソリューションである低レベルのキャッシングについて説明します。サーバー間で共有できる外部ストアに値をキャッシュし、有効期限のタイムアウトと動的キャッシュキーを使用してキャッシュの無効化を管理できるようにします。


  1. RailsでのTailwindCSSの使用

    CSSは魔法のようですが、時間がかかります。美しく、機能的で、アクセスしやすいサイトを使用するのは楽しいことですが、独自のCSSを作成するのは大変です。 Bootstrapなどの多くのCSSライブラリは近年爆発的に増加しており、Tailwindは2021年にパックをリードしています。 RailsにはTailwindが付属していませんが、この記事では、TailwindCSSを新しいRubyon Railsプロジェクトに追加する方法を説明します。これにより、設計の実装にかかる時間を節約できます。また、Tailwindのユーティリティクラスを使用した設計のウォークスルーも行います。このチュートリア

  2. Rails5でのAngularの使用

    あなたは前にその話を聞いたことがあります。分散型で完全に機能するバックエンドAPIと、通常のツールセットで作成されたフロントエンドで実行されているアプリケーションがすでにあります。 次に、Angularに移動します。または、AngularをRailsプロジェクトと統合する方法を探しているだけかもしれません。これは、この方法を好むためです。私たちはあなたを責めません。 このようなアプローチを使用すると、両方の世界を活用して、たとえばRailsとAngularのどちらの機能を使用してフォーマットするかを決定できます。 構築するもの 心配する必要はありません。このチュートリアルは、この目的のた