【问题标题】:Recursion faster than iteration递归比迭代快
【发布时间】:2013-09-21 12:01:06
【问题描述】:

我在 C# 中实现了一个四叉树,并且遇到了一个奇怪的情况,即递归似乎比迭代执行得更好,尽管看起来应该相反。

我的节点如下所示:

class QuadNode
{
    private QuadNode topLeft;
    private QuadNode topRight;
    private QuadNode bottomRight;
    private QuadNode bottomLeft;
    // other fields...
}

为了遍历树,我使用了以下递归方法,我在根节点上调用它:

Traverse()
{
    // visit 'this'

    if (topLeft != null)
        topLeft.Traverse();
    if (topRight != null)
        topRight.Traverse();
    if (bottomRight != null)
        bottomRight.Traverse();
    if (bottomLeft != null)
        bottomLeft.Traverse();
}

主要是出于兴趣,我尝试创建一种遍历树的迭代方法。

我向每个节点添加了以下字段:private QuadNode next,当我创建树时,我使用队列执行广度优先遍历,将每个节点的 next 字段链接到行中的下一个节点。本质上,我从树的节点创建了一个单链表。
此时我可以使用以下方法遍历树:

Traverse()
{
    QuadNode node = this;
    while (node != null)
    {
        // visit node

        node = node.next;
    }
}

在测试了每种方法的性能后,我很惊讶地发现迭代版本始终比递归版本慢得多。我已经在大树和小树上进行了测试,递归方法总是更快。 (我使用 aStopwatch 进行基准测试)
我已经确认这两种方法都成功遍历了整个树,并且迭代版本只按计划访问每个节点一次,所以它不是它们之间的链接有问题。

在我看来,迭代版本的性能似乎很明显......这可能是什么原因?我是否忽略了为什么递归版本更快的一些明显原因?

我正在使用 Visual Studio 2012 并在 Release 下编译,任何 CPU(首选 32 位未选中)。

编辑:
我打开了一个新项目并创建了一个简单的测试,它也证实了我的结果。
以下是完整代码:http://pastebin.com/SwAsTMjQ
代码没有注释,但我认为它是非常自我记录的。

【问题讨论】:

  • 您的测量结果在哪里?
  • @Arie 他所看到的完全相反,因此他提出了问题。在他的情况下,迭代较慢
  • 如何存储“下一个”指针?您是否将您的代码更改为在更改为下一个时仅存储一个节点指针?
  • 你为什么不采用与递归相同的方法(深度优先)并在接下来访问一堆元素?计时时,有多少额外的时间用于创建链表?

标签: c# performance recursion iteration complexity-theory


【解决方案1】:

缓存局部性正在扼杀速度。试试:

public void LinkNodes()
{
    var queue = new Queue<QuadNode>();
    LinkNodes(queue);

    QuadNode curr = this;

    foreach (var item in queue)
    {
        curr.next = item;
        curr = item;
    }
}

public void LinkNodes(Queue<QuadNode> queue)
{
    queue.Enqueue(this);

    if (topLeft != null)
        topLeft.LinkNodes(queue);
    if (topRight != null)
        topRight.LinkNodes(queue);
    if (bottomRight != null)
        bottomRight.LinkNodes(queue);
    if (bottomLeft != null)
        bottomLeft.LinkNodes(queue);
}

现在迭代版本应该比递归版本快 30/40%。

缓慢的原因是您的迭代算法将采用广度优先而不是深度优先。您创建了元素深度优先,因此它们在内存中按深度优先排序。我的算法创建了深度优先遍历列表。

(请注意,我在 LinkNodes() 中使用了 Queue 以使其更容易理解,但实际上你可以不这样做)

public QuadNode LinkNodes(QuadNode prev = null)
{
    if (prev != null)
    {
        prev.next = this;
    }

    QuadNode curr = this;

    if (topLeft != null)
        curr = topLeft.LinkNodes(curr);

    if (topRight != null)
        curr = topRight.LinkNodes(curr);

    if (bottomRight != null)
        curr = bottomRight.LinkNodes(curr);

    if (bottomLeft != null)
        curr = bottomLeft.LinkNodes(curr);

    return curr;
}

【讨论】:

  • 在测试这个时,我注意到我创建树的方式甚至不是深度优先的,如果你看一下代码,它会略有不同。我解决了这个问题,现在迭代版本确实更快 - 但仅限于 32 位。在 64 位上,“小”树的递归版本较慢,但深度为 10 或更大的树仍然显示递归方法稍微快一点。我现在会继续研究这个。
  • 实际上,我在代码中犯了一个错误,似乎迭代版本(固定拆分和链接方法)在任何情况下都能更快地工作。难以置信,非常感谢!
【解决方案2】:

查看您的代码,这两种方法似乎工作相同,但是在递归方法中,您在“循环”中访问 4 个节点,这意味着您不会在 3 个测试之间“跳转”,而在迭代中您“跳转” " 到每次运行循环的开头。 我想说,如果您想看到几乎相似的行为,您必须将迭代循环展开为:​​

Traverse(int depth)
{
    QuadNode node = this;
    while (node != null)
    {
        // visit node

        node = node.next;
        if (node!=null) node=node.next;
        if (node!=null) node=node.next;
        if (node!=null) node=node.next;

    }
}

【讨论】:

  • 循环展开是由大多数现代编译器完成的,手动完成只会让你的代码更难阅读。
猜你喜欢
  • 2015-02-27
  • 2020-08-14
  • 2019-03-25
  • 1970-01-01
  • 2015-03-26
  • 2014-09-05
  • 2012-09-15
相关资源
最近更新 更多