Въведение в Guava Memoizer

1. Общ преглед

В този урок ще проучим функциите за запомняне на библиотеката Гуава на Googles.

Мемоизирането е техника, която избягва многократното изпълнение на изчислително скъпа функция чрез кеширане на резултата от първото изпълнение на функцията.

1.1. Мемоизиране срещу кеширане

Мемоизирането е подобно на кеширането по отношение на съхранението в паметта. И двете техники се опитват да увеличат ефективността чрез намаляване на броя на обажданията към изчислително скъп код.

Въпреки това, докато кеширането е по-общ термин, който адресира проблема на ниво създаване на клас, извличане на обект или извличане на съдържание, мемоизирането решава проблема на ниво изпълнение на метод / функция.

1.2. Guava Memoizer и Guava Cache

Guava поддържа както запомняне, така и кеширане. Напомнянето се отнася за функции без аргумент ( Доставчик ) и функции с точно един аргумент ( Функция ). Доставчикът и функцията тук се отнасят до функционалните интерфейси на Guava, които са директни подкласове на Java 8 Функционални API интерфейси със същото име.

От версия 23.6, Guava не поддържа запомняне на функции с повече от един аргумент.

Можем да извикаме API за запомняне при поискване и да посочим политика за изселване, която контролира броя на записите, съхранявани в паметта и предотвратява неконтролираното нарастване на използваната памет, като изважда / премахва запис от кеша, след като отговаря на условието на политиката.

Мемоизирането използва кеша на Guava; за по-подробна информация относно кеша Guava, моля, вижте нашата статия за кеш Guava

2. Мемоизиране на доставчика

Има два метода в класа на доставчиците, които позволяват запомняне : memoize и memoizeWithExpiration .

Когато искаме да изпълним запомнения метод, можем просто да извикаме метода get на върнатия доставчик . В зависимост от това дали връщаната стойност на метода съществува в паметта, методът get или ще върне стойността в паметта, или ще изпълни запомнения метод и ще предаде връщаната стойност на повикващия.

Нека разгледаме всеки метод за запомняне на доставчика .

2.1. Мемоизиране на доставчика без изселване

Можем да използваме метода за запомняне на доставчиците и да посочим делегирания доставчик като референтен метод:

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

Тъй като не сме посочили политика за изселване, след като бъде извикан методът get , върнатата стойност ще остане в паметта, докато приложението Java все още работи. Всички обаждания, които ще получите след първоначалното повикване, ще върнат запомнената стойност.

2.2. Мемоизиране на доставчика с изгонване по време на живот (TTL)

Да предположим, че искаме да запазим върнатата стойност от Доставчика в бележката само за определен период.

Ние можем да използваме доставчици " memoizeWithExpiration метод и определете времето за изтичане на неговата съответна единица време (например, на второ място, минута), в допълнение към делегиран доставчик :

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

След определеното време е преминал (5 секунди), кеша ще изгони върнатата стойност на Доставчика от паметта и всяко последващо обаждане до 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))); }

Първият GET метод разговор отнема две секунди, като се симулира в generateBigNumber метод; обаче, следващи покани да получите () ще изпълни значително по-бързо, тъй като generateBigNumber резултатът е бил memoized.

3. Функциониране на паметта

За да memoize метод, който отнема поне един аргумент, ние се изгради LoadingCache карта, използвайки CacheLoader е от метод за предоставяне на строителя относно нашия метод като Guava Function.

LoadingCache е едновременна карта, със стойности, автоматично заредени от CacheLoader . CacheLoader попълва картата, като изчислява функцията, посочена в метода от , и поставя върнатата стойност в LoadingCache . За по-подробна информация вижте Javadoc.

LoadingCache "ключ и е функция Аргументът / вход, докато стойността на картата е най- Function е върната стойност:

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

Тъй като LoadingCache е едновременна карта, тя не позволява нулеви ключове или стойности. Следователно трябва да гарантираме, че функцията не поддържа null като аргумент или връща null стойности.

3.1. Функционални Memoization от изгонване Политики

Можем да приложим различна политика за изселване на Guava Cache, когато запомним функция, както е споменато в раздел 3 от статията за Guava Cache.

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

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

Next, let's take a look at two use cases of Function memoization: Fibonacci sequence and factorial.

3.2. Fibonacci Sequence Example

We can recursively compute a Fibonacci number from a given number 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)); } }

Without memoization, when the input value is relatively high, the execution of the function will be slow.

To improve the efficiency and performance, we can memoize getFibonacciNumber using CacheLoader and CacheBuilder, specifying the eviction policy if necessary.

In the following example, we remove the oldest entry once the memo size has reached 100 entries:

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

Here, we use getUnchecked method which returns the value if exists without throwing a checked exception.

В този случай, ние не се нуждаем, за да се справят с изрично изключение, когато сте посочили getFibonacciNumber референтен метод в CacheLoader е от извикване на метод.

За по-подробна информация вижте 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 за извършване на запомняне на методите на доставчика и функцията . Показахме също как да зададем политиката за изселване на резултата от съхранената функция в паметта.

Както винаги, изходният код може да бъде намерен в GitHub.