【问题标题】:High performance recursive functions in C++C++ 中的高性能递归函数
【发布时间】:2021-02-07 09:47:04
【问题描述】:

我正在学习 C++,我正在玩链表。

考虑以下极其简单的实现:


static std::size_t n_constructors {0};


template <typename T>
class list {
public:
    list(T n) : item{n} { n_constructors++; }
    list(T n, list* l) : item{n}, next {l} { n_constructors++; }
    list(const list&);
    ~list() { delete next; }
    void insert(T n);
    void print() const;
    list reverse() const;
    list reverse_iter() const;
private:
    T item;
    list* next {nullptr};
};


template <typename T>
list<T>::list(const list& src)
    :
    item {src.item}
{
    n_constructors++;
    if (src.next) {
        next = new list {*src.next};
    }
}


template <typename T>
void list<T>::insert(T n) {
    if (next == nullptr)
        next = new list {n};
    else
        next->insert(n);
}


template <typename T>
void list<T>::print() const {
    std::cout << item << " ";
    if (next)
        next->print();
    else
        std::cout << std::endl;
}


template <typename T>
list<T> list<T>::reverse() const {
    if (next) {
        auto s = next->reverse();
        s.insert(item);
        return s;
    } else {
        return list {item};
    }
}


template <typename T>
list<T> list<T>::reverse_iter() const {
    auto sptr = new list<T> {item};
    auto prev = next;
    auto link = next;
    while (link) {
        auto tmp = new list<T>(link->item, sptr);
        link = link->next;
        prev = link;
        sptr = tmp;
    }
    return *sptr;
}

如您所见,我编写了两个反向函数:一个迭代函数和一个递归函数。 为了测试它们,我尝试了这个:

int main() {
    list<int> s{1};
    for (int i = 2; i < 10000; ++i)
        s.insert(i);
    std::cout << "initial constructor\n";
    std::cout << "called " << n_constructors << " constructors.\n";
    auto t0 = std::chrono::high_resolution_clock::now();
    auto s2 = s.reverse_iter();
    auto t = std::chrono::high_resolution_clock::now();
    std::cout << "iterative reverse\n";
    std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(t - t0).count() <<" ms\n";
    std::cout << "called " << n_constructors << " constructors.\n";
    t0 = std::chrono::high_resolution_clock::now();
    auto s3 = s.reverse();
    t = std::chrono::high_resolution_clock::now();
    std::cout << "recursive reverse\n";
    std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(t - t0).count() <<" ms\n";
    std::cout << "called " << n_constructors << " constructors.\n";
}

这是我的基准测试的典型结果(使用 g++ 9.3.0 编译,使用 -O2 打开优化器):

initial constructor
called 9999 constructors.
iterative reverse
0 ms
called 29997 constructors.
recursive reverse
1692 ms
called 50034995 constructors.

性能上的差异是惊人的。 我猜递归版本的问题是分配的数量要大得多,所以我实现了一个移动构造函数:

template <typename T>
list<T>::list(list&& src)
    :
    item {src.item},
    next {src.next}
{
    n_constructors++;
    src.next = nullptr;
    src.item = 0;
}

这是结果:

initial constructor
called 9999 constructors.
iterative reverse
0 ms
called 29997 constructors.
recursive reverse
90 ms
called 49994 constructors.

很好,现在至少递归函数执行的分配次数与迭代函数一样多。 然而,性能差异仍然很大。 我用 100 000 个元素再试一次:

initial constructor
called 99999 constructors.
iterative reverse
7 ms
called 299997 constructors.
recursive reverse
9458 ms
called 499994 constructors.

在我看来,递归逆向比迭代逆向更具可读性和优雅。

为什么递归版本这么慢? 有什么办法可以让它更快吗?

编辑

按照 Will Ness 在 cmets 中的建议,我添加了两个算法的经验渐近增长顺序的图。

【问题讨论】:

  • 你好像在问similar question
  • reverse_iter 是线性的,reverse 是二次的(因为它调用 insert 本身是线性的)。你花时间遍历链接,而不是调用构造函数。
  • @JaMiT 如果没有办法让递归比循环更快,我很高兴知道为什么递归要慢得多(3 个数量级!)尽管分配与迭代一样多版本。
  • 这与递归本身无关。这是关于每次都必须遍历半构建列表以在末尾插入节点。 reverse 的工作方式是,它遍历列表到最后,并复制最后一个节点。然后它将倒数第二个节点的副本插入到这个单元素列表中。然后它将前一个音符的副本插入到这个二元素列表中,遍历它到最后。等等。对于每个节点,它最终都会从头到尾遍历部分构建的反向列表。相比之下,reverse_iter 一次性完成所有工作。
  • @IgorTandetnik 说得通,谢谢。我想我可以通过保持指向列表最后一个元素的指针再次使递归算法线性化,听起来对吗?

标签: c++ recursion linked-list


【解决方案1】:

您不仅要在递归和迭代之间进行测试,还要在具有不同增长顺序的两种算法之间进行测试。

您的reverse 的复杂度为 T(n) = T(n - 1) + O(n),因此链接数为二次方。您的 reverse_iter 只有线性复杂度。因此,这种比较是不公平的。

【讨论】:

  • 你的意思是 T(n) = T(n - 1) + O(n)?
  • @J.D.这将使它成为二次的,nn。此外,*one 大小点的性能比较在很大程度上没有意义。见this。有意义的是时间与问题大小的对数图,至少有两个大小点。
  • @WillNess 点了,谢谢。我认为 Ami Tavory 的答案确实想指出 reverse 是二次的,所以它不能是 T(n) = T(n - 1) + O(1),它在 n 中是线性的。
  • @J.D.啊,对。我以为它指的是另一个。 :) 顺便说一句,有意义的比较是时间与问题大小的对数图,至少有两个大小点。请参阅this 以获得一个很好的例子。那是为了幂律增长;对于指数,它当然是对数图。
  • @WillNess 这是一个非常有意义的情节,我将尝试为我的代码制作一个类似的情节(我希望明天)。尽管性能存在巨大差异,但对数日志图应该可以比较两种算法。我在您的个人资料页面中看到了对经验增长顺序的呼声,您是否担心渐近增长顺序可能过于渐近?
猜你喜欢
  • 1970-01-01
  • 2017-10-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-02
  • 1970-01-01
  • 1970-01-01
  • 2014-01-22
相关资源
最近更新 更多