Rubyで手軽に高速化!メモ化の基礎と活用法10選

Rubyメモ化解説記事のサムネイルRuby
この記事は約17分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

プログラミングの世界には、さまざまなテクニックが存在します。

その中でも、特にパフォーマンス改善に役立つ「メモ化」に焦点を当てて解説します。

この記事を読み終えるころには、Rubyでのメモ化の基本から活用法までを理解し、あなた自身のコードを高速化するための具体的な手段を手に入れることができるでしょう。

●Rubyとは

Rubyは、まつもとゆきひろ氏によって開発されたオブジェクト指向スクリプト言語です。

直感的で人間中心の設計がされており、シンプルかつ明瞭なコードを書くことができます。

Rubyはウェブアプリケーションの開発をはじめとする様々な場面で用いられています。

●メモ化とは

メモ化は、計算結果を保存して再利用することで、同じ計算を繰り返すことなく、プログラムの実行時間を短縮するテクニックです。

特に再帰関数や、同じ引数で何度も呼び出される関数の計算時間を削減するのに有効です。

○メモ化の基本

メモ化は基本的に「既に計算したことがあるかどうか」を覚えておくことで実現します。

具体的には、関数が返す結果をあるデータ構造(例えばハッシュマップなど)に保存しておき、同じ引数の呼び出し時には保存しておいた結果を返す、という流れです。

●メモ化の使い方

Rubyでのメモ化は非常にシンプルです。

基本的なメモ化の使用例を紹介していきます。

○サンプルコード1:基本的なメモ化

def expensive_function(n)
  @memo ||= {}
  return @memo[n] if @memo.has_key?(n)

  # 何かしらの高コストな計算
  result = n * n # ここでは簡単のために二乗の計算をしています

  @memo[n] = result
  result
end

このコードでは、@memoというインスタンス変数に計算結果を保存しています。

呼び出し時にはまず@memoに結果が保存されているかどうかを確認し、保存されていればそれを返します。

保存されていなければ計算を行い、結果を@memoに保存します。

この例では、数値の二乗を計算しています。

この関数を例えば次のように呼び出すと、

p expensive_function(5)
p expensive_function(5)

最初の呼び出しでは@memoに5の二乗の結果が保存され、次の呼び出しではその保存した結果が直ちに返されるので、計算を省略できます。

次に、再帰関数でのメモ化の使用例を見てみましょう。

○サンプルコード2:再帰関数のメモ化

def fibonacci(n)
  @fib ||= {0 => 0, 1 => 1}
  return @fib[n] if @fib.has_key?(n)

  @fib[n] = fibonacci(n-1) + fibonacci(n-2)
end

このコードではフィボナッチ数列の計算にメモ化を適用しています。

@fibというインスタンス変数に計算結果を保存し、フィボナッチ数列のn番目の値が求められる度にメモを参照しています。

その結果、再計算を避けて全体のパフォーマンスを向上させています。

この関数を次のように呼び出すと、

p fibonacci(10)
p fibonacci(10)

最初の呼び出しで計算されたフィボナッチ数列の10番目の値が@fibに保存され、次の呼び出しではその保存した結果がすぐに返されます。

これにより、不必要な計算が省かれます。

このように、メモ化は高コストな関数の計算時間を大幅に削減するのに有効な手段です。

○サンプルコード3:クラスメソッドのメモ化

クラスメソッドにもメモ化は適用可能です。

ここでは、クラスメソッド内で何かしらの高コストな計算を行う例を紹介します。

class MyCalculation
  @memo = {}

  def self.expensive_calculation(n)
    return @memo[n] if @memo.has_key?(n)

    # 高コストな計算
    result = n * n 

    @memo[n] = result
    result
  end
end

このコードでは、MyCalculationというクラス内に@memoというクラスインスタンス変数を用意し、そこに計算結果を保存しています。

expensive_calculationというクラスメソッドは、引数nの二乗を計算するものとしています。

このクラスメソッドを次のように呼び出すと、

p MyCalculation.expensive_calculation(5)
p MyCalculation.expensive_calculation(5)

最初の呼び出しで計算された結果が@memoに保存され、次の呼び出しではその保存した結果が直ちに返されます。

これにより、不必要な計算が省かれるため、高速化が期待できます。

