将数据移动到 Rc/Arc 是否总是将其从堆栈复制到堆?

Al *_*ndy 5 rust

看下面这个简单的例子:

use std::rc::Rc;

struct MyStruct {
    a: i8,
}

fn main() {
    let mut my_struct = MyStruct { a: 0 };
    my_struct.a = 5;
    let my_struct_rc = Rc::new(my_struct);

    println!("my_struct_rc.a = {}", my_struct_rc.a);
}
Run Code Online (Sandbox Code Playgroud)

的官方文档Rc说:

该类型Rc<T>提供T了在堆中分配的 type 值的共享所有权。

理论上是清楚的。但是,首先my_struct不是立即包装成Rc,其次MyStruct是一个非常简单的类型。我可以在这里看到 2 个场景。

  1. my_struct被移入Rc内存内容时,它会从堆栈中复制到堆中。
  2. 编译器能够解决my_struct将移入 的问题Rc,因此它从一开始就将其放在堆中。

如果数字 1 为真,那么可能存在一个隐藏的性能瓶颈,因为在阅读代码时没有明确看到内存被复制(我假设MyStruct要复杂得多)。

如果数字 2 为真,我想知道编译器是否总是能够解决这些问题。提供的示例非常简单,但我可以想象它my_struct要复杂得多,并且在移动到Rc.

tre*_*tcl 7

Tl;dr这可能是任何一种情况,但在大多数情况下,您应该以最明显的方式编写代码,然后让编译器担心它。

\n

根据抽象机的语义,即定义 Rust 行为的计算理论模型,总有一个副本。事实上,至少有两种情况my_struct首先是在 的堆栈帧中创建main,但随后必须移入 的堆栈帧中Rc::new。然后Rc::new必须创建分配并my_struct第二次从其自己的堆栈帧移动到新分配的内存*。这些动作中的每一个在概念上都是一个副本。

\n

然而,这种分析对于预测实践中代码的性能并不是特别有用,原因有以下三个:

\n
    \n
  1. 副本实际上非常便宜。从长远来看,从一个地方移动my_struct到另一个地方实际上可能比用指针引用它要便宜得多。在现代处理器上复制一大块字节很容易优化;跟随指向某个任意位置的指针则不是。(还要记住,结构的复杂性是无关紧要的,因为所有移动都是字节复制;例如,移动 anyVec只是复制 3 个usizes,而不管内容如何。)

    \n

    如果您还没有测量性能并表明过度复制是一个问题,那么您一定不能假设它是没有证据的:您可能会意外地悲观而不是优化您的代码。先测量一下。

    \n
  2. \n
  3. 抽象机器的语义不是你真实机器的语义。优化编译器的全部意义在于找出将一种编译器转换为另一种编译器的最佳方法。在合理的假设下,这里的代码不太可能在打开优化的情况下产生 2 个副本如何消除一个或两个副本可能取决于代码的其余部分:不仅取决于包含它们的代码片段,还取决于数据的初始化方式等等。真实的机器性能很复杂,通常需要一次分析多于几行。同样,这就是优化编译器的全部要点:它可以进行更全面的分析,比你我更快。

    \n

    即使编译器“在桌面上”留下了一个副本,您也不应该在没有证据的情况下假设删除该副本会让事情变得更好,仅仅因为它是一个副本。先测量一下。

    \n
  4. \n
  5. 在这种情况下,无论如何,这可能并不重要。从堆中请求新的分配可能比将一堆字节从一个地方复制到另一个地方更昂贵,因此摆弄 1 个快速复制与不复制,同时忽略(看似合理的)大瓶颈可能是浪费时间。在分析应用程序或库以查看性能损失最多的地方之前,不要尝试进行优化。先测量一下。

    \n
  6. \n
\n

也可以看看

\n

由于意外地将大量数据放入堆栈而导致堆栈溢出的问题(解决方案通常是使用Vec而不是数组):

\n\n
\n

*Rc虽然是标准库的一部分,但却是用纯 Rust 代码编写的,这就是我在这里分析它的方式。Rc理论上可以受到普通代码无法实现的有保证的优化,但这恰好与本例无关。

\n

\xe2\x80\xa0 至少取决于分配器以及是否必须从操作系统获取新内存或者是否可以重新使用最近释放的分配。

\n