共享指针是否会破坏尾调用优化?

时间:2017-10-23 21:35:54

标签: c++ recursion shared-ptr compiler-optimization tail-recursion

前言

我正在练习C ++并尝试实现不可变列表。 在我的一个测试中,我试图以递归方式创建一个包含大量值(100万个节点)的列表。所有值都是const,所以我不能执行常规循环,这也不是功能,你知道。 测试以Segmentation fault失败。

我的系统是带有Linux 4.4的64位Xubuntu 16.04 LTS。 我用g ++ 5.4和clang ++ 3.8使用--std=c++14 -O3标志编译我的代码。

源代码

我写了一个简单的例子,它显示了这种情况,当尾调用应该很容易优化,但出现问题并出现Segmentation fault。函数f只等待amount次迭代,然后创建一个指向单int的指针并返回它

#include <memory>

using std::shared_ptr;

shared_ptr<int> f(unsigned amount) {
    return amount? f(amount - 1) : shared_ptr<int>{new int};
}

int main() {
    return f(1E6) != nullptr;
}

注意此示例仅在g++时失败,而clang++使其正常。 虽然,在更复杂的例子中,它也没有优化。

这是一个带有递归插入元素的简单列表的示例。 我还添加了destroy函数,这有助于避免在销毁期间堆栈溢出。 在这里,我得到Segmentation fault两个编译器

#include <memory>

using std::shared_ptr;

struct L {
    shared_ptr<L> tail;

    L(const L&) = delete;
    L() = delete;
};

shared_ptr<L> insertBulk(unsigned amount, const shared_ptr<L>& tail) {
    return amount? insertBulk(amount - 1, shared_ptr<L>{new L{tail}})
                 : tail;
}

void destroy(shared_ptr<L> list) {
    if (!list) return;

    shared_ptr<L> tail = list->tail;
    list.reset();

    for (; tail; tail = tail->tail);
}

int main() {
    shared_ptr<L> list = shared_ptr<L>{new L{nullptr}};
    destroy(insertBulk(1E6, list));
    return 0;
}

注意 两个编译器都很好地优化了使用常规指针的实现。

问题

在我的情况下,shared_ptr真的打破了尾部调用优化吗? 它是编译器吗? shared_ptr实施问题或问题?

1 个答案:

答案 0 :(得分:3)

答案

简短回答是:是和否。

C ++中的共享指针不会破坏尾调用优化, 但它使这种递归函数的创建变得复杂,可以通过编译器转换为循环。

详细

在递归构建长列表期间避免堆栈溢出

我记得shared_ptr有一个析构函数,C ++有RAII。 这使得优化尾部调用的构造更加困难,正如Can Tail Call Optimization and RAII Co-Exist?问题中讨论的那样。

@KennyOstrom建议使用普通指针来解决这个问题

static const List* insertBulk_(unsigned amount, const List* tail=nullptr) {
    return amount? insertBulk_(amount - 1, new List{tail})
                 : tail;
}

使用以下构造函数

List(const List* tail): tail{tail} {}

tail的{​​{1}}是List的实例时,尾调用已成功优化。

在销毁期间避免堆栈溢出

需要自定义销毁策略。 幸运的是,shared_ptr允许我们设置它, 所以我通过shared_ptr隐藏了List的析构函数, 并将其用于列表销毁

private

构造函数应将此销毁函数传递给static void destroy(const List* list) { if (!list) return; shared_ptr<const List> tail = list->tail; delete list; for (; tail && tail.use_count() == 1; tail = tail->tail); } 初始化列表

tail

避免内存泄漏

在例外的情况下,我没有适当的清理,所以问题还没有解决。 我想使用List(const List* tail): tail{tail, List::destroy} {} ,因为它是安全的,但现在我不会将它用于当前列表头,直到构造结束。

需要观察“裸”指针,直到它被包装到共享指针中,并在紧急情况下释放它。 让我们将尾指针的引用传递给shared_ptr而不是指针本身。 这将允许最后一个好的指针在函数

