不久前,我在编译器类中学习了闭包转换,并且想知道调用闭包与调用函数相比会产生多少性能开销。
考虑一个函数返回一个带有零个参数的闭包(因此没有变量捕获),唯一的区别是调用所述闭包将跳转到绝对地址,而不是调用被翻译为的函数的相对地址一个标签。
与相对跳转相比,绝对跳转是否有任何性能开销?例如,在 ARM64 处理器中,
bx Rm
比 b label
慢吗?
假设两种情况下跳转到的位置都已被缓存。
我们可以查看函数指针、lambda、闭包来了解闭包的一些定义。我们可以看到闭包允许访问调用者中的变量。在 pascal 中,您可以按引用或按值传递。 C++ 中的“&references”也是如此。这是针对单个值的。使用闭包,调用者的整个范围都是可访问的,但它是由实现定义的(需要哪些外部引用)。
我们可以以 C++ 中的“vtable”为例,它负责实现基类的继承重写。 “vtable”是从 this 指针引用的函数指针表。派生对象将具有一个 vtable,其条目与基类不同。
您提供的示例汇编程序是
bx Rm
和 b label
。这确实更像是C++虚方法的区别。虚拟方法通常使用 bx Rm
来调度,而不是直接调用。速度减慢是最小的,您可以找到各种对“C++ 虚拟函数开销”的引用。
想知道调用闭包与调用函数相比有多少性能开销?
如果闭包可以捆绑,只需要作为数据指针传递即可。所以对变量的访问就像
ldr Rn, [Rclosure, #variable]
。然后可以像按值传递一样使用“Rn”。通常,函数需要堆栈帧,特别是在寄存器压力很大的情况下。我想象生成汇编程序的语言会在调用者中安排函数“序言”,并且它将设置一个堆栈帧和/或一个允许访问调用者变量的变量“vtable”。就像“C++ 虚函数开销”一样,闭包引用应该处于相同的顺序(即非常非常小)。
当例程完成时,当然还有一个额外的存储来更新调用者的值。
stm Rclosure, {reglist}
,在某些情况下可能是可能的和/或编译器可以进行值分析/SSA 并决定在任何更新“完成”时存储值。因此,它很可能是每个使用的闭包变量的加载/存储周期。
OP有一个观点,
bx Rm
可能相关。 “thunk”可以是通过闭包代码将闭包数据编组为公共布局。然后它会在返回时写回数据。
这在一定程度上取决于您对调用者和被调用者/thunk 付出的努力。如果调用者可以强制执行某些标准布局,那么您可以直接在闭包中使用该数据。这在实践中可能几乎是不可能的,因此可能会使用“thunk”来整理数据。
这里的开销是2N*加载/存储+虚拟调度。我们需要将来自调用者的数据编组到序言中的闭包布局。然后,thunk 中的尾声将需要将数据封送回来。每个编组操作都需要“n”(引用变量)来加载/存储。
另一种替代方法是通过数据指针访问所有变量。在这里,数据只需要放置在一个公用表中即可访问“n”个变量。然后由被调用者将值缓存在寄存器中和/或根据需要写回。
例如。类似“C”的表示法。
enum {char *, short *, int *} references;
closure(references data[n], arg1, arg2, ...);
Thunk 的优点是可以动态创建。即,调用者不需要有关闭包实现的信息,它们可以在链接时生成。
这将是全局编译(lto)或模块构建然后链接(传统编译)之间的设计选择。最有可能使用数据指针方法,因为它最大限度地减少了耦合,并且只需要修改一点对象信息即可实现 thunk 生成。您需要额外的基础设施来实现类型安全,这可以通过 mangling、rtti 或许多其他机制来完成。