Guava Memoizerの紹介

Guava Memoizerの概要

1. 概要

このチュートリアルでは、GoogleのGuavaライブラリのメモ化機能について説明します。

メモ化は、関数の最初の実行結果をキャッシュすることにより、計算負荷の高い関数の繰り返し実行を回避する手法です。

1.1. メモ化と キャッシング

メモ化は、メモリストレージに関してキャッシュに似ています。 どちらの手法もincrease efficiency by reducing the number of calls to computationally expensive code.を試みます

ただし、キャッシングは、クラスのインスタンス化、オブジェクトの取得、またはコンテンツの取得のレベルで問題に対処するより一般的な用語ですが、memoization solves the problem at the level of method/function execution.

1.2. Guavaメモ化とGuavaキャッシュ

Guavaは、メモ化とキャッシュの両方をサポートしています。 ここでのMemoization applies to functions with no argument (Supplier) and functions with exactly one argument (Function).SupplierおよびFunctionは、同じ名前のJava8関数型APIインターフェースの直接のサブクラスであるGuava関数型インターフェースを指します。

バージョン23.6の時点で、Guavaは複数の引数を持つ関数のメモ化をサポートしていません。

メモ化APIをオンデマンドで呼び出し、メモリに保持されているエントリの数を制御し、ポリシーの条件に一致したエントリをキャッシュから削除/削除することにより、使用中のメモリの無制限な増加を防ぐエビクションポリシーを指定できます。

メモ化はGuavaキャッシュを利用します。 Guava Cacheの詳細については、Guava Cache articleを参照してください。

2. Supplierメモ化

Suppliersクラスには、メモ化を有効にする2つのメソッドがあります。memoizememoizeWithExpirationです。

メモ化されたメソッドを実行する場合は、返されたSuppliergetメソッドを呼び出すだけです。 Depending on whether the method’s return value exists in memory, the get method will either return the in-memory value or execute the memoized method and pass the return value to the caller.

Supplierのメモ化の各方法を調べてみましょう。

2.1. Supplierエビクションなしのメモ化

Suppliersmemoizeメソッドを使用して、委任されたSupplierをメソッド参照として指定できます。

Supplier memoizedSupplier = Suppliers.memoize(
  CostlySupplier::generateBigNumber);

エビクションポリシーを指定していないため、once the get method is called, the returned value will persist in memory while the Java application is still running.最初の呼び出しの後にgetを呼び出すと、メモ化された値が返されます。

2.2. Supplier存続可能時間(TTL)によるエビクションを使用したメモ化

メモのSupplierからの戻り値を一定期間だけ保持したいとします。

SuppliersmemoizeWithExpirationメソッドを使用して、委任されたSupplierに加えて、対応する時間単位(秒、分など)で有効期限を指定できます。

Supplier memoizedSupplier = Suppliers.memoizeWithExpiration(
  CostlySupplier::generateBigNumber, 5, TimeUnit.SECONDS);

After the specified time has passed (5 seconds), the cache will evict the returned value of the Supplier from memoryおよびその後のgetメソッドの呼び出しは、generateBigNumberを再実行します。

詳細については、Javadocを参照してください。

2.3. 例

generateBigNumberという名前の計算コストの高いメソッドをシミュレートしてみましょう。

public class CostlySupplier {
    private static BigInteger generateBigNumber() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {}
        return new BigInteger("12345");
    }
}

この例のメソッドは、実行に2秒かかり、その後、BigIntegerの結果を返します。 memoizeまたはmemoizeWithExpiration API.のいずれかを使用してメモ化できます

簡単にするために、エビクションポリシーは省略します。

@Test
public void givenMemoizedSupplier_whenGet_thenSubsequentGetsAreFast() {
    Supplier memoizedSupplier;
    memoizedSupplier = Suppliers.memoize(CostlySupplier::generateBigNumber);

    BigInteger expectedValue = new BigInteger("12345");
    assertSupplierGetExecutionResultAndDuration(
      memoizedSupplier, expectedValue, 2000D);
    assertSupplierGetExecutionResultAndDuration(
      memoizedSupplier, expectedValue, 0D);
    assertSupplierGetExecutionResultAndDuration(
      memoizedSupplier, expectedValue, 0D);
}

private  void assertSupplierGetExecutionResultAndDuration(
  Supplier supplier, T expectedValue, double expectedDurationInMs) {
    Instant start = Instant.now();
    T value = supplier.get();
    Long durationInMs = Duration.between(start, Instant.now()).toMillis();
    double marginOfErrorInMs = 100D;

    assertThat(value, is(equalTo(expectedValue)));
    assertThat(
      durationInMs.doubleValue(),
      is(closeTo(expectedDurationInMs, marginOfErrorInMs)));
}

