使用placement new[]的开销

11
当前草案标准明确说明,placement new[] 可能会有空间开销:

这个开销可能适用于所有数组 new-expressions,包括那些引用库函数 operator new[](std::size_­t, void*) 和其他放置分配函数的表达式。开销量可能因每次新调用而异。

所以他们可能已经考虑到了编译器为什么需要这种开销。这是为什么?编译器能否利用这种开销做出任何有用的事情?
在我看来,要析构这个数组,唯一的解决方案是在循环中调用析构函数(我对此理解正确吗?),因为没有 placement delete[](顺便说一句,我们不应该有 placement delete[] 来正确地析构数组,而不仅仅是它的元素吗?)。所以编译器不必知道数组长度。
我认为由于这种开销无法用于任何有用的事情,编译器不使用它(因此这在实践中不是一个问题)。我已经检查过这个简单代码的编译器:
#include <stdio.h>
#include <new>

struct Foo {
    ~Foo() { }
};

int main() {
    char buffer1[1024];
    char buffer2[1024];

    float *fl = new(buffer1) float[3];
    Foo *foo = new(buffer2) Foo[3];

    printf("overhead for float[]: %d\n", (int)(reinterpret_cast<char*>(fl) - buffer1));
    printf("overhead for Foo[]  : %d\n", (int)(reinterpret_cast<char*>(foo) - buffer2));
}

GCC和clang根本不使用任何开销。但是,MSVC在Foo情况下使用8个字节的开销。MSVC可能出于什么目的使用这种开销?
这是一些背景信息,解释了我提出这个问题的原因。
以前有关于这个主题的问题:
- Array placement-new requires unspecified overhead in the buffer? - Can placement new for arrays be used in a portable way? 就我所看到的,这些问题的道德准则是避免使用放置new[],而是在循环中使用放置new。但是这种解决方案不会创建数组,而是创建相邻的元素,这不是一个数组,对它们使用operator[]是未定义的行为。这些问题更多的是关于如何避免放置new[],但这个问题更多的是关于“为什么”。

1
小心从这个简单的测试中解释“行为差异”。我将你的代码粘贴到godbolt中,发现gcc已经意识到对placement new[]的调用是完全多余的,并将其删除了! https://godbolt.org/g/94Deyp - Richard Hodges
@RichardHodges:嗯,这与这里有什么关联呢? - geza
@RichardHodges: 哦,我明白了。但是我的示例输出与:关闭优化时相同,如果我将new(buffer2) Foo[3];移到一个单独的函数中(并将buffer2作为输入参数),则也相同。所以这不是优化问题。 - geza
2
@RichardHodges:它是一样的,没有额外开销。 - geza
@geza 没错。为什么不呢?请看我的回答。 - Paul Sanders
显示剩余4条评论
4个回答

4
当前的草案明确规定了......
为了澄清,这个规则(可能)自标准的第一个版本就存在了(我可以访问的最早版本是C++03,它包含了这个规则,并且我没有找到有关需要添加该规则的缺陷报告)。
因此,我怀疑标准委员会并没有想到任何特定的用例,而是为了让现有的编译器保持此行为的兼容性而添加了这个规则。
关于MSVC使用这种开销的目的,“为什么”问题只能由MS编译器团队进行肯定回答,但是我可以提出一些猜测:
这个空间可以被调试器使用,以便它可以显示数组的所有元素。它可以被地址消毒剂用来验证数组未被溢出。也就是说,我认为这两个工具都可以将数据存储在外部结构中。
考虑到这种开销仅在存在非平凡析构函数的情况下才被保留,可能是它用于存储到目前为止已构造的元素数量,以便编译器在其中一个构造函数发生异常时知道要销毁哪些元素。同样地,据我所知,这也可以存储在堆栈上的单独临时对象中。
值得一提的是,Itanium C++ ABI认为这种开销是不必要的:
如果使用的是 ::operator new [](size_t,void *),则不需要cookie。
其中cookie指的是数组长度开销。

但是添加了规则,以便使现有的编译器遵守这种行为。遵守什么?另外:空间可以被调试器使用,这将允许它显示数组的所有元素。有趣的想法,尽管可能有点牵强。还有:Itanium C++ ABI认为不需要这种开销:也很有趣,看来他们正在变得聪明起来。更新了我在GitHub上发布的问题。 - Paul Sanders
@Jonathan 哦,是的!好主意,今天早上我抽了太多大麻了。那么MSVC怎么了?也许可以看看这个链接:https://bigthink.com/the-proverbial-skeptic/those-who-do-not-learn-history-doomed-to-repeat-it-really。 - Paul Sanders
他们现在可能无法或不愿意更改他们的ABI以消除具有平凡解构函数类型的开销。 - Jonathan Wakely
@Jonathan,就我所看到的而言,这不是ABI问题(请记住我们正在谈论“放置”new,因此析构函数并不涉及其中)。 - Paul Sanders
1
@PaulSanders Compliant with what? 要符合 C++ 标准。为了澄清:MSVC 先于该标准存在,并且可能具有添加开销到放置数组新的行为,我猜测标准规则的存在是为了允许此行为,以便 MSVC(和其他编译器如果存在这样的行为)可以符合标准而不会破坏向后兼容性。 - eerorika
显示剩余6条评论

