2411rust,正与整128
原文
长期以来,Rust
在x86-32
和x86-64
架构上128
位整数的对齐
与C语言
不一致.最近已解决此问题
,但该修复
带来了一些值得注意
的效果
.
作为用户,除非如下,否则不用担心:
1,假设i128/u128
对齐,而不是用align_of
2,忽略improper_ctypes*
检查,并在FFI
中使用这些类.
除x86-32
和x86-64
外,其他架构
不变.如果你的代码大量使用128
位整数,会注意到运行时性能
提高,但可能会增加内存使用
.
背景
数据类型
有两个与内存
中的排列方式
有关的内部值
:大小和对齐
.类型的大小
是它在内存
中消费的空间量
,对齐
指定了允许在哪些地址放置它
.
像原语
此类简单类型
的大小
一般是无歧义
的,是它们所表示的数据
的没有填充(未使用的空间)
的确切大小
.如,i64
的大小总是为64
位或8字节
.
但是,对齐
可能会有所不同.可在(1字节对齐
)任意内存地址
中保存8字节整数
,但大多数64
位计算机如果按8的倍数(8字节对齐)
保存,则会取得最佳性能
.
因此,与其他语言
一样,Rust
中的原语
默认有该最有效的对齐
.在创建复合类型
时可见该效果:
use core::mem::{align_of, offset_of};
#[repr(C)]
struct Foo {
a: u8, //1字节对齐
b: u16, //2字节对齐
}
#[repr(C)]
struct Bar {
a: u8, //1字节对齐
b: u64, //8字节对齐
}
println!("Offset of b (u16) in Foo: {}", offset_of!(Foo, b));
println!("Alignment of Foo: {}", align_of::<Foo>());
println!("Offset of b (u64) in Bar: {}", offset_of!(Bar, b));
println!("Alignment of Bar: {}", align_of::<Bar>());
输出:
`Foo`中`b(u16)`的偏移:2
`Foo`对齐:2
`栏`中`b(u64)`的偏移:8
`bar`对齐:8
看到,在一个结构
中,总是在它的偏移
是其对齐的倍数
位置放置一个类型
,即使表明未使用的空间
,当不使用repr(C)
时,Rust
默认最小化它.
这些数字
不是任意的;应用二进制接口
(ABI
)说明了它们应该是什么
.在系统V
(Unix&Linux
)的x86-64psABI
(处理器相关的ABI
)中,图3.1
:标量类型
准确地告诉了应该如何表示原语
:
C型 | Rust 等价 | sizeof | 对齐(字节) |
---|---|---|---|
符 | i8 | 1 | 1 |
正符 | u8 | 1 | 1 |
短 | i16 | 2 | 2 |
正短 | u16 | 2 | 2 |
长 | i64 | 8 | 8 |
正长 | u64 | 8 | 8 |
ABI
仅指定了C类型
,但Rust
在兼容和性能优势方面
都遵守相同定义
.
错误的对齐问题
如果两个实现
在数据类型
的对齐
上有分歧,则无法可靠
地共享包含该类型的数据
.Rust
对128
位类型的对齐
不一致:
println!("alignment of i128: {}", align_of::<i128>());
//`rustc1.76.0`版本
// `i128`对齐:8
printf("alignment of __int128: %zu\n", _Alignof(__int128));
//`GCC`版本`13.2`
// __int128对齐:16
// Clang17.0.1
// __int128对齐:16
回头看一下psABI
,可见Rust
在此的对齐
是错误的:
C型 | Rust 等价 | sizeof | 对齐(字节) |
---|---|---|---|
__int128 | i128 | 16 | 16 |
正__int128 | u128 | 16 | 16 |
表明,这并不是因为Rust
积极地做错
了什么:原语
的布局
来自Rust
和Clang
等语言使用的LLVMcodegen
后端,且它有硬编码
为8字节
的i128
对齐.
Clang
使用正确的对齐
只是因为变通,即在把类型交给LLVM
前,手动按16
字节设置对齐
.这解决
了布局问题
,但也是其他一些小问题的根源
.
Rust
无此手动调整,因此在https://github.com/rustlang/rust/issues/54341
上报告了它
.
调用约定问题
还有一个问题
:LLVM
在按函数参数
传递128
位整数时,并不总是正确
.在发现它与Rust
相关前,这是LLVM
中的一个已知问题
.
调用函数
时,会在寄存器
中传递参数
,直到没有更多的槽
,然后会"溢出"
到栈中(程序的内存
).
ABI
在3.2.3
传递参数一节中,也告诉了该怎么做
:
__int128
类型的参数
与INTEGER
操作相同
,但它们不适合一个通用寄存器
,而是需要两个寄存器
.为了分类,按如下
实现对待__int128
:
typedef struct {
long low, high;
} __int128;
但在内存中保存
的__int128
类型的参数
必须在16
字节边界
上对齐
.
可手动实现调用约定
来试此操作.在下面C示例
中,用内联汇编
按val
为0x11223344556677889900aabbccddeeff
值,来调用foo(0xaf,val,val,val)
.
x86-64
使用RDI,RSI,RDX,RCX,R8
和R9
寄存器,来按顺序
传递函数参数
.每个寄存器
适合一个字(64位
),不合适的都压进
栈中.
/*`<https://godbolt.org/z/5c8cb5cxs>`的完整示例*/
/*要查看问题,需要一个`内边距`值来"搞砸"参数对齐*/
void foo(char pad, __int128 a, __int128 b, __int128 c) {
printf("%#x\n", pad & 0xff);
print_i128(a);
print_i128(b);
print_i128(c);
}
int main() {
asm(
/*`加载`适合`寄存器`的参数*/
"movl $0xaf,%edi\n\t"/*第1个槽位`(EDI)`:填充符(`"EDI"`是*与`"RDI"`相同,只是访问大小较小)*/
"movq $0x9900aabbccddeeff,%rsi\n\t"/*第2个槽`(RSI):"a"`的下半部分*/
"movq $0x1122334455667788,%rdx\n\t"/*第3个槽`(RDX):"a"`的上半部分*/
"movq $0x9900aabbccddeeff,%rcx\n\t"/*第4个槽`(RCX):"b"`的下半部分*/
"movq $0x1122334455667788,%r8\n\t"/*第5个槽位`(r8):'b'`的上半部分*/
"movq $0xdeadbeef4c0ffee0,%r9\n\t"/*第6个槽`(R9)`:应该未使用,但来欺骗`Clang`!*/
/*重用保存的`寄存器`来加载栈*/
"pushq %rdx\n\t"/*在栈上传递`'c'`的上半部分*/
"pushq %rsi\n\t"/*在栈上传递`'c'`的下半部分*/
"call foo\n\t"/*调用函数*/
"addq $16,%rsp\n\t"/*重置栈*/
);
}
使用GCC
运行上述操作打印以下期望输出:
0xaf
0x11223344556677889900aabbccddeeff
0x11223344556677889900aabbccddeeff
0x11223344556677889900aabbccddeeff
但是使用Clang17
打印:
0xaf
0x11223344556677889900aabbccddeeff
0x11223344556677889900aabbccddeeff
0x9900aabbccddeeffdeadbeef4c0ffee0
^^^^^^^^^^^^^^^^这应该是下半部分
^^^^^^^^^^^^^^^^很熟悉
惊喜!
这说明了第二个问题:LLVM
期望i128
在可能时一半在寄存器
中传递,一半在栈
上传递,但ABI
禁止这样做
.
因为该行为来自LLVM
且没有合理的解决方法
,因此这在Clang
和Rust
中都是一个问题
.
方法
NikitaPopov
修复了D158169
的调用约定问题.这两项更改
都已纳入LLVM18
,即所有相关的ABI
问题都使用在此版本
的Clang
和Rust
中得到解决.
因为这些更改
,Rust
现在生成正确的对齐
:
println!("alignment of i128: {}", align_of::<i128>());
//`rustc1.77.0`版本
i128
对齐:16
如上,ABI
指定数据类型对齐
的部分原因
是因为它在该架构
上效率更高
.更改手动对齐
的初始性能
运行,表明大大改进了编译器性能
(严重依赖128
位整数来处理整数文字).
增加对齐
的缺点是在内存中复合类型
并不总是很好地组合
在一起,从而导致使用量增加
.可惜,即需要牺牲一些性能优势
,以避免增加内存成本
.
兼容
总之,使用LLVM18
(默认版本从1.78
开始)的Rust
的i128
和u128
将与版本的GCC
及Clang18
及更高版本
(2024
年3月发布)完全兼容
.