之外可见
insertBulk_

然后需要类似static const List* insertBulk_(unsigned amount, const List*& tail) { if (!amount) { const List* result = tail; tail = nullptr; return result; } return insertBulk_(amount - 1, tail = new List{tail}); } 以自动销毁指针,在异常的情况下会泄漏

Finally

解决方案

现在,我猜,问题已经解决了:

  • static const shared_ptr<const List> insertBulk(unsigned amount) { struct TailGuard { const List* ptr; ~TailGuard() { List::destroy(this->ptr); } } guard{}; const List* result = insertBulk_(amount, guard.ptr); return amount? shared_ptr<const List>{result, List::destroy} : nullptr; } g++成功优化了长列表的递归创建;
  • 列表仍然使用clang++;
  • 普通指针似乎是安全的。

源代码

最终代码是

shared_ptr

源代码的较短版本

#include <memory> #include <cassert> using std::shared_ptr; class List { private: const shared_ptr<const List> tail; /** * I need a `tail` to be an instance of `shared_ptr`. * Separate `List` constructor was created for this purpose. * It gets a regular pointer to `tail` and wraps it * into shared pointer. * * The `tail` is a reference to pointer, * because `insertBulk`, which called `insertBulk_`, * should have an ability to free memory * in the case of `insertBulk_` fail * to avoid memory leak. */ static const List* insertBulk_(unsigned amount, const List*& tail) { if (!amount) { const List* result = tail; tail = nullptr; return result; } return insertBulk_(amount - 1, tail = new List{tail}); } unsigned size_(unsigned acc=1) const { return this->tail? this->tail->size_(acc + 1) : acc; } /** * Destructor needs to be hidden, * because it causes stack overflow for long lists. * Custom destruction method `destroy` should be invoked first. */ ~List() {} public: /** * List needs custom destruction strategy, * because default destructor causes stack overflow * in the case of long lists: * it will recursively remove its items. */ List(const List* tail): tail{tail, List::destroy} {} List(const shared_ptr<const List>& tail): tail{tail} {} List(const List&) = delete; List() = delete; unsigned size() const { return this->size_(); } /** * Public iterface for private `insertBulk_` method. * It wraps `insertBulk_` result into `shared_ptr` * with custom destruction function. * * Also it creates a guard for tail, * which will destroy it if something will go wrong. * `insertBulk_` should store `tail`, * which is not yet wrapped into `shared_ptr`, * in the guard, and set it to `nullptr` in the end * in order to avoid destruction of successfully created list. */ static const shared_ptr<const List> insertBulk(unsigned amount) { struct TailGuard { const List* ptr; ~TailGuard() { List::destroy(this->ptr); } } guard{}; const List* result = insertBulk_(amount, guard.ptr); return amount? shared_ptr<const List>{result, List::destroy} : nullptr; } /** * Custom destruction strategy, * which should be called in order to delete a list. */ static void destroy(const List* list) { if (!list) return; shared_ptr<const List> tail = list->tail; delete list; /** * Watching references count allows us to stop, * when we reached the node, * which is used by another list. * * Also this prevents long loop of construction and destruction, * because destruction calls this function `destroy` again * and it will create a lot of redundant entities * without `tail.use_count() == 1` condition. */ for (; tail && tail.use_count() == 1; tail = tail->tail); } }; int main() { /** * Check whether we can create multiple lists. */ const shared_ptr<const List> list{List::insertBulk(1E6)}; const shared_ptr<const List> longList{List::insertBulk(1E7)}; /** * Check whether we can use a list as a tail for another list. */ const shared_ptr<const List> composedList{new List{list}, List::destroy}; /** * Checking whether creation works well. */ assert(list->size() == 1E6); assert(longList->size() == 1E7); assert(composedList->size() == 1E6 + 1); return 0; } 函数

中没有注释和检查的List
main