Dim*_*nov 10 c++ stl std c++17
GCC和LLVM实现都在对象std::any中存储函数指针any,并使用Op/Action参数调用该函数来执行不同的操作。以下是 LLVM 中该函数的示例:
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();
}
Run Code Online (Sandbox Code Playgroud)
注意:这只是一个__handle函数,但每个实现中有两个这样的函数any:一个用于在内部分配的小对象(小缓冲区优化)any,一个用于在堆上分配的大对象。使用哪一个取决于对象中存储的函数指针的值any。
在运行时选择两种实现之一并从预定义的方法列表中调用特定方法的能力本质上是虚拟表的手动实现。我想知道为什么要这样实施。简单地存储指向虚拟类型的指针不是更容易吗?
我找不到任何有关此实施原因的信息。考虑一下,我想使用虚拟类在两个方面并不是最优的:
any会涉及两个间接间接操作:首先通过存储在 vtable 中的指针any来获取 vtable,然后通过存储在 vtable 中的指针。我不确定它的性能是否与switch上面基于 - 的方法有什么不同。这些是使用基于switch-ing 操作码的实现的原因吗?目前的实施还有其他主要优点吗?您知道有关该技术的一般信息的链接吗?
考虑 a 的典型用例std::any:您在代码中传递它,移动它数十次,将其存储在数据结构中并稍后再次获取它。特别是,您可能会经常从函数中返回它。
就像现在一样,指向单个“执行所有操作”函数的指针存储在any. 鉴于它是一个相当小的类型(GCC x86-64 上为 16 字节),any适合一对寄存器。现在,如果您any从函数返回 an ,则指向该“执行所有操作”函数的指针any已经在寄存器或堆栈中!您可以直接跳转到它,而无需从内存中获取任何内容。最有可能的是,您甚至根本不需要接触内存:您知道any构造它时的类型,因此函数指针值只是加载到适当寄存器中的常量。稍后,您使用该寄存器的值作为跳转目标。这意味着不会出现错误预测跳转的情况,因为没有什么可预测的,该值就在那里供 CPU 消耗。
换句话说:通过此实现免费获得跳转目标的原因是CPU必须已经以any某种方式接触过才能首先获得它,这意味着它已经知道跳转目标并且可以跳转到它没有额外的延迟。
这意味着如果当前的实现已经是“热”的,那么实际上就没有间接可言,any大多数情况下都是“热”,特别是如果它被用作返回值的话。
另一方面,如果您在只读部分中的某处使用函数指针表(并让实例any指向该部分),则每次想要移动时都必须进入内存(或缓存)或访问它。在这种情况下,an 的大小any仍然是 16 字节,但是从内存中获取值比访问寄存器中的值要慢得多,特别是如果它不在缓存中的话。在很多情况下,移动 anany就像将其 16 字节从一个位置复制到另一个位置一样简单,然后将原始实例清零。这在任何现代 CPU 上几乎都是免费的。但是,如果您采用指针表路线,则每次都必须从内存中获取,等待读取完成,然后进行间接调用。现在考虑一下,您经常需要对any(即移动,然后销毁) 执行一系列调用,这很快就会增加。问题是,每次触摸 时,您不仅仅获得要免费跳转到的函数的地址any,CPU 还必须显式获取它。间接跳转到从内存读取的值是相当昂贵的,因为 CPU 只有在整个内存操作完成后才能退出跳转操作。这不仅包括获取值(由于缓存,可能会非常快),还包括地址生成、存储转发缓冲区查找、TLB 查找、访问验证,甚至可能还包括页表遍历。因此,即使跳转地址计算得很快,跳转也不会在很长一段时间内退出。一般来说,“从内存间接跳转到地址”操作是 CPU 管道可能发生的最糟糕的事情之一。
TL;DR:就像现在一样,返回 anany不会停止 CPU 的管道(跳转目标已经在寄存器中可用,因此跳转可以立即退出)。对于基于表的解决方案,返回 anany将使管道停顿两次:一次获取移动函数的地址,然后另一次获取析构函数。这会大大延迟跳转的退出,因为它不仅必须等待内存值,还必须等待 TLB 和访问权限检查。
另一方面,代码内存访问不受此影响,因为代码无论如何都以微码形式保存(在 \xc2\xb5Op 缓存中)。因此,在该 switch 语句中获取并执行一些条件分支非常快(当分支预测器正确时更是如此,它几乎总是如此)。
\n| 归档时间: |
|
| 查看次数: |
343 次 |
| 最近记录: |