std :: make_unique вызывает значительное замедление?

Недавно я начал модернизировать свою кодовую базу C++, используя C++ 14 вместо C++ 11.

После замены единственного вхождения std::unique_ptr.reset(new...) с std::make_unique из C++ 14 я понял, что мой тестовый набор (который состоит из около 30 C++ тестовых программ) составил около 50% помедленнее.

Старый C++ 11 код (быстрый):

class Foo
{
  public:
    Foo(size_t size)
    {
        array.reset(new char[size]);
    }
  private:
    std::unique_ptr<char[]> array;
};

Новый код C++ 14 (медленный):

class Foo
{
  public:
    Foo(size_t size)
    {
        array = std::make_unique<char[]>(size);
    }
  private:
    std::unique_ptr<char[]> array;
};

Как GCC, так и Clang работают намного медленнее, используя код C++ 14 с std::make_unique. Когда я тестирую обе версии с использованием valgrind, он сообщает, что оба кода C++ 11 и C++ 14 используют одинаковое количество распределений и одинаковое количество выделенной памяти, и утечек памяти нет.

Когда я смотрю на сгенерированную сборку тестовых программ выше, у меня возникает подозрение, что версия C++ 14 с использованием std::make_unique сбрасывает память после выделения с использованием memset. Версия C++ 11 не делает этого:

C++ 11 сборка (GCC 7.4, x64)

main:
sub rsp, 8
movsx rdi, edi
call operator new[](unsigned long)
mov rdi, rax
call operator delete[](void*)
xor eax, eax
add rsp, 8
ret

C++ 14 сборка (GCC 7.4, x64)

main:
push rbx
movsx rbx, edi
mov rdi, rbx
call operator new[](unsigned long)
mov rcx, rax
mov rax, rbx
sub rax, 1
js .L2
lea rax, [rbx-2]
mov edx, 1
mov rdi, rcx
cmp rax, -1
cmovge rdx, rbx
xor esi, esi
call memset
mov rcx, rax
.L2:
mov rdi, rcx
call operator delete[](void*)
xor eax, eax
pop rbx
ret

Вопросы:

Является ли инициализация памяти известной функцией std::make_unique? Если не то, что еще может объяснить замедление производительности, которое я испытываю?

Ответы

Ответ 1

  Является ли инициализация памяти известной особенностью std::make_unique?

Это зависит от того, что вы подразумеваете под "известным". Но да, это разница между вашими делами. Из cppreference вызов make_unique<T>(size) выполняет:

unique_ptr<T>(new typename std::remove_extent<T>::type[size]())
//                                                         ~~~~

Вот как это указано.

new char[size] выделяет память и инициализирует ее по умолчанию. new char[size]() выделяет память и инициализирует ее значение, которое инициализируется нулями в случае char. По умолчанию многие вещи в стандартной библиотеке будут инициализировать значения, а не инициализировать по умолчанию.

Аналогично, make_unique<T>() делает new T(), а не new T... make_unique<char>() дает вам 0, new char дает вам неопределенное значение.

В качестве аналогичного примера, если я хочу изменить размер vector<char> на неинициализированный буфер заданного размера (чтобы он был сразу же заполнен чем-то другим), я должен либо использовать свой собственный распределитель, либо использовать тип, который не char, которая заключается в его инициализации.


С++ 20 представит новые вспомогательные функции для решения этой проблемы, предоставлено P11020R1:

  • make_unique_default_init
  • make_shared_default_init
  • allocate_shared_default_init

В то время как make_unique<T>() будет делать new T(), они делают new T. То есть дополнительного обнуления нет. В конкретном случае OP, std::make_unique_default_init<char[]>(size) это то, что вы хотите.