メモ化によるレールの高速化
アプリケーションを開発するとき、実行速度が遅いメソッドがあることがよくあります。おそらく、データベースにクエリを実行するか、外部サービスにアクセスする必要があります。どちらの場合も、速度が低下する可能性があります。そのデータが必要になるたびにメソッドを呼び出してオーバーヘッドを受け入れることもできますが、パフォーマンスが懸念される場合は、いくつかのオプションがあります。
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のソリューションである低レベルのキャッシングについて説明します。サーバー間で共有できる外部ストアに値をキャッシュし、有効期限のタイムアウトと動的キャッシュキーを使用してキャッシュの無効化を管理できるようにします。
-
RailsでのTailwindCSSの使用
CSSは魔法のようですが、時間がかかります。美しく、機能的で、アクセスしやすいサイトを使用するのは楽しいことですが、独自のCSSを作成するのは大変です。 Bootstrapなどの多くのCSSライブラリは近年爆発的に増加しており、Tailwindは2021年にパックをリードしています。 RailsにはTailwindが付属していませんが、この記事では、TailwindCSSを新しいRubyon Railsプロジェクトに追加する方法を説明します。これにより、設計の実装にかかる時間を節約できます。また、Tailwindのユーティリティクラスを使用した設計のウォークスルーも行います。このチュートリア
-
Rails5でのAngularの使用
あなたは前にその話を聞いたことがあります。分散型で完全に機能するバックエンドAPIと、通常のツールセットで作成されたフロントエンドで実行されているアプリケーションがすでにあります。 次に、Angularに移動します。または、AngularをRailsプロジェクトと統合する方法を探しているだけかもしれません。これは、この方法を好むためです。私たちはあなたを責めません。 このようなアプローチを使用すると、両方の世界を活用して、たとえばRailsとAngularのどちらの機能を使用してフォーマットするかを決定できます。 構築するもの 心配する必要はありません。このチュートリアルは、この目的のた