Действительно ли make shared более эффективен, чем new?

я экспериментировал с shared_ptr и make_shared из C++11 и запрограммировал маленький игрушечный пример, чтобы увидеть, что на самом деле происходит при вызове make_shared. В качестве инфраструктуры я использовал llvm / clang 3.0 вместе с библиотекой llvm std c++ в XCode4.

class Object
{
public:
    Object(const string& str)
    {
        cout << "Constructor " << str << endl;
    }

    Object()
    {
        cout << "Default constructor" << endl;

    }

    ~Object()
    {
        cout << "Destructor" << endl;
    }

    Object(const Object& rhs)
    {
        cout << "Copy constructor..." << endl;
    }
};

void make_shared_example()
{
    cout << "Create smart_ptr using make_shared..." << endl;
    auto ptr_res1 = make_shared<Object>("make_shared");
    cout << "Create smart_ptr using make_shared: done." << endl;

    cout << "Create smart_ptr using new..." << endl;
    shared_ptr<Object> ptr_res2(new Object("new"));
    cout << "Create smart_ptr using new: done." << endl;
}

теперь взгляните на выход, пожалуйста:

создать smart_ptr с помощью make_shared...

make_shared в конструктор

конструктор копирования...

конструктор копирования...

деструктор

деструктор

создать smart_ptr с помощью make_shared: готово.

создать smart_ptr с помощью new...

конструктор new

создать smart_ptr с помощью new: done.

деструктор

деструктор

получается, что make_shared вызывает конструктор копирования в два раза. Если я выделяю память для Object через обычный new этого не происходит, только один есть.

мне интересно следующее. Я слышал это make_shared должен быть более эффективным, чем использование new(1, 2). Одна из причин-потому что make_shared выделяет счетчик ссылок вместе с объектом можно управлять в том же блоке памяти. Ладно,я понял. Это, конечно, более эффективно, чем два отдельные операции распределения.

напротив, я не понимаю, почему это должно идти со стоимостью двух вызовов конструктора копирования Object. Из-за этого я не уверен, что make_shared более эффективно, чем распределение с помощью new на каждый случае. Я ошибаюсь? Ну хорошо, можно реализовать конструктор перемещения для Object но все же я не уверен, что это более эффективно, чем просто выделение Object через new. По крайней мере не во всех случаях. Это было бы верно, если копирование Object дешевле, чем выделение памяти для счетчика ссылок. Но ... --1--> - внутренний счетчик ссылок может быть реализован с использованием нескольких примитивных типов данных, верно?

можете ли вы помочь и объяснить, почему make_shared это путь с точки зрения эффективности, несмотря на описанные накладные расходы на копирование?

4 ответов


в качестве инфраструктуры я использовал llvm / clang 3.0 вместе с библиотекой llvm std c++ в XCode4.

Ну, это, кажется, ваша проблема. Стандарт C++11 устанавливает следующие требования к make_shared<T>allocate_shared<T>), в разделе 20.7.2.2.6:

требуется: выражение:: new(pv) T(std::forward (args)...), где pv имеет тип void* и указывает на хранилище, пригодное для хранения объекта типа T, должно быть хорошо сформировано. A быть распределителем (17.6.3.5). Конструктор копирования и деструктор не должен бросать исключений.

T is не требуется, чтобы быть copy-constructable. Действительно,T даже не требуется, чтобы быть не-размещение-новый конструктивный. Требуется только быть конструктивным на месте. Это означает, что только make_shared<T> могу сделать с T is new это на месте.

таким образом, результаты, которые вы получаете, не соответствуют стандарту. Инфраструктура LLVM это libc++ сломан в этом отношении. Подать отчет об ошибке.

для справки, вот что произошло, когда я взял ваш код в VC2010:

Create smart_ptr using make_shared...
Constructor make_shared
Create smart_ptr using make_shared: done.
Create smart_ptr using new...
Constructor new
Create smart_ptr using new: done.
Destructor
Destructor

