https://intoraw.xyz/blog/feed.xml

Rust mutable reference and `noalias`

2024-05-27

起因

最近在看 tokio 源代码的时候看到一段代码

/// `tokio/src/util/linked_list.rs`
/// We do not want the compiler to put the `noalias` attribute on mutable
/// references to this type, so the type has been made `!Unpin` with a
/// `PhantomPinned` field.
///
/// Additionally, we never access the `prev` or `next` fields directly, as any
/// such access would implicitly involve the creation of a reference to the
/// field, which we want to avoid since the fields are not `!Unpin`, and would
/// hence be given the `noalias` attribute if we were to do such an access.
/// As an alternative to accessing the fields directly, the `Pointers` type
/// provides getters and setters for the two fields, and those are implemented
/// using raw pointer casts and offsets, which is valid since the struct is
/// #[repr(C)].
///
/// See this link for more information:
/// <https://github.com/rust-lang/rust/pull/82834>
#[repr(C)]
struct PointersInner<T> {
    /// The previous node in the list. null if there is no previous node.
    ///
    /// This field is accessed through pointer manipulation, so it is not dead code.
    #[allow(dead_code)]
    prev: Option<NonNull<T>>,

    /// The next node in the list. null if there is no previous node.
    ///
    /// This field is accessed through pointer manipulation, so it is not dead code.
    #[allow(dead_code)]
    next: Option<NonNull<T>>,

    /// This type is !Unpin due to the heuristic from:
    /// <https://github.com/rust-lang/rust/pull/82834>
    _pin: PhantomPinned,
}

其中提到了为了不让编译器对&mut生成noalias属性,使用了 PhantomPined。 看到这里就有一些疑惑,什么是noalias,以及编译器为什么会对&mut生成 noalias 的属性。

noalias

在 LLVM 中noalias 是函数的Parameter Attribute

noalias的官方解释是

This indicates that memory locations accessed via pointer values based on the argument or return value are not also accessed, during the execution of the function, via pointer values not based on the argument or return value. This guarantee only holds for memory locations that are modified, by any means, during the execution of the function. The attribute on a return value also has additional semantics described below. The caller shares the responsibility with the callee for ensuring that these requirements are met. For further details, please see the discussion of the NoAlias response in alias analysis.

简而言之,就是在函数的执行过程中,带有 noalias 标记的指针具有对被指向地址访问的唯一性。如果两个参数/返回值都被标记为 noalias,那么这两个指针指向的地址必须不一样。 另外这里还提到了指针based关系的(定义)[https://llvm.org/docs/LangRef.html#pointeraliasing]。 其中访问一个结构体的subfield 也属于 based 关系。

rustc 对&mut生成的代码

来一个例子体会一下rustc 生成的 llvm 代码

use std::hint::black_box;

#[inline(never)]
fn hello(x: &mut u32, y: &mut u32) {
    if *x > 100 {
        *y += 1;
    }

    if *y > 1 {
        *x += 100;
    }
}

fn main() {
    let mut x = black_box(100);
    let mut y = black_box(0);

    let ref_x = &mut x;
    let ref_y = &mut y;

    hello(ref_x, ref_y);
}

这里要在 release 模式下才会生成noalias的 attribute

cargo rustc --release -- --emit="llvm-ir"
; a::hello
; Function Attrs: mustprogress nofree noinline norecurse nosync nounwind willreturn memory(argmem: readwrite) uwtable
define internal fastcc void @_ZN1a5hello17hda125466b804c551E(ptr noalias nocapture noundef align 4 dereferenceable(4) %x, ptr noalias nocapture noundef align 4 dereferenceable(4) %y) unnamed_addr #4 {
start:
  %_4 = load i32, ptr %x, align 4, !noundef !4
  %_3 = icmp ugt i32 %_4, 100
  %0 = load i32, ptr %y, align 4
  br i1 %_3, label %bb1, label %bb3

bb1:                                              ; preds = %start
  %1 = add i32 %0, 1
  store i32 %1, ptr %y, align 4
  br label %bb3

bb3:                                              ; preds = %start, %bb1
  %_6 = phi i32 [ %1, %bb1 ], [ %0, %start ]
  %_5 = icmp ugt i32 %_6, 1
  br i1 %_5, label %bb4, label %bb6

bb4:                                              ; preds = %bb3
  %2 = add i32 %_4, 100
  store i32 %2, ptr %x, align 4
  br label %bb6

bb6:                                              ; preds = %bb3, %bb4
  ret void
}

Unsafe rust 与noalias

这个issue之后,rust 默认对函数的参数和返回值中的&mut 生成 noalias 标注。这样的话在写 unsafe rust 的时候要注意了,在cast 一个指针到&mut 的时候要考虑这个&mut 是否会被用作函数参数。 例如下面的代码片段在 debug 模式(没有生成 noalias)和 release 模式下(生成 noalias)的结果完全不同.

use std::hint::black_box;

#[inline(never)]
fn hello(x: &mut u32, y: &mut u32) {
    if *x > 100 {
        *y += 10;
    }

    if *y > 100 {
        *x += 10;
    }
}

