标量“新T”与数组“新T [1]”

dde*_*nne 38 c++ new-operator

我们最近发现有些代码正在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对象数组,它不能是派生类型,因此可以省略对空析构函数的调用。

  • 一个有趣的消息是,编译器始终知道数组删除的最派生类型,因此不会虚拟地调用 dtor。但并不能完全平衡额外的簿记。 (2认同)

Pet*_*ker 9

不可以,不允许编译器替换new T[1]new Toperator newoperator new[](以及相应的删除)是可替换的([basic.stc.dynamic] / 2)。用户定义的替换可以检测到哪个被调用,因此,按条件规则不允许此替换。

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


Bat*_*eba 5

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

由于使用了as-if规则,确实允许编译器new T[1]变成简单的new T(并进行delete[]适当的处理)。我还没有遇到可以做到这一点的编译器。

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

  • 问题显然不是关于无与伦比的标量/数组new / delete (4认同)
  • 在极少数情况下,编译器可以将“ new T [1]”转换为简单的“ new T”-它必须能够证明它具有所有相应`delete []表达式的可见性。证明这是一个非常棘手的问题,即是否从不同的编译单元中的函数之间传递了从新表达式中获得的指针。依靠编译器进行大量分析以获得小的收益的优化通常不会实现。除开发人员过早优化外。 (3认同)