Dra*_*-On 8 c++ gcc language-lawyer type-punning c++20
假设我正在开发一个名为 libModern 的库。该库使用称为 libLegacy 的遗留 C 库作为实现策略。libLegacy 的界面如下所示:
typedef uint32_t LegacyFlags;
struct LegacyFoo {
uint32_t x;
uint32_t y;
LegacyFlags flags;
// more data
};
struct LegacyBar {
LegacyFoo foo;
float a;
// more data
};
void legacy_input(LegacyBar const* s); // Does something with s
void legacy_output(LegacyBar* s); // Stores data in s
Run Code Online (Sandbox Code Playgroud)
出于各种原因,libModern 不应该向用户公开 libLegacy 的类型,其中包括:
处理这种情况的教科书方法是 pimpl 习惯用法:libModern 将提供一个包装类型,该类型内部有一个指向遗留数据的指针。然而,这在这里是不可能的,因为 libModern 无法分配动态内存。一般来说,其目标不是增加大量开销。
因此,libModern 定义了自己的类型,这些类型与遗留类型布局兼容,但具有更好的接口。在此示例中,它使用强标志enum
而不是普通uint32_t
标志:
enum class ModernFlags : std::uint32_t
{
first_flag = 0,
second_flag = 1,
};
struct ModernFoo {
std::uint32_t x;
std::uint32_t y;
ModernFlags flags;
// More data
};
struct ModernBar {
ModernFoo foo;
float a;
// more data
};
Run Code Online (Sandbox Code Playgroud)
现在的问题是:libModern 如何在传统类型和现代类型之间进行转换而不需要太多开销?我知道有3个选择:
reinterpret_cast
。这是未定义的行为,但实际上会产生完美的装配。我想避免这种情况,因为我不能依赖它明天仍然可以工作或依赖另一个编译器。std::memcpy
。在简单的情况下,这会生成相同的最佳装配,但在任何重要的情况下,这都会增加显着的开销。std::bit_cast
. 在我的测试中,它最多生成与memcpy
. 在某些情况下,情况更糟。这是与 libLegacy 接口的 3 种方法的比较:
legacy_input()
reinterpret_cast
:
void input_ub(ModernBar const& s) noexcept {
legacy_input(reinterpret_cast<LegacyBar const*>(&s));
}
Run Code Online (Sandbox Code Playgroud)
集会:
input_ub(ModernBar const&):
jmp legacy_input
Run Code Online (Sandbox Code Playgroud)
这是完美的代码生成器,但它调用了 UB。memcpy
:
input_ub(ModernBar const&):
jmp legacy_input
Run Code Online (Sandbox Code Playgroud)
集会:
input_memcpy(ModernBar const&):
sub rsp, 24
movdqu xmm0, XMMWORD PTR [rdi]
mov rdi, rsp
movaps XMMWORD PTR [rsp], xmm0
call legacy_input
add rsp, 24
ret
Run Code Online (Sandbox Code Playgroud)
明显更糟。bit_cast
:
void input_memcpy(ModernBar const& s) noexcept {
LegacyBar ls;
std::memcpy(&ls, &s, sizeof(ls));
legacy_input(&ls);
}
Run Code Online (Sandbox Code Playgroud)
集会:
input_bit_cast(ModernBar const&):
sub rsp, 40
movdqu xmm0, XMMWORD PTR [rdi]
mov rdi, rsp
movaps XMMWORD PTR [rsp+16], xmm0
mov rax, QWORD PTR [rsp+16]
mov QWORD PTR [rsp], rax
mov rax, QWORD PTR [rsp+24]
mov QWORD PTR [rsp+8], rax
call legacy_input
add rsp, 40
ret
Run Code Online (Sandbox Code Playgroud)
我不知道这里发生了什么。reinterpret_cast
:
input_memcpy(ModernBar const&):
sub rsp, 24
movdqu xmm0, XMMWORD PTR [rdi]
mov rdi, rsp
movaps XMMWORD PTR [rsp], xmm0
call legacy_input
add rsp, 24
ret
Run Code Online (Sandbox Code Playgroud)
集会:
output_ub():
sub rsp, 56
lea rdi, [rsp+16]
call legacy_output
mov rax, QWORD PTR [rsp+16]
mov rdx, QWORD PTR [rsp+24]
add rsp, 56
ret
Run Code Online (Sandbox Code Playgroud)
memcpy
:
void input_bit_cast(ModernBar const& s) noexcept {
LegacyBar ls = std::bit_cast<LegacyBar>(s);
legacy_input(&ls);
}
Run Code Online (Sandbox Code Playgroud)
集会:
output_memcpy():
sub rsp, 56
lea rdi, [rsp+16]
call legacy_output
mov rax, QWORD PTR [rsp+16]
mov rdx, QWORD PTR [rsp+24]
add rsp, 56
ret
Run Code Online (Sandbox Code Playgroud)
bit_cast
:
input_bit_cast(ModernBar const&):
sub rsp, 40
movdqu xmm0, XMMWORD PTR [rdi]
mov rdi, rsp
movaps XMMWORD PTR [rsp+16], xmm0
mov rax, QWORD PTR [rsp+16]
mov QWORD PTR [rsp], rax
mov rax, QWORD PTR [rsp+24]
mov QWORD PTR [rsp+8], rax
call legacy_input
add rsp, 40
ret
Run Code Online (Sandbox Code Playgroud)
集会:
output_bit_cast():
sub rsp, 72
lea rdi, [rsp+16]
call legacy_output
movdqa xmm0, XMMWORD PTR [rsp+16]
movaps XMMWORD PTR [rsp+48], xmm0
mov rax, QWORD PTR [rsp+48]
mov QWORD PTR [rsp+32], rax
mov rax, QWORD PTR [rsp+56]
mov QWORD PTR [rsp+40], rax
mov rax, QWORD PTR [rsp+32]
mov rdx, QWORD PTR [rsp+40]
add rsp, 72
ret
Run Code Online (Sandbox Code Playgroud)
您可以在此处找到编译器资源管理器的完整示例。
我还注意到,代码生成根据结构的确切定义(即成员的顺序、数量和类型)而有很大差异。但 UB 版本的代码始终优于其他两个版本,或者至少与其他两个版本一样好。
现在我的问题是:
在编译器资源管理器链接中,Clang 为所有输出情况生成相同的代码。我不知道GCC在这种情况下遇到什么问题std::bit_cast
。
对于输入情况,三个函数不能生成相同的代码,因为它们具有不同的语义。
使用 时input_ub
,对 的调用legacy_input
可能会修改调用者的对象。其他两个版本不可能出现这种情况。因此,编译器无法优化副本,不知道legacy_input
其行为如何。
如果您按值传递给输入函数,则所有三个版本至少会在编译器资源管理器链接中使用 Clang 生成相同的代码。
要重现原始代码生成的代码,input_ub
您需要不断将调用者对象的地址传递给legacy_input
.
如果legacy_input
是一个extern C
函数,那么我认为标准没有指定两种语言的对象模型应该如何在此调用中交互。因此,出于标记的目的language-lawyer
,我将假设它legacy_input
是一个普通的 C++ 函数。
直接传递地址的问题在于,同一地址处&s
通常不存在可与该对象进行指针相互转换的对象。因此,如果尝试通过指针访问成员,那就是 UB。LegacyBar
ModernBar
legacy_input
LegacyBar
理论上,您可以在所需的地址创建一个 LegacyBar 对象,重用该ModernBar
对象的对象表示形式。ModernBar
但是,由于调用者可能会期望调用后仍然存在一个对象,因此您需要ModernBar
通过相同的过程在存储中重新创建一个对象。
但不幸的是,并不总是允许您以这种方式重用存储。例如,如果传递的引用引用一个const
完整的对象,则为 UB,并且还有其他要求。问题还在于调用者对旧对象的引用是否会引用新对象,这意味着这两个ModernBar
对象是否可以透明地替换。情况也并非总是如此。
因此,总的来说,如果不对传递给函数的引用施加额外的约束,我认为您无法在没有未定义行为的情况下实现相同的代码生成。
归档时间: |
|
查看次数: |
669 次 |
最近记录: |