generateBigNumberメソッドでシミュレートされているように、最初のgetメソッド呼び出しには2秒かかります。ただし、subsequent calls to get() will execute significantly faster, since the generateBigNumber result has been memoized.

3. Functionメモ化

単一の引数をとるメソッドをメモ化するには、build a LoadingCache map using CacheLoader‘s from method to provision the builder concerning our method as a Guava Function.

LoadingCacheは並行マップであり、値はCacheLoaderによって自動的にロードされます。 CacheLoader populates the map by computing the Function specified in the from method,および戻り値をLoadingCacheに入れます。 詳細については、Javadocを参照してください。

LoadingCacheのキーはFunctionの引数/入力であり、マップの値はFunctionの戻り値です。

LoadingCache memo = CacheBuilder.newBuilder()
  .build(CacheLoader.from(FibonacciSequence::getFibonacciNumber));

LoadingCacheは並行マップであるため、it doesn’t allow null keys or values.したがって、Functionが引数としてnullをサポートしたり、null値を返したりしないようにする必要があります。

3.1. Functionエビクションポリシーによるメモ化

Guava Cache articleのセクション3で説明したように、Functionをメモ化するときに、異なるGuavaCacheのエビクションポリシーを適用できます。

たとえば、2秒間アイドル状態だったエントリを削除できます。

LoadingCache memo = CacheBuilder.newBuilder()
  .expireAfterAccess(2, TimeUnit.SECONDS)
  .build(CacheLoader.from(Fibonacci::getFibonacciNumber));

次に、Functionのメモ化の2つのユースケース(フィボナッチ数列と階乗)を見てみましょう。

3.2. フィボナッチ数列の例

与えられた数nからフィボナッチ数を再帰的に計算できます。

public static BigInteger getFibonacciNumber(int n) {
    if (n == 0) {
        return BigInteger.ZERO;
    } else if (n == 1) {
        return BigInteger.ONE;
    } else {
        return getFibonacciNumber(n - 1).add(getFibonacciNumber(n - 2));
    }
}

メモ化しないと、入力値が比較的高い場合、関数の実行が遅くなります。

効率とパフォーマンスを向上させるために、必要に応じてエビクションポリシーを指定するCacheLoaderCacheBuilder,を使用してgetFibonacciNumberをメモ化できます。

次の例では、メモのサイズが100エントリに達すると、最も古いエントリを削除します。

public class FibonacciSequence {
    private static LoadingCache memo = CacheBuilder.newBuilder()
      .maximumSize(100)
      .build(CacheLoader.from(FibonacciSequence::getFibonacciNumber));

    public static BigInteger getFibonacciNumber(int n) {
        if (n == 0) {
            return BigInteger.ZERO;
        } else if (n == 1) {
            return BigInteger.ONE;
        } else {
            return memo.getUnchecked(n - 1).add(memo.getUnchecked(n - 2));
        }
    }
}

ここでは、チェックされた例外をスローせずに存在する場合に値を返すgetUncheckedメソッドを使用します。

この場合、CacheLoaderfromメソッド呼び出しでgetFibonacciNumberメソッド参照を指定するときに、例外を明示的に処理する必要はありません。

詳細については、Javadocを参照してください。

3.3. 階乗の例

次に、特定の入力値nの階乗を計算する別の再帰的メソッドがあります。

public static BigInteger getFactorial(int n) {
    if (n == 0) {
        return BigInteger.ONE;
    } else {
        return BigInteger.valueOf(n).multiply(getFactorial(n - 1));
    }
}

メモ化を適用することにより、この実装の効率を高めることができます。

public class Factorial {
    private static LoadingCache memo = CacheBuilder.newBuilder()
      .build(CacheLoader.from(Factorial::getFactorial));

    public static BigInteger getFactorial(int n) {
        if (n == 0) {
            return BigInteger.ONE;
        } else {
            return BigInteger.valueOf(n).multiply(memo.getUnchecked(n - 1));
        }
    }
}

4. 結論

この記事では、GuavaがSupplierメソッドとFunctionメソッドのメモ化を実行するためのAPIをどのように提供するかを見てきました。 また、メモリに保存された関数結果のエビクションポリシーを指定する方法も示しました。

いつものように、ソースコードはover on GitHubで見つけることができます。