https://www.youtube.com/watch?v=A4cKi7PTJSs
在这个视频中,我想讨论为什么你可能会考虑在Rust代码中使用Arc<[T]>而不是Vec
Arc<[T]>对于不可变数据来说可能是一个很好的选择。如果你构建了一个大的数据序列,之后不再修改,你可以考虑使用Arc<[T]>。它非常适合存储在结构体、数组或集合中的数据,或者是需要到处传递的数据。我不是在讨论那些作为局部变量快速收集的Vec,或者仅用于一些快速临时存储的Vec。我指的是你要长期存储的数据,你可以考虑使用Arc<[T]>,特别是对于实现了Clone trait的数据。
这不应该是一个很大的惊喜,因为Clone某种程度上是Arc的超能力,我们稍后会讨论这一点。如果你经常需要克隆大型不可变数据序列,Arc可以比Vec
为什么我推荐这个呢?有三个很大的原因:
我们已经提到过,Arc有一个极其廉价的常数时间克隆。无论你指向的序列有多大,克隆它所需的时间都是相同的,只涉及增加一个整数和内存复制Arc指针本身,这非常快,而且不涉及任何内存分配,而通常克隆Vec是需要分配内存的。所以这是一个你可以通过简单地切换到Arc<[T]>就能获得的巨大优化。
Arc<[T]>只有16字节,它只需要存储一个指针和一个长度。而Vec需要存储一个指针、一个长度和一个容量,使其为24字节。虽然这只是8字节的差异,看起来不大,但如果你存储大量这样的东西,特别是在结构体或数组中,这额外的内存可能会累积起来,可能会降低缓存局部性,使得遍历这些东量图案件变得更困难。
Arc<[T]>实现了Deref<Target=[T]>,就像Vec一样。所以你可以在Arc<[T]>上做所有与Vec相同的只读操作。你可以询问其长度,可以遍历它,可以索引它。这一点非常重要,因为前两点很酷,但如果Arc<[T]>比Vec难用得多,我不会特意去推荐它们。但事实并非如此,因为它实现了Deref<Target=[T]>,它也实现了许多其他你可能需要的trait,这使得Arc<[T]>在很多情况下可以直接替代Vec
。所以前两点是很好的性能提升,第三点意味着使用Arc<[T]>基本上不比使用Vec 难,所以你不妨先考虑使用它。
我一直在讨论Arc<[T]>与Vec的对比,实际上我接下来会讨论Arc
我还想提到,任何时候使用Arc时,如果你不需要Arc提供的线程安全性,你应该总是先尝试使用Rc。我在这里说Arc是因为这是最通用的建议,但如果你可以使用Rc,你绝对应该尝试,因为它的开销比Arc低。
我还想澄清一点,我说的是Arc
假设我正在开发一个游戏,我的系统中有一个叫做MonsterID的类型,在底层它只是用一些文本来表示,我想使用String类型来管理这些文本。
首先,我可能想在我的MonsterID结构体上派生一堆trait。我想能够克隆这个东西,我想能够打印调试表示,比较它,哈希它。我还想使用serde来做一些序列化和反序列化,可能是从配置文件或者一些保存数据。所以我想在我的MonsterID结构体上加上所有这些派生,并且我希望它们都能工作。使用String时,这些都能工作。
接下来,我想在我的MonsterID结构体上有一个方法,可以将底层表示作为字符串切片获取。也许我想把它记录到控制台,也许我想在某个地方用它做分析。所以我只想以字符串切片的形式访问底层数据,这在使用String时很容易做到。
然后,我想在某个地方有一些配置数据,这将是一个从MonsterID到它们统计数据的哈希映射。你知道,它们的伤害、生命值等等,它将以MonsterID为键。这就是为什么我需要派生Eq和Hash,而使用String作为底层数据类型时,这一切都能正常工作。
接下来,我想把这些东西存储在一个大列表中,包含我曾经生成的所有敌人。注意,我在这里使用的是Vec,因为这将是我在游戏运行时不断添加的东西,我可能会将MonsterID克隆到这里,这个列表可能会变得相当大,它将是一排排的MonsterID。
接下来,我需要一些功能来基于MonsterID创建一个我将在游戏中使用的实际敌人。我可能会将MonsterID克隆到这个函数中,可能会将MonsterID克隆到敌人实例中,这样每个敌人实例都知道它是哪个怪物。
最后,假设我在一个B-tree映射中存储一些统计数据,这个映射从MonsterID到我在游戏过程中摧毁该怪物的次数。我在这里使用B-tree映射只是作为一种不同于哈希映射的数据结构,我们可以使用MonsterID作为B-tree映射中的键,因为它实现了Ord。
这里有一些不同的用例,可能还有更多。这只是一个例子,说明像这样的基本类型如何开始在整个系统中扩散,你可能需要经常克隆它,你可能需要将它存储在所有这些不同的数据结构中,突然间,这些操作的成本开始变得重要,开始累积起来。所以让我们看看当我们使用String时,克隆这个东西的成本和整体内存占用是多少。
下面是String的表示方式:
String中的实际字符串数据表示为堆上的字符缓冲区。String分配足够的内存来存储你的文本,显然,但然后它还分配了一些额外的空间,以便它可以在不需要重新分配的情况下增长。这里我们有”Goblin”和四个额外的字节备用容量,以防字符串需要进一步增长,尽管我们知道它不会,因为Goblin已经是一个完整的怪物名称了。
接下来,String结构本身由三个8字节的字组成:一个指针、一个长度和一个容量。指针直接指向字符串数据,长度指的是G-O-B-L-I-N,所以是6,容量包括末尾的额外四个字节,所以是10。
让我们看看克隆String是什么样子的。首先,我们需要克隆整个字符缓冲区,所以首先我们需要分配更多的内存,然后我们需要复制所有的字符,这是一个线性时间操作,换句话说,字符串越长,它需要的时间就越长。然后我们需要在栈上创建一个新的String结构,并将它指向新的字符缓冲区。注意,这次我们删除了额外的容量,因为String的克隆不会过度分配,它会给你一个确切大小的缓冲区。所以我们现在只有这个6的额外备用容量,这对我们现在并不真正有帮助,因为它与长度相同。
如果我们想再做一次克隆,我们必须做完全相同的事情。我们需要分配一个新的缓冲区,再次复制所有的字符,然后在栈上创建一个新的String结构,指向它。再次,我们有这个额外的容量字段,对我们来说并不真正有用。
希望你能看到,这个分配新内存的过程(顺便说一下,这是非常昂贵的)有点麻烦,而且String结构本身对于简单的目的来说有点大而且过于笨重,比如只是为了谈论goblins。
现在让我们看看同样的情况,但使用Arc
使用Arc
我们栈上的Arc只是一个指针和一个长度,它们只有16字节,因为它们没有String那个额外的容量字段。所以它只是指向我们这里的引用计数。
让我们看看克隆时会发生什么。
我们所需要做的就是复制栈上的那个结构,并增加我们的引用计数,所以现在是2。你注意到我们没有做任何类型的内存分配,我们没有对我们的字符串数据进行深度复制,我们只是在两个地方引用相同的字符串数据。
所以我们可以非常便宜地制作这个的克隆,而且字符串数据在多个Arc之间共享的事实也增加了它在我们查找时在缓存中的机会,因为任何时候我使用这四个Arc中的任何一个来读取它,它都会被加载到缓存中。相比之下,String指向的内存只有在我最近使用那个特定的String查找过时才会在缓存中。
所以你可以看到,这整个东西就轻量得多。这些Arc本身更小,所以在相同的内存量中可以容纳更多。坦率地说,这对于我们在程序中传递的不可变字符串来说,是一种更聪明的资源使用方式。
此外,我们在堆上为此付出的这两个额外字,是分摊到我们拥有的每一个Arc上的。所以它们是某种程度上被摊销的,而我们必须为栈上使用的每一个String付出一个额外的字。这两个在堆上的额外字,是的,2比1多,但它们在每个Arc实例之间共享,所以最终比每个String必须携带的那个额外字要少得多。
让我们看看如果我们直接将Arc
首先,我们所有的派生都能像以前一样工作。你可以克隆一个Arc
那么我们的as_str访问器呢?这个函数根本不需要改变,因为Arc
接下来,我们的MonsterID到敌人统计的哈希映射呢?我们的MonsterID结构体仍然实现了Eq和Hash,因为Arc
继续,我的MonsterID的Vec现在将更加缓存效率,因为我从每个MonsterID中削减了整整一个字,现在它的大小只有之前的三分之二。
我的用于基于MonsterID创建实际敌人实例的函数现在可能更高效了,因为我想象这涉及某种MonsterID的克隆,而现在我们使用Arc,克隆变得更加高效。
最后,我们的B树映射与哈希映射类似,使用不可变数据作为键类型更合适,因为无论如何你都不允许修改B树映射中的键。这个B树映射现在也会更高效,因为它存储的数据更小,而且不需要为那些不需要可变性的情况付出可变性的代价。
所以Arc
这里有一个我一直在暗示但现在要明确说出来的要点:Vec和String是用来修改一堆东西的缓冲区的。它们用于推入、弹出、扩展和截断。如果你不需要这些功能,就不要使用它们,因为它们有一些与之相关的额外成本。
你注意到Vec和String上所有有趣的方法都需要一个可变的self参数。它们都是关于修改缓冲区,改变它的形状。如果你不需要这些功能,那就不要使用Vec和String。如果你只需要查看一些数据,比如问它的长度,看它是否为空,对它进行索引,遍历它,分割它,在其中搜索,所有这些功能都是由str和[T]直接提供的,你可以通过Arc很容易地访问这两者。所以如果这就是你所需要的,你就不需要String和Vec的全部功能,而且通过不为String和Vec付费,你可以获得很多性能提升。
这就是为什么你可能想要使用Arc<[T]>而不是Vec,或者使用Arc
现在我想展示一下Arc
Arc
最后,我提到如果你不需要克隆,你可以通过使用Box
你不可能比这做得更好了。但当你克隆时,你是在对那个堆数据进行深度克隆,所以那将是一个内存分配和一个线性时间的复制。但如果你不支持克隆,在内存效率方面,你不可能做得比这更好了。所以如果这是你的用例,可以考虑Box
就这些了,这就是全部内容。让我知道你的想法。谢谢收听,下次再见。
原文链接: https://dashen.tech/2018/07/17/Use-Arc-Instead-of-Vec/
版权声明: 转载请注明出处.