为什么在使用-fno-rtti选项时,std::make_shared会执行两次分配操作?

25
#include <memory>
struct foo { };
int main() { std::make_shared<foo>(); }

对于上述代码,g++7clang++5使用-fno-exceptions -Ofast生成的汇编代码:

  • 如果未传递-fno-rtti,则仅包含对operator new的单个调用。

  • 如果传递了-fno-rtti,则包含两个独立的operator new调用。

这可以很容易地在gcc.godbolt.org (clang++5版本)上进行验证:

上面的godbolt链接的屏幕截图,突出显示了operator new的调用

为什么会发生这种情况?为什么禁用RTTI会防止make_shared统一对象控制块的分配?


1
相关内容:https://dev59.com/95jga4cB1Zd3GeqPMpuR - YSC
1
由于您禁用了虚函数,因此库无法使用紧凑结构(元素、引用计数和删除器),因为这需要类型擦除。因此,该库需要单独分配元素+引用计数和删除器。 - David Haim
2
这也是一个很好的例子,可以证明“没有运行时类型信息和异常处理可以产生最快的C++代码”这一观点有些开发人员坚持认为是正确的。下面是一个例子,证明了运行时类型信息实际上可以生成更好的代码。 - David Haim
3
没有禁用任何虚函数;这是 libstdc++ 的 QoI 失败。libc++ 在两种情况下都提供了优化实现。 - dascandy
1
你使用 std::shared_ptr<foo>(new foo()) 会得到3个分配吗?不,只有2个。嗯。 - Yakk - Adam Nevraumont
3个回答

12

为什么禁用 RTTI 会阻止 make_shared 统一对象和控制块分配?

您可以从汇编代码中看到(直接粘贴文本比链接和拍照都更可取),统一版本不仅分配简单的 foo,而是一个 std::_Sp_counted_ptr_inplace,并且该类型具有虚表(通常需要虚析构函数以处理自定义删除器)。

mov QWORD PTR [rax], OFFSET FLAT:
  vtable for
  std::_Sp_counted_ptr_inplace<foo, std::allocator<foo>,
  (__gnu_cxx::_Lock_policy)2>+16

如果禁用RTTI,它就无法生成原地计数指针,因为那需要虚拟化。

请注意,非原地版本仍然引用vtable,但似乎只是直接存储去虚拟化的析构函数地址。


1
“仅粘贴文本比链接和拍照更可取。” - 文本在上面链接的godbolt中可用。在我的意见中,将生成的汇编代码粘贴到OP中是一个不好的主意,因为点击屏幕截图或链接要比浏览大量汇编代码更好。此外,这是否应该被视为QoI(“实现质量”)问题? - Vittorio Romeo
2
是的,代码已经在godbolt上可用了。但是谁知道这个网站还能持续多久呢?而且,我认为这是一个QoI实现,因为实现者非常慷慨地写了第二个版本,完全没有RTTI。我不会责怪他们只是说_shared_ptr无法在没有RTTI的情况下实现。 - Useless
2
如果您使用libc ++,即使禁用了rtti,也只需进行单个分配。 您可以使用-fno-rtti执行除使用dynamic_casttypeid之外的所有操作。 - dascandy
2
不,shared_ptr 没有在删除器(模板)上进行参数化。实例确实有一个删除器,但 make_shared 必须创建自己的删除器来销毁 T 而不是释放它。而且使用 make_shared 时,你知道要销毁的确切类型。需要运行时类型信息的销毁器是荒谬的。 - Yakk - Adam Nevraumont
1
@Oktalist 我想我已经解码了下面的 get_deleter 是用来做什么的。基本上,引用计数块的内存布局类似于 {strong_count, weak_count, ???, deleter}。删除器可能是可变大小的吗?对于 make_shared,他们以某种方式重用删除器对象存储来存储对象;也许在 ??? 中有一个 void(*)(void*) 无状态调用删除器对象。get_deleter 在这里依赖于 typeid 是偶然的,而不是核心。 - Yakk - Adam Nevraumont
显示剩余9条评论

11
自然地,std::shared_ptr 的实现是基于编译器支持 rtti 的假设。但是它也可以在没有 rtti 的情况下实现。请参见无RTTI的shared_ptr?
从这个旧的GCC libstdc++ #42019 bug 中得到启示。我们可以看到Jonathan Wakely添加了一个修复程序,使得这种情况在没有RTTI的情况下成为可能。
在GCC的libstdc++中,std::make_shared使用std::allocated_shared的服务,后者使用了非标准构造函数(如下面所示的代码)。
作为补丁,从753行开始中所示,您可以看到如果启用RTTI,则获取默认删除器只需要使用typeid的服务,否则,它需要单独分配,不依赖于RTTI。 编辑:2017年5月9日:已删除此处先前发布的受版权保护的代码 我还没有调查libcxx,但我想他们做了类似的事情...

这是受版权保护的代码,因此在 SO 上发布它会违反服务条款。该代码可在网上浏览(带有必要的版权标头),因此您可以链接到它。 - Jonathan Wakely
@JonathanWakely。哇!我不知道这一点。谢谢你提供的信息。我已经删除了代码。我以为只要提供相关引用和确认,人们可以自由地发布“开源”许可库中的片段。尤其是GLPv3代码。那么,我的假设是错误的吗? - WhiZTiM
1
@JonathanWakely 我查看了服务条款。它只禁止侵犯版权的内容。你真的认为,为教育目的而摘录一部受版权保护但免费提供的作品,并且以不影响该作品市场的方式进行,就会侵犯其版权吗?如果是这样的话,你可能需要查看一下17 USC 107。 - David Schwartz
TOS表示:“您同意您向网络贡献的所有订阅者内容在创作共用署名相同许可证下永久且不可撤销地授权给Stack Exchange。”也许我错了,但是将其他人受版权保护的代码块粘贴到强制执行其自己条款的网络上似乎是错误的。我的反对是SO的“所有您的内容都属于我们”的政策,而不是任何人出于教育目的使用libstdc++代码。SO是一家企业,它的内容被一些更加不良的网络复制,这些网络只是从SO中吸取内容。 - Jonathan Wakely

6
没有充分的理由。这看起来像是libstdc++中的QoI问题。
使用clang 4.0,libc++没有这个问题。,而libstdc++有
带有RTTI的libstdc++实现依赖于get_deleter
void* __p = _M_refcount._M_get_deleter(typeid(__tag));
                  _M_ptr = static_cast<_Tp*>(__p);
                  __enable_shared_from_this_helper(_M_refcount, _M_ptr, _M_ptr);
_M_ptr = static_cast<_Tp*>(__p);

总的来说,没有运行时类型信息是不可能实现get_deleter的。
在这个实现中,似乎使用了删除器位置和标记来存储T
基本上,带有RTTI的版本使用了get_deleterget_deleter依赖于RTTI。让make_shared在没有RTTI的情况下工作需要重写它,并且他们采取了简单的方法,导致它执行了两次分配。 make_sharedT和引用计数块合并在一起。我想对于变量大小的删除器和变量大小的T,事情变得很麻烦,因此他们重用了删除器的可变大小块来存储T
一个修改后(内部)没有进行RTTI并返回void*get_deleter可能足以做到他们从这个删除器中所需的功能;但可能不行。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接