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

Posted

技术标签:

【中文标题】将数据移动到 Rc/Arc 是不是总是将其从堆栈复制到堆?【英文标题】:Does moving data to Rc/Arc always copy it from the stack to the heap?将数据移动到 Rc/Arc 是否总是将其从堆栈复制到堆? 【发布时间】:2021-10-20 21:07:30 【问题描述】:

看看下面这个简单的例子:

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);

Rc的官方文档说:

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

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

    my_struct 移动到Rc 时,内存内容实际上是从堆栈复制到堆。 编译器能够解析my_struct 将被移动到Rc,因此它从一开始就把它放在堆上。

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

如果数字 2 为真,我想知道编译器是否总是能够解决此类问题。提供的示例非常简单,但我可以想象my_struct 要复杂得多,并且在移动到Rc 之前会被不同的函数多次变异。

【问题讨论】:

这可能对reddit.com/r/rust/comments/hemjx0/… 有所帮助,并且还可以搜索 rust 中的新位置。 Rc::new() 的函数调用将值移动到函数中,并且无论何时执行此操作,该值可能实际上会被复制——这里没有隐藏任何内容。编译器也可能能够优化它,但你永远无法保证它会。我建议不要担心。即使对于更复杂的值,复制一个值也很便宜,而且在几乎所有情况下,分配的成本都会使移动的成本相形见绌。可能存在一些重要的极端情况,但它们应该非常罕见。 【参考方案1】:

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

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

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

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

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

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

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

    在这种情况下,这可能并不重要。从堆请求新分配可能比从一个地方复制一堆字节到另一个地方更昂贵,因此在忽略(可能的)大瓶颈的情况下摆弄一个快速副本与无副本可能是浪费时间。在分析应用程序或库以查看性能损失最大的地方之前,不要尝试优化事物。 先测量。

另见

关于不小心放入大数据导致堆栈溢出的问题(解决方法通常是使用Vec而不是数组):

How to allocate arrays on the heap in Rust 1.0? Thread '<main>' has overflowed its stack when allocating a large array using Box

* Rc,虽然是标准库的一部分,但是是用纯 Rust 代码编写的,这就是我在这里分析的方式。 Rc 理论上可能会受到普通代码无法使用的保证优化的影响,但这恰好与这种情况无关。

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

【讨论】:

这个 Rust 抽象机器是否定义在任何地方?我可以阅读它的架构吗?你知道吗? 关于第 3 点。这就是我实际问这个问题的原因。在 C 中,分配总是显式的,在 Rust 中它可能是隐式的。我不明白为什么 Rc 需要将数据放在堆中。 Rust 中也没有隐式分配,所以我不知道你的意思。 Rc 将数据放入堆中,因为 Rc 就是这样:堆分配的共享指针。如果你不想要分配,你就不要Rc。就这么简单。 至于确切的语义,它们是一项正在进行的工作,并没有正式化,所以很遗憾我无法真正找到我的第一个声明。您可以阅读有关为 Rust 定义形式语义的论文,但我不知道从哪里开始是一个好地方。我要强调的是,形式语义只是一种抽象,并不对应于现实世界的 ABI 或性能特征。抽象机器不存在;他们没有表现。因此,如果您担心性能,那可能不是正确的树。 好吧,我想我漏掉了一点。在堆栈和Rc 中具有价值根本没有意义,因为当它在堆栈中时,您(或编译器)只需知道范围和生命周期。

以上是关于将数据移动到 Rc/Arc 是不是总是将其从堆栈复制到堆?的主要内容,如果未能解决你的问题,请参考以下文章

IE浏览器是不是有任何动作迫使他们将其从现实世界中完全拆除?

我已使用 selenium web 驱动程序 JAVA 将产品添加到手推车中,然后将其从购物车中删除。我如何断言产品是不是被移除?

如何使用 angular.js 推送通知?

将二进制数据移动到一个文件中的特定值到另一个文件中

将导航堆栈移动到更多选项卡

如何将 MongooseMap 转换为 JSON 对象以将其从 Node js 发送到 React?