解除引用原始指针的语义是什么?

Ste*_*fan 11 rust

对于共享引用和可变引用,语义是明确的:只要您具有对值的共享引用,其他任何内容都不能具有可变访问权限,并且不能共享可变引用.

所以这段代码:

#[no_mangle]
pub extern fn run_ref(a: &i32, b: &mut i32) -> (i32, i32) {
    let x = *a;
    *b = 1;
    let y = *a;
    (x, y)
}
Run Code Online (Sandbox Code Playgroud)

编译(在x86_64上):

run_ref:
    movl    (%rdi), %ecx
    movl    $1, (%rsi)
    movq    %rcx, %rax
    shlq    $32, %rax
    orq     %rcx, %rax
    retq
Run Code Online (Sandbox Code Playgroud)

请注意,内存a指向只读一次,因为编译器知道写入b必须没有修改内存 a.

原始指针更复杂.原始指针算术和强制转换是"安全的",但取消引用它们不是.

我们可以将原始指针转换回共享和可变引用,然后使用它们; 这肯定意味着通常的引用语义,编译器可以相应地进行优化.

但是如果我们直接使用原始指针,那么语义是什么?

#[no_mangle]
pub unsafe extern fn run_ptr_direct(a: *const i32, b: *mut f32) -> (i32, i32) {
    let x = *a;
    *b = 1.0;
    let y = *a;
    (x, y)
}
Run Code Online (Sandbox Code Playgroud)

编译为:

run_ptr_direct:
    movl    (%rdi), %ecx
    movl    $1065353216, (%rsi)
    movl    (%rdi), %eax
    shlq    $32, %rax
    orq     %rcx, %rax
    retq
Run Code Online (Sandbox Code Playgroud)

虽然我们写了不同类型的值,但第二次读取仍然转到内存 - 似乎允许为两个参数调用具有相同(或重叠)内存位置的函数.换句话说, const原始指针不禁止共存的mut原始指针; 并且可能很好地将两个mut原始指针(可能是不同类型)指向同一个(或重叠的)内存位置.

请注意,正常的优化C/C++ - 编译器将消除第二次读取(由于"严格别名"规则:在大多数情况下,通过不同("不兼容")类型的指针修改/读取相同的内存位置是UB):

struct tuple { int x; int y; };

extern "C" tuple run_ptr(int const* a, float* b) {
    int const x = *a;
    *b = 1.0;
    int const y = *a;
    return tuple{x, y};
}
Run Code Online (Sandbox Code Playgroud)

编译为:

run_ptr:
    movl    (%rdi), %eax
    movl    $0x3f800000, (%rsi)
    movq    %rax, %rdx
    salq    $32, %rdx
    orq     %rdx, %rax
    ret
Run Code Online (Sandbox Code Playgroud)

带有Rust代码示例的游乐场

带有C示例的godbolt Compiler Explorer

那么:如果我们直接使用原始指针,那么语义是什么:引用数据是否可以重叠?

这应该直接影响是否允许编译器通过原始指针重新排序内存访问.

Mat*_* M. 16

这里没有尴尬的严格别名

C++ strict-aliasing是一条木腿上的补丁.C++没有任何别名信息,并且没有别名信息会阻止大量优化(正如您在此处所述),因此要重新获得一些性能严格别名已修补...

不幸的是,严格别名在系统语言中很尴尬,因为重新解释原始内存是系统语言设计要做的本质.

而且不幸的是,它无法实现许多优化.例如,从一个数组复制到另一个数组必须假定数组可能重叠.

restrict (来自C)稍微有点帮助,虽然它一次只适用于一个级别.


相反,我们有基于范围的别名分析

Rust中别名分析的本质是基于词法范围(限制线程).

你可能知道的初级水平解释是:

  • 如果你有一个&T,那么就没有&mut T相同的实例,
  • 如果你有一个&mut T,那么没有&T&mut T同一个实例.

适合初学者,它是一个略微缩写的版本.例如:

fn main() {
    let mut i = 32;
    let mut_ref = &mut i;
    let x: &i32 = mut_ref;

    println!("{}", x);
}
Run Code Online (Sandbox Code Playgroud)

即使a &mut i32(mut_ref)和a &i32(x)都指向同一个实例,也很好!

但是,如果你试图mut_ref在形成之后进入x,那么真相就会揭晓:

fn main() {
    let mut i = 32;
    let mut_ref = &mut i;
    let x: &i32 = mut_ref;
    *mut_ref = 2;
    println!("{}", x);
}
Run Code Online (Sandbox Code Playgroud)
error[E0506]: cannot assign to `*mut_ref` because it is borrowed
  |
4 |         let x: &i32 = mut_ref;
  |                       ------- borrow of `*mut_ref` occurs here
5 |         *mut_ref = 2;
  |         ^^^^^^^^^^^^ assignment to borrowed `*mut_ref` occurs here
Run Code Online (Sandbox Code Playgroud)

因此,它是精细兼得&mut T,并&T指向在同一时间同一个内存位置; 但是&mut T,只要&T存在,就会禁用变异.

在某种意义上,&mut T暂时降级为&T.


那么,指针是什么?

首先,让我们回顾一下参考文献:

  • 不能保证指向有效的存储器和甚至不保证是非NULL(不像两者Box&);
  • 不需要任何自动清理Box,因此需要手动资源管理;
  • 是普通的数据,也就是说,它们不会移动所有权,再次不同Box,因此Rust编译器无法防止像free-after之后的bug;
  • 缺乏任何形式的生命,不像&,因此编译器无法推理悬挂指针; 和
  • 除了不允许直接通过a的突变之外,不保证别名或可变性*const T.

显然缺席的是任何禁止将其*const T转为a的规则*mut T.这是正常的,这是允许的,因此最后一点实际上更像是一个棉绒,因为它可以很容易地解决.

Nomicon

如果没有指向Nomicon,对不安全的Rust的讨论就不会完整.

从本质上讲,不安全Rust的规则相当简单:如果它是安全的Rust,那么支持编译器可以保证的任何保证.

这并没有那么有用,因为这些规则尚未确定; 抱歉.

那么,解除引用原始指针的语义是什么?

据我所知1:

  • 如果您从原始指针(&T&mut T)形成引用,那么您必须确保维护这些引用服从的别名规则,
  • 如果你立即读/写,这暂时形成一个参考.

也就是说,假设调用者具有对该位置的可变访问:

pub unsafe fn run_ptr_direct(a: *const i32, b: *mut f32) -> (i32, i32) {
    let x = *a;
    *b = 1.0;
    let y = *a;
    (x, y)
}
Run Code Online (Sandbox Code Playgroud)

应该是有效的,因为*a有类型i32,所以引用中的生命周期没有重叠.

但是,我希望:

pub unsafe fn run_ptr_modified(a: *const i32, b: *mut f32) -> (i32, i32) {
    let x = &*a;
    *b = 1.0;
    let y = *a;
    (*x, y)
}
Run Code Online (Sandbox Code Playgroud)

要成为未定义的行为,因为它x会被*b用来修改其内存.

请注意变化是多么微妙.在unsafe代码中打破不变量很容易.

1 我现在可能错了,或者将来可能会出错