提问人:Dimitar Asenov 提问时间:4/8/2022 最后编辑:Dimitar Asenov 更新时间:4/8/2022 访问量:322
为什么 std::any 的实现使用函数指针 + 函数操作码,而不是指向虚拟表 + 虚拟调用的指针?
Why does the implementation of std::any use a function pointer + function op codes, instead of a pointer to a virtual table + virtual calls?
问:
GCC 和 LLVM 实现都将函数指针存储在对象中,并使用 / 参数调用该函数以执行不同的操作。以下是 LLVM 中该函数的示例:std::any
any
Op
Action
static void* __handle(_Action __act, any const * __this,
any * __other, type_info const * __info,
void const* __fallback_info)
{
switch (__act)
{
case _Action::_Destroy:
__destroy(const_cast<any &>(*__this));
return nullptr;
case _Action::_Copy:
__copy(*__this, *__other);
return nullptr;
case _Action::_Move:
__move(const_cast<any &>(*__this), *__other);
return nullptr;
case _Action::_Get:
return __get(const_cast<any &>(*__this), __info, __fallback_info);
case _Action::_TypeInfo:
return __type_info();
}
__libcpp_unreachable();
}
注意:这只是一个函数,但每个实现中都有两个这样的函数:一个用于在堆中分配的小对象(小缓冲区优化),另一个用于在堆上分配的大对象。使用哪一个取决于存储在对象中的函数指针的值。__handle
any
any
any
在运行时选择两种实现之一并从预定义的方法列表中调用特定方法的能力实质上是虚拟表的手动实现。我想知道为什么它以这种方式实现。简单地存储指向虚拟类型的指针不是更容易吗?
我找不到有关此实现原因的任何信息。仔细想想,我想使用虚拟类在两个方面是次优的:
- 它需要一个对象实例并管理一个单例,而实际上一个 vtable(没有实例)就足够了。
- 调用函数将涉及两个间接:首先通过存储在 中的指针获取 vtable,然后通过存储在 vtable 中的指针。我不确定其性能是否与上述基于 -的方法有任何不同。
any
any
switch
这些是使用基于 -ing 操作码的实现的原因吗?当前实现还有其他主要优势吗?您知道有关此技术的一般信息的链接吗?switch
答:
考虑一个典型的用例:你在代码中传递它,移动它几十次,把它存储在一个数据结构中,然后再取它。特别是,您可能会经常从函数中返回它。std::any
就像现在一样,指向单个“执行所有操作”函数的指针存储在 中的数据旁边。鉴于它是一个相当小的类型(GCC x86-64 上为 16 个字节),适合一对寄存器。现在,如果你从一个函数返回一个,指向 的“做所有事情”函数的指针已经在寄存器或堆栈中!您可以直接跳转到它,而无需从内存中获取任何内容。最有可能的是,你甚至根本不需要接触内存:你知道在你构造它的时候是什么类型,所以函数指针值只是一个加载到相应寄存器中的常量。稍后,您将该寄存器的值用作跳转目标。这意味着没有机会对跳跃进行错误预测,因为没有什么可预测的,值就在那里供 CPU 使用。any
any
any
any
any
换句话说:通过此实现免费获得跳转目标的原因是 CPU 必须已经以某种方式接触了它才能首先获得它,这意味着它已经知道跳转目标并且可以跳转到它而不会出现额外的延迟。any
这意味着,如果 已经是“热”的,那么当前的实现就真的没有间接可言了,大多数时候都是这样,尤其是当它被用作返回值时。any
另一方面,如果您在只读部分的某处使用函数指针表(并让实例指向该表),则每次想要移动或访问它时都必须转到内存(或缓存)。在这种情况下,an 的大小仍然是 16 个字节,但从内存中获取值比访问寄存器中的值要慢得多,尤其是当它不在缓存中时。在很多情况下,移动一个就像将其 16 个字节从一个位置复制到另一个位置一样简单,然后将原始实例清零。这在任何现代 CPU 上几乎都是免费的。但是,如果采用指针表路由,则每次都必须从内存中提取,等待读取完成,然后进行间接调用。现在考虑一下,您经常需要对 进行一系列调用(即移动,然后销毁),这将很快累积起来。问题在于,您不仅在每次触摸时免费获得要跳转到的函数的地址,CPU还必须显式获取它。间接跳转到从内存中读取的值非常昂贵,因为 CPU 只能在整个内存操作完成后停用跳转操作。这不仅包括获取值(由于缓存的原因,这可能非常快),还包括地址生成、存储转发缓冲区查找、TLB 查找、访问验证,甚至可能包括页表遍历。因此,即使快速计算了跳转地址,跳转也不会在很长一段时间内停用。通常,“间接跳转到内存地址”操作是 CPU 管道可能发生的最糟糕的事情之一。any
any
any
any
any
TL的;DR:就像现在一样,返回 an 不会停止 CPU 的流水线(跳转目标已经在寄存器中可用,因此跳转几乎可以立即停用)。对于基于表的解决方案,返回 an 将使管道停止两次:一次用于获取移动函数的地址,另一次用于获取析构函数。这大大延迟了跳转的停用,因为它不仅要等待内存值,还要等待 TLB 和访问权限检查。any
any
另一方面,代码存储器访问不受此影响,因为代码无论如何都以微码形式保存(在 μOp 缓存中)。因此,在该 switch 语句中获取和执行一些条件分支非常快(当分支预测器正确处理时更是如此,它几乎总是这样做)。
评论
any
any
any
any
any
any
any
any
any
handler
deleter
deleter
any
any
any
评论
virtual
switch
virtual
switch
any