23

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?

Linoliumz
  • 2,381
  • 3
  • 28
  • 35
  • 2
    This does indeed seem like an "I didn't as for this, why am I paying for it" type of situation... – rubenvb Apr 13 '18 at 13:51
  • @Richard Compiler explorer does reproduce the issue even with -O3. Your example does not show the issue because your size is hardcoded to 10. – Linoliumz Apr 13 '18 at 14:04
  • Compiler explorer link of std::make_unique issue: https://godbolt.org/g/7URbfP – Linoliumz Apr 13 '18 at 14:06

1 Answers1

24

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.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • 1
    What do you mean by that last bit about `char` lying about its own initialization? – rubenvb Apr 13 '18 at 14:10
  • 2
    @rubenvb e.g. `struct my_char { my_char() { /* nothing */ } char c; };` – Barry Apr 13 '18 at 14:32
  • Forgive me for being thick on a Friday afternoon, but where is the lie in that fragment? – rubenvb Apr 13 '18 at 14:36
  • @rubenvb in the sense that `my_char`'s constructor does not actually initialise a `my_char` into a defined state. – Richard Hodges Apr 13 '18 at 14:52
  • @Richard That is not lying. That is default initialization for fundamental types and aggregates. I bumped into this a while ago: https://stackoverflow.com/q/17923683/256138 – rubenvb Apr 13 '18 at 14:53
  • 4
    @rubenvb: I suppose the lie is just that a constructor like `my_char()` usually makes you believe that it takes care of correct initialisation of all data members. – Christian Hackl Apr 13 '18 at 14:54
  • 1
    @rubenvb I didn't say it was, Barry did. Surely you understand why he refers to it as such? We don't normally enjoy having objects constructed in an undefined state. – Richard Hodges Apr 13 '18 at 14:55
  • Yet every time you type `char c;` you get exactly the same undefined state initialization... Granted, it's surprising, but it's part of the pay for what you use thing. – rubenvb Apr 13 '18 at 14:55
  • @rubenvb: Yes, because `char` is a fundamental type. When I type `std::string s;` or `std::vector v;`, I (correctly) expect to get a defined state. – Christian Hackl Apr 13 '18 at 14:56
  • Well, then you just messed up `my_char::my_char` IMO. – rubenvb Apr 13 '18 at 15:04
  • 4
    @rubenvb • it's not messed up, it's an intentional lie so as not to pay the initialization performance hit for when it is not desired. – Eljay Apr 13 '18 at 15:11