Scalar `new T` vs array `new T[1]`
If T
doesn't have trivial destructor, then for usual compiler implementations, new T[1]
has an overhead compared to new T
. The array version will allocate a little bit larger memory area, to store the number of elements, so at delete[]
, it knows how many destructors must be called.
So, it has an overhead:
- a little bit larger memory area must be allocated
delete[]
will be a little bit slower, as it needs a loop to call the destructors, instead calling a simple destructor (here, the difference is the loop overhead)
Check out this program:
#include <cstddef>
#include <iostream>
enum Tag { tag };
char buffer[128];
void *operator new(size_t size, Tag) {
std::cout<<"single: "<<size<<"\n";
return buffer;
}
void *operator new[](size_t size, Tag) {
std::cout<<"array: "<<size<<"\n";
return buffer;
}
struct A {
int value;
};
struct B {
int value;
~B() {}
};
int main() {
new(tag) A;
new(tag) A[1];
new(tag) B;
new(tag) B[1];
}
On my machine, it prints:
single: 4
array: 4
single: 4
array: 12
Because B
has a non-trivial destructor, the compiler allocates extra 8 bytes to store the number of elements (because it is 64-bit compilation, it needs 8 extra bytes to do this) for the array version. As A
does trivial destructor, the array version of A
doesn't need this extra space.
Note: as Deduplicator comments, there is a slight performance advantage of using the array version, if the destructor is virtual: at delete[]
, the compiler doesn't have to call the destructor virtually, because it knows that the type is T
. Here's a simple case to demonstrate this:
struct Foo {
virtual ~Foo() { }
};
void fn_single(Foo *f) {
delete f;
}
void fn_array(Foo *f) {
delete[] f;
}
Clang optimizes this case, but GCC doesn't: godbolt.
For fn_single
, clang emits a nullptr
check, then calls the destructor+operator delete
function virtually. It must do this way, as f
can point to a derived type, which has a non-empty destructor.
For fn_array
, clang emits a nullptr
check, and then calls straight to operator delete
, without calling the destructor, as it is empty. Here, the compiler knows that f
actually points to an array of Foo
objects, it cannot be a derived type, hence it can omit the calls to empty destructors.
No, the compiler is not allowed to replace new T[1]
with new T
. operator new
and operator new[]
(and the corresponding deletes) are replaceable ([basic.stc.dynamic]/2). A user-defined replacement could detect which one is called, so the as-if rule doesn't allow this replacement.
Note: if the compiler could detect that these functions had not been replaced, it could make that change. But there's nothing in the source code that indicates that the compiler-supplied functions are being replaced. The replacement is generally done at link time, simply by linking in the replacement versions (which hide the library-supplied version); that's generally too late for the compiler to know about it.
The rule is simple: delete[]
must match new[]
and delete
must match new
: the behaviour on using any other combination is undefined.
The compiler is indeed allow to turn new T[1]
into a simple new T
(and deal with the delete[]
appropriately), due to the as-if rule. I haven't come across a compiler that does this though.
If you have any reservations about performance, then profile it.