使编译器在类型擦除中使用 lambda 优化函数间接调用
Make the compiler optimize away function call indirection with lambda in type erasure
我正在使用类型擦除来获取具有成员函数的任何class full
void 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的常见问题解答会提供一些见解。
- 使用仅使用一次的变量调用的复制构造函数.这可能是通过调用move构造函数进行编译器优化的情况吗
- 纯函数,为什么没有优化
- 线性优化目标函数中的绝对值
- 这个C++编译器优化(在自身的实例上调用对象自己的构造函数)的名称是什么,它是如何工作的?
- 何时允许编译器优化复制构造函数
- C++延迟后的优化器调用函数
- 未使用的C++未优化的静态成员函数/变量
- 我应该保留这个函数来查找第 n 个素数还是可以优化?
- 如何使用 g2o 优化多约束函数
- GCC 能否优化具有相同主体的函数的代码大小?
- 对于优化级别为 0 的 std::vector,析构函数被调用两次
- 尾递归函数未被 g++ 优化
- 函数优化传递
- 优化在网格图中查找哈密尔循环的函数?
- C++将 lambda 函数另存为成员变量,而不使用函数指针进行优化
- 使用谷神星优化多维函数
- 创建一个始终返回零但优化器不知道的函数
- 编译器优化-函数没有地址
- 整数到字符串优化函数
- c++:优化函数,没有副作用