对于共享引用和可变引用,语义是明确的:只要您具有对值的共享引用,其他任何内容都不能具有可变访问权限,并且不能共享可变引用.
所以这段代码:
#[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)
带有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)
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
因此,它是精细兼得&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 我现在可能错了,或者将来可能会出错