为什么取消引用 `Arc<T> where T : MyTrait` 对齐到 0x10?
Why does dereferencing `Arc<T> where T : MyTrait` align to 0x10?
我对我的库进行了过度微优化,我正在查看生成的 ASM。我注意到调用 Arc<T> where T : MyTrait
的方法会产生一些我认为正在将存储在 ArcInner
中的指针与 0x10 对齐的东西。
我用这段代码复制了它:
#![feature(test)]
extern crate test;
use std::sync::Arc;
struct DA;
trait Drain {
fn log(&self, &DA);
}
struct BlackBoxDrain;
impl Drain for BlackBoxDrain {
fn log(&self, da: &DA) {
test::black_box::<&DA>(da);
}
}
fn f(d: Arc<Drain>) {
d.log(&DA)
}
fn main() {
let arc_d = Arc::new(BlackBoxDrain);
f(arc_d);
}
Rust playground(设置 Nightly + Release 然后点击 ASM)
有问题的 ASM 代码是:
movq 16(%r15), %rdi
leaq 15(%rdi), %rax
negq %rdi
andq %rax, %rdi
addq %r14, %rdi
重要的是这个操作要尽可能快。由于 ASM 取消引用是 5 条指令,其中 3 条似乎可能是不必要的,所以我想了解为什么会发生这种情况以及是否可以提供帮助。也许我只是不明白这里的汇编指令。
编辑:我的最小示例并不完全相同,因为看起来需要板条箱边界来防止 compiler/linker 优化该序列。但是在我的例子中,顺序是完全一样的,在一个紧密的(rust bench)循环中,没有涉及析构函数:只有一个方法调用 Arc<TraitObject>
.
该指令序列(至少当我 运行 它时)在函数 _ZN33_$LT$alloc..arc..Arc$LT$T$GT$$GTdrop_slow17h09d36c48f370a93dE
中,它分解为 <alloc::arc::Arc<T>>::drop_slow
。这是释放函数。 Looking at the source:
unsafe fn drop_slow(&mut self) {
let ptr = *self.ptr;
// Destroy the data at this time, even though we may not free the box
// allocation itself (there may still be weak pointers lying around).
ptr::drop_in_place(&mut (*ptr).data);
if self.inner().weak.fetch_sub(1, Release) == 1 {
atomic::fence(Acquire);
deallocate(ptr as *mut u8, size_of_val(&*ptr), align_of_val(&*ptr))
}
}
序列正在查找 ArcInner<T>
的 data
成员的偏移量,其定义为(大致):
struct ArcInner<T: ?Sized> {
strong: atomic::AtomicUSize, // 64-bit or 8 byte atomic count
weak: atomic::AtomicUsize, // ditto
data: T, // The actual data payload.
}
作为背景,trait object contains a data pointer and vtable pointer, and the vtable starts with destructor, size, and alignment。
更新:由于 dpc.dw 的附加 research/answer.
更正了我的理解
data
成员需要根据 T
类型适当对齐。但是,由于我们是通过 Arc<Trait>
访问它的,此时编译器不知道对齐是什么!例如,我们可能已经存储了一个理论上的 SIMD 类型,具有 64 字节对齐。但是,fat trait 对象指针确实包含对齐,从中可以计算到 data
的偏移量。这就是这里发生的事情:
// assumption: rbx is pointer to trait object
movq (%rbx), %r14 // Get the ArcInner data pointer into r14
movq 8(%rbx), %r15 // Get vtable pointer into r15
movq 16(%r15), %rdi // Load T alignment from vtable into rdi
leaq 15(%rdi), %rax // rax := align+15
negq %rdi // rdi = -rdi (-align)
andq %rax, %rdi // rdi = (align+15) & (-align)
addq %r14, %rdi // Add now aligned offset to `data` to call drop
callq *(%r15) // Call destructor (first entry in vtable in r15)
感谢 Chris Emerson 的回答,我意识到它与 vtable 和对齐规则有关。然后我在 Mozilla 的 Rust IRC 频道上四处询问,aatch 和 talchas 弄明白了:
rustc
将始终计算存储在 ArcInner<T>
中的数据 (T
) 的对齐偏移量 - 因为每个 struct
实现 [=11] 的偏移量可能不同=].这没什么大不了的 - 因为这些指令非常快,并且会受到良好指令级别 CPU 并行化的影响。
我对我的库进行了过度微优化,我正在查看生成的 ASM。我注意到调用 Arc<T> where T : MyTrait
的方法会产生一些我认为正在将存储在 ArcInner
中的指针与 0x10 对齐的东西。
我用这段代码复制了它:
#![feature(test)]
extern crate test;
use std::sync::Arc;
struct DA;
trait Drain {
fn log(&self, &DA);
}
struct BlackBoxDrain;
impl Drain for BlackBoxDrain {
fn log(&self, da: &DA) {
test::black_box::<&DA>(da);
}
}
fn f(d: Arc<Drain>) {
d.log(&DA)
}
fn main() {
let arc_d = Arc::new(BlackBoxDrain);
f(arc_d);
}
Rust playground(设置 Nightly + Release 然后点击 ASM)
有问题的 ASM 代码是:
movq 16(%r15), %rdi
leaq 15(%rdi), %rax
negq %rdi
andq %rax, %rdi
addq %r14, %rdi
重要的是这个操作要尽可能快。由于 ASM 取消引用是 5 条指令,其中 3 条似乎可能是不必要的,所以我想了解为什么会发生这种情况以及是否可以提供帮助。也许我只是不明白这里的汇编指令。
编辑:我的最小示例并不完全相同,因为看起来需要板条箱边界来防止 compiler/linker 优化该序列。但是在我的例子中,顺序是完全一样的,在一个紧密的(rust bench)循环中,没有涉及析构函数:只有一个方法调用 Arc<TraitObject>
.
该指令序列(至少当我 运行 它时)在函数 _ZN33_$LT$alloc..arc..Arc$LT$T$GT$$GTdrop_slow17h09d36c48f370a93dE
中,它分解为 <alloc::arc::Arc<T>>::drop_slow
。这是释放函数。 Looking at the source:
unsafe fn drop_slow(&mut self) {
let ptr = *self.ptr;
// Destroy the data at this time, even though we may not free the box
// allocation itself (there may still be weak pointers lying around).
ptr::drop_in_place(&mut (*ptr).data);
if self.inner().weak.fetch_sub(1, Release) == 1 {
atomic::fence(Acquire);
deallocate(ptr as *mut u8, size_of_val(&*ptr), align_of_val(&*ptr))
}
}
序列正在查找 ArcInner<T>
的 data
成员的偏移量,其定义为(大致):
struct ArcInner<T: ?Sized> {
strong: atomic::AtomicUSize, // 64-bit or 8 byte atomic count
weak: atomic::AtomicUsize, // ditto
data: T, // The actual data payload.
}
作为背景,trait object contains a data pointer and vtable pointer, and the vtable starts with destructor, size, and alignment。
更新:由于 dpc.dw 的附加 research/answer.
更正了我的理解data
成员需要根据 T
类型适当对齐。但是,由于我们是通过 Arc<Trait>
访问它的,此时编译器不知道对齐是什么!例如,我们可能已经存储了一个理论上的 SIMD 类型,具有 64 字节对齐。但是,fat trait 对象指针确实包含对齐,从中可以计算到 data
的偏移量。这就是这里发生的事情:
// assumption: rbx is pointer to trait object
movq (%rbx), %r14 // Get the ArcInner data pointer into r14
movq 8(%rbx), %r15 // Get vtable pointer into r15
movq 16(%r15), %rdi // Load T alignment from vtable into rdi
leaq 15(%rdi), %rax // rax := align+15
negq %rdi // rdi = -rdi (-align)
andq %rax, %rdi // rdi = (align+15) & (-align)
addq %r14, %rdi // Add now aligned offset to `data` to call drop
callq *(%r15) // Call destructor (first entry in vtable in r15)
感谢 Chris Emerson 的回答,我意识到它与 vtable 和对齐规则有关。然后我在 Mozilla 的 Rust IRC 频道上四处询问,aatch 和 talchas 弄明白了:
rustc
将始终计算存储在 ArcInner<T>
中的数据 (T
) 的对齐偏移量 - 因为每个 struct
实现 [=11] 的偏移量可能不同=].这没什么大不了的 - 因为这些指令非常快,并且会受到良好指令级别 CPU 并行化的影响。