Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

std::make_unique causes big slowdown?

I have recently started to modernize my C++ codebase by using C++14 instead of C++11.

After replacing a single occurrence of std::unique_ptr.reset(new ...) with std::make_unique from C++14 I realized that my test suite (which consists of about 30 C++ test programs) ran about 50% slower.

Old C++11 code (fast):

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

New C++14 code (slow):

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

Both GCC and Clang run much slower using the C++14 code with std::make_unique. When I test both versions using valgrind it reports that both the C++11 and C++14 code use the same amount of allocations and the same amount of allocated memory and there are no memory leaks.

When I look at the generated assembly of the test programs above I have the suspicion that the C++14 version using std::make_unique resets the memory after the allocation using memset. The C++11 version does not do this:

C++11 assembly (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 assembly (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

Questions:

Is initializing memory a known feature of std::make_unique? If not what else could explain the performance slowdown I am experiencing?

like image 744
Linoliumz Avatar asked Apr 13 '18 13:04

Linoliumz


1 Answers

Is initializing memory a known feature of std::make_unique?

It depends on what you mean by "known." But yeah, that is the difference between your cases. From cppreference, the make_unique<T>(size) call does:

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

This is how it is specified.

new char[size] allocates memory and default-initializes it. new char[size]() allocates memory and value-initializes it, which zero-initializes in the case of char. By default, a lot of the things in the standard library will value-initialize and not default initialize.

Likewise, make_unique<T>() does new T() and not new T... make_unique<char>() gives you 0, new char gives you an indeterminate value.

As a similar example, if I want to have a vector<char> resize to an uninitialized buffer of a given size (to be immediately populated by something else), I have to either use my own allocator or use a type that isn't char which lies about its initialization.


C++20 will introduce new helper functions to alleviate this problem, courtesy of P11020R1:

  • make_unique_default_init
  • make_shared_default_init
  • allocate_shared_default_init

Whereas make_unique<T>() would do new T(), these do new T. That is, there is no extra zeroing. In the specific case of OP, std::make_unique_default_init<char[]>(size) is what you want.

like image 58
Barry Avatar answered Oct 31 '22 20:10

Barry