Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scalar `new T` vs array `new T[1]`

We recently discovery that some code was using new T[1] systematically (properly matched to delete[]), and I'm wondering if this is harmless, or there are some downsides in the generated code (in space or time/performance). Of course, this was hidden behind layers of functions and macros, but that's beside the point.

Logically, it appears to me that both are similar, but are they?

Are compilers allowed to turn this code (using a literal 1, not a variable, but via functions layers, that 1 turns into an argument variable 2 or 3 times before reaching the code using thus new T[n]) into a scalar new T?

Any other considerations/things to know about the difference between these two?

like image 278
ddevienne Avatar asked Sep 30 '19 09:09

ddevienne


2 Answers

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.

like image 183
geza Avatar answered Sep 28 '22 05:09

geza


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.

like image 39
Pete Becker Avatar answered Sep 28 '22 07:09

Pete Becker