【问题标题】:Why inefficient factorial computation is...efficient (and fast)?为什么低效的阶乘计算是……高效(而且快速)?
【发布时间】:2016-04-20 07:14:16
【问题描述】:

我编写了这个简单的程序来测试memoization 技术:

int main() {
    function<double(double)> f = [&f](double i) -> double {
        if (i == 1)
            return 1;
        else
            return i * f(i - 1);
    };
    cout << f(100) << endl;
}

我希望在几秒钟内执行这段代码(因为它的递归效率低下),但实际上只花了几毫秒......为什么?我认为引擎盖下有一些编译器优化,但我不明白会发生什么。

额外问题: 您能否给我一个执行效率低下的简单程序(编译器优化与否),以便我测试记忆化的好处?

【问题讨论】:

  • 几秒钟计算 100 次乘法?你是在算盘上运行这个吗?
  • 这可能是尾递归优化的情况,也可能是其他情况(就像没有那么多调用,真的)。唯一确定的方法是查看生成的汇编代码。
  • 您的输入数字太小,无法显示递归实现的缺点。它仍然只计算 100 次乘法——不断增加的堆栈大小还不是问题。尝试使用 10000 或更大来记录问题。奖励问题的答案:尝试斐波那契序列:f(i) := f(i-1) + f(i-2) 和两个基本情况 f(0) := 1; f(1) := 1 由于它不止一次递归地调用 f,因此您的堆栈增长速度快于使用 i 时的线性增长。
  • 只是一个提示 - 尝试使用递归方法计算斐波那契(或将结果乘以 f(i - 2)
  • 澄清一下:lambda 中的 f(i - 1) 不是直接递归调用(如果您认为 function 与 lambda 不是同一个对象,那就不足为奇了)。它调用foperator()(double),后者又调用f 包装的lambda 对象的operator()(double)(等等)。在所有语法糖的背后隐藏着很多间接性,编译器可能无法将其全部解开。

标签: c++ recursion memoization


【解决方案1】:

记忆技术旨在用于优化昂贵的函数调用。阶乘函数并非如此。 C++ 非常快,因此阶乘函数调用的计算时间永远不会超过几毫秒。 (至少如果不使用多精度)。 factorial(100) 是“仅”100 次乘法,所以对于 C++ 来说没有任何意义。

如果这只是为了测试或演示目的,我会简单地在函数调用中引入延迟(睡眠、长虚拟循环或其他)。 随着记忆的实施,这种延迟不应该发生,所以它“几乎”没有时间运行。

这是我会做的一个例子。 factorial 是昂贵的函数。 memo_factorial 是实现了记忆技术的包装。 在第一次调用函数时,输入和输出的字典被更新,在接下来的调用相同的输入中,之前存储的值被返回,所以“真正的”函数不会再次执行。

#define ELAPSE(cmd) { clock_t s = clock();\
    long ret = cmd;\
    cout << "\t" << #cmd\
         << " = " << ret \
         << "\t(" << (clock()-s)/double(CLOCKS_PER_SEC) << " secs)" \
         << endl; }

long factorial(long i) {
    for(clock_t s = clock(); (clock()-s)<CLOCKS_PER_SEC; );
    return i<=1 ? 1 : i*factorial(i-1);
}
long memo_factorial(long i) {
    static map<long,long> saved;
    map<long,long>::const_iterator it = saved.find(i);
    return ( it==saved.end() ) ? (saved[i] = memo_factorial(i)) : it->second;
}

int main() {
    cout << "first execution WITHOUT memoization" << endl;
    for(int i=1; i<5; ++i) {
        ELAPSE( memo_factorial(i) )
    }

    cout << "second execution WITH memoization" << endl;
    for(int i=1; i<5; ++i) {
        ELAPSE( memo_factorial(i) )
    }

    return 0;
}

输出应该是:

first execution WITHOUT memoization
    memo_factorial(i) = 1   (1 secs)
    memo_factorial(i) = 2   (1 secs)
    memo_factorial(i) = 6   (1 secs)
    memo_factorial(i) = 24  (1 secs)
second execution WITH memoization
    memo_factorial(i) = 1   (0 secs)
    memo_factorial(i) = 2   (0 secs)
    memo_factorial(i) = 6   (0 secs)
    memo_factorial(i) = 24  (0 secs)

希望你觉得它有用。

问候, 亚历克斯

注意:阶乘通常定义在整数值上。当然,它只是一个乘法序列,因此它可以应用于其他类型。

【讨论】:

  • memo_factorial() 应该递归调用自身,而不是调用factorial()。这样,对memo_factorial(100) 的调用将记住i &lt;= 100 所在的所有值,而不仅仅是一个值。这是记忆递归函数的关键部分。
  • 感谢@JørgenFogh。已更新。
【解决方案2】:

Memoization 主要是一种改进计算的algorithmic complexity 的技术,而不是避免递归。这就是为什么斐波那契数是比阶乘函数更好的示例(即使 WikiPedia 页面使用阶乘函数)。

看看wikibook on dynamic programming 中的数字。第二个图中被划掉的所有调用都是你从记忆中获得的节省。使用阶乘函数,什么都不会被划掉。

【讨论】:

    猜你喜欢
    • 2010-12-17
    • 2012-12-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-15
    • 2015-05-28
    • 1970-01-01
    • 2013-07-09
    相关资源
    最近更新 更多