fn main() {
    let mut x = black_box(200);

    let ref_x = &mut x;
    let ref_y = unsafe { std::mem::transmute_copy(&ref_x) };

    hello(ref_x, ref_y);
    println!("{} ", x);
}

避免生成 noalias

根据 PR #82834

noalias is not emitted for types that are !Unpin, as a heuristic for self-referential structures (see #54878 and #63818).

如果代码中有很多地方用到了 unsafe ,同时又不想每次函数调用去检查是否会出现上面提到的问题,可以在对应数据结构中内置一个 PhatonmPinned 的 Marker type 。

use std::marker::PhantomPinned;

pub struct Foo {
    value: u32,
    _pin: PhantomPinned,
}

impl Foo {
    pub fn new(value: u32) -> Self {
        Self {
            value,
            _pin: PhantomPinned,
        }
    }

    #[inline(never)]
    pub fn value_mut(&mut self) -> &mut u32 {
        &mut self.value
    }
}

#[inline(never)]
fn hello(x: &mut Foo, y: &mut Foo) {
    if *x.value_mut() > 100 {
        *y.value_mut() += 10;
    }
    if *y.value_mut() > 100 {
        *x.value_mut() += 10;
    }
}

#[inline(never)]
fn world(x: &mut u32, y: &mut u32) {
    if *x > 100 {
        *y += 10;
    }
    if *y > 100 {
        *x += 10;
    }
}

fn main() {
    let mut x = Foo::new(200);
    let mut y = Foo::new(10);

    let ref_x = &mut x;
    let ref_y = &mut y;

    hello(ref_x, ref_y);

    let ref_x = x.value_mut();
    let ref_y = y.value_mut();
    world(ref_x, ref_y);
}

hello函数生成的 llvm ir,没有 noalias

; a::hello
; Function Attrs: mustprogress nofree noinline norecurse nosync nounwind willreturn memory(argmem: readwrite) uwtable
define internal fastcc void @_ZN1a5hello17he6f9e03264499f38E(ptr nocapture noundef nonnull align 4 %x, ptr nocapture noundef nonnull align 4 %y) unnamed_addr #4 {
start:
  %_4 = load i32, ptr %x, align 4, !noundef !4
  %_3 = icmp ugt i32 %_4, 100
  %_8.pre = load i32, ptr %y, align 4
  br i1 %_3, label %bb2, label %bb5

bb2:                                              ; preds = %start
  %0 = add i32 %_8.pre, 10
  store i32 %0, ptr %y, align 4
  br label %bb5

bb5:                                              ; preds = %start, %bb2
  %_8 = phi i32 [ %_8.pre, %start ], [ %0, %bb2 ]
  %_7 = icmp ugt i32 %_8, 100
  br i1 %_7, label %bb7, label %bb10

bb7:                                              ; preds = %bb5
  %1 = load i32, ptr %x, align 4, !noundef !4
  %2 = add i32 %1, 10
  store i32 %2, ptr %x, align 4
  br label %bb10

bb10:                                             ; preds = %bb5, %bb7
  ret void
}

world函数生成的 llvm ir,依然有 noalias

; a::world
; Function Attrs: mustprogress nofree noinline norecurse nosync nounwind willreturn memory(argmem: readwrite) uwtable
define internal fastcc void @_ZN1a5world17hec9a993fc9bac6c2E(ptr noalias nocapture noundef align 4 dereferenceable(4) %x, ptr noalias nocapture noundef align 4 dereferenceable(4) %y) unnamed_addr #4 {
start:
  %_4 = load i32, ptr %x, align 4, !noundef !4
  %_3 = icmp ugt i32 %_4, 100
  %0 = load i32, ptr %y, align 4
  br i1 %_3, label %bb1, label %bb3

bb1:                                              ; preds = %start
  %1 = add i32 %0, 10
  store i32 %1, ptr %y, align 4
  br label %bb3

bb3:                                              ; preds = %start, %bb1
  %_6 = phi i32 [ %1, %bb1 ], [ %0, %start ]
  %_5 = icmp ugt i32 %_6, 100
  br i1 %_5, label %bb4, label %bb6

bb4:                                              ; preds = %bb3
  %2 = add i32 %_4, 10
  store i32 %2, ptr %x, align 4
  br label %bb6

bb6:                                              ; preds = %bb3, %bb4
  ret void
}

所以在 tokio 的链表中,虽然 PointersInner 结构不会生成 noalias,但如果对 prev 和 next 定义 get_mut函数的话依然会对 prev和 next生成 noalias,对此 tokio 的做法是#repr[(C)]使用 C 的内存布局,定义 get 和 set 函数,直接操作裸指针。

    fn set_prev(&mut self, value: Option<NonNull<T>>) {
        // SAFETY: prev is the first field in PointersInner, which is #[repr(C)].
        unsafe {
            let inner = self.inner.get();
            let prev = inner as *mut Option<NonNull<T>>;
            ptr::write(prev, value);
        }
    }
    fn set_next(&mut self, value: Option<NonNull<T>>) {
        // SAFETY: next is the second field in PointersInner, which is #[repr(C)].
        unsafe {
            let inner = self.inner.get();
            let prev = inner as *mut Option<NonNull<T>>;
            let next = prev.add(1);
            ptr::write(next, value);
        }
    }
}