优化编译器是否可以消除 std::unique_ptr 中的所有运行时开销?

6
阅读关于std :: unique_ptr的内容,位于http://en.cppreference.com/w/cpp/memory/unique_ptr上,我的天真印象是,足够智能的编译器可以用裸指针替换正确使用的unique_ptr,并在销毁unique_ptr时仅插入delete。这实际上是这种情况吗?如果是,那么任何主流优化编译器都会这样做吗?如果没有,是否可能编写一些具有unique_ptr编译时安全性好处的代码,可以优化为没有运行时成本(在空间或时间方面)?

注意:对于那些(正确地)担心过早优化的人来说:这里的答案不会阻止我使用std :: unique_ptr,我只是很好奇它是否是一个真正棒极了的工具。

编辑 2013/07/21 20:07 EST:

好的,所以我使用以下程序进行了测试(请告诉我其中是否有问题):

#include <climits>
#include <chrono>
#include <memory>
#include <iostream>

static const size_t iterations = 100;

int main (int argc, char ** argv) {
    std::chrono::steady_clock::rep smart[iterations];
    std::chrono::steady_clock::rep dumb[iterations];
    volatile int contents;
    for (size_t i = 0; i < iterations; i++) {
        auto start = std::chrono::steady_clock::now();
        {
            std::unique_ptr<int> smart_ptr(new int(5));
            for (unsigned int j = 0; j < UINT_MAX; j++)
                contents = *smart_ptr;
        }
        auto middle = std::chrono::steady_clock::now();
        {
            int *dumb_ptr = new int(10);
            try {
                for (unsigned int j = 0; j < UINT_MAX; j++)
                    contents = *dumb_ptr;
                delete dumb_ptr;
            } catch (...) {
                delete dumb_ptr;
                throw;
            }
        }
        auto end = std::chrono::steady_clock::now();
        smart[i] = (middle - start).count();
        dumb[i] = (end - middle).count();
    }
    std::chrono::steady_clock::rep smartAvg;
    std::chrono::steady_clock::rep dumbAvg;
    for (size_t i = 0; i < iterations; i++) {
        smartAvg += smart[i];
        dumbAvg += dumb[i];
    }
    smartAvg /= iterations;
    dumbAvg /= iterations;

    std::cerr << "Smart: " << smartAvg << " Dumb: " << dumbAvg << std::endl;
    return contents;
}

使用 g++ --std=c++11 -O3 test.cc 编译 g++ 4.7.3 版本,得到的结果为 Smart: 1130859 Dumb: 1130005,这意味着智能指针与愚蠢指针之间的差距仅有0.076%,几乎可以确定是噪音所致。


4
编译器还能做些什么?!对于类的数据内容而言,唯一指针只是一个单独的指针。 - Kerrek SB
一个 unique_ptr 有哪些运行时成本?至于空间,对我来说 sizeof(myuniqueptr)sizeof(myptr) 完全相同,都是8个字节的 int - Rapptz
6
这些测试是在未经优化的版本上执行的。没有优化的情况下进行性能分析是毫无意义的。 - Nicol Bolas
1
作为对我早先评论的让步,写一个效率较低的实现是完全可能的(如链接问题中提供的那样,即需要偏移调整以进行解引用),如果实现没有检查删除器是否为空(并且仅在适当时应用空基类优化),则具有有状态删除器的实现可能会导致这种效率较低的实现。 - Kerrek SB
@SheaLevy:答案应该放在答案部分,而不是问题中。这就是其中一个原因。 - Nicol Bolas
显示剩余6条评论
2个回答

5

作为一名合格的编译器,我期望它只是一个简单指针和调用delete析构函数的包装器,因此生成的机器码应该如下:

x *p = new X;
... do stuff with p. 
delete p; 

并且

unique_ptr<X> p(new X);
... do stuff with p; 

代码将完全相同。


1
更像是 x *p; try { p = new X; /* ... */ } catch(...) { delete p; throw; }... - Kerrek SB
unique_ptr 真的需要 try/catch 吗? - 当然,在两种情况下,如果在 new 中分配失败,整个函数都会抛出异常,无论是使用普通指针 p 还是智能指针 p(在前一种情况下,它没有被“构造”)。我猜如果构造函数或 X 抛出异常,你需要担心这个问题... - Mats Petersson
2
这是关于代码中的异常处理!(所以我应该这样说:x *p = new X; try { /* ... */ } catch(...) { delete p; throw; }。) - Kerrek SB
好的,所以我希望没有这些... ;) - Mats Petersson

4
严格来说,答案是否定的。
回想一下,unique_ptr不仅是基于指针类型,而且还基于删除器类型的模板参数。它的声明如下:
template <class T, class D = default_delete<T>> class unique_ptr;

此外,unique_ptr<T, D> 不仅包含 T*,还包含一个 D。下面的代码(在 MSVC 2010 和 GCC 4.8.1 上编译)说明了这一点:

#include <memory>

template <typename T>
struct deleter {
    char filler;
    void operator()(T* ptr) {}
};

int main() {
    static_assert(sizeof(int*) != sizeof(std::unique_ptr<int, deleter<int>>), "");
    return 0;
}

当您移动一个unique_ptr<T, D>时,其成本不仅是将源指针T*复制到目标指针的开销(与原始指针相同),因为它还必须复制/移动D
确实,智能指针的实现可以检测D是否为空,并且具有不执行任何操作的复制/移动构造函数(这是default_delete<T>的情况),在这种情况下,避免复制D的开销。此外,它可以通过不添加任何额外的字节来节省内存。Dunique_ptr的析构函数在调用删除器之前必须检查T*是否为空。对于default_delete<T>,我认为优化器可能会消除此测试,因为删除空指针是可以的。
但是,std::unique_ptr<T,D>的移动构造函数必须做一件额外的事情,而T*则不需要。由于所有权从源传递到目标,因此必须将源设置为null。类似的论点适用于unique_ptr的赋值。
话虽如此,对于default_delete<T>而言,开销非常小,我认为很难通过测量来检测到。

我认为unique_ptr已经足够优化了,只要size(default_delete)和*操作符没有额外开销,它几乎是保护原始指针所持有的内存的完美工具。 - StereoMatching

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接