我们最近发现一些代码系统地使用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 T
。operator new
和operator new[]
(以及相应的删除(是可替换的([basic.stc.dynamic]/2(。用户定义的替换可以检测调用了哪一个,因此 as-if 规则不允许此替换。
注意:如果编译器可以检测到这些函数未被替换,则可以进行该更改。但是源代码中没有任何内容表明编译器提供的函数正在被替换。替换通常在链接时完成,只需链接替换版本(隐藏库提供的版本(;编译器通常为时已晚。
规则很简单:delete[]
必须匹配new[]
,delete
必须匹配new
:使用任何其他组合的行为是不确定的。
由于as-if规则,编译器确实允许将new T[1]
转换为简单的new T
(并适当地处理delete[]
(。不过,我还没有遇到这样做的编译器。
如果您对性能有任何保留,请对其进行分析。