【问题标题】:Why would a C++ compiler not eliminate null check of pointer returned by new?为什么 C++ 编译器不会消除对 new 返回的指针的空检查?
【发布时间】:2012-01-03 00:59:00
【问题描述】:

最近我在 ideone.com (gcc-4.3.4) 上运行了以下代码

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <new>

using namespace std;

void* operator new( size_t size ) throw(std::bad_alloc)
{
     void* ptr = malloc( 2 * 1024 * 1024 * 1024);
     printf( "%p\n", ptr );
     return ptr;
}

void operator delete( void* ptr )
{
    free( ptr );
}

int main()
{
    char* ptr = new char;
    if( ptr == 0 ) {
        printf( "unreachable\n" );
    }
    delete ptr;
}

得到这个输出:

(nil)
unreachable

虽然new 不应该返回一个空指针,因此调用者可以依靠它,编译器可以消除ptr == 0 检查并将依赖代码视为不可访问。

为什么编译器不删除该代码?只是错过了优化还是有其他原因?

【问题讨论】:

  • new 永远不应该返回空指针”是什么意思? 写了operator new()!显然 your 版本确实很高兴返回一个空指针。如果你不尊重标准的规则,任何事情都可能发生。
  • 是的,这个问题毫无意义。您要求编译器执行实际上会导致错误代码的优化。保留new的默认实现,然后查看汇编输出,看看编译器是否消除了死代码。
  • @Kerren SB:我的意思是编译器应该相信标准并假设new 在任何情况下都不应该返回 null 并优化检查,如果我的代码因为我的替换返回 null 而中断 -反正这是我的错。
  • @sharptooth:new 表达式不仅仅是一个分配。我认为您无法优化整个命令链。有关详细信息,请参阅我的答案。 (如果你说void * p = ::operator new(1);,那将是一个完全不同的问题。)

标签: c++ compiler-construction new-operator compiler-optimization


【解决方案1】:

我认为这很简单,你有两个根本不同的东西混淆:

  1. ma​​lloc() 可以返回任何值,尤其是零。

  2. 标准要求全局 C++ 分配函数 void * operator new(size_t) throw(std::bad_alloc) 要么返回指向所需存储量的指针(+ 适当对齐),要么通过异常退出。

如果您想替换全局分配功能,您有责任提供一个符合标准规则的替换。最简单的版本如下所示:

void * operator new(size_t n) throw(std::bad_alloc) {
  void * const p = std::malloc(n);
  if (p == NULL) throw std::bad_alloc();
  return p;
}

任何严肃的实现实际上都应该包含一个循环来调用注册的新处理程序,直到分配成功,并且只有在没有更多的新处理程序时才抛出。

您编写的程序只是格式错误。


题外话: 为什么new 是这样定义的?当您说T * p = ::new T(); 时,请考虑标准分配顺序。相当于这样:

void * addr = ::operator new(sizeof(T));  // allocation
T * p = ::new (addr) T();                 // construction

如果第二行抛出(即构造失败),则使用相应的释放函数释放内存。但是,如果第一次调用失败,那么执行绝不能到达第二行!实现这一点的唯一方法是通过异常退出。 (分配函数的无抛出版本仅供手动使用,用户代码可以在继续构造之前检查分配器的结果。)

【讨论】:

  • 好的,我有责任提供一个格式良好的替换,但我没有做,但是为什么编译器不指望我并优化检查呢?
  • @sharptooth:正如我在另一条评论中所说,new 表达式可能过于复杂,无法在中途优化这一步骤。此外,标准并没有明确说明new char; 永远不会为零,只是::operator new(1),具有bad_alloc 异常规范的那个,永远不会返回空指针。如果我想你可以尝试看看 that 是否被优化...
  • 我明白你的意思。行为与显式调用 ::operator new() 相同。
  • 有趣。好吧,编译器不是必需 来优化任何东西:-) 这可能是一个如此晦涩的利基情况,不值得打扰。任何理智的 user 代码都不会包含 null 检查,所以为什么要为坏代码添加优化例程……不过,不妨试试其他一些编译器。
【解决方案2】:

C++11 在这个问题上很清楚:

void* operator new(std::size_t size); : ... 3 要求的行为:返回一个非空指针,指向适当对齐的存储 (3.7.4),否则抛出 bad_alloc 异常。 此要求在此函数的替换版本上具有约束力。

您遇到了未定义的行为。

[编辑] 现在,为什么这会阻碍优化?编译器供应商倾向于花时间对常用的代码模式进行优化。优化更快的未定义行为通常对他们没有什么好处。 (某些 UB 可能在该特定编译器上已明确定义并且仍会进行优化,但上面的示例可能不会)。