●メモ化の応用例

以上のように、メモ化は計算結果を保存することで高コストな計算の負荷を軽減しますが、更に応用的な利用方法もあります。

次に、いくつかの応用例を紹介します。

○サンプルコード4:フィボナッチ数列のメモ化

フィボナッチ数列は、メモ化の典型的な応用例です。

フィボナッチ数列のn番目の数を求める際に、再帰的にn-1番目とn-2番目の数を求めることになりますが、これらの計算結果を保存しておくことで再計算を避けることができます。

def fibonacci(n)
  @fib ||= {0 => 0, 1 => 1}
  return @fib[n] if @fib.has_key?(n)

  @fib[n] = fibonacci(n-1) + fibonacci(n-2)
end

このコードを次のように呼び出すと、

p fibonacci(20)
p fibonacci(20)

フィボナッチ数列の20番目の数を計算するのに必要な再帰的な計算は最初の呼び出し時のみ行われ、その結果が@fibに保存されます。

次の呼び出しでは、その保存した結果が直ちに返されるので、計算時間の短縮が可能となります。

○サンプルコード5:メモ化を使った高速化例

次に、より具体的な高速化例を見てみましょう。

ここでは、2つのパラメータを取る高コストな計算を行うメソッドがあると仮定します。

このメソッドは次のようになります。

class MyClass
  @memo = {}

  def self.expensive_calculation(x, y)
    return @memo[[x, y]] if @memo.has_key?([x, y])

    # 高コストな計算
    result = x ** y

    @memo[[x, y]] = result
    result
  end
end

ここでは、メソッドexpensive_calculationは引数xyを取り、xy乗を計算しています。

ここでもメモ化を活用し、@memoというクラスインスタンス変数に計算結果を保存しています。

このコードを以下のように呼び出すと、

p MyClass.expensive_calculation(2, 10)
p MyClass.expensive_calculation(2, 10)

最初の呼び出しで計算された結果が@memoに保存され、次の呼び出しではその保存した結果が直ちに返されます。

これにより、不必要な計算が省かれるため、高速化が期待できます。

○サンプルコード6:リストの検索最適化

メモ化は高コストな計算だけでなく、データの検索にも応用可能です。

ここでは、大量のデータを持つ配列から特定の値を検索する例を紹介します。

class Searcher
  @memo = {}

  def self.find(array, target)
    return @memo[target] if @memo.has_key?(target)

    # 高コストな検索
    result = array.find { |x| x == target }

    @memo[target] = result
    result
  end
end

この例では、findメソッドは引数のarrayからtargetを検索しています。

最初の検索結果は@memoに保存され、次回からはその保存結果が直ちに返されるため、時間を節約することができます。

このコードを次のように呼び出すと、

p Searcher.find([1, 2, 3, 4, 5], 3)
p Searcher.find([1, 2, 3, 4, 5], 3)

最初の呼び出しで検索された結果が@memoに保存され、次の呼び出しではその保存した結果が直ちに返されます。

これにより、高速化が期待できます。

○サンプルコード7:API呼び出し結果のキャッシュ化

次に紹介するのはAPI呼び出し結果のキャッシュ化です。

APIのレスポンスをメモ化することで、同じリクエストに対してはAPIを呼び出さずに済むようになります。

これにより通信時間を節約し、アプリケーションの応答速度を向上させることが可能になります。

require 'net/http'
require 'uri'

class ApiClient
  @memo = {}

  def self.get(url)
    return @memo[url] if @memo.has_key?(url)

    # 高コストなAPI呼び出し
    uri = URI.parse(url)
    response = Net::HTTP.get_response(uri)

    @memo[url] = response
    response
  end
end

ここでは、getメソッドは引数のurlに対してHTTP GETリクエストを発行し、その結果を返しています。

最初のリクエスト結果は@memoに保存され、次回からはその保存結果が直ちに返されます。

このコードを次のように呼び出すと、

p ApiClient.get("http://example.com")
p ApiClient.get("http://example.com")

最初の呼び出しで取得された結果が@memoに保存され、次の呼び出しではその保存した結果が直ちに返されます。

これにより、不必要なAPI呼び出しが省かれるため、高速化が期待できます。

また、APIのレートリミットに引っかかるリスクも減ります。

