对于共享引用和可变引用,语义是明确的: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
所以:如果我们直接使用原始指针,那么语义是什么:是否可以 引用数据重叠?
这应该对是否允许编译器有直接影响 通过原始指针重新排序内存访问。
答案 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 i32
(mut_ref
)和&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);
}
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 我现在可能错了,或者我将来可能会出错