2012-06-22 3 views
6

Ich habe diese Frage komplett neu geschrieben, da das Original unlösbar war. Um es einfach zu halten, verwende ich Fibonacci-Zahlen als Spielzeugbeispiel. Die trivial recursive cached computation endet mit einem sehr langen StackTrace, wie erwartet. Deshalb Ich mag würde eine abstrakte Klasse wie IterativeLoadingCache haben, , die ich wie here durch so etwas wieWie konvertiert man Rekursion in Iteration mit LoadingCache?

@Override 
protected Integer computeNonRecursivelly(Integer key) { 
    final Integer x1 = getOrEnqueue(key-1); 
    final Integer x2 = getOrEnqueue(key-2); 
    if (x1==null) return null; 
    if (x2==null) return null; 
    return x1+x2; 
} 

verlängern könnte und welche würde kümmern sich um all das Caching und Berechnung ohne Rekursion.

Ich bin wirklich nicht auf der Suche nach einer effizienten Berechnung von Fibonacci-Zahlen. Ich brauche etwas, das erlaubt, Caching zusammen mit rekursiven Funktionen zu verwenden, wobei die Rekursionstiefe beliebig hoch werden kann.

Ich habe schon eine Art Lösung, aber es ist ziemlich ineffizient und sehr hässlich, also hoffe ich, einen guten Rat zu bekommen. Ich bin auch neugierig, ob jemand anderes es braucht oder es vielleicht bereits implementiert hat.

+0

Haben Sie Dr. Heinz M. Kabutz 'Artikel über [Fork/Join mit Fibonacci und Karatsuba] (http://www.javaspecialists.eu/archive/Issue201.html) angeschaut. Einige seiner Ideen können zutreffen. – OldCurmudgeon

+0

@OldCurmudgeon: Jetzt habe ich, es ist interessant, aber hat nicht wirklich geholfen. – maaartinus

+0