1
动态数组分配是与实现相关的。但是实现动态数组分配的常见做法之一是在其开始之前存储其大小(我的意思是在第一个元素之前存储大小)。这与以下完全重叠:
表示数组分配开销;new表达式的结果将从operator new[]返回的值偏移这个量。
“放置删除”没有太多意义。delete所做的就是调用析构函数并释放内存。delete调用所有数组元素上的析构函数并释放它。显式调用析构函数在某种意义上是“放置删除”。

2
我们已经知道delete需要开销。问题是:鉴于没有放置delete,为什么需要*特别使用放置new*? - n. m.
@nm 没错。答案是:它不是。 - Paul Sanders

0
当前的草案标准明确指出,放置 new[] 可能会有空间开销...
是的,这也让我感到困惑。我在 GitHub 上发布了这个问题(无论对错),请参见:

https://github.com/cplusplus/draft/issues/2264

所以他们可能有想法,为什么编译器需要这种开销。这是什么?编译器可以使用这个开销做些有用的事情吗?
就我所知,好像不行。
在我的理解中,销毁这个数组的唯一解决方案是在循环中调用析构函数(我对此正确吗?),因为没有放置删除[](顺便说一下,我们不应该有放置删除[]来正确地销毁数组,而不仅仅是它的元素吗?)。所以编译器不必知道数组长度。
对于你所说的第一部分,绝对是这样。但我们不需要放置delete [](我们只需在循环中调用析构函数,因为我们知道有多少个元素)。
我认为,由于这种开销无法用于任何有用的事情,编译器不会使用它(因此在实践中这不是一个问题)。我已经检查了这个简单代码的编译器:
...
GCC和clang根本不使用任何开销。但是,MSVC对于Foo案例使用8个字节。MSVC可能会出于什么目的使用这个开销呢?

这真是令人沮丧。我真的以为所有编译器都不会这样做,因为没有意义。它只被delete []使用,而你无法在放置new中使用它,所以...

因此,总结一下,放置new [ ]目的应该是让编译器知道数组中有多少个元素,以便它知道要调用多少个构造函数。这就是全部的内容。句号。


-1

(编辑:添加更多细节)

但是这个解决方案并没有创建一个数组,而是创建了相邻的元素,这不是一个数组,对它们使用 operator[] 是未定义的行为。

据我所知,这并不完全正确。

[basic.life]
类型为 T 的对象的生命周期始于:
(1.1) — 获得了适当大小和类型 T 对齐的存储空间,并且
(1.2) — 如果该对象具有非平凡初始化,则其初始化完成

数组的初始化包括其元素的初始化。(重要提示:这个声明可能不被标准直接支持。如果确实不支持,那么这是标准中的一个缺陷,使得除了通过 new[] 创建可变长度数组之外的其他方式都是未定义的。特别是,用户不能编写自己的替代 std::vector。我不认为这是标准的意图。)

因此,只要有一个适当大小和类型 T 对齐的 char 数组用于存放类型为 TN 个对象,第一个条件就得到满足。

为了满足第二个条件,需要初始化T类型的N个独立的对象。这种初始化可以通过每次将原始char数组地址增加sizeof(T)并在结果指针上调用放置new来实现可移植性。

1
数组的初始化包括对其元素的初始化。我认为这并不完全正确。例如,如果你有一个类(对象),这个类有成员(子对象),你需要使用new来创建这个类。仅仅对其子对象进行new操作是不够的。同样地,如果想要正确地创建一个数组,需要使用new来创建数组对象,仅仅对其元素(子对象)进行new操作也是不够的。 - geza
"数组的初始化包括对其元素的初始化。" - Passer By
但不仅限于此。如果只是对象的连接,那么怎么样? - Paul Sanders
2
一个数组是一种特殊类型的对象,它不仅仅是一组相同类型的对象相邻排列。你需要实际创建一个数组,而不只是创建一组相邻的对象。这实际上使得 std::vector 没有定义(参见 https://wg21.link/cwg2182,该文并没有很好地描述问题)。 - Jonathan Wakely
@geza 当元素类型具有空初始化时,数组本身也是如此。因此,数组在其子对象初始化方面没有任何要添加的内容。 - n. m.
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接