【问题标题】:How to convert recursion into iteration with LoadingCache?如何使用 LoadingCache 将递归转换为迭代?
【发布时间】:2012-06-22 09:03:56
【问题描述】:

我完全重写了这个问题,因为原来的问题无法解决。为了简单起见,我使用斐波那契数作为玩具示例。

正如预期的那样,trivial recursive cached computation 以很长的堆栈跟踪结束。这就是为什么我想要一个像IterativeLoadingCache 这样的抽象类, 我可以像here 这样扩展

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

它会在不使用递归的情况下处理所有缓存和计算。

真的不是在寻找斐波那契数的有效计算。我需要一些允许将缓存与递归函数一起使用的东西,其中递归深度可以任意高。

我已经有了一种解决方案,但是效率很低而且很丑陋,所以我希望得到一些好的建议。我也很好奇其他人是否需要它或者可能已经实现了它。

【问题讨论】:

  • 您看过 Heinz M. Kabutz 博士在 Fork/Join With Fibonacci and Karatsuba 上的文章吗?他的一些想法可能适用。
  • @OldCurmudgeon:现在我有了,这很有趣,但并没有真正的帮助。
  • 我发现这个cache Fibonacci example 似乎适用。请参阅底部的“播放时间”部分。它似乎通过将先前计算的数字保留在缓存中并逐出旧值来工作。通过对load 方法中的语句进行仔细排序,可以避免递归加载。正如您所说,我不确定这是否可以扩展到“玩具”示例之外。
  • @PatrickM 我很确定,它不会工作(我想要的方式)。简单地调用fibonacciCache.getUnchecked(N) 意味着你得到一个深度递归N,这会破坏堆栈。除此之外,他们通过迭代i 来作弊。如果他们只是打电话给getUnchecked(10),会发生什么?

标签: java caching recursion tree guava


【解决方案1】:

既然你已经重写了你的问题,这里有一个新的答案。

首先,在我看来,您对 computeNonRecursivelly 的实现仍然是递归的,因为 getOrEnqueue 调用了它。

我认为您不能直接使用Cache,因为您需要在计算中有两个步骤:一个说明所需值的依赖关系,另一个在满足依赖关系后进行计算。不过,它只有在你从来没有循环依赖时才有效(这与递归中的要求相同)。

这样,您可以将尚未在缓存中的依赖项(及其依赖项等)排队,然后以正确的顺序计算它们。类似于:

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

现在您可以实现一个安全调用缓存的TwoStepCacheLoader

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

我已经对其进行了单元测试,它似乎运行正确。

【讨论】:

  • 还有其他方法可以覆盖以正确(并且始终如一地)扩展ForwardingLoadingCache,但它们对于示例并不重要(getAll()apply() 等)。
  • 关于“computeNonRecursively 仍然递归”,你是对的,但这只是从我丑陋的解决方案中提取时出现的错误。该调用可以而且必须被删除。
  • 1 我不确定是否有办法让更多的工作人员解决问题,我也看不出两个并发的对需要公共依赖项的值的请求是否可以以某种方式合作。我不知道是否可以简单地重写getDependenciesToCompute。我知道这是我从未写过的东西。 2 getDependencies 的想法非常好。恐怕它不适用于像f(n) = f(n-1) + f(f(log(n))) 这样的疯狂案例,但我想我永远不需要它。 3 那就是说我感谢您提供的出色解决方案。我需要更多时间来评估它。
  • 还有一个问题:在computeDependencies 中,您调用cache.get,如果密钥在此期间过期,则反过来可能会调用computeDependencies。但这一切似乎都能以某种方式解决。
  • @maaartinus 1 如果并发请求需要一个公共的依赖子集,它们都会将它们排队,但它们的处理将是一次未命中和一次或多次缓存命中.那里没有问题,除了比必要的更多的命中。 2如果你依赖f(f(log(n)),我认为你的算法本质上是递归的,你将很难转换它。
【解决方案2】:

编辑:更改了实现以允许在多个线程中将相同的 Expression 作为参数传递时进行单个计算。

不要使用LoadingCache,只需将结果缓存在eval 中(一旦修改为使用迭代而不是递归):

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

【讨论】:

  • 这应该可以工作,但是我缺少以下功能:"如果另一个调用 get 或 getUnchecked 当前正在加载 key 的值,只需等待该线程完成并返回其加载的值。” 虽然我可以自己做一些锁定,但这可能会显着增加开销。
  • Cache.get(K, Callable&lt;V&gt;) 没有记录此功能,因为它适用于LoadingCache.get(K),但由于这两种方法最终都会调用LocalCache.get(K, CacheLoader),因此它应该可以工作。不过,不确定它是真正的功能还是实现细节可能会发生变化。
  • Guava 贡献者:是的,该保证适用于Cache.get(K, Callable)。我已经filed an issue 确保在文档中更清楚地说明了这一点。
  • @Frank Pavageau:实际上,您的解决方案与使用 CacheLoader 一样递归。但这不是你的错,我要求的是不可能的事情:在计算表达式期间询问子表达式的计算只是意味着递归。我需要以某种方式使用一些占位符......我会的当我弄清楚更多时回来。
猜你喜欢
  • 1970-01-01
  • 2016-01-26
  • 2017-03-05
  • 2017-02-22
  • 2021-07-06
  • 2021-06-18
相关资源
最近更新 更多