Introdução ao Guava Memoizer

Introdução ao Guava Memoizer

1. Visão geral

Neste tutorial, exploraremos os recursos de memorização da biblioteca do Googles 'Guava.

Memoização é uma técnica que evita a execução repetida de uma função computacionalmente cara, armazenando em cache o resultado da primeira execução da função.

1.1. Memoization vs. Armazenamento em cache

Memoização é semelhante ao cache em relação ao armazenamento de memória. Ambas as técnicas tentamincrease efficiency by reducing the number of calls to computationally expensive code.

No entanto, enquanto o cache é um termo mais genérico que aborda o problema no nível de instanciação de classe, recuperação de objeto ou recuperação de conteúdo,memoization solves the problem at the level of method/function execution.

1.2. Guava Memoizer e Guava Cache

O Goiaba suporta memoização e cache. Memoization applies to functions with no argument (Supplier) and functions with exactly one argument (Function).SuppliereFunction aqui se referem a interfaces funcionais Guava que são subclasses diretas de interfaces Java 8 Functional API com os mesmos nomes.

A partir da versão 23.6, o Guava não suporta memoização de funções com mais de um argumento.

Podemos chamar APIs de memorização sob demanda e especificar uma política de despejo que controla o número de entradas mantidas na memória e evita o crescimento descontrolado da memória em uso, despejando / removendo uma entrada do cache, uma vez que ela corresponda à condição da política.

A memorização faz uso do Guava Cache; para obter informações mais detalhadas sobre o Guava Cache, consulte nossoGuava Cache article.

2. Supplier Memoização

Existem dois métodos na classeSuppliers que permitem a memoização:memoize ememoizeWithExpiration.

Quando queremos executar o método memoized, podemos simplesmente chamar o métodoget doSupplier retornado. 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.

Vamos explorar cada método de memoização deSupplier.

2.1. Supplier Memoização sem despejo

Podemos usar o métodoSuppliersmemoize e especificar oSupplier delegado como uma referência de método:

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

Como não especificamos uma política de despejo,once the get method is called, the returned value will persist in memory while the Java application is still running. Todas as chamadas paraget após a chamada inicial retornarão o valor memorizado.

2.2. Supplier Memoização com despejo por tempo de vida (TTL)

Suponha que desejamos apenas manter o valor retornado deSupplier no memorando por um determinado período.

Podemos usar o métodoSuppliersmemoizeWithExpiration e especificar o tempo de expiração com sua unidade de tempo correspondente (por exemplo, segundo, minuto), além daSupplier delegada:

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 e qualquer chamada subsequente ao métodoget irá reexecutargenerateBigNumber.

Para obter informações mais detalhadas, consulteJavadoc.

2.3. Exemplo

Vamos simular um método computacionalmente caro chamadogenerateBigNumber:

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

Nosso método de exemplo levará 2 segundos para ser executado e, em seguida, retornará um resultadoBigInteger. Poderíamos memorizá-lo usando as APIsmemoize oumemoizeWithExpiration.

Para simplificar, omitiremos a política de despejo:

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

A primeira chamada do métodoget leva dois segundos, conforme simulado no métodogenerateBigNumber; no entanto,subsequent calls to get() will execute significantly faster, since the generateBigNumber result has been memoized.

3. Function Memoização

Para memorizar um método que leva um único argumento, nósbuild a LoadingCache map using CacheLoader‘s from method to provision the builder concerning our method as a Guava Function.

LoadingCache é um mapa concorrente, com valores carregados automaticamente porCacheLoader. CacheLoader populates the map by computing the Function specified in the from method,e colocando o valor retornado emLoadingCache. Para obter informações mais detalhadas, consulteJavadoc.

A chave deLoadingCache é o argumento / entrada deFunction, enquanto o valor do mapa é o valor retornado deFunction:

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

ComoLoadingCache é um mapa simultâneo,it doesn’t allow null keys or values. Portanto, precisamos garantir queFunction não suporte nulo como argumento ou retorne valores nulos.

3.1. Function Memoização com políticas de despejo

Podemos aplicar diferentes políticas de despejo do Guava Cache quando memorizarmos umFunction, conforme mencionado na Seção 3 doGuava Cache article.

Por exemplo, podemos remover as entradas que ficaram ociosas por 2 segundos:

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

A seguir, vamos dar uma olhada em dois casos de uso de memoizaçãoFunction: sequência de Fibonacci e fatorial.

3.2. Exemplo de sequência de Fibonacci

Podemos calcular recursivamente um número de Fibonacci a partir de um determinado númeron:

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

Sem memoização, quando o valor de entrada é relativamente alto, a execução da função será lenta.

Para melhorar a eficiência e o desempenho, podemos memoizegetFibonacciNumber usandoCacheLoadereCacheBuilder, especificando a política de despejo, se necessário.

No exemplo a seguir, removemos a entrada mais antiga quando o tamanho da nota atingir 100 entradas:

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

Aqui, usamos o métodogetUnchecked que retorna o valor se existir sem lançar uma exceção verificada.

Nesse caso, não precisamos lidar explicitamente com a exceção ao especificar a referência do métodogetFibonacciNumber na chamada do métodoCacheLoaderfrom.

Para obter informações mais detalhadas, consulteJavadoc.

3.3. Exemplo Fatorial

A seguir, temos outro método recursivo que calcula o fatorial de um determinado valor de entrada, n:

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

Podemos melhorar a eficiência dessa implementação aplicando a memorização:

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. Conclusão

Neste artigo, vimos como o Guava fornece APIs para realizar a memoização dos métodosSuppliereFunction. Também mostramos como especificar a política de despejo do resultado da função armazenada na memória.

Como sempre, o código-fonte pode ser encontradoover on GitHub.