Действительно ли make_shared более эффективен, чем новый? - PullRequest
50 голосов
/ 16 февраля 2012

Я экспериментировал с 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

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

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

Destructor

Destructor

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

Создать smart_ptr, используя новый ...

Конструктор новый

Создать smart_ptr, используя new: done.

Destructor

Destructor

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

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

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

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

Ответы [ 4 ]

37 голосов
/ 16 февраля 2012

В качестве инфраструктуры я использовал 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, должно быть правильно сформировано.А должен быть распределителем (17.6.3.5).Конструктор копирования и деструктор A не должны генерировать исключения.

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

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

Для справки, вот что произошло, когда я взял ваш код в 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 ++ нарушено.

32 голосов
/ 16 февраля 2012

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

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 нет никакой разницы между двумя версиями. Более эффективен сам указатель общего доступа, а не управляемый объект.)

6 голосов
/ 16 февраля 2012

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

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

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

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

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

struct T {
    int reference_count;
    Object object;
};

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

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

Для ясности, это то, что говорит о повышении make_shared

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

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

3 голосов
/ 16 февраля 2012

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

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 более эффективен, поскольку он может быть реализован с использованием только одного динамического выделения вместо двух, а также потому, что для него требуется память на один указатель меньше учета для каждого общего объекта.

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

...