什么是引用/借用的底层机制?即栈指针与堆指针。

huangapple go评论66阅读模式
英文:

What exactly is a reference/borrow under the hood? a.k.a. stack pointers vs heap pointers

问题

I understand borrows conceptually; they're a value that is not owned and must be returned to the owner by being dropped by the borrower.

That makes sense, but I don't actually understand what a reference is. I read that rust references do not perform heap allocations, but that they also are, in fact, pointers.

So much so that the following code can directly cast a reference to a raw heap-allocated pointer:

// owned value
let some_val = 1;

// reference
let some_ref = &some_val;

// pointer
let some_ptr = some_ref as *const i32;

// prints "1 1 1" as expected
unsafe {
    println!("{} {} {}", some_val, some_ref, *some_ptr);
}

What part of my understanding is wrong here? It is my assumption that being behind a pointer and being stack-allocated are mutually-exclusive, but am I wrong? Is the source that told me references are not heap-allocated wrong?

I guess a secondary question would be to ask what exactly the memory looks like in the above snippet.

As far as my understanding goes, it would be as follows:

// load const 1 and store as some_val
let mut some_val = 1;

// load 1 onto the stack and invoke do_something
do_something(some_val);

// still load 1 onto the stack and invoke do_something, but ownership is not given to do_something()
do_something(&some_val);

// still load 1 onto the stack and invoke do_something & ownership is not given, but the borrow checker also makes sure that some_val is not mutably borrowed until do_something is done with &mut some_val
do_something(&mut some_val);

// allocate a copy of some_val to the heap, load a raw pointer to the new memory location onto the stack, and invoke do_something on the raw pointer. We still own some_val
do_something(&some_val as *const i32;)

I assume some part of that understanding is wrong, and I'd appreciate a correction!

英文:

I understand borrows conceptually; they're a value that is not owned and must be returned to the owner by being dropped by the borrower

That makes sense, but I don't actually understand what a reference is. I read that rust references to not perform heap allocations, but that they also are in fact pointers.

So much so that the following code can directly cast a reference to a raw heap-allocated pointer:

// owned value
let some_val = 1;

// reference
let some_ref = &some_val;

// pointer
let some_ptr = some_ref as *const i32;

// prints "1 1 1" as expected
unsafe {
    println!("{} {} {}", some_val, some_ref, *some_ptr);
}

What part of my understanding is wrong here? It is my assumption that being behind a pointer and being stack-allocated are mutually-exclusive, but am I wrong? Is the source that told me references are not heap-allocated wrong?

I guess a secondary question would be to ask what exactly the memory looks like in the above snippet ^

As far as my understanding goes, it would be as follows:

// load const 1 and store as some_val
let mut some_val = 1;

// load 1 onto the stack and invoke do_something
do_something(some_val);

// still load 1 onto the stack and invoke do_something, but ownership is not given to do_something()
do_something(&some_val);

// still load 1 onto the stack and invoke do_something & ownership is not given, but the borrow checker also makes sure that some_val is not mutably borrowed until do_something is done with &mut some_val
do_something(&mut some_val);

// allocate a copy of some_val to the heap, load a raw pointer to the new memory location onto the stack, and invoke do_something on the raw pointer. We still own some_val
do_something(&some_val as *const i32;)

I assume some part of that understanding is wrong, and I'd appreciate a correction!

答案1

得分: 8

你的理解是错误的。指针可以“指向”内存中的任何值,包括堆栈和堆。指针只是内存中的地址,在底层,位于堆栈或堆上的值仍然有一个地址。引用只是指针之上的一种安全抽象,借用检查器可以对其进行推理。

我现在将解释您的示例:

// 在堆栈上分配的拥有值 `1_i32`
let some_val = 1;

// 对 `some_val` 的共享(不可变)引用
// 实际上是一个 `*const i32` 指针,持有 `some_val` 的内存地址
let some_ref = &some_val;

// 从 `some_ref` 转换而来的原始指针
// 与 `some_ref` 具有相同的内存值
let some_ptr = some_ref as *const i32;

// 调用 do_something,通过值传递 `some_val`
// 直接将值 `1` 传递给函数
// 所有权被转移,如果 `some_val` 的类型不是可以平凡复制的,那么 `some_val` 将不再在此范围内可访问
do_something(some_val);

// 调用 do_something,通过共享引用传递 `some_val`
// 将 `some_val` 的指针(内存地址)传递给函数
// 所有权未被转移,do_something 不允许修改 `some_val` 中保存的值
do_something(&some_val);

// 调用 do_something,通过排他引用传递 `some_val`
// 将 `some_val` 的指针(内存地址)传递给函数
// 所有权未被转移,但 do_something 允许修改 `some_val` 中保存的值
do_something(&mut some_val);

