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

时间:2018-02-20 09:57:06

标签: rust

对于共享引用和可变引用,语义是明确的:as 只要你有一个值的共享引用,其他任何东西都不能有 可变访问,并且不能共享可变引用。

所以这段代码:

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

将(在x86_64上)编译为:

run_ref:
    movl    (%rdi), %ecx
    movl    $1, (%rsi)
    movq    %rcx, %rax
    shlq    $32, %rax
    orq     %rcx, %rax
    retq

请注意,内存a指向只读一次,因为 编译器知道写入b一定不能修改内存 a

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

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

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

#[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_ptr_direct:
    movl    (%rdi), %ecx
    movl    $1065353216, (%rsi)
    movl    (%rdi), %eax
    shlq    $32, %rax
    orq     %rcx, %rax
    retq

虽然我们写了不同类型的值,但第二次读取仍然存在 记忆 - 似乎允许用相同的方式调用此函数 (或重叠)两个参数的内存位置。换句话说,一个 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_ptr:
    movl    (%rdi), %eax
    movl    $0x3f800000, (%rsi)
    movq    %rax, %rdx
    salq    $32, %rdx
    orq     %rdx, %rax
    ret

Playground with Rust code examples

godbolt Compiler Explorer with C example

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

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

1 个答案:

答案 0 :(得分: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);
}

完全没问题,即使&mut i32mut_ref)和&i32x)都指向同一个实例!

如果您在形成mut_ref后尝试访问x,那么事实就会揭晓:

fn main() {
    let mut i = 32;
    let mut_ref = &mut i;
    let x: &i32 = mut_ref;
    *mut_ref = 2;
    println!("{}", x);
}
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

因此,同时指向同一内存位置的&mut T&T 正常;但是,只要&mut T存在,就会禁用&T变异。

从某种意义上说,&mut T 暂时降级为&T

那么,指针是什么?

首先,让我们回顾the reference

  
      
  • 不保证指向有效内存,甚至不保证是非NULL(与Box&不同);
  •   
  • Box不同,没有任何自动清理,因此需要手动资源管理;
  •   
  • 是普通旧数据,也就是说,它们不会移动所有权,与Box不同,因此Rust编译器无法防止像free-after-free之类的错误;
  •   
  • 缺少任何形式的生命周期,与&不同,因此编译器无法推理悬挂指针;和
  •   
  • 除了不允许直接通过*const T进行突变外,不保证别名或可变性。
  •   

显然缺席的是禁止将*const T投射到*mut T的任何规则。这是正常的,允许,因此最后一点实际上更多的是 lint ,因为它可以很容易解决。

<强> Nomicon

如果没有pointing to the 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)
}

应该有效,因为*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)
}

要成为未定义的行为,因为x将处于活动状态,而*b用于修改其内存。

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

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