Я также портировал его на оригинал Boost shared_ptr и make_shared, и я получил то же самое, что и VC2010.

Я бы предложил подать отчет об ошибке, так как поведение libc++нарушено.


вы должны сравнить эти две версии:

std::shared_ptr<Object> p1 = std::make_shared<Object>("foo");
std::shared_ptr<Object> p2(new Object("foo"));

в вашем коде вторая переменная - это просто голый указатель, а не общий указатель вообще.


теперь о мясе. make_shared и (на практике) более эффективно, потому что он выделяет опорный блок управления вместе с фактическим объектом в одном динамическом распределении. Напротив, конструктор для shared_ptr который принимает голый указатель объекта должен выделить другое динамическая переменная для счетчика ссылок. Компромисс в том, что make_shared (или его двоюродный брат allocate_shared) не позволяет указать пользовательский делетер, так как распределение выполняется распределителем.

(это не влияет на конструкцию самого объекта. От ObjectС точки зрения нет никакой разницы между двумя версиями. Более эффективным является сам общий указатель, а не управляемый объект.)


так что одна вещь, чтобы иметь в виду ваши настройки оптимизации. Измерение производительности, особенно в отношении c++, - это бессмысленно без включенной оптимизацией. Я не знаю, действительно ли вы компилировали с оптимизациями, поэтому я подумал, что это стоит упомянуть.

это сказало, что вы измеряете с этим тестом не так что make_shared более эффективно. Проще говоря, вы измеряете неправильно :-П.

здесь сделка. Обычно при создании общего указателя он имеет по крайней мере 2 элемента данных (возможно, больше). Один для указателя, и один для отсчета ссылок. Этот счетчик ссылок выделяется в куче (чтобы его можно было разделить между shared_ptr С разных жизней...в этом-то все и дело!)

так что если вы создаете объект с чем-то вроде std::shared_ptr<Object> p2(new Object("foo")); есть по крайней мере 2 звонки new. Для Object и одно для отсчета ссылки объект.

make_shared имеет опцию (я не уверен, что она должна), чтобы сделать один new который достаточно большой, чтобы удерживать объект, на который указывают, и счетчик ссылок в одном и том же смежном блоке. Эффективно выделяя объект, который выглядит примерно так (иллюстративный, а не буквально, что это такое).

struct T {
    int reference_count;
    Object object;
};

поскольку счетчик ссылок и время жизни объекта связаны друг с другом (не имеет смысла жить дольше, чем другой). Весь этот блок может быть deleted в то же время.

таким образом, эффективность заключается в распределении, а не в копировании (что, как я подозреваю, связано с оптимизацией больше всего).

чтобы быть ясным, это то, что boost должен сказать о make_shared

http://www.boost.org/doc/libs/1_43_0/libs/smart_ptr/make_shared.html

кроме удобства и стиля, такая функция также сейф исключения и значительно быстрее, потому что он может использовать одно распределение для и объект и свой соответствуя блок управления, исключая a значительная часть строительных накладных расходов shared_ptr. Этот устраняет одну из основных жалоб на эффективность shared_ptr.


вы не должны получать никаких дополнительных копий там. Вывод должен быть:

Create smart_ptr using make_shared...
Constructor make_shared
Create smart_ptr using make_shared: done.
Create smart_ptr using new...
Constructor new
Create smart_ptr using new: done.
Destructor

Я не знаю, почему вы получаете дополнительные копии. (хотя я вижу, что вы получаете один "деструктор" слишком много, поэтому код, который вы использовали для вывода, должен отличаться от кода, который вы опубликовали)

make_shared является более эффективным, потому что он может быть реализован с использованием только одного динамического выделения вместо двух, и потому, что он нуждается в памяти одного указателя меньше учета на общий объект.

Edit: я не проверял с Xcode 4.2, но с Xcode 4.3 я получаю правильный вывод, который я показываю выше, а не неправильный вывод, показанный в вопросе.