Ich fand das [Cache-Fibonacci-Beispiel] (http://vladmihalcea.com/2014/03/03/caching-best-practices/), das scheint zu gelten. Siehe den Abschnitt "Spielzeit" nach unten. Es scheint zu funktionieren, indem die vorher berechnete Zahl im Cache gehalten wird und ältere Werte entfernt werden. Die rekursive Last wird durch eine sorgfältige Reihenfolge der Anweisungen in der 'load' Methode vermieden.Ich bin nicht sicher, ob das über das "Spielzeug" -Beispiel hinaus ausgedehnt werden kann, wie Sie es nennen. –

Antwort

3

EDIT: hat die Implementierung geändert, um eine einzelne Berechnung zu ermöglichen, wenn dieselbe Expression als Parameter in mehreren Threads übergeben wird.

Seien Sie kein LoadingCache, einfach zwischenspeichern das Ergebnis in eval verwenden (wenn es Iteration statt Rekursion zu verwenden, modifiziert wurde):

public Node eval(final Expression e) { 
    if (e==null) return null; 
    return cache.get(e, new Callable<Node>() { 
     @Override 
     public Node call() { 
      final Node n0 = eval(leftExpression(e)); 
      final Node n1 = eval(rightExpression(e)); 
      return new Node(n0, n1); 
     } 
    }); 
} 

private final Cache<Expression, Node> cache 
= CacheBuilder.newBuilder().build(); 
+0

Das sollte funktionieren, aber ich vermisse die folgende Funktion: * "Wenn ein anderer Aufruf zum Abrufen oder GetUnchecked den Wert für Schlüssel gerade lädt, wartet einfach auf diesen Thread zu beenden und den geladenen Wert zurückgibt." * Während ich tun konnte einige sperren mich selbst, dies würde wahrscheinlich den Overhead deutlich erhöhen. – maaartinus

+0

Diese Funktion ist nicht dokumentiert für 'Cache.get (K, Callable )' 'wie für' LoadingCache.get (K) ', aber da beide Methoden schließlich' LocalCache.get (K, CacheLoader) 'aufrufen, sollte dies geschehen Arbeit. Nicht sicher, ob es sich um ein echtes Feature oder ein Implementierungsdetail handelt, das sich jedoch ändern kann. –

+0

Guava-Mitwirkender hier: Ja, diese Garantie gilt für 'Cache.get (K, Callable)'. Ich habe [ein Problem angemeldet] (https://code.google.com/p/guava-libraries/issues/detail?id=1040), um sicherzustellen, dass dies in der Dokumentation verdeutlicht wird. –

4

Da Sie Ihre Frage neu geschrieben haben, hier ist eine neue Antwort .

Zuerst sieht es für mich wie Ihre Implementierung von computeNonRecursivelly ist immer noch rekursiv, seit getOrEnqueue nennt es.

Ich glaube nicht, dass Sie eine Cache direkt verwenden können, weil Sie 2 Schritte in Ihrer Berechnung haben müssen: eine, die die Abhängigkeiten für den gewünschten Wert angibt, und eine, die die Berechnung durchführt, sobald die Abhängigkeiten erfüllt sind. Es wird jedoch nur funktionieren, wenn Sie nie zyklische Abhängigkeiten haben (es ist die gleiche Anforderung wie bei der Rekursion).

Auf diese Weise können Sie Abhängigkeiten, die sich nicht bereits im Cache befinden (und deren Abhängigkeiten usw.) in die Warteschlange stellen und sie dann in der richtigen Reihenfolge berechnen. Etwas entlang der Linien von:

public abstract class TwoStepCacheLoader<K, V> extends CacheLoader<K, V> { 
    public abstract Set<K> getDependencies(K key); 
} 

public class TwoStepCache<K, V> extends ForwardingLoadingCache<K, V> { 
    private final TwoStepCacheLoader<K, V> loader; 
    private LoadingCache<K, V> cache; 

    public TwoStepCache(TwoStepCacheLoader<K, V> loader) { 
     this.loader = loader; 
     cache = CacheBuilder.newBuilder().build(loader); 
    } 

    @Override 
    public V get(K key) 
      throws ExecutionException { 
     V value = cache.getIfPresent(key); 
     if (value != null) { 
      return value; 
     } 

     Deque<K> toCompute = getDependenciesToCompute(key); 
     return computeDependencies(toCompute); 
    } 

    private Deque<K> getDependenciesToCompute(K key) { 
     Set<K> seen = Sets.newHashSet(key); 
     Deque<K> dependencies = new ArrayDeque<K>(seen), toCompute = new ArrayDeque<K>(seen); 
     do { 
      for (K dependency : loader.getDependencies(dependencies.remove())) { 
       if (seen.add(dependency) && // Deduplication in the dependencies 
        cache.getIfPresent(dependency) == null) { 
        // We need to compute it. 
        toCompute.push(dependency); 
        // We also need its dependencies. 
        dependencies.add(dependency); 
       } 
      } 
     } while (!dependencies.isEmpty()); 
     return toCompute; 
    } 

    private V computeDependencies(Deque<K> toCompute) 
      throws ExecutionException { 
     V value; 
     do { 
      value = cache.get(toCompute.pop()); 
     } while (!toCompute.isEmpty()); 
     // The last computed value is for our key. 
     return value; 
    } 

    @Override 
    public V getUnchecked(K key) { 
     try { 
      return get(key); 
     } catch (ExecutionException e) { 
      throw new UncheckedExecutionException(e.getCause()); 
     } 
    } 

    @Override 
    protected LoadingCache<K, V> delegate() { 
     return cache; 
    } 
} 

Jetzt können Sie einen TwoStepCacheLoader implementieren, die den Cache sicher ruft:

public class Fibonacci { 
    private LoadingCache<Integer, Integer> cache = new TwoStepCache<Integer, Integer>(new FibonacciCacheLoader()); 


    public int fibonacci(int n) { 
     return cache.getUnchecked(n); 
    } 


    private class FibonacciCacheLoader extends TwoStepCacheLoader<Integer, Integer> { 
     @Override 
     public Set<Integer> getDependencies(Integer key) { 
      if (key <= 1) { 
       return ImmutableSet.of(); 
      } 
      return ImmutableSet.of(key - 2, key - 1); 
     } 


     @Override 
     public Integer load(Integer key) 
       throws Exception { 
      if (key <= 1) { 
       return 1; 
      } 
      return cache.get(key - 2) + cache.get(key - 1); 
     } 
    } 
} 

Ich habe einen Komponententest darauf ausgeführt werden, so scheint es, um korrekt zu funktionieren.

+0

Es gibt andere Methoden zum Überschreiben, um '' '' ForwardingLoadingCache''' korrekt (und konsistent) zu erweitern, aber sie sind für das Beispiel nicht wichtig ('' 'getAll()' '' '' 'apply()' ' 'usw.). –

+0

In Bezug auf "computeNonRecursively immer noch rekursiv", Sie haben Recht, aber es war nur ein Fehler beim Extrahieren von meiner hässlichen Lösung. Der Anruf kann und muss entfernt werden. – maaartinus

+0

** 1 ** Ich bin mir nicht sicher, ob es eine Möglichkeit gibt, mehr Arbeiter in ein Problem zu bringen, noch kann ich sehen, ob zwei gleichzeitige Anfragen nach Werten, die gemeinsame Abhängigkeiten erfordern, irgendwie zusammenarbeiten könnten. Es könnte möglich sein mit einem einfachen Umschreiben von 'getDependenciesToCompute' oder nicht, kann ich nicht sagen. Ich bin mir bewusst, dass das etwas ist, worüber ich nie geschrieben habe. ** 2 ** Die 'getDependencies' Idee ist wirklich nett. Ich fürchte, es gilt nicht für verrückte Fälle wie 'f (n) = f (n-1) + f (f (log (n)))', aber ich nehme an, dass ich es nie brauchen werde. ** 3 ** Das sagte ich danke Ihnen für die nette Lösung. Ich brauche noch etwas Zeit, um es zu bewerten. – maaartinus

Verwandte Themen