标量"新 T "与数组"新 T[1]"

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

本文关键字:quot 数组 标量      更新时间:2023-10-16

我们最近发现一些代码系统地使用new T[1](与delete[]正确匹配(,我想知道这是否无害,或者生成的代码中存在一些缺点(在空间或时间/性能方面(。当然,这隐藏在函数和宏层后面,但这不是重点。

从逻辑上讲,在我看来,两者是相似的,但它们是吗?

编译器是否可以将此代码(使用文字 1,不是变量,而是通过函数层,1转换为参数变量 2 或 3 次,然后才能使用这样new T[n]的代码(转换为标量new T

关于这两者之间的区别,还有其他注意事项/事情要知道吗?

如果T没有简单的析构函数,那么对于通常的编译器实现,与new T相比,new T[1]具有开销。数组版本将分配更大的内存区域,以存储元素的数量,因此在delete[],它知道必须调用多少个析构函数。

因此,它有一个开销:

  • 必须分配稍大的内存区域
  • delete[]会慢一点,因为它需要一个循环来调用析构函数,而不是调用一个简单的析构函数(这里,区别在于循环开销(

看看这个程序:

#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];
}

在我的机器上,它打印:

single: 4
array: 4
single: 4
array: 12

由于B具有非平凡析构函数,因此编译器为数组版本分配额外的 8 个字节来存储元素数(因为它是 64 位编译,所以需要 8 个额外的字节来执行此操作(。正如A使用简单的析构函数一样,A的数组版本不需要这个额外的空间。


注意:正如重复数据删除器所评论的那样,如果析构函数是虚拟的,则使用数组版本具有轻微的性能优势:在delete[],编译器不必虚拟调用析构函数,因为它知道该类型是T。这里有一个简单的案例来证明这一点:

struct Foo {
virtual ~Foo() { }
};
void fn_single(Foo *f) {
delete f;
}
void fn_array(Foo *f) {
delete[] f;
}

Clang优化了这种情况,但GCC没有:godbolt。

对于fn_single,clang 发出nullptr检查,然后虚拟调用destructor+operator delete函数。它必须这样做,因为f可以指向具有非空析构函数的派生类型。

对于fn_array,clang 发出一个nullptr检查,然后直接调用operator delete,而不调用析构函数,因为它是空的。在这里,编译器知道f实际上指向一个Foo对象的数组,它不能是派生类型,因此它可以省略对空析构函数的调用。

不,编译器不允许将new T[1]替换为new Toperator newoperator new[](以及相应的删除(是可替换的([basic.stc.dynamic]/2(。用户定义的替换可以检测调用了哪一个,因此 as-if 规则不允许此替换。

注意:如果编译器可以检测到这些函数未被替换,则可以进行该更改。但是源代码中没有任何内容表明编译器提供的函数正在被替换。替换通常在链接时完成,只需链接替换版本(隐藏库提供的版本(;编译器通常为时已晚。

规则很简单:delete[]必须匹配new[]delete必须匹配new:使用任何其他组合的行为是不确定的。

由于as-if规则,编译器确实允许将new T[1]转换为简单的new T(并适当地处理delete[](。不过,我还没有遇到这样做的编译器。

如果您对性能有任何保留,请对其进行分析。