考虑一个简单的程序:
int main() {
int* ptr = nullptr;
delete ptr;
}
使用GCC(7.2),在结果程序中有call
指令operator delete
。对于Clang和Intel编译器,没有这样的指令,空指针删除被完全优化(在所有情况下都为-O2
)。你可以在这里测试:https://godbolt.org/g/JmdoJi。
我想知道这样的优化能否以某种方式与GCC一起开启? (我更广泛的动机源于可移动类型的自定义swap
vs std::swap
问题,其中删除空指针可以表示第二种情况下的性能损失;有关详细信息,请参阅https://stackoverflow.com/a/45689282/580083。)
更新
澄清我对这个问题的动机:如果我在{em>移动分配操作符和析构函数中仅使用delete ptr;
而没有if (ptr)
防护某个类,然后std::swap
与该类的对象产生3个call
指令与GCC。这可能是相当大的性能损失,例如,在对这些对象的数组进行排序时。
此外,我可以在任何地方写if (ptr) delete ptr;
,但是想知道,这是否也不会成为性能损失,因为delete
表达式也需要检查ptr
。但是,在这里,我猜,编译器只生成一个单独的检查。
另外,我真的很喜欢在没有后卫的情况下拨打delete
的可能性,这对我来说是一个惊喜,它可以产生不同的(表现)结果。
更新
我只是做了一个简单的基准测试,即排序对象,它们在移动赋值运算符和析构函数中调用delete
。来源位于:https://godbolt.org/g/7zGUvo
在Xeon E2680v3上使用GCC 7.1和std::sort
标记测量的-O2
的运行时间:
链接代码中存在错误,它会比较指针,而不是指向的值。更正结果如下:
if
后卫:if
后卫:if
后卫和自定义swap
:这些结果在许多运行中绝对一致,偏差最小。前两种情况之间的性能差异很大,我不会说这是一些“非常罕见的角落案例”,如代码。
答案 0 :(得分:29)
根据C ++ 14 [expr.delete] / 7:
如果delete-expression的操作数的值不是空指针值,则:
- [...省略...]
否则,未指定是否将调用释放函数。
因此两个编译器都符合标准,因为未指定是否调用<img src="uploads/images/banner.jpg" alt="" draggable="false">
来删除空指针。
请注意,godbolt在线编译器只是编译源文件而不进行链接。因此,该阶段的编译器必须允许operator delete
被其他源文件替换的可能性。
正如在另一个答案中已经推测的那样 - 在替换operator delete
的情况下,gcc可能正在寻求一致的行为;这个实现意味着有人可以为调试目的重载该函数,并在operator delete
表达式的所有调用中断,即使它恰好是删除空指针。
更新:删除了这可能不是一个实际问题的猜测,因为OP提供的基准显示它实际上是。
答案 1 :(得分:7)
标准实际上说明何时应调用分配和释放函数以及何时不调用。本条款(@ n4296)
该库提供全局分配的默认定义 释放功能。一些全局分配和解除分配 功能是可替换的(18.6.1)。 C ++程序应提供at 可替换分配或释放的大多数定义 功能。任何此类函数定义都将替换默认版本 图书馆提供(17.6.4.6)。以下分配和 释放函数(18.6)在全局范围内隐式声明 在程序的每个翻译单元中。
可能是为什么那些函数调用不被任意省略的主要原因。如果是这样的话,替换它们的库实现会导致编译程序的功能不连贯。
在第一个替代(删除对象)中,操作数的值 delete可以是空指针值,指向非数组对象的指针 由前一个new-expression创建,或者是一个指向子对象的指针 (1.8)代表这种对象的基类(第10条)。如果不, 行为未定义。
如果参数在标准中给出了释放函数 library是一个不是空指针值的指针(4.10),. deallocation函数应解除分配引用的存储 指针,渲染无效指向任何部分的所有指针 解除分配存储。间接通过无效指针值和 将无效指针值传递给释放函数有 未定义的行为。任何其他使用无效指针值的方法都有 实现定义的行为。
...
如果delete-expression的操作数值不为null 指针值,然后
如果没有省略要删除的对象的new-expression的分配调用,并且没有扩展分配(5.3.4), delete-expression应调用释放函数(3.7.4.2)。 从new-expression的分配调用返回的值 应作为释放函数的第一个参数传递。
否则,如果扩展分配或通过扩展另一个newexpression的分配来提供分配,并且由具有扩展new-expression提供的存储的new-expression生成的每个其他指针值的delete-expression已经评估过,delete-expression应该调用a 释放功能。从分配调用返回的值 扩展的new-expression应作为第一个参数传递给 释放功能。
- 否则,delete-expression将不会调用释放函数
否则,未指定是否将调用释放函数。
标准说明如果指针为非null,应该怎么做。暗示在这种情况下删除是noop,但是没有指定。
答案 2 :(得分:7)
答案 3 :(得分:5)
让程序使用nullptr调用operator delete
始终是安全的(为了正确)。
对于性能而言,让编译器生成的asm实际执行额外测试并且跳过对operator delete
的调用的条件分支将是一场胜利是非常罕见的。 (您可以帮助gcc优化远离编译时nullptr
删除而不添加运行时检查;但请参阅下文。
首先,真正热点之外的较大代码大小会增加L1I缓存的压力,而x86 CPU上的解码uop缓存会更小(Intel SnB-family,AMD Ryzen)。 / p>
其次,额外的条件分支使用分支预测高速缓存中的条目(BTB =分支目标缓冲区等)。根据CPU的不同,即使是从未采用的分支也可能会使其他分支的预测恶化,如果它在BTB中将它们混淆的话。 (在其他情况下,这样的分支永远不会在BTB中获得条目,以保存分支的条目,其中默认静态预测掉落是准确的。)请参阅https://xania.org/201602/bpu-part-one。
如果nullptr
在给定的代码路径中很少见,那么平均检查&amp;分支以避免call
结束,您的计划在支票上花费的时间比支票节省的时间长。
如果分析显示您有一个包含delete
的热点,并且检测/日志记录显示它通常实际上使用nullptr调用delete
,那么它值得尝试
if (ptr) delete ptr;
而非delete ptr;
分支预测可能在一个呼叫站点上比在operator delete
内的分支更好运,特别是如果与其他附近分支有任何关联。 (显然,现代BPU并不是孤立地看待每个分支。)这是将无条件call
保存到库函数中(加上来自PLT存根的另一个jmp
,来自动态链接Unix / Linux上的开销。)
如果由于任何其他原因检查null,那么将delete
置于代码的非null分支内是有意义的。
如果gcc可以证明(在内联后)指针为空,但没有进行运行时检查(如果不是),则可以避免delete
次调用:
static inline bool
is_compiletime_null(const void *ptr) {
#ifdef __GNUC__
// __builtin_constant_p(ptr) is false even for nullptr,
// but the checking the result of booleanizing works.
return __builtin_constant_p(!ptr) && !ptr;
#else
return false;
#endif
}
它总是会与clang一起返回false,因为它在内联之前评估__builtin_constant_p
。但是因为当它可以证明指针为空时,clang已经跳过delete
次调用,所以你不需要它。
这可能对std::move
个案件有帮助,你可以安全地在任何地方使用它(理论上)没有性能下降。我总是编译为if(true)
或if(false)
,因此它与if(ptr)
非常不同,这可能会导致运行时分支,因为编译器可能无法证明在大多数情况下,指针也是非空的。 (但是,取消引用可能是因为null deref是UB,现代编译器基于代码不包含任何UB的假设而优化。)
你可以把它变成一个宏来避免膨胀非优化版本(因此它会工作&#34;无需先内联)。您可以使用GNU C语句表达式来避免双重评估宏arg(see examples for GNU C min()
and max()
)。对于没有GNU扩展的编译器的后备,你可以编写((ptr), false)
或者其他东西来评估arg一次产生副作用,同时产生false
结果。
示范:asm from gcc6.3 -O3 on the Godbolt compiler explorer
void foo(int *ptr) {
if (!is_compiletime_null(ptr))
delete ptr;
}
# compiles to a tailcall of operator delete
jmp operator delete(void*)
void bar() {
foo(nullptr);
}
# optimizes out the delete
rep ret
它使用MSVC(也在编译器资源管理器链接上)正确编译,但是测试总是返回false,bar()
是:
# MSVC doesn't support GNU C extensions, and doesn't skip nullptr deletes itself
mov edx, 4
xor ecx, ecx
jmp ??3@YAXPEAX_K@Z ; operator delete
有趣的是,MSVC的operator delete
将对象大小视为
函数arg(mov edx, 4
),但gcc / Linux / libstdc ++代码只传递指针。
相关:我发现this blog post,使用C11(而不是C ++ 11)_Generic
尝试在静态初始化器内部执行类似__builtin_constant_p
空指针检查的操作。
答案 4 :(得分:2)
我认为,编译器不了解“删除”,特别是“删除null”是一个NOOP。
您可以明确地编写它,因此编译器不需要暗示有关删除的知识。
警告:我不建议将其作为一般实施方案。下面的例子应该显示,如何在一个非常特殊和有限的程序中“说服”有限的编译器删除代码
int main() {
int* ptr = nullptr;
if (ptr != nullptr) {
delete ptr;
}
}
我记得没错,有办法用自己的功能取代“删除”。如果编译器的优化会出错。
@RichardHodges:当为编译器提供删除呼叫的提示时,为什么要进行去优化?
删除null通常是NOOP(无操作)。但是,由于可以替换或覆盖删除,因此不存在所有情况的保证。
因此,编译器需要知道并决定是否使用删除null总是可以删除的知识。两个选择都有很好的论据
但是,总是允许编译器删除死代码,这个“if(false){...}”或“if(nullptr!= nullptr){...}”
因此,编译器将删除死代码,然后在使用显式检查时,它看起来像
int main() {
int* ptr = nullptr;
// dead code if (ptr != nullptr) {
// delete ptr;
// }
}
请告诉我,哪里有去优化?
我将我的提案称为防御式编码,但不是去优化
如果有人可能会争辩,那么现在非nullptr会导致两次检查nullptr,我必须回复
@Peter Cordes:我同意使用if不是一般优化规则。但是,一般优化不是开启者的问题。问题是为什么有些编译器不会在非常短的非感知程序中删除删除。无论如何,我展示了让编译器消除它的方法。
如果情况发生在那个短程序中,可能还有其他错误。一般来说,我会尽量避免使用new / delete(malloc / free),因为调用相当昂贵。如果可能,我更喜欢使用堆栈(自动)。
当我看一下同时记录的真实情况时,我会说,X类设计错误,导致性能不佳和内存过多。 (https://godbolt.org/g/7zGUvo)
而不是
class X {
int* i_;
public:
...
in will design
class X {
int i;
bool valid;
public:
...
或更早,我会问排序空/无效项目的感觉。最后,我也希望摆脱“有效”。
答案 5 :(得分:2)
首先,我只是同意一些以前的回答者,因为它不是一个错误,GCC可能会在这里随心所欲。也就是说,我想知道这是否意味着一些普通和简单的RAII代码在GCC上可能比Clang慢,因为没有进行简单的优化。
所以我为RAII写了一个小测试用例:
struct A
{
explicit A() : ptr(nullptr) {}
A(A &&from)
: ptr(from.ptr)
{
from.ptr = nullptr;
}
A &operator =(A &&from)
{
if ( &from != this )
{
delete ptr;
ptr = from.ptr;
from.ptr = nullptr;
}
return *this;
}
int *ptr;
};
A a1;
A getA2();
void setA1()
{
a1 = getA2();
}
正如您可能会看到here,GCC 确实在delete
中第二次调用setA1
(对于在getA2
中创建的临时移动致电a1
)。第一次调用对于程序正确性是必要的,因为之前可能已将a1.ptr
或if ( ptr != nullptr )
分配给。{/ p>
显然我更喜欢更多&#34;押韵和理由&#34; - 有时为什么要进行优化但并非总是如此 - 但我还是不愿意在我的RAII代码中进行多余的asp.net-mvc-5
检查。