【讨论】:

  • 这并不禁止编译器以任何方式执行我所询问的优化。
  • “优化更快的未定义行为通常对他们没有什么好处”——但这不是我们所要求的;在 new 之后检查 null 不是 UB。假设编码标准说总是检查 NULL 指针,无一例外(我有这样的客户端)。对他们来说,优化很有价值。
  • @JimBalter:然后修复客户端。给定int *pp = new intif (p) 的后置条件都是 p 不为空。如果您需要签入第一种情况,您必须在逻辑上也签入第二种情况。 IE。 if (p) { if (p) ...。由于后者显然是多余的,所以前者也是。
  • 客户 = 客户。我无法修复它们。我很清楚你关于逻辑等价的观点......但它与你的论点背道而驰:编译器确实if(p)之后优化p的空测试。
  • @JimBalter:啊,但这实际上是一种普遍现象。当编译器内联一个函数并且调用者和被调用者都具有相同的空指针检查时,就会发生这种情况。这也是消除幂等检查的第二次执行的特例。
【解决方案3】:

我认为您对优化器的期望过高。当优化器到达这段代码时,它认为new char 只是另一个函数调用,其返回值存储在堆栈中。所以它不认为if 条件值得特殊处理。

这可能是因为你覆盖了operator new,并且超出了优化器的薪酬等级,看到你叫malloc,它可以返回NULL,并决定这个覆盖的版本不会返回NULLmalloc 看起来只是另一个函数调用。谁知道?你也可能在你自己的版本中链接。

在 C++ 中还有其他几个重写运算符改变其行为的示例:operator &amp;&amp;operator ||operator ,。这些中的每一个在未被覆盖时都有特殊的行为,但在被覆盖时表现得像标准运算符。例如,如果左侧计算为 falseoperator &amp;&amp;甚至不会计算其右侧。但是,如果被覆盖,operator &amp;&amp;两边 会在 将它们传递给 operator &amp;&amp; 之前计算;短路功能完全消失。 (这样做是为了支持在 C++ 中使用运算符重载来定义迷你语言;有关其中一个示例,请参阅 Boost Spirit 库。)

【讨论】:

    【解决方案4】:

    编译器为什么要这样做?

    对于new 的不透明实现,不可能知道实现是否正确。你的是非标准的,所以你很幸运它确实检查了。

    【讨论】:

      【解决方案5】:

      operator new不止一个;见here。而且您没有声明您的一个可能引发异常。所以编译器不应该推断它永远不会返回一个空指针。

      我不太了解最新的 C++11 标准,但我猜它只是定义的标准operator new(一个抛出异常)应该返回一个非零指针,而不是任何用户定义的。

      在当前的 GCC 主干中,文件libstdc++-v3/libsupc++/new 似乎不包含任何特定属性告诉 GCC 永远不会返回 nil ......即使我认为通过抛出 new 获得 nil 是未定义的行为。

      【讨论】:

      • 好吧,我尝试添加throw(std::bad_alloc) - 行为是一样的。
      • 我很困惑。我认为operator new(size_t) 的标准声明是针对抛出异常的版本,而您必须执行operator new(size_t, std::nothrow) 才能获得返回空值的版本。​​
      • 异常规范并非旨在声明存在异常,而是不存在任何其他类型的异常。即使这样,它也是一个运行时检查......
      • @MikeDeSimone:你是对的。标准形式仅将大小作为参数,任何其他形式都可能存在,并且补充参数必须在调用 new(std::nothrow()) T() 时显式传递以用于不抛出版本。
      • 更令人惊讶的是,大多数编译器(包括 GCC)在优化和内联时并不认为 this 永远不会为零。
      【解决方案6】:

      虽然 new 不应该返回一个空指针

      它不应该在正常操作中。但是,当有人拔掉内存,或者它干脆死掉,或者它就被填满时,会发生什么疯狂的情况呢?

      为什么编译器不删除该代码?难道只是错过 优化还是有其他原因?

      因为新的可能会失败。如果你使用 no-throw 版本,它可以返回 NULL(或 c++11 中的 nulptr)。

      【讨论】:

      • 我怀疑编译器有足够的数据可以看出它不是 nothrow 版本。
      • @sharptooth 您正在重载运算符 new 和 delete,它们不会抛出。
      • @VJo:standard 不会抛出。 覆盖的那些可以抛出,并且抛出通常会导致对std::terminate()的调用。
      • @MikeDeSimone "被覆盖的可以抛出" 你不能“覆盖”operator new,你只能替换或重载它。
      【解决方案7】:

      我检查了使用 g++ -O3 -S 生成的程序集,标准的新 gcc (4.4.5) 没有删除 if(ptr == 0)。 似乎 gcc 也没有编译器标志或函数属性来优化 NULL 检查。

      所以看来gcc目前不支持这种优化。

      【讨论】:

        猜你喜欢
        • 2011-05-24
        • 1970-01-01
        • 1970-01-01
        • 2010-11-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-10-07
        相关资源
        最近更新 更多