【问题标题】:Passing null pointer to placement new将空指针传递给新位置
【发布时间】:2013-07-08 09:50:33
【问题描述】:

默认位置 new 运算符在 18.6 [support.dynamic] ¶1 中声明,具有不引发异常规范:

void* operator new (std::size_t size, void* ptr) noexcept;

这个函数除了return ptr;什么都不做,所以它是noexcept是合理的,但是根据5.3.4 [expr.new]¶15这意味着编译器必须检查它之前没有返回null调用对象的构造函数:

-15-
[注意: 除非使用非抛出异常规范 (15.4) 声明分配函数,否则它会通过抛出 std::bad_alloc 异常来指示分配存储失败(第 15 条,第 18.6.2.1 条);否则它返回一个非空指针。如果分配函数声明为不抛出异常规范,则返回 null 以指示分配存储失败,否则返回非空指针。 —尾注]如果分配函数返回null,则不进行初始化,不调用deallocation函数,new-expression的值为null。

在我看来(特别是对于放置 new,不是一般情况)这个空检查是一个不幸的性能损失,尽管很小。

我一直在调试一些代码,其中放置 new 被用于对性能非常敏感的代码路径中,以改进编译器的代码生成,并在程序集中观察到 null 检查。通过提供一个特定于类的放置 new 重载,该重载使用抛出异常规范(即使它不可能抛出)声明,条件分支被删除,这也允许编译器为周围的内联函数生成更小的代码.说放置new 函数可以抛出,即使它不能,结果是明显更好的代码。

所以我一直想知道对于放置new 案例是否真的需要空检查。它可以返回 null 的唯一方法是如果你将它传递给 null。虽然写是可能的,而且显然是合法的:

void* ptr = nullptr;
Obj* obj = new (ptr) Obj();
assert( obj == nullptr );

我不明白为什么这会有用,我建议如果程序员在使用位置 new 之前必须明确检查 null 会更好,例如

Obj* obj = ptr ? new (ptr) Obj() : nullptr;

有没有人需要放置new 来正确处理空指针情况? (即没有添加明确的检查 ptr 是一个有效的内存位置。)

我想知道禁止将空指针传递给默认位置new 函数是否合理,如果不是,是否有更好的方法来避免不必要的分支,而不是试图告诉编译器值不为空,例如

void* ptr = getAddress();
(void) *(Obj*)ptr;   // inform the optimiser that dereferencing pointer is valid
Obj* obj = new (ptr) Obj();

或者:

void* ptr = getAddress();
if (!ptr)
  __builtin_unreachable();  // same, but not portable
Obj* obj = new (ptr) Obj();

注意这个问题被有意标记为微优化,我建议您为所有类型重载放置 new 以“提高”性能。这种影响在一个非常具体的性能关键案例中被注意到,并且基于分析和测量。

更新:DR 1748 将使用带有新位置的空指针设为未定义行为,因此不再需要编译器进行检查。

【问题讨论】:

  • 啊,对并发编辑感到抱歉。在您使用条件运算符的表达式中,您是否忘记了placement new 的(ptr) 部分?
  • 与在调用构造函数之前让placement new 执行检查相比,在使用placement new 之前检查null 有何改进?这是同一张支票——只是在不同的时刻。要么您在某处进行了空值检查,要么您冒着调用构造函数的风险来获取空值。该标准试图避免后者。
  • @SanderDeDycker 不同之处在于,如果您知道指针不为空,则可以在调用placement new 之前留下支票。由于该标准的理念之一是“不要为不需要的东西买单”,我认为无条件强制检查是标准中的缺陷。
  • @ArneMertz, JonathanWakely :可能有点过分了,但程序员知道指针不为空的唯一方法是,如果代码保证它不能为空(例如::静态分配,计算结果,代码中存在事先检查,...)。在所有这些情况下,编译器也可以确定它不能为空(与程序员一样),并且可能会在构造函数调用之前优化空检查。我没有任何编译器走到这一步的例子,但这样做会将标准的安全角度与您的性能角度结合起来。
  • @SanderDeDycker,可能是因为函数调用 abort() 如果它无法获取内存,但它是在不同的翻译单元中定义的,我没有使用 LTO。编译器如何知道我所知道的一切?是 NSA 写的吗?

标签: c++ micro-optimization placement-new noexcept


【解决方案1】:

除了“有没有人需要放置 new 来正确处理空指针情况?”之外,我看不到很多问题。 (我没有),我认为这个案子很有趣,足以让人们对这个问题产生一些想法。

我认为标准被破坏或不完整 wrt 放置新功能和一般分配功能的要求。

如果您仔细查看引用的 §5.3.4,13,它意味着必须检查 每个 分配函数是否有返回的空指针,即使它不是 noexcept。所以应该改写成

如果分配函数用非抛出异常规范声明并且返回null,则不应进行初始化,不应调用释放函数,并且new-expression的值应为空。

这不会损害分配函数抛出异常的有效性,因为它们必须遵守§3.7.4.1

[...] 如果成功,它将返回存储块的开始地址,其字节长度至少应与请求的大小一样大。 [...]返回的指针应适当对齐,以便可以将其转换为具有基本对齐要求(3.11)的任何完整对象类型的指针,然后用于访问分配的存储中的对象或数组(直到通过调用相应的释放函数显式释放存储)。

以及§5.3.4,14

[ 注意:当分配函数返回一个非空值时,它必须是一个指向已为对象保留空间的存储块的指针。假定存储块已适当对齐并具有请求的大小。 [...] - 尾注]

显然,仅返回给定指针的放置 new 无法合理地检查可用存储大小和对齐方式。因此,

§18.6.1.3,1关于安置新说

[...] (3.7.4) 的规定不适用于 operator new 和 operator delete 的这些保留放置形式。

(我猜他们在那个地方没有提到§5.3.4,14。)

但是,这些段落间接说“如果你将垃圾指针传递给 palcement 函数,你会得到 UB,因为违反了 §5.3.4,14”。因此,由您来检查放置新位置的任何 poitner 的健全性。

本着这种精神,并通过重写 §5.3.4,13,该标准可以将 noexcept 从放置 new 中删除,从而导致对该间接结论的补充:“......如果你传递 null,你也得到 UB”。另一方面,与空指针相比,指针未对齐或指向内存太少的可能性要小得多。

但是,这将消除检查 null 的需要,并且非常符合“不要为不需要的东西付费”的理念。分配函数本身不需要检查,因为 §18.6.1.3,1 明确说明了这一点。

为了总结,可以考虑添加第二个重载

 void* operator new(std::size_t size, void* ptr, const std::nothrow_t&) noexcept;

遗憾的是,向委员会提出此建议不太可能导致更改,因为它会破坏现有代码,依赖于放置 new 可以使用空指针。

【讨论】:

  • 感谢您的深入分析。我会再放一两天,以防其他人有什么要补充的,但我希望我会接受这个。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-12-21
  • 1970-01-01
  • 1970-01-01
  • 2019-10-19
  • 2021-01-03
  • 1970-01-01
  • 2018-12-31
相关资源
最近更新 更多