Einführung in den Guava Memoizer

Einführung in Guava Memoizer

1. Überblick

In diesem Tutorial werden die Memo-Funktionen der Googles Guava-Bibliothek erläutert.

Memoisierung ist eine Technik, die die wiederholte Ausführung einer rechenintensiven Funktion vermeidet, indem das Ergebnis der ersten Ausführung der Funktion zwischengespeichert wird.

1.1. Memoisierung vs. Caching

Das Speichern von Memos ähnelt dem Speichern im Cache. Beide Techniken versuchenincrease efficiency by reducing the number of calls to computationally expensive code.

Während Caching ein allgemeinerer Begriff ist, der das Problem auf der Ebene der Klasseninstanziierung, des Objektabrufs oder des Inhaltsabrufs behandelt,memoization solves the problem at the level of method/function execution.

1.2. Guava Memoizer und Guava Cache

Guava unterstützt sowohl das Auswendiglernen als auch das Zwischenspeichern. Memoization applies to functions with no argument (Supplier) and functions with exactly one argument (Function).Supplier undFunction beziehen sich hier auf Guava-Funktionsschnittstellen, die direkte Unterklassen von Java 8-Funktions-API-Schnittstellen mit demselben Namen sind.

Ab Version 23.6 unterstützt Guava das Speichern von Funktionen mit mehr als einem Argument nicht mehr.

Wir können Memoization-APIs bei Bedarf aufrufen und eine Räumungsrichtlinie angeben, die die Anzahl der im Speicher befindlichen Einträge steuert und die unkontrollierte Zunahme des verwendeten Speichers verhindert, indem ein Eintrag aus dem Cache entfernt / entfernt wird, sobald er den Bedingungen der Richtlinie entspricht.

Das Auswendiglernen verwendet den Guaven-Cache. Weitere Informationen zum Guava-Cache finden Sie in unserenGuava Cache article.

2. Supplier Auswendiglernen

In der KlasseSuppliersgibt es zwei Methoden, die das Speichern ermöglichen:memoize undmemoizeWithExpiration.

Wenn wir die gespeicherte Methode ausführen möchten, können wir einfach dieget-Methode der zurückgegebenenSupplier aufrufen. 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.

Lassen Sie uns jede Methode der Memoisierung vonSupplier untersuchens erkunden.

2.1. Supplier Auswendiglernen ohne Räumung

Wir können dieSuppliersmemoize Methode verwenden und die delegiertenSupplier als Methodenreferenz angeben:

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

Da wir keine Räumungsrichtlinie angegeben haben, gebenonce the get method is called, the returned value will persist in memory while the Java application is still running. Alle Aufrufe vonget nach dem ersten Aufruf den gespeicherten Wert zurück.

2.2. Supplier Auswendiglernen mit Räumung durch Time-To-Live (TTL)

Angenommen, wir möchten den zurückgegebenen Wert nur für einen bestimmten Zeitraum vonSupplier im Memo behalten.

Wir können dieSuppliersmemoizeWithExpiration-Methode verwenden und die Ablaufzeit mit der entsprechenden Zeiteinheit (z. B. Sekunde, Minute) zusätzlich zu den delegiertenSupplier angeben:

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 und jeder nachfolgende Aufruf derget-Methode führtgenerateBigNumber erneut aus.

Weitere Informationen finden Sie inJavadoc.

2.3. Beispiel

Simulieren wir eine rechenintensive Methode namensgenerateBigNumber:

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

Unsere Beispielmethode dauert 2 Sekunden, um die Ausführung auszuführen, und gibt dann einBigInteger-Ergebnis zurück. Wir könnten es uns entweder mit den APIsmemoize odermemoizeWithExpiration. merken

Der Einfachheit halber wird die Räumungsrichtlinie weggelassen:

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

Der erste Methodenaufruf vongetdauert zwei Sekunden, wie in der MethodegenerateBigNumberimuliert. subsequent calls to get() will execute significantly faster, since the generateBigNumber result has been memoized.

3. Function Auswendiglernen

Um eine Methode auswendig zu lernen, die ein einzelnes Argument akzeptiert, müssen wirbuild a LoadingCache map using CacheLoader‘s from method to provision the builder concerning our method as a Guava Function.

LoadingCache ist eine gleichzeitige Zuordnung, bei der die Werte automatisch vonCacheLoader geladen werden. CacheLoader populates the map by computing the Function specified in the from method, und Setzen des zurückgegebenen Werts inLoadingCache. Weitere Informationen finden Sie inJavadoc.

Der Schlüssel vonLoadingCacheist das Argument / die Eingabe vonFunction, während der Wert der Karte der vonFunctionzurückgegebene Wert ist:

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

DaLoadingCache eine gleichzeitige Zuordnung ist, müssenit doesn’t allow null keys or values. sicherstellen, dassFunction null nicht als Argument unterstützt oder Nullwerte zurückgibt.

3.1. Function Auswendiglernen mit Räumungsrichtlinien

Wir können verschiedene Räumungsrichtlinien von Guava Cache anwenden, wenn wir unsFunction merken, wie in Abschnitt 3 derGuava Cache article erwähnt.

Zum Beispiel können wir die Einträge entfernen, die 2 Sekunden lang inaktiv waren:

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

Schauen wir uns als nächstes zwei Anwendungsfälle für die Memoisierung vonFunctionan: Fibonacci-Sequenz und Fakultät.

3.2. Fibonacci-Sequenzbeispiel

Wir können eine Fibonacci-Zahl rekursiv aus einer gegebenen Zahln berechnen:

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

Ohne Memoisierung ist die Ausführung der Funktion langsam, wenn der Eingabewert relativ hoch ist.

Um die Effizienz und Leistung zu verbessern, können wirgetFibonacciNumber mitCacheLoader undCacheBuilder, speichern und gegebenenfalls die Räumungsrichtlinie angeben.

Im folgenden Beispiel wird der älteste Eintrag entfernt, sobald die Memogröße 100 Einträge erreicht hat:

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

Hier verwenden wir die MethodegetUnchecked, die den Wert zurückgibt, falls vorhanden, ohne eine aktivierte Ausnahme auszulösen.

In diesem Fall müssen wir die Ausnahme nicht explizit behandeln, wenn wir die Methodenreferenz vongetFibonacciNumberim MethodenaufruffromvonCacheLoaderangeben.

Weitere Informationen finden Sie inJavadoc.

3.3. Fakultätsbeispiel

Als nächstes haben wir eine andere rekursive Methode, die die Fakultät eines gegebenen Eingabewerts berechnet, n:

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

Wir können die Effizienz dieser Implementierung steigern, indem wir Memoization anwenden:

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. Fazit

In diesem Artikel haben wir gesehen, wie Guava APIs bereitstellt, um die MethodenSupplierundFunctionzu speichern. Wir haben auch gezeigt, wie die Räumungsrichtlinie des gespeicherten Funktionsergebnisses im Speicher angegeben wird.

Wie immer kann der Quellcodeover on GitHub gefunden werden.