为什么 C++编译器不会消除 new 返回的指针的空检查吗?
最近,我在 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
检查并将依赖代码视为不可访问。
为什么编译器不消除该代码?这只是一个错过的优化还是有其他原因?
Recently I ran the following code on 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;
}
and got this output:
(nil)
unreachable
although new
should never return a null pointer and so the caller can count on that and the compiler could have eliminated the ptr == 0
check and treat dependent code as unreachable.
Why would the compiler not eliminate that code? Is it just a missed optimization or is there some other reason for that?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(8)
我认为这非常简单,您混淆了两个根本不同的事情:
malloc() 可以返回任何内容,特别是零。
如果您想替换全局分配函数,您有责任提供遵守标准规则的替换函数。最简单的版本如下所示:
任何严肃的实现实际上都应该包含一个循环来调用注册的新处理程序,直到分配成功,并且只有在没有更多新处理程序时才抛出异常。
您编写的程序根本就是格式不正确的。
题外话:为什么这个
new
要这样定义?当您说T * p = ::new T();
时,请考虑标准分配顺序。等价于:如果第二行抛出异常(即构造失败),则使用相应的释放函数释放内存。但是,如果第一次调用失败,则执行绝不能到达第二行!实现此目的的唯一方法是通过异常退出。 (分配函数的无抛出版本仅用于手动使用,其中用户代码可以在继续构造之前检查分配器的结果。)
I think this is very simple and you have two fundamentally different things confused:
malloc() can return anything, in particular zero.
the global C++ allocation function
void * operator new(size_t) throw(std::bad_alloc)
is required by the standard to either return a pointer to the required amount of storage (+ suitably aligned), or otherwise exit through an exception.If you want to replace the global allocation function, it is your responsibility to provide a replacement that abides by the rules of the standard. The simplest version looks like this:
Any serious implementation should actually contain a loop to call the registered new-handler until the allocation succeeds, and only throw once there are no more new-handlers.
The program that you wrote is simply ill-formed.
Digression: Why is this
new
defined that way? Consider the standard allocation sequence when you sayT * p = ::new T();
. It is equivalent to this:If the second line throws (i.e. construction fails), the memory is deallocated with the corresponding deallocation function. If the first call fails, though, then the execution must never reach the second line! The only way to achieve this is by exiting through an exception. (The no-throw versions of the allocation functions are only for manual use where the user code can inspect the result of the allocator before proceeding to construction.)
C++11 在这个问题上很明确:
void* operator new(std::size_t size);
: ... 3 所需行为:返回非空指针适当对齐存储 (3.7.4),否则抛出bad_alloc
异常。 此要求对此函数的替换版本具有约束力。您点击了“未定义的行为”。
[编辑]
现在,为什么这会阻碍优化呢?编译器供应商倾向于花时间来优化常用的代码模式。对于更快的未定义行为进行优化通常对他们没有什么好处。 (某些 UB 可能在该特定编译器上定义良好,并且仍然可以优化,但上面的示例可能不会)。
C++11 is clear on the issue:
void* operator new(std::size_t size);
: ... 3 Required behavior: Return a non-null pointer to suitably aligned storage (3.7.4), or else throw abad_alloc
exception. This requirement is binding on a replacement version of this function.You hit Undefined Behavior.
[edit]
Now, why would this impede optimization? Compiler vendors tend to spend their time dreaming up optimizations for code patterns that are commonly used. There's usually little benefit for them to optimize for faster Undefined Behavior. (Some UB may be well-defined on that particular compiler and still be optimized, but the above example likely wouldn't be).
我认为您对优化器的期望太多了。当优化器到达此代码时,它认为 new char 只是另一个函数调用,其返回值存储在堆栈中。因此它不认为
if
条件值得特殊对待。这可能是由于您覆盖了
operator new
这一事实而触发的,并且它超出了优化器的支付等级,看看您调用的malloc
,它可以返回NULL
,并确定此覆盖版本不会返回NULL
。malloc
看起来就像只是另一个函数调用。谁知道?您也可能会链接到您自己的版本。在 C++ 中,还有一些重写运算符改变其行为的其他示例:
operator &&
、operator ||
和operator ,
。其中每一个在不被重写时都有特殊的行为,但在被重写时表现得像标准运算符。例如,如果左侧计算结果为“假”,运算符 &&
将根本不会计算其右侧。但是,如果被覆盖,运算符 && 的两边都会在将它们传递给运算符 &&之前进行计算。 /代码>;短路功能完全消失。 (这样做是为了支持使用运算符重载来定义 C++ 中的迷你语言;有关示例,请参阅 Boost Spirit 库。)
I think you're expecting way too much of the optimizer. By the time the optimizer gets to this code, it considers
new char
to be just another function call whose return value is stored on the stack. So it doesn't see theif
condition as deserving special treatment.This is probably triggered by the fact that you overrode
operator new
, and it's beyond the optimizer's pay grade to look in there, see you calledmalloc
, which can returnNULL
, and decide that this overridden version won't returnNULL
.malloc
looks like Just Another Function Call. Who knows? You might be linking in your own version of that, too.There are a couple other examples of overridden operators changing their behavior in C++:
operator &&
,operator ||
, andoperator ,
. Each of these has a special behavior when not overridden, but behave like standard operators when overridden. For example,operator &&
will not even compute its right hand side at all if the left hand side evaluates asfalse
. However, if overridden, both sides of theoperator &&
are computed before passing them tooperator &&
; the short-circuit feature goes away completely. (This is done to support using operator overloading to define mini-languages in C++; for one example of this, see the Boost Spirit library.)编译器为什么要这样做?
对于
new
的不透明实现,不可能知道该实现是否正确。你的是非标准的,所以你很幸运它毕竟进行了检查。Why should the compiler do so ?
With an opaque implementation of
new
it's impossible to know whether the implementation is correct or not. Yours is non-standard, so you are lucky that it did check after all.存在多个
operator new
;请参阅此处。而且您没有声明您有可能抛出异常。所以编译器不应该推断它永远不会返回空指针。我不太了解最新的 C++11 标准,但我猜想只有标准定义的
operator new
(抛出异常的那个)应该返回一个非零指针,而不是任何用户定义的。在当前的 GCC 主干中,文件 libstdc++-v3/libsupc++/new 似乎不包含任何特定属性告诉 GCC nil 永远不会返回......即使我相信这是未定义的行为扔新的就为零。
There are more than one
operator new
; See here. And you did not declare your one as possiblity throwing an exception. So the compiler should not infer it does never return a null pointer.I don't know very well the latest C++11 standard, but I guess that it is only the standard defined
operator new
(the one throwing exception) which is supposed to return a non-nil pointer, not any user defined ones.And in the current GCC trunk, file
libstdc++-v3/libsupc++/new
don't seem to contain any specific attribute telling GCC that nil is never returned... even if I believe it is undefined behavior to get nil with a throwing new.Clang 做了您期望的优化:
Clang does the optimization you expected:
在正常操作中不应该返回空指针。但是,如果有人拔掉内存,或者内存直接死掉,或者内存已满,那么疯狂的情况又如何呢?
因为新的可能会失败。如果使用无抛出版本,它可以返回 NULL(或 c++11 中的 nulptr)。
It shouldn't in normal operation. But how about a crazy situation when someone plugs out the memory, or it simply died, or it just gets full?
Because new can fail. If you use no-throw version, it can return NULL (or nulptr in c++11).
我检查了使用 g++ -O3 -S 生成的程序集和标准的新 gcc (4.4.5) 不会删除
if(ptr == 0)
。看来 gcc 也没有编译器标志或函数属性来优化 NULL 检查。
看来gcc目前不支持这种优化。
I checked the assembly produced with g++ -O3 -S and the standard new, gcc (4.4.5) does not remove the
if(ptr == 0)
.It seems that gcc does not have compiler flags or function attributes to optimize NULL checks either.
So it appears that gcc does not support this kind of optimization at the current time.