【问题标题】:Why does dereferencing `Arc<T> where T : MyTrait` align to 0x10?为什么取消引用 `Arc<T> where T : MyTrait` 对齐到 0x10?
【发布时间】:2017-02-19 20:39:07
【问题描述】:

我正在过度优化我的库,并且正在查看生成的 ASM。我注意到调用Arc&lt;T&gt; 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 条似乎可能是不必要的,我想了解为什么会发生这种情况以及是否可以提供帮助。也许我只是不明白这里的汇编指令。

编辑:我的最小示例并不完全相同,因为看起来需要 crate 边界来防止编译器/链接器优化该序列。但是在我的例子中,顺序是完全相同的,在一个紧密的(rust bench)循环中,没有涉及到析构函数:只有 Arc&lt;TraitObject&gt; 上的方法调用。

【问题讨论】:

  • 既然你在优化,你知道指令数量不一定与程序速度相关吗?例如,转到矢量化指令可能需要更多指令,但每个周期处理更多字节。

标签: assembly rust x86-64 micro-optimization


【解决方案1】:

感谢 Chris Emerson 的回答,我意识到这与 vtable 和对齐规则有关。然后我在 Mozilla 的 Rust IRC 频道上四处询问,aatch 和 talchas 想通了:

rustc 将始终为存储在 ArcInner&lt;T&gt; 中的数据 (T) 计算对齐偏移量 - 因为每个实现 Tstruct 可能不同。这没什么大不了的 - 因为这些指令非常快,并且会受到良好的指令级 CPU 并行化。

【讨论】:

  • 我已经更新了我的答案,使其更加准确,谢谢。
【解决方案2】:

该指令序列(至少在我运行它时)位于函数 _ZN33_$LT$alloc..arc..Arc$LT$T$GT$$GT$9drop_slow17h09d36c48f370a93dE 中,该函数与 &lt;alloc::arc::Arc&lt;T&gt;&gt;::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&lt;T&gt;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 包含一个数据指针和 vtable 指针,以及 vtable starts with destructor, size, and alignment

更新:感谢 dpc.dw 的额外研究/回答,更正我的理解。

data 成员需要针对T 类型进行适当对齐。然而,由于我们通过Arc&lt;Trait&gt; 访问它,此时编译器不知道对齐是什么!例如,我们可能已经存储了具有 64 字节对齐的理论 SIMD 类型。但是,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)

【讨论】:

  • 我认为您是对的,这些说明来自特征对象释放。我将不得不重新调查我的原始代码并仔细检查我只是对kcachegrind 中的这些说明感到困惑。
  • 毕竟我原来的程序好像不是这样的。那里的析构函数永远不会被调用,但是对齐计算序列就在那里。在我看来,这只是 llvm 优化器在某个地方失败了。
  • 会不会是在任何 trait 对象上调用方法都需要 0x10 对齐。例如查找 vtable(或其中的东西)?
猜你喜欢
  • 2021-02-23
  • 1970-01-01
  • 1970-01-01
  • 2019-03-05
  • 1970-01-01
  • 2016-10-24
  • 1970-01-01
  • 1970-01-01
  • 2011-02-24
相关资源
最近更新 更多