// 调用 do_something,将 `some_val` 作为原始的 `*const` 指针传递
// 将 `some_val` 的指针(内存地址)传递给函数
// 所有权未被转移,但 do_something 不允许修改 `some_val` 中保存的值
// (但是,需要使用不安全操作的原始指针上的操作不受编译器强制执行)
do_something(&some_val as *const i32);

在内存中,some_val 可能位于堆栈上的地址范围 0xFFF00xFFF3(对于 i32 的 4 个字节)。some_ref 也将分配在堆栈上,放置在 0xFFE80xFFEF(对于 64 位系统的 8 个字节),并保存值 0xFFF0。指针也可以类似地放置在 0xFFE00xFFE7,保存相同的值 0xFFF0

当然,大多数编译器会优化这些操作,以便 some_val 实际上可能在寄存器中而不是在堆栈内存中,并且使用引用或指针的任何操作都可以内联以直接使用寄存器。但从心理模型的角度来看,这并不重要。

英文:

> What part of my understanding is wrong here? It is my assumption that being behind a pointer and being stack-allocated are mutually-exclusive, but am I wrong?

Your understanding is incorrect. Pointers can "point" at any value in memory, including both the stack and the heap. Pointers are just an address into memory under the hood, and values that live on the stack or heap still have an address. References are just a safe abstraction on top of pointers which the borrow checker can reason about.

I'll now explain your examples:

// Owned value of `1_i32` on the stack
let some_val = 1;

// Shared (immutable) reference to `some_val`
// Implemented as a `*const i32` pointer holding `some_val` memory address
let some_ref = &some_val;

// Raw pointer to `some_val`, cast from `some_ref`
// Has the same value in memory as `some_ref`
let some_ptr = some_ref as *const i32;

// Invoke do_something, passing `some_val` by value
// Passes the value `1` directly to the function
// Ownership is given, and if the type of `some_val` is not trivially
// copyable, `some_val` will not be accessible to this scope anymore
do_something(some_val);

// Invoke do_something, passing `some_val` by shared reference
// Passes the pointer (memory address) of `some_val` to the function
// Ownership is not given, and do_something is not allowed 
// to modify the value held in `some_val`
do_something(&some_val);

// Invoke do_something, passing `some_val` by exclusive reference
// Passes the pointer (memory address) of `some_val` to the function
// Ownership is not given, but do_something is allowed
// to modify the value held in `some_val`
do_something(&mut some_val);

// Invoke do_something, passing `some_val` as a raw `*const` pointer
// Passes the pointer (memory address) of `some_val` to the function
// Ownership is not given, and do_something is not allowed 
// to modify the value held in `some_val`
// (but operations on raw pointers, requiring unsafe, are not enforced by the compiler)
do_something(&some_val as *const i32);

In memory, some_val might live at address 0xFFF0 through 0xFFF3 on the stack (4 bytes for i32). some_ref will also be allocated on the stack, placed at 0xFFE8 through 0xFFEF (8 bytes for 64-bit systems) and holding the value 0xFFF0. And the pointer could likewise be placed at 0xFFE0 through 0xFFE7 holding the same value 0xFFF0.

Of course, most compilers will optimize things so some_val could really be in a register instead of in memory on the stack and any operations using a reference or pointer to it could be inlined to use the register directly. But in terms of mental model, that isn't important.

答案2

得分: 1

答案很简单,引用可以在底层被视为指针,尽管通常可以被编译器优化掉。

你的误解在于这一句:

> 以下代码可以直接将引用强制转换为原始堆分配的指针

这并不是实际发生的情况。指针可以指向堆,但它们也可以指向其他地方,包括栈,栈上通常存放局部变量。顺便提一下,“堆分配的指针”描述的是存在于堆上的指针。你要找的术语是“指向堆分配值的指针”。

> > // 在堆上分配 some_val 的副本,将新内存位置的原始指针加载到栈上,并在原始指针上调用 do_something。我们仍然拥有 some_val > do_something(&some_val as *const i32;) >

这是不正确的;它不会在堆上分配任何东西,而是获取栈上值的指针。

英文:

The simple answer is that references can be thought of as pointers under the hood, though often they can be optimized away by the compiler.

Your misunderstanding is captured in this line:

> the following code can directly cast a reference to a raw heap-allocated pointer

That's not what's happening at all. Pointers can point to the heap, but they can point other places as well, including the stack, where local variables often live. (As an aside, "heap-allocated pointer" describes a pointer that exists on the heap. The term you are looking for is "pointer to a heap-allocated value.")

>
> // allocate a copy of some_val to the heap, load a raw pointer to the new memory location onto the stack, and invoke do_something on the raw pointer. We still own some_val
> do_something(&some_val as *const i32;)
>

This is incorrect; it does not allocate anything on the heap, rather it obtains a pointer to the value on the stack.

huangapple
  • 本文由 发表于 2023年6月2日 06:30:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/76386102.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定