○サンプルコード8:動的計画法のメモ化

メモ化は動的計画法においても非常に有用です。

動的計画法は、大きな問題を小さな部分問題に分割し、それぞれの部分問題の結果を利用して大きな問題を解決する手法です。

ここでは、フィボナッチ数列の計算を動的計画法で解く例を紹介します。

フィボナッチ数列は、次の数が前の2つの数の和になるような数列です。

class Fibonacci
  @memo = {0 => 0, 1 => 1}

  def self.calc(n)
    return @memo[n] if @memo.has_key?(n)

    # 動的計画法による計算
    result = calc(n - 1) + calc(n - 2)

    @memo[n] = result
    result
  end
end

このコードでは、calcメソッドは引数のnに対してフィボナッチ数列のn番目の数を計算しています。

計算結果は@memoに保存され、次回からはその保存結果が直ちに返されます。

このコードを次のように呼び出すと、

p Fibonacci.calc(10)
p Fibonacci.calc(10)

最初の呼び出しで計算された結果が@memoに保存され、次の呼び出しではその保存した結果が直ちに返されます。

これにより、再帰的な計算が効率的になり、高速化が期待できます。

○サンプルコード9:メモ化のカスタマイズ例

Rubyでメモ化を行う方法は、自身で必要に応じてカスタマイズすることが可能です。

次に紹介するコードは、期限付きメモ化という手法を用いた例です。

この手法では、キャッシュされた結果がある期間経過したら自動的に削除され、再度計算が行われるように設定されます。

class ExpiringMemoize
  @memo = {}
  @expires_in = 60

  def self.get(key)
    # キャッシュが存在し、かつ、有効期限内なら結果を返す
    if @memo.has_key?(key) && Time.now - @memo[key][:time] < @expires_in
      return @memo[key][:value]
    end

    # 重い計算やAPI呼び出しなどの処理
    result = heavy_calculation_or_api_call(key)

    @memo[key] = { value: result, time: Time.now }
    result
  end

  private

  def self.heavy_calculation_or_api_call(key)
    # ここで何らかの重い処理を行う
  end
end

このコードではgetメソッドは引数のkeyに対して何らかの重い処理を行い、その結果を返しています。

最初の処理結果は@memoに保存され、有効期限内であればその保存結果が直ちに返されます。

このコードを次のように呼び出すと、

p ExpiringMemoize.get("key")
sleep(30)
p ExpiringMemoize.get("key")

初めての呼び出しで取得された結果が@memoに保存され、次の呼び出しではその保存した結果が直ちに返されます。

しかし、60秒以上経過すると、保存した結果は無効になり、再度重い処理が行われます。

このような期限付きメモ化は、APIの結果が時間経過とともに変わる可能性がある場合や、最新の結果を保証したい場合に役立ちます。

一方で、キャッシュ期間は適切に設定する必要があります。

○サンプルコード10:複数パラメータのメモ化

メモ化を利用すると、複数のパラメータを持つメソッドでも結果をキャッシュすることが可能です。

次のコードは、2つのパラメータを受け取り、その和を計算するメソッドの例です。

class Summation
  @memo = {}

  def self.calc(x,

 y)
    key = "#{x},#{y}"

    return @memo[key] if @memo.has_key?(key)

    result = x + y

    @memo[key] = result
    result
  end
end

このコードではcalcメソッドは引数のxyの和を計算し、その結果を返しています。

引数が複数存在する場合、それらを一意なキーとして扱うために文字列に変換しています。

最初の計算結果は@memoに保存され、次回からはその保存結果が直ちに返されます。

このコードを次のように呼び出すと、

p Summation.calc(3, 7)
p Summation.calc(3, 7)

最初の呼び出しで計算された結果が@memoに保存され、次の呼び出しではその保存した結果が直ちに返されます。

このような複数パラメータのメモ化は、引数の組み合わせによる処理結果を効率的に管理することができます。

●注意点と対処法

Rubyでメモ化を利用する際には、いくつかの注意点と対処法を把握しておくことが重要です。

これにより、メモ化を上手に活用し、プログラムのパフォーマンスを改善することができます。

まず一つ目の注意点として、「メモ化を行う際はメモリ使用量を考慮する必要がある」ということです。

