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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With