Введение в Guava Memoizer

Введение в Guava Memoizer

1. обзор

В этом уроке мы рассмотрим функции запоминания библиотеки Guava Googles.

Мемоизация - это метод, позволяющий избежать повторного выполнения вычислительно дорогой функции путем кэширования результата первого выполнения функции.

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 поддерживает как запоминание, так и кэширование. Memoization applies to functions with no argument (Supplier) and functions with exactly one argument (Function).Supplier иFunction здесь относятся к функциональным интерфейсам Guava, которые являются прямым подклассом одноименных интерфейсов функционального API Java 8.

Начиная с версии 23.6, Guava не поддерживает мемоизацию функций более чем с одним аргументом.

Мы можем вызвать API-интерфейсы напоминания по требованию и указать политику удаления, которая контролирует количество записей, хранящихся в памяти, и предотвращает неконтролируемый рост используемой памяти, удаляя / удаляя запись из кэша, как только она соответствует условию политики.

Мемоизация использует кэш Guava; для получения более подробной информации о Guava Cache, пожалуйста, обратитесь к нашемуGuava Cache article.

2. Supplier Воспоминание

В классеSuppliers есть два метода, которые включают мемоизацию:memoize иmemoizeWithExpiration.

Когда мы хотим выполнить мемоизированный метод, мы можем просто вызвать методget возвращенногоSupplier. 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. Мы могли бы запоминать его, используя APImemoize илиmemoizeWithExpiration.

Для простоты мы опускаем политику выселения:

@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)));
}

Первый вызов методаget занимает две секунды, как моделируется в методеgenerateBigNumber; однако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 в качестве аргумента или не возвращает нулевые значения.

3.1. Function Мемоизация с политиками выселения

Мы можем применить другую политику выселения Guava Cache, когда запомнимFunction, как указано в разделе 3Guava Cache article.

Например, мы можем удалить записи, которые простаивали в течение 2 секунд:

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

Затем давайте рассмотрим два варианта использования мемоизацииFunction: последовательность Фибоначчи и факториал.

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));
    }
}

Без мемоизации, когда входное значение относительно велико, выполнение функции будет медленным.

Чтобы повысить эффективность и производительность, мы можем запоминатьgetFibonacciNumber, используяCacheLoader иCacheBuilder,, при необходимости указывая политику выселения.

В следующем примере мы удаляем самую старую запись, когда размер заметки достиг 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, который возвращает значение, если оно существует, без выдачи проверенного исключения.

В этом случае нам не нужно явно обрабатывать исключение при указании ссылки на методgetFibonacciNumber в вызове методаCacheLoader‘sfrom.

Для более подробной информации, пожалуйста, обратитесь к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 предоставляет API для выполнения мемоизации методовSupplier иFunction. Мы также показали, как указать политику удаления из памяти сохраненной функции в памяти.

Как всегда, исходный код можно найтиover on GitHub.