引用的内存结构 Memory layout
Rust 中的引用, 跟 C++ 中的引用一样, 在底层都是指针.
先看一段代码 C++ 代码片段:
#include <cassert>
int addr1() {
int x = 42;
int& x_ptr = x;
x_ptr += 1;
return x;
}
int main(void) {
assert(addr1() == 43);
return 0;
}
这段 C++ 代码对应的汇编代码如下:
.file "reference-address.cpp"
.text
.globl _Z5addr1v
.type _Z5addr1v, @function
_Z5addr1v:
.LFB3:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $42, -12(%rbp)
leaq -12(%rbp), %rax
movq %rax, -8(%rbp)
movq -8(%rbp), %rax
movl (%rax), %eax
leal 1(%rax), %edx
movq -8(%rbp), %rax
movl %edx, (%rax)
movl -12(%rbp), %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.
然后我们用 C 语言重新写一遍相同的功能, 如下:
#include <stdlib.h>
#include <assert.h>
int addr1() {
int x = 42;
int* x_ptr = &x;
*x_ptr += 1;
return x;
}
int main(void) {
assert(addr1() == 43);
return 0;
}
这段 C 代码对应的汇编代码如下:
.file "reference-address.c"
.text
.globl addr1
.type addr1, @function
addr1:
.LFB6:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $42, -12(%rbp)
leaq -12(%rbp), %rax
movq %rax, -8(%rbp)
movq -8(%rbp), %rax
movl (%rax), %eax
leal 1(%rax), %edx
movq -8(%rbp), %rax
movl %edx, (%rax)
movl -12(%rbp), %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
可以发现, 两段汇编代码是完全一样的. 简单地说, C++中的引用, 在底层就是通过指针实现的.
最后, 我们看一下对应的 Rust 代码如何写:
fn add1() -> i32 { let mut x: i32 = 42; let x_ref: &mut i32 = &mut x; *x_ref += 1; x } fn main() { assert_eq!(add1(), 43); }
当然, 也要把它生成汇编代码:
.section .text._ZN17reference_address4add117h385aa07c715333f2E,"ax",@progbits
.p2align 4, 0x90
.type _ZN17reference_address4add117h385aa07c715333f2E,@function
_ZN17reference_address4add117h385aa07c715333f2E:
.cfi_startproc
pushq %rax
.cfi_def_cfa_offset 16
movl $42, 4(%rsp)
movl 4(%rsp), %eax
incl %eax
movl %eax, (%rsp)
seto %al
jo .LBB12_2
movl (%rsp), %eax
movl %eax, 4(%rsp)
movl 4(%rsp), %eax
popq %rcx
.cfi_def_cfa_offset 8
retq
.LBB12_2:
.cfi_def_cfa_offset 16
leaq .L__unnamed_3(%rip), %rdi
movq _ZN4core9panicking11panic_const24panic_const_add_overflow17h343c6c3f46bad3f5E@GOTPCREL(%rip), %rax
callq *%rax
.Lfunc_end12:
.size _ZN17reference_address4add117h385aa07c715333f2E, .Lfunc_end12-_ZN17reference_address4add117h385aa07c715333f2E
.cfi_endproc
可以看到, 生成的汇编代码里压根就没有用到 x_ref
这个引用. 这是因为表达式太简单, 被优化掉了,
我们使用 rustc --emit asm -g reference-address.rs
重新生成汇编, 得到以下内容:
#![allow(unused)] fn main() { .section .text._ZN17reference_address4add117h385aa07c715333f2E,"ax",@progbits .p2align 4, 0x90 .type _ZN17reference_address4add117h385aa07c715333f2E,@function _ZN17reference_address4add117h385aa07c715333f2E: .Lfunc_begin12: .file 11 "/home/shaohua/dev/rust/intro-to-rust/src/references/assets" "reference-address.rs" .loc 11 5 0 .cfi_startproc subq $24, %rsp .cfi_def_cfa_offset 32 ; let x = 42; movl $42, 12(%rsp) ; let x_ref = &mut x; leaq 12(%rsp), %rax movq %rax, 16(%rsp) movl 12(%rsp), %eax incl %eax movl %eax, 8(%rsp) seto %al jo .LBB12_2 movl 8(%rsp), %eax movl %eax, 12(%rsp) movl 12(%rsp), %eax addq $24, %rsp .cfi_def_cfa_offset 8 retq .LBB12_2: .cfi_def_cfa_offset 32 .Ltmp40: .loc 11 8 5 leaq .L__unnamed_3(%rip), %rdi movq _ZN4core9panicking11panic_const24panic_const_add_overflow17h343c6c3f46bad3f5E@GOTPCREL(%rip), %rax callq *%rax .Ltmp41: .Lfunc_end12: .size _ZN17reference_address4add117h385aa07c715333f2E, .Lfunc_end12-_ZN17reference_address4add117h385aa07c715333f2E .cfi_endproc }
上面几行重要的汇编代码都加了注释, 可以看到:
x_ref
存储的也是x
的内存地址
这个跟上面 C/C++ 代码里的行为是一致的.
简单地说, Rust 中的引用, 在底层也是通过指针实现的.
但是, 除了我们用过的这些引用类型之外, Rust 还有一些更复杂的引用类型, 它们的内存结构也更复杂.
切片 slice 的引用
上段介绍过了引用本身占用的内存大小只是一个指针大小, 即 usize
. 这个类似于 C/C++ 中的
指针.
但切片以及 trait object 的引用, 这两类都是胖指针(fat pointer), 即除了指向内存地址之外, 还有别的属性.
指向 slice 的指针包含两个成员, 第一个是指向 slice 某个元素的内存地址; 第二个 是定义了该引用可访问的元素个数.
指向 trait object 的引用包含了两个 field, 第一个是指向该值的内存地址, 第二个指向 该值对该 trait 的实现的地址, 以方便调用该 trait 定义了的方法.
但还有两个特殊类型的引用, 它们都占两个指针大小, 即 usize * 2
:
切片 slice 的引用是一个胖指针, 该指针包含两个字段:
- slice 起始地址
- slice 长度
先看一段代码示例:
use std::mem::size_of_val; fn main() { // 整数数组 let numbers: [i32; 5] = [1, 2, 3, 4, 5]; assert_eq!(size_of_val(&numbers), 20); // 数组的引用 let numbers_ref: &[i32; 5] = &numbers; assert_eq!(size_of_val(numbers_ref), 20); // 切片引用, 该切片指向数组中的第 3 个元素, 切片长度为 3 let slice_ref: &[i32] = &numbers[2..]; assert_eq!(size_of_val(slice_ref), 12); assert_eq!(slice_ref.len(), 3); }
这里面创建了两个引用:
numbers_ref
只是一般的引用, 它指向数组numbers
第 1 个元素的地址slice_ref
是一个切片引用, 它里面的指针指向numbers
的第 3 个元素的地址; 而且切片的长度为 3
其内存结构如下图如示:
如果还不相信的话, 我们可以用调试器直接查看它们的内存, 首先是 numbers
数组, 注意它的第 3 个元素的内存地址:
可以看到, 这个整数数组的内存都是连续存放的, 每个元素占用 4 个字节.
然后是 slice_ref
的内存结构:
可以看到, slice_ref
确实包含两个成员:
- 指针, 指向数组
numbers
的第 3 个元素 - 切片长度, 是 3
trait object 的引用
trait object 的引用也是一个胖指针, 包含两部分:
- object 数据地址
- object virtual table 地址, 即该值对该 trait 的具体实现