使编译器在类型擦除中使用 lambda 优化函数间接调用

Make the compiler optimize away function call indirection with lambda in type erasure

本文关键字:优化 函数 调用 lambda 编译器 类型 擦除      更新时间:2024-09-26

我正在使用类型擦除来获取具有成员函数的任何class fullvoid work(char&)带有class erased的类型擦除句柄。

// erase.hxx
#pragma once
#include <memory>
struct erased
{
private:
using fn_t = void(*)(void*, char&);
void* self;
fn_t  fn;
public:
template<typename F>
explicit
erased(F& full) noexcept
: self(std::addressof(full)),
fn([](void* self, char& c) { static_cast<F*>(self)->work(c); })
{}
void work(char& c) { fn(self, c); }
};
// full.hxx
#pragma once
struct full
{
void work(char&);
};
// full.cxx
#include "full.hxx"
#include <cstdio>
// Implemented here to prevent inlining.
void full::work(char&) { puts("Working hard!"); }
// main.cxx
#include "erased.hxx"
#include "full.hxx"
template erased::erased(full&);
int main()
{
char c;
auto x  = full{};
auto ex = erased{x};
ex.work(c);
}

在查看生成的程序集(GCC 10.2.0 和 Clang 11.1.0 在 -O3 处)时,我现在出现了问题:

0000000000001190 <erased::erased<full>(full&)>:
1190:   48 89 37                mov    QWORD PTR [rdi],rsi
1193:   48 8d 05 06 00 00 00    lea    rax,[rip+0x6]        # 11a0 <erased::erased<full>(full&)::{lambda(void*, char&)#1}::__invoke(void*, char&)>
119a:   48 89 47 08             mov    QWORD PTR [rdi+0x8],rax
119e:   c3                      ret    
119f:   90                      nop
00000000000011a0 <erased::erased<full>(full&)::{lambda(void*, char&)#1}::__invoke(void*, char&)>:
11a0:   e9 0b 00 00 00          jmp    11b0 <full::work(char&)>
11a5:   66 2e 0f 1f 84 00 00    cs nop WORD PTR [rax+rax*1+0x0]
11ac:   00 00 00 
11af:   90                      nop

erased::fn指向erased::construct中创建的λ,而这个λ的主体除了立即将控制权交给full::work之外什么都不做。

由于lambda和full::work似乎是二进制兼容的,我希望编译器取消lambda并直接将full::work的地址存储在erased::fn中,从而消除了不必要的间接寻址。

所以我的问题是:

  • 为什么编译器没有这样做?更重要的是,
  • 如何告诉编译器这样做?

编辑

我更改了struct erase的实现,使其无法使用任何不是指向 lambda 中static_cast<F*>中使用的相同类型F的指针来调用erased::fn

即使没有这个,我怀疑假设传递给 lambda 的void* self始终是指向F的指针也是可以的,因为F::work被调用它并且:

通过函数类型

与被调用函数定义的函数类型不同的表达式调用函数会导致未定义的行为。

此外,我通过将函数类型更改为using fn_t = void(*)(void*,char&)使示例更加现实:除了 this 指针之外,它现在还需要一个参数。

这是为了说明,即使在上面的例子中,我要求的优化应该是可能的,但当F::work具有签名void work(char)时,这是不可能的:必须制作c的副本:lambda 的主体将不再仅包含jmp

我更喜欢两种情况都有效并且编译器决定是否可以优化的解决方案。

否则,我知道我可以强制参数类型与此完全匹配:

template<typename M, typename... Args>
struct method_with_args : std::false_type {};
template<typename F, typename R, typename... Args>
struct method_with_args<R(F::*)(Args...), Args...>
: std::true_type{};

(也许我不在这里,但是)"lambda和full::work似乎是二进制兼容的">并不是真的。成员函数有一个隐藏的第一个参数:指向成员对象的指针(此处:F* this)。

因此,指向F::work的函数指针将void(F::*)(void)。这与void(*)(void*)不同,因此不能被替换。

也许这个关于isocpp的常见问题解答会提供一些见解。