我们最近发现有些代码正在new T[1]系统地使用(正确匹配delete[]),我想知道这是否无害,或者所生成的代码在空间或时间/性能方面存在一些缺点。当然,这被隐藏在函数和宏的层后面,但这不重要。
从逻辑上讲,在我看来两者都是相似的,但是对吗?
是否允许编译器将此代码(使用文字1,而不是变量,而是通过函数层,1将其转换为实参变量,然后使用new T[n])转换为参数2或3次,将其转换为标量new T?
关于这两者之间的区别还有其他需要考虑的事情吗?
gez*_*eza 26
如果T没有琐碎的析构函数,则对于常规的编译器实现,new T[1]与相比会产生开销new T。数组版本将分配一点更大的内存区域来存储元素的数量,因此在处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];
}
Run Code Online (Sandbox Code Playgroud)
在我的机器上,它打印:
single: 4
array: 4
single: 4
array: 12
Run Code Online (Sandbox Code Playgroud)
由于B具有非平凡的析构函数,因此编译器会为数组版本分配额外的8个字节来存储元素数(因为它是64位编译,因此需要8个额外的字节)。与A琐碎的析构函数一样,的数组版本A不需要此额外的空间。
注意:正如Deduplicator所说,如果析构函数是虚拟的,则使用数组版本会有一点性能优势:at delete[],编译器不必虚拟调用析构函数,因为它知道类型为T。这是一个简单的例子来证明这一点:
struct Foo {
virtual ~Foo() { }
};
void fn_single(Foo *f) {
delete f;
}
void fn_array(Foo *f) {
delete[] f;
}
Run Code Online (Sandbox Code Playgroud)
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)。用户定义的替换可以检测到哪个被调用,因此,按条件规则不允许此替换。
注意:如果编译器可以检测到尚未替换这些功能,则可以进行更改。但是源代码中没有任何内容表明编译器提供的功能已被替换。替换通常在链接时完成,只需链接替换版本(隐藏库提供的版本)即可;对于编译器来说,现在为时已晚。
规则很简单:delete[]必须匹配new[]且delete必须匹配new:使用任何其他组合的行为是不确定的。
由于使用了as-if规则,确实允许编译器new T[1]变成简单的new T(并进行delete[]适当的处理)。我还没有遇到可以做到这一点的编译器。
如果您对性能有任何保留,请对其进行概要分析。
| 归档时间: |
|
| 查看次数: |
1432 次 |
| 最近记录: |