放置新[]的开销

Overhead of placement new[]

本文关键字:开销      更新时间:2023-10-16

当前的标准草案明确规定放置new[]可能有空间开销:

此开销可能应用于所有数组new表达式,包括那些引用库函数运算符new[](std​::​size_t、void*)和其他位置分配函数。每次调用new时,开销的大小可能会有所不同。

所以他们可能有一些想法,为什么编译器需要这种开销。它是什么?编译器能把这个开销用于任何有用的事情吗?

在我的理解中,要破坏这个数组,唯一的解决方案是在循环中调用析构函数(我说得对吗?),因为没有放置delete[](顺便说一句,我们不应该有放置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[]: %dn", (int)(reinterpret_cast<char*>(fl) - buffer1));
printf("overhead for Foo[]  : %dn", (int)(reinterpret_cast<char*>(foo) - buffer2));
}

GCC和clang根本不使用任何开销。但是,MSVC在Foo的情况下使用8个字节。MSVC将这笔开销用于什么目的?


以下是我提出这个问题的一些背景。

之前有关于这个主题的问题:

  • 新的数组放置是否需要缓冲区中的未指定开销
  • 阵列的新放置可以以可移植的方式使用吗

在我看来,这些问题的寓意是避免使用放置new[],并在循环中使用放置new。但该解决方案不创建数组,而是相邻的元素,这是而不是数组,使用operator[]对它们来说是未定义的行为。这些问题更多地是关于如何避免放置new[],但这个问题更多的是关于"为什么?"。

当前标准草案明确规定。。。

为了澄清,该规则(可能)自标准的第一个版本以来就已经存在(我可以访问的最早版本是C++03,它确实包含该规则,我没有发现需要添加该规则的缺陷报告)。

所以他们可能有一些想法,为什么编译器需要这个开销

我的怀疑是,标准委员会没有考虑任何特定的用例,但添加了规则,以保持现有编译器符合这种行为。

MSVC将此开销用于什么目的?"为什么?">

只有MS编译器团队才能自信地回答这些问题,但我可以提出一些猜测:

调试器可以使用该空间,这样可以显示数组的所有元素。地址消毒剂可以使用它来验证数组是否溢出。也就是说,我相信这两种工具都可以将数据存储在外部结构中。

考虑到开销仅在非平凡析构函数的情况下保留,它可能用于存储迄今为止构造的元素数量,以便编译器可以知道在其中一个构造函数中发生异常时要销毁哪些元素。同样,据我所知,这也可以存储在堆栈上的一个单独的临时对象中。


就其价值而言,安腾C++ABI同意不需要开销:

如果使用的new operator::operator new[](size_t, void*),则不需要cookie。

其中cookie表示数组长度开销。

动态数组分配是特定于实现的。但实现动态数组分配的常见做法之一是在开始之前存储其大小(我的意思是在第一个元素之前存储大小)。这与完全重叠

表示数组分配开销;的结果新表达式将从返回的值中偏移这个量操作员new[]。

"Placement delete"没有多大意义。delete的作用是调用析构函数并释放内存。delete调用所有数组元素的析构函数并释放它。显式调用析构函数在某种意义上是"placement-delete"。

当前的标准草案明确指出placement-new[]可能会有空间开销。。。

是的,我也快累死了。我把它作为一个问题(正确或错误)发布在GitHub上,请参阅:

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

所以他们可能有一些想法,为什么编译器需要这种开销。它是什么?编译器能把这个开销用于任何有用的事情吗?

据我所见,没有。

在我的理解中,要破坏这个数组,唯一的解决方案是在循环中调用析构函数(我说得对吗。因此编译器不必知道数组的长度。

对于你在那里说的第一部分,绝对。但我们不需要放置delete [](我们可以在循环中调用析构函数,因为我们知道有多少元素)。

我认为由于这个开销不能用于任何有用的事情,编译器不会使用它(所以这在实践中不是问题)。我已经检查了使用以下简单代码的编译器:

GCC和clang根本不使用任何开销。但是,MSVC在Foo情况下使用了8个字节。MSVC将这笔开销用于什么目的?

这太令人沮丧了。我真的认为所有编译器都不会这么做,因为这毫无意义。它只由delete []使用,无论如何都不能与放置new一起使用,所以…

总之,放置new [ ]目的应该是让编译器知道数组中有多少元素,这样它就知道要调用多少构造函数。这就是它应该做的一切。

(编辑:添加更多细节)

但是这个解决方案不创建数组,但是相邻的元素不是数组,使用运算符[]对它们来说是未定义的行为。

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

[basic.life]
类型T的对象的生存期开始于:
(1.1)-获得具有适当对齐和大小的类型T的存储,

数组的初始化包括其元素的初始化。(重要提示:标准可能不直接支持此语句。如果确实不支持,则这是标准中的一个缺陷,导致未定义new[]以外的可变长度数组的创建。特别是,用户无法编写自己的std::vector替代品。我不认为这是标准的意图)。

因此,每当对于类型为TN对象的阵列存在适当大小和对齐的char阵列时,就满足第一个条件。

为了满足第二个条件,需要初始化类型为TN单个对象。这种初始化可以通过一次将原始char阵列地址增加sizeof(T),并在得到的指针上调用放置new来实现。