メモ化は計算結果をキャッシュすることで、同じ計算を繰り返さないことでパフォーマンスを改善しますが、その一方で、メモリを多く使用します。

そのため、計算結果が大量に存在し、それら全てをキャッシュするとメモリが足りなくなる可能性があります。

この問題への対処法として、「必要な計算結果だけをキャッシュする」ことがあります。

たとえば、特定のパラメータに対する計算結果だけをキャッシュし、それ以外の結果はキャッシュしないという方法が考えられます。

二つ目の注意点として、「メモ化は副作用のないメソッドでのみ使用する」ことです。

メモ化されたメソッドが何らかの副作用(例えば、外部のデータを変更する)を持つと、それがキャッシュによってスキップされてしまい、意図しない動作を引き起こす可能性があります。

この問題に対する対処法としては、単純に「副作用のあるメソッドに対してはメモ化を適用しない」ことです。

メモ化は計算結果が同じになることを保証するためのものであり、その前提を満たさないメソッドに対して使用すると、予期せぬ問題を引き起こす可能性があります。

三つ目の注意点として、「メモ化の結果はメソッド呼び出しの間で共有される」ことを理解することが重要です。

つまり、一度計算された結果は次回のメソッド呼び出しでも使用されます。

そのため、メソッドの呼び出し間で状態を共有したくない場合は、メモ化を慎重に使用する必要があります。

この問題に対する対処法としては、状態を共有したくない場合は「新しいインスタンスを作成する」ことが一つの解決策となります。

これにより、それぞれのインスタンスが独自のメモ化された結果を持つことができます。

これらの注意点と対処法を理解し、適切に活用することで、Rubyのメモ化を効果的に使用してプログラムのパフォーマンスを向上させることができます。

●カスタマイズ方法

Rubyのメモ化をさらに活用するためには、そのカスタマイズ方法を理解することが有効です。

カスタマイズによって、より具体的な問題に対応した形でメモ化を適用することが可能となります。

それでは具体的なカスタマイズ方法を見ていきましょう。

最初に紹介するのは、「キャッシュの有効期限の設定」です。

この方法を用いると、キャッシュの有効期限を指定することができます。

キャッシュの有効期限を1時間に設定したメモ化の例を紹介します。

def expensive_method
  @cache ||= {}
  @cache[:expensive_method] ||= {}
  if @cache[:expensive_method][:time] && Time.now - @cache[:expensive_method][:time] < 60 * 60
    return @cache[:expensive_method][:value]
  end

  # 実際の計算
  value = perform_expensive_calculation

  @cache[:expensive_method][:time] = Time.now
  @cache[:expensive_method][:value] = value
  value
end

このコードでは、メモ化された結果が1時間以上経過した場合には再度計算を行い、その結果をキャッシュするようにしています。

これにより、時間の経過によって結果が変わるような場合でも、適切な結果を得ることができます。

次に紹介するカスタマイズ方法は、「キャッシュの削除」です。この方法を用いると、特定の条件下でキャッシュを削除することができます。

特定の条件を満たすときにキャッシュを削除する例を紹介します。

def reset_cache
  @cache[:expensive_method] = nil if some_condition
end

このコードでは、some_conditionが真である場合にキャッシュを削除しています。

これにより、条件に応じてキャッシュをクリアし、新たな計算結果を得ることが可能となります。

まとめ

この記事では、Rubyでのメモ化の基礎と活用法について解説しました。

具体的なサンプルコードを通じて、メモ化がどのように動作するのか、またそれがどのようにプログラムのパフォーマンスを向上させるのかを理解していただけたことと思います。

メモ化は、必要な計算結果を一度だけ計算し、それ以降はその結果を再利用することで計算時間を削減する強力な手法です。

ただし、メモリ消費が増える点や、結果が変わる可能性がある場合には注意が必要となることを忘れないでください。

また、メモ化のキャッシュの有効期限の設定やキャッシュの削除といったカスタマイズ方法も紹介しました。

これらのテクニックを駆使して、自分のコードをより高速化させることが可能です。

プログラミングにおいて、パフォーマンス改善は常に重要な課題の一つです。

Rubyで手軽に高速化を実現するメモ化の基礎と活用法をぜひ活用し、あなたのコードをさらに強力にしてください。