2012-06-22 6 views
6

Ho completamente riscritto questa domanda perché quella originale era irrisolvibile. Per semplificare, sto usando un numero di Fibonacci come esempio di un giocattolo.Come convertire la ricorsione in iterazione con LoadingCache?

Il trivial recursive cached computation termina con uno stacktrace molto lungo, proprio come previsto. Ecco perché mi piacerebbe avere una classe astratta come IterativeLoadingCache, quale ho potuto estendere come here da qualcosa come

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

e che sarebbe preso cura di tutto il caching e di calcolo senza l'utilizzo di ricorsione.

Sono in realtà non cercando un calcolo efficiente dei numeri di Fibonacci. Ho bisogno di qualcosa che permetta di usare la cache insieme a funzioni ricorsive, dove la profondità di ricorsione può diventare arbitrariamente alta.

Ho già una sorta di soluzione, ma è abbastanza inefficiente e molto brutto, quindi spero di ottenere qualche buon consiglio. Sono anche curioso di sapere se qualcun altro ha bisogno o forse lo ha già implementato.

+0

Avete guardato l'articolo del Dr. Heinz M. Kabutz su [Fork/Join With Fibonacci and Karatsuba] (http://www.javaspecialists.eu/archive/Issue201.html). Alcune delle sue idee potrebbero essere applicate. – OldCurmudgeon

+0

@OldCurmudgeon: Ora ce l'ho, è interessante ma non mi è stato di grande aiuto. – maaartinus

+0

Ho trovato questo [esempio di cache Fibonacci] (http://vladmihalcea.com/2014/03/03/caching-best-practices/) che sembra applicarsi. Vedi la sezione chiamata "Tempo di gioco" verso il basso. Sembra funzionare mantenendo il numero precedente calcolato nella cache e sfrattando i vecchi valori. Il carico ricorsivo è evitato da un attento ordinamento delle istruzioni nel metodo 'load'.Non sono sicuro che sia possibile estenderlo oltre l'esempio del 'giocattolo', come dici tu. –

risposta

3

MODIFICA: modificato l'implementazione per consentire un singolo calcolo quando lo stesso Expression viene passato come parametro in diversi thread.

Non utilizzare un LoadingCache, è sufficiente memorizzare nella cache il risultato in eval (una volta che è stato modificato per utilizzare l'iterazione invece di ricorsione):

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

Questo dovrebbe funzionare, tuttavia mi manca la seguente funzionalità: * "Se un'altra chiamata per ottenere o getUnchecked sta caricando il valore per la chiave, aspetta semplicemente che quel thread finisca e restituisca il suo valore caricato." * Mentre potevo fare alcuni chiudendo me stesso, questo probabilmente aumenterebbe significativamente il sovraccarico. – maaartinus

+0

Questa funzione non è documentata per 'Cache.get (K, Callable )' come per 'LoadingCache.get (K)', ma poiché entrambi i metodi chiamano 'LocalCache.get (K, CacheLoader)', dovrebbe lavoro. Non sono sicuro se sia una caratteristica reale o un dettaglio di implementazione che è soggetto a modifiche, però. –

+0

Contributo di guava qui: sì, quella garanzia è valida per 'Cache.get (K, Callable)'. Ho [archiviato un problema] (https://code.google.com/p/guava-libraries/issues/detail?id=1040) per accertarmi che sia stato reso più chiaro nella documentazione. –

4

Dal momento che hai riscritto la tua domanda, ecco una nuova risposta .

In primo luogo, sembra a me come l'implementazione di computeNonRecursivelly è ancora ricorsivo, dal momento getOrEnqueue lo chiama.

Non penso che sia possibile utilizzare direttamente uno Cache, perché è necessario disporre di due passaggi nel calcolo: uno che indica le dipendenze per il valore desiderato e uno che esegue il calcolo una volta soddisfatte le dipendenze. Funzionerà solo se non si hanno mai delle dipendenze cicliche, tuttavia (è lo stesso requisito della ricorsione).

In questo modo, è possibile accodare le dipendenze che non sono già nella cache (e le loro dipendenze, ecc.) Quindi calcolarle nell'ordine corretto. Qualcosa sulla falsariga di:

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

Ora è possibile implementare un TwoStepCacheLoader che chiama la cache in modo sicuro:

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

Ho eseguito un test di unità su di esso, sembra funzionare correttamente.

+0

Esistono altri metodi per eseguire l'override per estendere '' 'ForwardingLoadingCache''' correttamente (e coerentemente), ma non hanno importanza per l'esempio (' '' getAll() '' ',' '' apply() '' ', ecc.). –

+0

Per quanto riguarda "computeNonRecursivelly ancora ricorsivo", hai ragione, ma si è trattato solo di un errore durante l'estrazione dalla mia brutta soluzione. La chiamata può e deve essere rimossa. – maaartinus

+0

** 1 ** Non sono sicuro se c'è un modo per mettere più lavoratori su un problema, né posso vedere se due richieste simultanee di valori che richiedono dipendenze comuni potrebbero in qualche modo cooperare. Potrebbe essere possibile con una semplice riscrittura di 'getDependenciesToCompute' o no, non posso dirlo. Sono consapevole che questo è qualcosa di cui non ho mai scritto. ** 2 ** L'idea di 'getDependencies' è davvero carina. Ho paura che non si applichi a casi pazzi come 'f (n) = f (n-1) + f (f (log (n)))', ma suppongo che non ne avrò mai bisogno. ** 3 ** Detto questo ti ringrazio per la bella soluzione. Mi serve ancora un po 'di tempo per valutarlo. – maaartinus