<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="/feeds/style.xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>manishearth</title>
    <description>rssume processed feed for manishearth</description>
    <link>/feeds/manishearth</link>
    <atom:link href="/feeds/manishearth" rel="self" type="application/rss+xml"/>
    <lastBuildDate>Thu, 4 Jun 2026 09:58:23 +0000</lastBuildDate>
    <generator>rssume</generator>
    <item>
      <title>零值甚至为负？（零拷贝#3）</title>
      <description>[AI 摘要] 介绍databake库，通过将数据序列化为Rust const代码实现零成本数据加载，完全避免反序列化步骤。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 介绍databake库，通过将数据序列化为Rust const代码实现零成本数据加载，完全避免反序列化步骤。</div><p><em>这是关于零拷贝反序列化有趣抽象的三部分系列的第三篇。这一部分将彻底消除反序列化步骤。第一部分是关于使其更易用，可在此<a href="http://manishearth.github.io/blog/2022/08/03/zero-copy-1-not-a-yoking-matter/" rel="noopener noreferrer">找到</a>；第二部分是关于使其适用于更多类型，可在此<a href="http://manishearth.github.io/blog/2022/08/03/zero-copy-2-zero-copy-all-the-things/" rel="noopener noreferrer">找到</a>。文章可以按任何顺序阅读，但只有第一篇解释了零拷贝反序列化<em>是什么</em>。</em></p><em>

<blockquote>
  <p>当亚历山大看到自己作品的广度时，他哭了。因为再也没有拷贝可以归零了。</p>
  <p>——汉斯·格鲁伯，在设计了三个越来越疯狂的零拷贝crate之后</p>
</blockquote>

<p>本系列的<a href="http://manishearth.github.io/blog/2022/08/03/zero-copy-1-not-a-yoking-matter/" rel="noopener noreferrer">第一部分</a>试图回答“我们如何使零拷贝反序列化<em>更愉快</em>”这个问题，而<a href="http://manishearth.github.io/blog/2022/08/03/zero-copy-2-zero-copy-all-the-things/" rel="noopener noreferrer">第二部分</a>回答了“我们如何使零拷贝反序列化<em>更有用</em>？”。</p>

<p>这更进一步，提出：“如果我们能完全避免反序列化会怎样？”。</p>

<div>
            <img width="60px" height="60px" title="Confused pion" alt="Speech bubble for character Confused pion" src="http://manishearth.github.io/images/pion-nought.png">
            <div></div>
            <div>
             等等，什么？
            </div>
        </div>

<p>请听我解释。</p>

<p>正如前面文章中提到的，像<a href="https://github.com/unicode-org/icu4x" rel="noopener noreferrer">ICU4X</a>这样的国际化库需要能够加载和管理大量的国际化数据。ICU4X特别希望这个过程尽可能灵活和高效。对效率的关注是我们几乎对所有东西都使用零拷贝反序列化的原因，而对灵活性的关注则催生了一个强大且可插拔的数据加载基础设施，允许您混合搭配数据源。</p>

<p>反序列化是加载数据的一种<em>绝佳</em>方式，因为它本身就相当灵活！您可以将数据打包成一个小巧的包裹，然后从文件系统加载它！或者通过网络发送！当您拥有像零拷贝反序列化这样高效的技术时，成本很低，效果就更好了。</p>

<p>但问题在于，仍然存在成本。即使采用零拷贝反序列化，您也必须<em>验证</em>接收到的数据。这通常是人们愿意付出的代价，但并非总是如此。</p>

<p>例如，假设您是一个<a href="https://www.mozilla.org/en-US/firefox/" rel="noopener noreferrer">希望使用ICU4X的Web浏览器</a>，您<em>非常</em>关心启动时间。浏览器在启动时（以及打开新标签页时）通常需要设置许多东西，为了给用户提供流畅的体验，每一毫秒都至关重要。浏览器通常也已经附带了所需的大部分国际化数据。将宝贵的时间花在反序列化您随身携带的数据上是不理想的。</p>

<p>理想情况下，它应该像这样工作：</p>

<div><div><pre><code><span>static</span> <span>DATA</span><span>:</span> <span>&amp;</span><span>Data</span> <span>=</span> <span>&amp;</span><span>serde_json</span><span>::</span><span>deserialize!</span><span>(</span><span>include_bytes!</span><span>(</span><span>"./testdata.json"</span><span>));</span>
</code></pre></div></div>

<p>数据可以在编译时反序列化并加载到静态变量中。不幸的是，Rust的<code>const</code>支持尚未达到可以在serde的泛型框架内实现上述代码的阶段，尽管可能在一年左右后实现。</p>

<p>您<em>可以</em>编写一个非常不安全的<code>serde::Deserialize</code>版本，它操作完全可信的数据，并使用某种易于零拷贝反序列化且避免任何验证的数据格式。然而，这仍然会有一些成本：您仍然需要扫描数据以重构完整的反序列化输出。更重要的是，它将需要一个并行的不安全serde-like trait宇宙，每个人都必须派生或实现，其中即使是手动实现中的小错误也可能导致内存损坏。</p>

<div>
            <img width="60px" height="60px" title="Positive pion" alt="Speech bubble for character Positive pion" src="http://manishearth.github.io/images/pion-plus.png">
            <div></div>
            <div>
             听起来你需要一种无需验证或扫描即可零拷贝反序列化，并且可以安全生成的格式。但这种格式不存在，对吧？
            </div>
        </div>

<p>它存在。</p>

<p>……但你不会喜欢我接下来要讲的。</p>

<div>
            <img width="60px" height="60px" title="Positive pion" alt="Speech bubble for character Positive pion" src="http://manishearth.github.io/images/pion-plus.png">
            <div></div>
            <div>
             哦，不。
            </div>
        </div>

<p>确实存在这样一种格式：<em>Rust代码</em>。具体来说，是<code>static</code>中的Rust代码。编译后，Rust的<code>static</code>加载基本上是“免费”的，除了将内存页面调入时涉及的典型成本。Rust编译器信任自己擅长代码生成，因此在从内存加载已编译的<code>static</code>时不需要验证。不过，可能存在代码生成错误，但我们对程序的其他部分也必须信任编译器这一点！</p>

<p>这甚至比“零拷贝反序列化”更“零”。常规的“零拷贝反序列化”仍然涉及扫描甚至验证步骤，它更多的是关于“零分配”，而不是真正避免<em>所有</em>复制。另一方面，当您加载Rust statics时，确实没有任何复制或其他操作；它已经作为一个<code>&amp;'static</code>引用准备就绪！</p>

<p>我们只需要找到一种方法将数据“序列化为<code>const</code> Rust代码”，这样生成的Rust代码就可以直接编译进二进制文件，需要将可信数据加载到ICU4X中的人们就可以免费加载它了！</p>

<div>
            <img width="60px" height="60px" title="Confused pion" alt="Speech bubble for character Confused pion" src="http://manishearth.github.io/images/pion-nought.png">
            <div></div>
            <div>
             在这个上下文中，“<code>const</code>代码”是什么意思？
            </div>
        </div>

<p>在Rust中，<code>const</code>代码本质上是可被证明没有副作用的代码，并且它是唯一允许在<code>static</code>、<code>const</code>和<code>const fn</code>中使用的代码类型。</p>

<div>
            <img width="60px" height="60px" title="Confused pion" alt="Speech bubble for character Confused pion" src="http://manishearth.github.io/images/pion-nought.png">
            <div></div>
            <div>
             我明白了！这段代码实际上必须是“常量”吗？
            </div>
        </div>

<p>不完全是！Rust支持在<code>const</code>代码中使用突变甚至像for循环这样的东西！最终，它必须是那种<em>可以</em>在编译时计算且行为没有差异的代码：因此不能读取文件或网络，也不能使用随机数。</p>

<p>很长一段时间内，<code>const</code>只允许非常简单的代码，但在过去一年里，该环境能做的事情范围已经大大扩展，实际上可以在这里做复杂的事情，这正是使我们能够以合理方式实现“序列化为Rust代码”的关键。</p>

<h2 id="databake"><code>databake</code></h2>

<p><em>此处的很多设计也可以在<a href="https://docs.google.com/document/d/192l7yr6hVnG11Dr8a7mDLonIb6c8rr6zq-iswrZtlXE/edit" rel="noopener noreferrer">设计文档</a>中找到。虽然我负责了这个crate的主要设计，但它几乎完全由<a href="https://github.com/robertbastian" rel="noopener noreferrer">Robert</a>实现，他还致力于将其集成到ICU4X中，并在此过程中清理了设计。</em></p>

<p>这就是<a href="https://docs.rs/databake" rel="noopener noreferrer"><code>databake</code></a>（原名<code>crabbake</code>）。<code>databake</code>是一个提供此功能的crate；能够将您的类型序列化为<code>const</code>代码，然后可在<code>static</code>中使用，从而实现真正的零成本数据加载，无需反序列化！</p>

<p><code>databake</code>的核心入口点是<code>Bake</code> trait：</p>

<div><div><pre><code><span>pub</span> <span>trait</span> <span>Bake</span> <span>{</span>
    <span>fn</span> <span>bake</span><span>(</span><span>&amp;</span><span>self</span><span>,</span> <span>ctx</span><span>:</span> <span>&amp;</span><span>CrateEnv</span><span>)</span> <span>-&gt;</span> <span>TokenStream</span><span>;</span>
<span>}</span>
</code></pre></div></div>

<p><code>TokenStream</code>是通常在Rust<a href="https://doc.rust-lang.org/reference/procedural-macros.html" rel="noopener noreferrer">过程宏</a>中用于表示一段Rust代码的类型。<code>Bake</code> trait允许您获取一个类型的实例，并将其转换为表示相同值的Rust代码。</p>

<p><code>CrateEnv</code>对象用于跟踪需要哪些crate，以便生成此代码的工具可以告知用户需要哪些直接依赖项。</p>

<p>此trait通过<a href="https://docs.rs/databake/0.1.1/databakee/derive.Bake.html" rel="noopener noreferrer"><code>#[derive(Bake)]</code></a>自定义派生宏进行增强，可将其自动应用于大多数类型：</p>

<div><div><pre><code><span>// inside crate `bar`, module `module.rs`</span>

<span>use</span> <span>databake</span><span>::</span><span>Bake</span><span>;</span>

<span>#[derive(Bake)]</span>
<span>#[databake(path</span> <span>=</span> <span>bar::module)]</span>
<span>pub</span> <span>struct</span> <span>Person</span><span>&lt;</span><span>'a</span><span>&gt;</span> <span>{</span>
   <span>pub</span> <span>name</span><span>:</span> <span>&amp;</span><span>'a</span> <span>str</span><span>,</span>
   <span>pub</span> <span>age</span><span>:</span> <span>u32</span><span>,</span>
<span>}</span>
</code></pre></div></div>

<p>与大多数自定义派生宏一样，这仅适用于包含其他已实现<code>Bake</code>类型的结构体和枚举。大多数不涉及强制分配的类型都应该能够实现。</p>

<h2 id="how-to-use-it">如何使用它</h2>

<p><code>databake</code>本身并不规定任何特定的代码生成策略。它可以在过程宏中、<code>build.rs</code>中使用，甚至可以在单独的二进制文件中使用。ICU4X采用后者，因为这正是ICU4X数据生成模型的工作方式：客户端可以使用该二进制文件来自定义所需数据的格式和内容。</p>

<p>因此，使用此crate的一种典型方式可能是在<code>build.rs</code>中这样做：</p>

<div><div><pre><code><span>use</span> <span>some_dep</span><span>::</span><span>Data</span><span>;</span>
<span>use</span> <span>databake</span><span>::</span><span>Bake</span><span>;</span>
<span>use</span> <span>quote</span><span>::</span><span>quote</span><span>;</span>

<span>fn</span> <span>main</span><span>()</span> <span>{</span>
   <span>// load data from file</span>
   <span>let</span> <span>json_data</span> <span>=</span> <span>include_str!</span><span>(</span><span>"data.json"</span><span>);</span>

   <span>// deserialize from json</span>
   <span>let</span> <span>my_data</span><span>:</span> <span>Data</span> <span>=</span> <span>serde_json</span><span>::</span><span>from_str</span><span>(</span><span>json_data</span><span>);</span>

   <span>// get a token tree out of it</span>
   <span>let</span> <span>baked</span> <span>=</span> <span>my_data</span><span>.bake</span><span>();</span>


   <span>// Construct rust code with this in a static</span>
   <span>// The quote macro is used by procedural macros to do easy codegen,</span>
   <span>// but it's useful in build scripts as well.</span>
   <span>let</span> <span>my_data_rs</span> <span>=</span> <span>quote!</span> <span>{</span>
      <span>use</span> <span>some_dep</span><span>::</span><span>Data</span><span>;</span>
      <span>static</span> <span>MY_DATA</span><span>:</span> <span>Data</span> <span>=</span> #<span>baked</span><span>;</span>
   <span>}</span>

   <span>// Write to file</span>
   <span>let</span> <span>out_dir</span> <span>=</span> <span>env</span><span>::</span><span>var_os</span><span>(</span><span>"OUT_DIR"</span><span>)</span><span>.unwrap</span><span>();</span>
   <span>let</span> <span>dest_path</span> <span>=</span> <span>Path</span><span>::</span><span>new</span><span>(</span><span>&amp;</span><span>out_dir</span><span>)</span><span>.join</span><span>(</span><span>"data.rs"</span><span>);</span>
   <span>fs</span><span>::</span><span>write</span><span>(</span>
      <span>&amp;</span><span>dest_path</span><span>,</span>
      <span>&amp;</span><span>my_data_rs</span><span>.to_string</span><span>()</span>
   <span>)</span><span>.unwrap</span><span>();</span>

   <span>// (Optional step omitted: run rustfmt on the file)</span>

   <span>// tell Cargo that we depend on this file</span>
   <span>println!</span><span>(</span><span>"cargo:rerun-if-changed=src/data.json"</span><span>);</span>
<span>}</span>
</code></pre></div></div>

<h2 id="what-it-look-like">它看起来像什么</h2>

<p>ICU4X将其所有测试数据生成为JSON、<a href="https://docs.rs/postcard" rel="noopener noreferrer"><code>postcard</code></a>和“baked”格式。例如，对于<a href="https://github.com/unicode-org/icu4x/blob/7b52dbfe57043da5459c12627671a779d467dc0f/provider/testdata/data/json/decimal/symbols%401/ar-EG.json" rel="noopener noreferrer">表示特定语言环境如何处理数字的这个JSON数据</a>，“baked”数据看起来像<a href="https://github.com/unicode-org/icu4x/blob/7b52dbfe57043da5459c12627671a779d467dc0f/provider/testdata/data/baked/decimal/symbols_v1.rs#L24-L41" rel="noopener noreferrer">这样</a>。这是一个相当简单的数据类型，但我们也用它处理更复杂的数据，比如<a href="https://raw.githubusercontent.com/unicode-org/icu4x/7b52dbfe57043da5459c12627671a779d467dc0f/provider/testdata/data/baked/datetime/datesymbols_v1.rs" rel="noopener noreferrer">日期时间符号数据</a>，可惜它太大了，GitHub无法正常渲染。</p>

<p>ICU4X生成此数据的代码在<a href="https://github.com/unicode-org/icu4x/blob/3f4d841ef0b168031d837433d075308bbebf34b7/provider/datagen/src/databake.rs" rel="noopener noreferrer">此文件中</a>。它很复杂主要是因为ICU4X的数据生成管道高度可配置且复杂。它所做的核心事情是，对于每一块数据，它<a href="https://github.com/unicode-org/icu4x/blob/3f4d841ef0b168031d837433d075308bbebf34b7/provider/datagen/src/databake.rs#L118" rel="noopener noreferrer">调用<code>tokenize()</code></a>，这是<a href="https://github.com/unicode-org/icu4x/blob/882e23403327620e4aafde28a9a407bcc6245a54/provider/core/src/datagen/payload.rs#L131-L136" rel="noopener noreferrer">对数据调用<code>.bake()</code>并进行其他操作</a>的一个薄包装。然后它获取所有数据并将其组织成类似上面链接的文件，每个数据块都有一个静态变量。在我们的例子中，我们将所有这些生成的Rust代码作为一个模块包含在我们的“testdata”crate中，但这里有很多可能性！</p>

<p>对于我们的“测试”数据，目前以<a href="https://docs.rs/postcard" rel="noopener noreferrer"><code>postcard</code></a>格式（针对轻量级优化）是2.7 MB，相同的数据最终在JSON中是11 MB，在生成的Rust代码中是18 MB！那……是大量的Rust代码，rust-analyzer等工具加载它都很吃力。不过，一旦编译进二进制文件，它当然会小得多，但这更难衡量，因为在baked版本中，Rust非常积极地将未使用的数据优化掉（它有充足的机会这样做）。从各种非正式测试来看，大约2MB的去重postcard数据对应约500KB的去重baked数据。这是合理的，因为可以预期baked数据接近不应用一些重度压缩的数据的理论最小值。此外，虽然我们在每语言环境级别去重baked数据，但它可以利用LLVM进一步去重statics的能力，因此，例如，如果两个不同的语言环境对于给定数据键<sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">1</a></sup>的数据<em>大部分</em>相同但有些差异，LLVM可能可以使用相同的statics来处理子数据。</p>

<h2 id="limitations">局限性</h2>

<p>Rust中的<code>const</code>支持还有很长的路要走。例如，它还不支持创建通常在堆上的对象，如<code>String</code>，尽管<a href="https://github.com/rust-lang/const-eval/issues/20" rel="noopener noreferrer">他们正在努力允许这样做</a>。这对我们来说不是大问题；我们所有的数据都已经支持零拷贝反序列化，这意味着对于我们的每个数据类型实例，都有<em>某种方式</em>将其表示为另一个<code>static</code>的借用。</p>

<p>一个更麻烦的限制是您无法在<code>const</code>环境中与trait交互。在某种程度上，如果可能的话，使<code>serde</code>管道支持<code>const</code><sup id="fnref:2"><a href="#fn:2" rel="noopener noreferrer">2</a></sup>也可以实现此crate的目的，那么本文开头的代码片段就可以工作：</p>

<div><div><pre><code><span>static</span> <span>DATA</span><span>:</span> <span>&amp;</span><span>Data</span> <span>=</span> <span>&amp;</span><span>serde_json</span><span>::</span><span>deserialize!</span><span>(</span><span>include_bytes!</span><span>(</span><span>"./testdata.json"</span><span>));</span>
</code></pre></div></div>

<p>这意味着对于像<code>ZeroVec</code>（参见<a href="http://manishearth.github.io/blog/2022/08/03/zero-copy-2-zero-copy-all-the-things/" rel="noopener noreferrer">第二部分</a>）这样的东西，我们实际上无法仅仅使其安全构造函数为<code>const</code>并传入待验证的数据——验证代码都在trait后面——因此我们必须不安全地构造它们。这有点不幸，但最终如果<code>zerovec</code>的字节表示往返有问题，我们会有更大的麻烦，所以这不是引入新的不安全表面。我们仍然能够在<em>生成</em>baked数据时进行验证，只是无法让编译器在同意编译<code>const</code>代码之前重新验证。</p>

<h2 id="try-it-out">试一试！</h2>

<p><a href="https://docs.rs/databake" rel="noopener noreferrer"><code>databake</code></a>与<a href="https://docs.rs/yoke" rel="noopener noreferrer"><code>yoke</code></a>和<a href="https://docs.rs/zerovec" rel="noopener noreferrer"><code>zerovec</code></a>相比成熟度低得多，但到目前为止它似乎运行得相当好。试试看！让我知道你的想法！</p>

<p><em>感谢<a href="https://twitter.com/plaidfinch" rel="noopener noreferrer">Finch</a>、<a href="https://twitter.com/yaahc_" rel="noopener noreferrer">Jane</a>、<a href="https://github.com/sffc" rel="noopener noreferrer">Shane</a>和<a href="https://github.com/robertbastian" rel="noopener noreferrer">Robert</a>审阅本文草稿</em></p>

<div>
  <ol>
    <li id="fn:1">
      <p>在ICU4X中，“数据键”可用于指代特定类型的数据，例如小数符号数据具有<code>decimal/symbols@1</code>数据键。<a href="#fnref:1" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn:2">
      <p>请注意，这并非易事，但它很可能会与生态系统很好地集成。<a href="#fnref:2" rel="noopener noreferrer">↩</a></p>
    </li>
  </ol>
</div></em><p><em>由 mimo-v2.5 模型翻译，花费 14361 tokens</em></p>]]></content:encoded>
      <link>http://manishearth.github.io/blog/2022/08/03/zero-copy-3-so-zero-its-dot-dot-dot-negative/</link>
      <guid isPermaLink="false">http://manishearth.github.io/blog/2022/08/03/zero-copy-3-so-zero-its-dot-dot-dot-negative</guid>
      <pubDate>Wed, 3 Aug 2022 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>万物皆可零拷贝！（零拷贝 #2）</title>
      <description>[AI 摘要] 本文介绍了为Rust语言开发的zerovec crate，它通过ZeroVec和VarZeroVec类型，扩展了零拷贝反序列化对更复杂数据类型（如整数向量和字符串向量）的支持。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 本文介绍了为Rust语言开发的zerovec crate，它通过ZeroVec和VarZeroVec类型，扩展了零拷贝反序列化对更复杂数据类型（如整数向量和字符串向量）的支持。</div><p><em>这是我关于零拷贝反序列化的一些有趣抽象概念的三部分系列文章的第二部分。第一部分是关于使其更易于使用，可以在<a href="http://manishearth.github.io/blog/2022/08/03/zero-copy-1-not-a-yoking-matter/" rel="noopener noreferrer">这里</a>找到；而第三部分是关于完全消除反序列化步骤，可以在<a href="http://manishearth.github.io/blog/2022/08/03/zero-copy-3-so-zero-its-dot-dot-dot-negative/" rel="noopener noreferrer">这里</a>找到。这些文章可以按任何顺序阅读，但只有第一篇包含了对什么是零拷贝反序列化的解释。</em></p>

<h2 id="background">背景</h2>

<p><em>本节与上一篇文章相同，如果您已经读过，可以跳过。</em></p>

<p>在过去的一年半里，我一直全职参与<a href="https://github.com/unicode-org/icu4x" rel="noopener noreferrer">ICU4X</a>的工作，这是一个在Unicode联盟下由多家公司协作构建的新的国际化库。</p>

<p>关于ICU4X我能说的很多，但为了聚焦一个核心价值主张：我们希望它在数据和代码方面都是<em>模块化的</em>。我们希望ICU4X能在嵌入式平台上使用，因为那里内存很宝贵。我们希望受下载大小限制的应用程序能够支持所有语言，而不是因为无法负担打包所有数据而只能选择少数几种流行语言。为此，我们希望数据加载是<em>快速的</em>且可插拔的。用户应该能够为他们的具体用例设计自己的数据加载策略。</p>

<p>请注意，执行正确国际化的一个关键部分是<em>数据</em>。不同的区域设置<sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">1</a></sup>有不同的做法，所有关于这些的信息都需要有地方存储，最好不要放在代码里。你需要数据来说明特定区域设置如何格式化日期<sup id="fnref:2"><a href="#fn:2" rel="noopener noreferrer">2</a></sup>，或者特定语言中复数如何工作，或者如何准确地分割像泰语这样通常没有空格分隔的语言，以便在适当的位置插入换行符。</p>

<p>鉴于对数据的关注，一个对我们来说<em>非常</em>有吸引力的选择是零拷贝反序列化。在努力做好零拷贝反序列化的过程中，我们构建了一些很酷的新库，本文就是关于其中一个的。</p>

<h2 id="what-can-you-zero-copy">哪些东西可以零拷贝？</h2>

<div>
            <img width="60px" height="60px" title="Positive pion" alt="Speech bubble for character Positive pion" src="http://manishearth.github.io/images/pion-plus.png">
            <div></div>
            <div>
             如果您不熟悉零拷贝反序列化，请查看<a href="http://manishearth.github.io/blog/2022/08/03/zero-copy-1-not-a-yoking-matter/" rel="noopener noreferrer">上一篇文章</a>中的解释！
            </div>
        </div>

<p>在<a href="http://manishearth.github.io/blog/2022/08/03/zero-copy-1-not-a-yoking-matter/" rel="noopener noreferrer">上一篇文章</a>中，我们探讨了通过消除生命周期（lifetime）如何使零拷贝反序列化更易于使用。本质上，我们是在扩展<em>你能用零拷贝数据做什么</em>的能力。</p>

<p>本文是关于扩展<em>我们能制作什么</em>为零拷贝数据。</p>

<p>我们之前看到了这个结构体：</p>

<div><div><pre><code><span>#[derive(Serialize,</span> <span>Deserialize)]</span>
<span>struct</span> <span>Person</span> <span>{</span>
    <span>// 此字段的构造几乎免费</span>
    <span>age</span><span>:</span> <span>u8</span><span>,</span>
    <span>// 构造此字段将涉及一次小的内存分配和复制</span>
    <span>name</span><span>:</span> <span>String</span><span>,</span>
    <span>// 这可能需要一些时间</span>
    <span>rust_files_written</span><span>:</span> <span>Vec</span><span>&lt;</span><span>String</span><span>&gt;</span><span>,</span>
<span>}</span>
</code></pre></div></div>

<p>并通过将其替换为<code>Cow&lt;'a, str&gt;</code>使<code>name</code>字段实现了零拷贝。然而，我们无法对<code>rust_files_written</code>字段做同样的事情，因为<a href="https://docs.rs/serde" rel="noopener noreferrer"><code>serde</code></a>除了<code>[u8]</code>和<code>str</code>之外，并不处理其他事物的零拷贝反序列化。更不用说像<code>Vec&lt;String&gt;</code>（作为<code>&amp;[&amp;str]</code>）这样的嵌套集合了，即使是<code>Vec&lt;u32&gt;</code>（作为<code>&amp;[u32]</code>）也无法轻松实现零拷贝！</p>

<p>这并不是零拷贝反序列化中的一个根本性限制，事实上，优秀的<a href="https://docs.rs/rkyv" rel="noopener noreferrer"><code>rkyv</code></a>库能够支持这样的数据。然而，它不像<code>str</code>和<code>[u8]</code>那样唾手可得，<a href="https://docs.rs/serde" rel="noopener noreferrer"><code>serde</code></a>希望不在这方面选择任何权衡，而是将其留给用户，这是可以理解的。</p>

<p>那么，这里真正的问题是什么？</p>

<h2 id="blefuscudian-bewilderment">字节序、对齐和间接寻址之惑</h2>

<p>简短的回答是：字节序、对齐，以及对于<code>Vec&lt;String&gt;</code>，还有间接寻址。</p>

<p>请看，零拷贝反序列化的工作方式是直接获取指向内存的指针并将其声明为期望的值。为了使其工作，该数据<em>必须</em>是在所有机器上看起来相同的类型，并且可以合法地获取其引用。</p>

<p>这对<code>[u8]</code>和<code>str</code>来说非常直接，它们的数据在每个系统上都是相同的。虽然<code>str</code>确实需要一个验证步骤来确保它是有效的UTF-8，但零拷贝序列化的总体思路是用更便宜的验证来替代昂贵的反序列化，所以我们对此没有问题。</p>

<p>另一方面，<code>Vec&lt;String&gt;</code>的借用版本<code>&amp;[&amp;str]</code>即使在同一个系统上程序的不同执行中，也不太可能看起来相同，因为它包含指针（间接寻址），这些指针会根据数据源在每次执行时发生变化！</p>

<p>指针很棘手。那<code>Vec&lt;u32&gt;</code>/<code>[u32]</code>呢？一堆整数总该没问题了吧？</p>

<figure><img src="http://manishearth.github.io/images/post/castlevania-data.png" width="400"><figcaption><p><small>德古拉，正在传授关于零拷贝反序列化的智慧。</small></p>
</figcaption></figure>

<p>这就是字节序和对齐发挥作用的地方。首先，一个<code>u32</code>在不同系统上并非完全相同，有些系统是“大端字节序”，整数<code>0x00ABCDEF</code>在内存中会表示为<code>[0x00, 0xAB, 0xCD, 0xEF]</code>，而其他系统是“小端字节序”，会表示为<code>[0xEF, 0xCD, 0xAB, 0x00]</code>。如今大多数系统是小端字节序，但并非全部，所以你可能需要关心这一点。</p>

<p>这意味着，如果我们在小端字节序系统上序列化一个<code>[u32]</code>，然后天真地在大端字节序系统上进行零拷贝反序列化，它会完全乱码。</p>

<p>其次，许多系统对像<code>u32</code>这样的类型施加<em>对齐</em>限制。一个<code>u32</code>不能在任意旧的内存地址上找到，在大多数现代系统上，它必须位于4的倍数的内存地址上。类似地，一个<code>u64</code>必须位于8的倍数的内存地址上，依此类推。然而，正在序列化的数据子部分可能位于任何地址。可以设计一个序列化框架，强制数据中的特定字段具有特定的对齐方式（<a href="https://docs.rs/rkyv/latest/rkyv/util/struct.AlignedVec.html" rel="noopener noreferrer">rkyv具有此功能</a>），但这有点棘手，并且需要你对原始加载数据的对齐方式有控制权，而这并不是serde模型的一部分。</p>

<p>那么我们如何解决这个问题呢？</p>

<h2 id="zerovec-and-varzerovec">ZeroVec 和 VarZeroVec</h2>

<p><em>此处的许多设计可以在<a href="https://github.com/unicode-org/icu4x/blob/main/utils/zerovec/design_doc.md" rel="noopener noreferrer">设计文档</a>中找到说明</em></p>

<p>在<a href="https://github.com/unicode-org/icu4x/issues/78#issuecomment-817090204" rel="noopener noreferrer">与Shane进行了大量讨论</a>之后，我们设计并编写了<a href="https://docs.rs/zerovec" rel="noopener noreferrer"><code>zerovec</code></a>，这是一个试图以与<a href="https://docs.rs/serde" rel="noopener noreferrer"><code>serde</code></a>兼容的方式解决此问题的crate。</p>

<p>该crate的核心抽象是两种类型：<a href="https://docs.rs/zerovec/latest/zerovec/enum.ZeroVec.html" rel="noopener noreferrer"><code>ZeroVec</code></a>和<a href="https://docs.rs/zerovec/latest/zerovec/enum.VarZeroVec.html" rel="noopener noreferrer"><code>VarZeroVec</code></a>，它们本质上是零拷贝启用的<code>Cow&lt;'a, [T]&gt;</code>版本，分别用于固定大小和可变大小的<code>T</code>类型。</p>

<p><a href="https://docs.rs/zerovec/latest/zerovec/enum.ZeroVec.html" rel="noopener noreferrer"><code>ZeroVec</code></a>可以用于任何实现<a href="https://docs.rs/zerovec/latest/zerovec/ule/trait.ULE.html" rel="noopener noreferrer"><code>ULE</code></a>的类型（稍后会解释其含义），默认情况下包括所有整数类型，并且可以扩展到<em>大多数</em><code>Copy</code>类型。它类似于<code>&amp;[T]</code>，但返回的是其元素的副本而不是<em>引用</em>。虽然<a href="https://docs.rs/zerovec/latest/zerovec/enum.ZeroVec.html" rel="noopener noreferrer"><code>ZeroVec</code></a>是一个类似<code>Cow</code>的借用或拥有类型<sup id="fnref:3"><a href="#fn:3" rel="noopener noreferrer">3</a></sup>，但有一个完全借用的变体<a href="https://docs.rs/zerovec/latest/zerovec/struct.ZeroSlice.html" rel="noopener noreferrer"><code>ZeroSlice</code></a>，它可以被解引用得到。</p>

<p>类似地，<a href="https://docs.rs/zerovec/latest/zerovec/enum.VarZeroVec.html" rel="noopener noreferrer"><code>VarZeroVec</code></a>可以与实现<a href="https://docs.rs/zerovec/latest/zerovec/ule/trait.VarULE.html" rel="noopener noreferrer"><code>VarULE</code></a>的类型一起使用（例如<code>str</code>）。它<em>能够</em>提供引用，<code>VarZeroVec&lt;str&gt;</code>的行为非常类似于<code>&amp;[str]</code>在Rust中允许存在的行为。你甚至可以嵌套它们，制作像<code>VarZeroVec&lt;VarZeroSlice&lt;ZeroSlice&lt;u32&gt;&gt;&gt;</code>这样的类型，它是<code>Vec&lt;Vec&lt;Vec&lt;u32&gt;&gt;&gt;</code>的零拷贝等价物。</p>

<p>还有一个<a href="https://docs.rs/zerovec/latest/zerovec/enum.ZeroMap.html" rel="noopener noreferrer"><code>ZeroMap</code></a>类型，它提供了一个基于二分搜索的映射，可与<a href="https://docs.rs/zerovec/latest/zerovec/enum.ZeroVec.html" rel="noopener noreferrer"><code>ZeroVec</code></a>或<a href="https://docs.rs/zerovec/latest/zerovec/enum.VarZeroVec.html" rel="noopener noreferrer"><code>VarZeroVec</code></a>兼容的类型一起工作。</p>

<p>例如，要使以下结构体零拷贝：</p>

<div><div><pre><code><span>#[derive(serde::Serialize,</span> <span>serde::Deserialize)]</span>
<span>struct</span> <span>DataStruct</span> <span>{</span>
    <span>nums</span><span>:</span> <span>Vec</span><span>&lt;</span><span>u32</span><span>&gt;</span><span>,</span>
    <span>chars</span><span>:</span> <span>Vec</span><span>&lt;</span><span>char</span><span>&gt;</span><span>,</span>
    <span>strs</span><span>:</span> <span>Vec</span><span>&lt;</span><span>String</span><span>&gt;</span><span>,</span>
<span>}</span>
</code></pre></div></div>

<p>你可以这样做：</p>

<div><div><pre><code><span>#[derive(serde::Serialize,</span> <span>serde::Deserialize)]</span>
<span>pub</span> <span>struct</span> <span>DataStruct</span><span>&lt;</span><span>'data</span><span>&gt;</span> <span>{</span>
    <span>#[serde(borrow)]</span>
    <span>nums</span><span>:</span> <span>ZeroVec</span><span>&lt;</span><span>'data</span><span>,</span> <span>u32</span><span>&gt;</span><span>,</span>
    <span>#[serde(borrow)]</span>
    <span>chars</span><span>:</span> <span>ZeroVec</span><span>&lt;</span><span>'data</span><span>,</span> <span>char</span><span>&gt;</span><span>,</span>
    <span>#[serde(borrow)]</span>
    <span>strs</span><span>:</span> <span>VarZeroVec</span><span>&lt;</span><span>'data</span><span>,</span> <span>str</span><span>&gt;</span><span>,</span>
<span>}</span>
</code></pre></div></div>

<p>反序列化后，数据可以通过<code>data.nums.get(index)</code>或<code>data.strs[index]</code>等方式进行访问。</p>

<p>自定义类型也可以通过一些努力在这些类型中得到支持，如果你想让以下复杂数据实现零拷贝：</p>

<div><div><pre><code><span>#[derive(Copy,</span> <span>Clone,</span> <span>PartialEq,</span> <span>Eq,</span> <span>Ord,</span> <span>PartialOrd,</span> <span>serde::Serialize,</span> <span>serde::Deserialize)]</span>
<span>struct</span> <span>Date</span> <span>{</span>
    <span>y</span><span>:</span> <span>u64</span><span>,</span>
    <span>m</span><span>:</span> <span>u8</span><span>,</span>
    <span>d</span><span>:</span> <span>u8</span>
<span>}</span>

<span>#[derive(Clone,</span> <span>PartialEq,</span> <span>Eq,</span> <span>Ord,</span> <span>PartialOrd,</span> <span>serde::Serialize,</span> <span>serde::Deserialize)]</span>
<span>struct</span> <span>Person</span> <span>{</span>
    <span>birthday</span><span>:</span> <span>Date</span><span>,</span>
    <span>favorite_character</span><span>:</span> <span>char</span><span>,</span>
    <span>name</span><span>:</span> <span>String</span><span>,</span>
<span>}</span>

<span>#[derive(serde::Serialize,</span> <span>serde::Deserialize)]</span>
<span>struct</span> <span>Data</span> <span>{</span>
    <span>important_dates</span><span>:</span> <span>Vec</span><span>&lt;</span><span>Date</span><span>&gt;</span><span>,</span>
    <span>important_people</span><span>:</span> <span>Vec</span><span>&lt;</span><span>Person</span><span>&gt;</span><span>,</span>
    <span>birthdays_to_people</span><span>:</span> <span>HashMap</span><span>&lt;</span><span>Date</span><span>,</span> <span>Person</span><span>&gt;</span>
<span>}</span>
</code></pre></div></div>

<p>你可以这样做：</p>

<div><div><pre><code><span>// 用于 ZeroVec 的自定义固定大小 ULE 类型</span>
<span>#[zerovec::make_ule(DateULE)]</span>
<span>#[derive(Copy,</span> <span>Clone,</span> <span>PartialEq,</span> <span>Eq,</span> <span>Ord,</span> <span>PartialOrd,</span> <span>serde::Serialize,</span> <span>serde::Deserialize)]</span>
<span>struct</span> <span>Date</span> <span>{</span>
    <span>y</span><span>:</span> <span>u64</span><span>,</span>
    <span>m</span><span>:</span> <span>u8</span><span>,</span>
    <span>d</span><span>:</span> <span>u8</span>
<span>}</span>

<span>// 用于 VarZeroVec 的自定义可变大小 VarULE 类型</span>
<span>#[zerovec::make_varule(PersonULE)]</span>
<span>#[zerovec::derive(Serialize,</span> <span>Deserialize)]</span> <span>// 为 PersonULE 添加 Serde 实现</span>
<span>#[derive(Clone,</span> <span>PartialEq,</span> <span>Eq,</span> <span>Ord,</span> <span>PartialOrd,</span> <span>serde::Serialize,</span> <span>serde::Deserialize)]</span>
<span>struct</span> <span>Person</span><span>&lt;</span><span>'data</span><span>&gt;</span> <span>{</span>
    <span>birthday</span><span>:</span> <span>Date</span><span>,</span>
    <span>favorite_character</span><span>:</span> <span>char</span><span>,</span>
    <span>#[serde(borrow)]</span>
    <span>name</span><span>:</span> <span>Cow</span><span>&lt;</span><span>'data</span><span>,</span> <span>str</span><span>&gt;</span><span>,</span>
<span>}</span>

<span>#[derive(serde::Serialize,</span> <span>serde::Deserialize)]</span>
<span>struct</span> <span>Data</span><span>&lt;</span><span>'data</span><span>&gt;</span> <span>{</span>
    <span>#[serde(borrow)]</span>
    <span>important_dates</span><span>:</span> <span>ZeroVec</span><span>&lt;</span><span>'data</span><span>,</span> <span>Date</span><span>&gt;</span><span>,</span>
    <span>// 注意：VarZeroVec 必须直接引用未确定大小的 ULE 类型</span>
    <span>#[serde(borrow)]</span>
    <span>important_people</span><span>:</span> <span>VarZeroVec</span><span>&lt;</span><span>'data</span><span>,</span> <span>PersonULE</span><span>&gt;</span><span>,</span>
    <span>#[serde(borrow)]</span>
    <span>birthdays_to_people</span><span>:</span> <span>ZeroMap</span><span>&lt;</span><span>'data</span><span>,</span> <span>Date</span><span>,</span> <span>PersonULE</span><span>&gt;</span>
<span>}</span>
</code></pre></div></div>

<p>不幸的是，内部的“ULE类型”工作原理并没有<em>完全</em>对用户隐藏，特别是对于<code>VarZeroVec</code>兼容的类型，但该crate做了很多努力来使其易于使用。</p>

<p>通常，<code>ZeroVec</code>应用于固定大小且实现<code>Copy</code>的类型，而<code>VarZeroVec</code>则应用于逻辑上包含可变数据量的类型，如向量、映射、字符串和它们的聚合体。<code>VarZeroVec</code>将始终与动态大小类型一起使用，提供对该类型的引用。</p>

<p>我之前提到过这些类型类似于<code>Cow&lt;'a, T&gt;</code>；它们可以以可变拥有的方式处理，但这并不是该crate的主要关注点。特别是，<code>VarZeroVec&lt;T&gt;</code>的修改速度将明显慢于<code>Vec&lt;String&gt;</code>之类的东西，因为所有操作都是在相同的缓冲区格式上完成的。该crate的一般理念是，你可能在性能约束不大的情况下<em>生成</em>数据，但你希望<em>读取</em>数据的操作是快速的。因此，在必要时，该crate会用修改性能来交换反序列化/读取性能。尽管如此，它并不是特别慢，只是需要注意并在必要时进行基准测试。</p>

<h2 id="how-it-works">工作原理</h2>

<p>该crate的大部分建立在<a href="https://docs.rs/zerovec/latest/zerovec/ule/trait.ULE.html" rel="noopener noreferrer"><code>ULE</code></a>和<a href="https://docs.rs/zerovec/latest/zerovec/ule/trait.VarULE.html" rel="noopener noreferrer"><code>VarULE</code></a>这两个trait之上。两者都是<code>unsafe</code> trait（尽管如上所示，大多数用户无需手动实现它们）。“ULE”代表“未对齐小端字节序”，标记那些没有对齐要求且在不同字节序间具有相同表示（在相关时偏好与小端字节序表示相同）的类型<sup id="fnref:4"><a href="#fn:4" rel="noopener noreferrer">4</a></sup>。</p>

<p>还有一个安全的<a href="https://docs.rs/zerovec/latest/zerovec/ule/trait.AsULE.html" rel="noopener noreferrer"><code>AsULE</code></a> trait，允许在类型与其对应的<code>ULE</code>类型之间进行转换。</p>

<div><div><pre><code><span>pub</span> <span>unsafe</span> <span>trait</span> <span>ULE</span><span>:</span> <span>Sized</span> <span>+</span> <span>Copy</span> <span>+</span> <span>'static</span> <span>{</span>
    <span>// 验证一个字节切片是否适合被视为此类型的引用</span>
    <span>fn</span> <span>validate_byte_slice</span><span>(</span><span>bytes</span><span>:</span> <span>&amp;</span><span>[</span><span>u8</span><span>])</span> <span>-&gt;</span> <span>Result</span><span>&lt;</span><span>(),</span> <span>ZeroVecError</span><span>&gt;</span><span>;</span>

    <span>// 省略不太相关的实用方法</span>
<span>}</span>

<span>pub</span> <span>trait</span> <span>AsULE</span><span>:</span> <span>Copy</span> <span>{</span>
    <span>type</span> <span>ULE</span><span>:</span> <span>ULE</span><span>;</span>

    <span>// 转换为 ULE 类型</span>
    <span>fn</span> <span>to_unaligned</span><span>(</span><span>self</span><span>)</span> <span>-&gt;</span> <span>Self</span><span>::</span><span>ULE</span><span>;</span>
    <span>// 从 ULE 类型转换回来</span>
    <span>fn</span> <span>from_unaligned</span><span>(</span><span>unaligned</span><span>:</span> <span>Self</span><span>::</span><span>ULE</span><span>)</span> <span>-&gt;</span> <span>Self</span><span>;</span>
<span>}</span>

<span>pub</span> <span>unsafe</span> <span>trait</span> <span>VarULE</span><span>:</span> <span>'static</span> <span>{</span>
    <span>// 验证一个字节切片是否适合被视为此类型的引用</span>
    <span>fn</span> <span>validate_byte_slice</span><span>(</span><span>_bytes</span><span>:</span> <span>&amp;</span><span>[</span><span>u8</span><span>])</span> <span>-&gt;</span> <span>Result</span><span>&lt;</span><span>(),</span> <span>ZeroVecError</span><span>&gt;</span><span>;</span>

    <span>// 从已知有效的字节切片构造对 Self 的引用</span>
    <span>// 这是必要的，因为 VarULE 类型是动态大小的，</span>
    <span>// 这些类型的胖指针的元数据工作方式各不相同</span>
    <span>unsafe</span> <span>fn</span> <span>from_byte_slice_unchecked</span><span>(</span><span>bytes</span><span>:</span> <span>&amp;</span><span>[</span><span>u8</span><span>])</span> <span>-&gt;</span> <span>&amp;</span><span>Self</span><span>;</span>

    <span>// 省略不太相关的实用方法</span>
<span>}</span>
</code></pre></div></div>

<p><code>ZeroVec&lt;T&gt;</code>接受<code>AsULE</code>类型，并将其内部存储为其ULE类型的切片（<code>&amp;[T::ULE]</code>）。这样的切片可以自由地进行零拷贝序列化。当你尝试索引一个<code>ZeroVec</code>时，它会将值转换回<code>T</code>，这个操作通常只是一次未对齐加载。</p>

<p><code>VarZeroVec&lt;T&gt;</code>稍微复杂一些。其内存的开头存储向量中每个元素的索引，然后是所有元素的数据，一个接一个地排列。只要动态大小的数据可以用<em>扁平</em>的方式表示（没有进一步的内部间接寻址），它就可以实现<code>VarULE</code>，从而用于<code>VarZeroVec&lt;T&gt;</code>。<code>str</code>实现了这一点，但<code>ZeroSlice&lt;T&gt;</code>和<code>VarZeroSlice&lt;T&gt;</code>也实现了这一点，允许<code>zerovec</code>类型的无限嵌套！</p>

<p><code>ZeroMap&lt;T&gt;</code>的工作方式类似于<a href="https://docs.rs/litemap" rel="noopener noreferrer"><code>litemap</code></a> crate，它是一个由两个向量构建的映射，使用二分搜索来查找键。这并不总是像哈希映射那样高效，但它可以通过<a href="https://docs.rs/zerovec/latest/zerovec/enum.ZeroVec.html" rel="noopener noreferrer"><code>ZeroVec</code></a>和<a href="https://docs.rs/zerovec/latest/zerovec/enum.VarZeroVec.html" rel="noopener noreferrer"><code>VarZeroVec</code></a>以零拷贝方式工作。有一系列trait基础设施，允许它根据键或值的类型自动为每个键和值向量选择<code>ZeroVec</code>或<code>VarZeroVec</code>。</p>

<h2 id="what-about-rkyv">那 rkyv 呢？</h2>

<p>当我们开始这条路时，一个重要的问题是：那<a href="https://docs.rs/rkyv" rel="noopener noreferrer"><code>rkyv</code></a>呢？它当时在Rust社区中刚刚受到相当多的关注，看起来是一个很酷的库，目标领域相同。</p>

<p>总的来说，如果你正在寻找零拷贝反序列化，我全心全意推荐看看它！这是一个令人印象深刻的库，投入了很多思考。当我在改进<a href="https://docs.rs/zerovec" rel="noopener noreferrer"><code>zerovec</code></a>时，我从<a href="https://docs.rs/rkyv" rel="noopener noreferrer"><code>rkyv</code></a>以及与<a href="https://github.com/djkoloski" rel="noopener noreferrer">David</a>的一些富有洞察力的讨论中学习了很多，并比较了方法。</p>

<p>对我们来说，主要的症结在于<a href="https://docs.rs/rkyv" rel="noopener noreferrer"><code>rkyv</code></a>的工作方式有点独立于<a href="https://docs.rs/serde" rel="noopener noreferrer"><code>serde</code></a>：它使用自己的trait和自己的序列化机制。我们真的很喜欢<a href="https://docs.rs/serde" rel="noopener noreferrer"><code>serde</code></a>的模型并希望继续使用它，特别是因为我们希望支持各种人类可读和非人类可读的数据格式，包括<a href="https://docs.rs/postcard" rel="noopener noreferrer"><code>postcard</code></a>，它专门为低资源环境设计。这对于数据交换甚至更为重要；我们希望其他语言编写的程序能够构建和发送数据，而不必受限于特定的线路格式。</p>

<p><a href="https://docs.rs/zerovec/latest/zerovec/enum.ZeroVec.html" rel="noopener noreferrer"><code>zerovec</code></a>的目标本质上是将<a href="https://docs.rs/rkyv" rel="noopener noreferrer"><code>rkyv</code></a>类似的改进引入<a href="https://docs.rs/serde" rel="noopener noreferrer"><code>serde</code></a>宇宙，而不太打乱那个宇宙。<code>zerovec</code>类型在人类可读格式（如JSON）上序列化为结构的普通人类可读表示，而在二进制格式（如<a href="https://docs.rs/postcard" rel="noopener noreferrer"><code>postcard</code></a>）上，则序列化为紧凑的、零拷贝友好的表示形式，一切正常。</p>

<h2 id="how-does-it-perform">性能如何？</h2>

<p>首先我要提到的是<a href="https://docs.rs/rkyv" rel="noopener noreferrer"><code>rkyv</code></a>维护着一个非常好的<a href="https://github.com/djkoloski/rust_serialization_benchmark" rel="noopener noreferrer">基准测试套件</a>，我真的需要将其与zerovec集成，但还没有做。</p>

<div>
            <img width="60px" height="60px" title="Negative pion" alt="Speech bubble for character Negative pion" src="http://manishearth.github.io/images/pion-minus.png">
            <div></div>
            <div>
             为什么不先去做那个呢？它会让你的帖子更好！
            </div>
        </div>

<p>嗯，我一直在推迟写这篇帖子，直到我集成了那些基准测试，但执行功能不是这样运作的，此时我宁愿用我手头的基准测试发布，而不是进一步延迟。我可能会稍后用好基准测试更新这篇文章！</p>

<div>
            <img width="60px" height="60px" title="Negative pion" alt="Speech bubble for character Negative pion" src="http://manishearth.github.io/images/pion-minus.png">
            <div></div>
            <div>
             哼。
            </div>
        </div>

<p>完整的基准测试运行详情可以在<a href="https://gist.github.com/Manishearth/056a0ec12f9c943d71d214713d448ac0" rel="noopener noreferrer">这里</a>找到（在<a href="https://github.com/unicode-org/icu4x/tree/1e072b3248b93a974e21f3d01bc6a165eb272554/utils/zerovec" rel="noopener noreferrer"><code>1e072b32</code></a>处通过<code>cargo bench</code>运行）。我提取了一些具体的数据点来说明：</p>

<p><code>ZeroVec</code>：</p>

<table>
<thead><tr><th>基准测试</th><th>Slice</th><th>ZeroVec</th></tr></thead>
<tbody>

   <tr><th>反序列化（使用 <code>bincode</code>）</th></tr>
   <tr><th>反序列化包含 100 个 u32 的向量</th><td>141.55 ns</td><td>12.166 ns</td></tr>
   <tr><th>反序列化包含 15 个 char 的向量</th><td>225.55 ns</td><td>25.668 ns</td></tr>
   <tr><th>反序列化然后对包含 20 个 u32 的向量求和</th><td>47.423 ns</td><td>14.131 ns</td></tr>

   <tr><th>元素获取性能</th></tr>
   <tr><th>对包含 75 个 u32 元素的向量求和</th><td>4.3091 ns</td><td>5.7108 ns</td></tr>
   <tr><th>对包含 1000 个 u32 元素的向量进行二分搜索，50 次</th><td>428.48 ns</td><td>565.23 ns</td></tr>
   <tr><th>对包含 1000 个 u32 元素的向量进行二分搜索，50 次</th><td>428.48 ns</td><td>565.23 ns</td></tr>
   <tr><th>序列化</th></tr>

   <tr><th>序列化包含 20 个 u32 的向量</th><td>51.324 ns</td><td>21.582 ns</td></tr>
   <tr><th>序列化包含 15 个 char 的向量</th><td>195.75 ns</td><td>21.123 ns</td></tr>
</tbody>
</table>

<p><br>
通常我们不太关心序列化性能，但这里的序列化很快，因为<code>ZeroVec</code>在内存中总是以其序列化的形式存储。这可能会使修改变慢。获取操作在<code>ZeroVec</code>上稍微慢一点。反序列化性能是我们真正获得优势的地方，有时速度可以超过十倍！</p>

<p><code>VarZeroVec</code>：</p>

<p>字符串是随机生成的，大小在 2 到 20 个代码点之间选择，对于任何给定行使用相同的字符串集。</p>

<table>
<thead><tr><th>基准测试</th><th><code>Vec&lt;String&gt;</code></th><th><code>Vec&lt;&amp;str&gt;</code></th><th>VarZeroVec</th></tr></thead>
<tbody>

   <tr><th>反序列化 (长度 100)</th><td>11.274 us</td><td>2.2486 us</td><td>1.9446 us</td></tr>

   <tr><th>计算代码点 (长度 100)</th><td colspan="2">728.99 ns</td><td>1265.0 ns</td></tr>
   <tr><th>二分搜索 1 个元素 (长度 500)</th><td colspan="2">57.788 ns</td><td>122.10 ns</td></tr>
   <tr><th>二分搜索 10 个元素 (长度 500)</th><td colspan="2">451.40 ns</td><td>803.67 ns</td></tr>

</tbody>
</table>
<p><br></p>

<p>在这里，获取操作稍微慢一些，因为它们需要读取索引数组，但对于零拷贝反序列化仍然有相当大的优势。对于更复杂的数据，反序列化的优势会叠加；对于<code>Vec&lt;String&gt;</code>，你可以通过使用<code>Vec&lt;&amp;str&gt;</code>获得<em>大部分</em>优势，但对于更复杂的东西不一定可能。我们目前没有<code>VarZeroVec</code>的修改基准测试，但修改可能很慢，如前所述，它不打算在客户端代码中经常使用。</p>

<p>其中一些仍在变动；例如，我们正在<a href="https://github.com/unicode-org/icu4x/pull/2306" rel="noopener noreferrer">使<code>VarZeroVec</code>的缓冲区格式可配置</a>，以便用户可以选择他们确切的权衡。</p>

<h2 id="try-it-out">试用一下！</h2>

<p>类似于<a href="https://docs.rs/yoke" rel="noopener noreferrer"><code>yoke</code></a>，我不认为<a href="https://docs.rs/zerovec/latest/zerovec/enum.ZeroVec.html" rel="noopener noreferrer"><code>zerov</code></a></p><p><em>由 mimo-v2.5 模型翻译，花费 23999 tokens</em></p>]]></content:encoded>
      <link>http://manishearth.github.io/blog/2022/08/03/zero-copy-2-zero-copy-all-the-things/</link>
      <guid isPermaLink="false">http://manishearth.github.io/blog/2022/08/03/zero-copy-2-zero-copy-all-the-things</guid>
      <pubDate>Wed, 3 Aug 2022 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>这不是玩笑（零拷贝系列 #1）</title>
      <description>[AI 摘要] 本文介绍了“yoke” crate，它通过自引用类型实现Rust零拷贝反序列化的生命周期擦除，使管理借用数据更便捷。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 本文介绍了“yoke” crate，它通过自引用类型实现Rust零拷贝反序列化的生命周期擦除，使管理借用数据更便捷。</div><p><em>这是我过去一年一直在研究的零拷贝反序列化有趣抽象系列的三部分中的第一篇。本部分关注如何让零拷贝反序列化更易于使用。第二部分关注如何使其支持更多类型，可在此处找到<a href="http://manishearth.github.io/blog/2022/08/03/zero-copy-2-zero-copy-all-the-things/" rel="noopener noreferrer">链接</a>；第三部分则关注完全消除反序列化步骤，可在此处找到<a href="http://manishearth.github.io/blog/2022/08/03/zero-copy-3-so-zero-its-dot-dot-dot-negative/" rel="noopener noreferrer">链接</a>。这些文章可以按任何顺序阅读，但本文包含对什么是零拷贝反序列化的解释。</em></p>

<h2 id="background">背景</h2>

<p>过去一年半，我一直在<a href="https://github.com/unicode-org/icu4x" rel="noopener noreferrer">ICU4X</a>上全职工作，这是一个在Unicode联盟下由多家公司合作构建的新国际化Rust库。</p>

<p>关于ICU4X有很多可以说的，但聚焦一个核心价值主张：我们希望它在<em>数据和代码</em>上都是<em>模块化</em>的。我们希望ICU4X能在内存受限的嵌入式平台上使用。我们希望受下载大小限制的应用程序能够支持所有语言，而不是因为无法负担所有数据的打包而只能选择几种流行语言。作为其中的一部分，我们希望数据加载是<em>快速</em>且可插拔的。用户应该能够为他们的具体用例设计自己的数据加载策略。</p>

<p>你看，执行正确国际化的一个关键部分是<em>数据</em>。不同的区域设置<sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">1</a></sup>做事方式不同，所有这些信息都需要一个地方存放，最好不是代码。你需要关于特定区域设置如何格式化日期<sup id="fnref:2"><a href="#fn:2" rel="noopener noreferrer">2</a></sup>的数据，或者某种语言中复数如何工作，或者如何准确地分割像泰语这样通常不用空格书写的语言以便在适当位置插入换行符。</p>

<p>考虑到对数据的侧重，零拷贝反序列化对我们来说是一个非常有吸引力的选项。在努力做好零拷贝反序列化的过程中，我们构建了一些很酷的新库，本文就是关于其中之一的。</p>

<figure><img src="http://manishearth.github.io/images/post/cow-tools.png" width="400"><figcaption><p><small>Gary Larson, <a href="https://en.wikipedia.org/wiki/Cow_Tools" rel="noopener noreferrer">“奶牛工具”</a>, <em>远方</em>. 1982年10月</small></p>
</figcaption></figure>

<h2 id="zero-copy-deserialization-the-basics">零拷贝反序列化：基础</h2>

<p><em>如果您已经熟悉Rust中的零拷贝反序列化，可以跳过此部分</em></p>

<p>反序列化通常涉及两个协同完成的任务：验证数据，以及构建可通过程序访问的内存表示形式；即最终的反序列化值。</p>

<p>根据格式，前者通常相当快，但后者可能非常慢，通常涉及任何需要新分配且通常需要大量复制的可变大小数据。</p>

<div><div><pre><code><span>#[derive(Serialize,</span> <span>Deserialize)]</span>
<span>struct</span> <span>Person</span> <span>{</span>
    <span>// 这个字段的构造几乎是免费的</span>
    <span>age</span><span>:</span> <span>u8</span><span>,</span>
    <span>// 构造这个字段将涉及一次小型分配和复制</span>
    <span>name</span><span>:</span> <span>String</span><span>,</span>
    <span>// 这个可能需要一些时间</span>
    <span>rust_files_written</span><span>:</span> <span>Vec</span><span>&lt;</span><span>String</span><span>&gt;</span><span>,</span>
<span>}</span>
</code></pre></div></div>

<p>典型的二进制数据格式可能会将其存储为一个字节的age，后跟<code>name</code>的长度，再后跟<code>name</code>的字节，再后跟向量的另一个长度，最后是每个<code>String</code>值的长度和字符串数据。反序列化<code>u8</code>类型的age只需读取它，但另外两个字段需要分配足够的内存并复制每个字节，此外还可能需要类型所需的任何验证。</p>

<p>这种情况下的一种常见技术是跳过分配和复制，只需<em>验证</em>字节并存储对原始数据的<em>引用</em>。这只能在数据在序列化文件和反序列化值中表示方式完全相同的序列化格式中完成。</p>

<p>在Rust中使用<a href="https://docs.rs/serde" rel="noopener noreferrer"><code>serde</code></a>时，通常通过使用带<code>#[serde(borrow)]</code>的<a href="https://doc.rust-lang.org/stable/std/borrow/struct.Cow.html" rel="noopener noreferrer"><code>Cow&lt;'a, T&gt;</code></a>来完成：</p>

<div><div><pre><code><span>#[derive(Serialize,</span> <span>Deserialize)]</span>
<span>struct</span> <span>Person</span><span>&lt;</span><span>'a</span><span>&gt;</span> <span>{</span>
    <span>age</span><span>:</span> <span>u8</span><span>,</span>
    <span>#[serde(borrow)]</span>
    <span>name</span><span>:</span> <span>Cow</span><span>&lt;</span><span>'a</span><span>,</span> <span>str</span><span>&gt;</span><span>,</span>
<span>}</span>

</code></pre></div></div>

<p>现在，当反序列化<code>name</code>时，反序列化器只需验证它确实是一个有效的UTF-8 <code>str</code>，而<code>name</code>的最终值将是与正在反序列化的原始数据本身的引用。</p>

<p>也可以使用<code>&amp;'a str</code>代替<code>Cow</code>，但这使得<code>Deserialize</code>实现的通用性大大降低，因为那些<em>不</em>将字符串存储为其内存表示形式的格式（例如，包含转义符的JSON字符串）将无法回退到拥有所有权的值。因此，在编写参与零拷贝反序列化的Rust代码时，拥有所有权或借用的<a href="https://doc.rust-lang.org/stable/std/borrow/struct.Cow.html" rel="noopener noreferrer"><code>Cow&lt;'a, T&gt;</code></a>通常是良好设计的基石。</p>

<div>您可能注意到这个新结构中找不到<code>rust_files_written</code>。这是因为<a href="https://docs.rs/serde" rel="noopener noreferrer"><code>serde</code></a>开箱即用时，出于非常充分的理由，无法处理除<code>str</code>和<code>[u8]</code>之外的任何类型的零拷贝反序列化。其他框架如<a href="https://docs.rs/rkyv" rel="noopener noreferrer"><code>rkyv</code></a>可以做到这一点，但我们也成功地用<a href="https://docs.rs/serde" rel="noopener noreferrer"><code>serde实现了这一点。我将在</code></a><code><a href="http://manishearth.github.io/blog/2022/08/03/zero-copy-2-zero-copy-all-the-things/" rel="noopener noreferrer">第二部分</a>更深入地讨论这些原因和我们的解决方案。</code></div><code>

<div>
            <img width="60px" height="60px" title="困惑的π介子" alt="角色困惑的π介子的对话气泡" src="http://manishearth.github.io/images/pion-nought.png">
            <div></div>
            <div>
             <code>age</code>字段这里难道不是仍然在发生复制吗？
            </div>
        </div>

<p>是的，“零拷贝”多少有些用词不当，它真正的意思是“零分配”，或者，“零大量复制”。这样看：像<code>age</code>这样的数据确实会被复制，但如果没有，比如说，分配一个<code>Person&lt;'a&gt;</code>的向量，你只会在单独反序列化几个<code>Person&lt;'a&gt;</code>时或反序列化包含几个<code>Person&lt;'a&gt;</code>的某个结构体时看到这种复制。要发生一次不涉及分配的大复制，你的类型必须在栈上本身就是那么大，而人们通常避免这样做，因为这意味着即使在非反序列化情况下，每次移动值时都会发生大复制。</p>

<h2 id="when-life-gives-you-lifetimes-">当生活给你生命周期时……</h2>

<p>Rust中的零拷贝反序列化有一个非常烦人的缺点：生命周期。突然间，你所有的反序列化类型上都有了生命周期。它们当然会有；它们不再是自包含的，而是包含了对它们最初反序列化数据的引用！</p>

<p>这也不是Rust独有的问题，零拷贝反序列化总是会在你的类型之间引入更复杂的依赖关系，不同的框架处理方式不同；从将生命周期管理留给用户，到使用引用计数或GC来确保数据保持存在。Rust序列化库如果愿意也可以做类似的事情。在这种情况下，<a href="https://docs.rs/serde" rel="noopener noreferrer"><code>serde</code></a>以非常Rust的方式，希望库用户能够精确控制这里的内存管理，并将此问题作为生命周期呈现出来。</p>

<p>不幸的是，这样的生命周期往往会渗透到所有地方。每个持有你反序列化类型的类型现在都需要一个生命周期，而且很可能会成为你的用户的问题。</p>

<p>此外，Rust生命周期是纯编译时构造。如果你的值是具有生命周期的类型，你需要在编译时知道它肯定不再被使用的时间，并且你需要一直持有其源数据直到那时。Rust的设计意味着你不需要担心<em>弄错</em>，因为编译器会抓住你，但你仍然需要<em>去做</em>。</p>

<p>所有这些对于你想要在运行时管理生命周期的情况并不理想，例如，如果你的数据是从一个更大的文件反序列化来的，并且你希望只要从其反序列化的数据仍然存在，就缓存加载的文件。</p>

<p>通常在这样的情况下你可以使用<a href="https://doc.rust-lang.org/stable/std/rc/struct.Rc.html" rel="noopener noreferrer"><code>Rc&lt;T&gt;</code></a>，它实际上是<code>&amp;'a T</code>安全共享引用的“运行时而非编译时”版本，但这只适用于你共享同构类型的情况，而在这种情况下我们试图共享从一个数据块反序列化的不同类型，而该数据块本身又是另一种类型。</p>

<p>ICU4X希望用户能够根据需要使用缓存和其他数据管理策略，所以这完全行不通。有一段时间ICU4X不是在其大多数类型中贯穿一个而是<em>两个</em>普遍的生命周期：这既令人困惑，也不符合我们的目标。</p>

<h2 id="-make-life-take-the-lifetimes-back">……让生活收回那些生命周期</h2>

<p><em>这里很多设计可以在<a href="https://github.com/unicode-org/icu4x/blob/main/utils/yoke/design_doc.md" rel="noopener noreferrer">设计文档</a>中找到解释</em></p>

<p>在<a href="https://github.com/unicode-org/icu4x/issues/667#issuecomment-828123099" rel="noopener noreferrer">一番讨论</a>之后，主要是与<a href="https://github.com/sffc" rel="noopener noreferrer">Shane</a>进行的，我设计了<a href="https://docs.rs/yoke" rel="noopener noreferrer"><code>yoke</code></a>，一个试图通过自引用类型在Rust中提供<em>生命周期擦除</em>的crate。</p>

<div>
            <img width="60px" height="60px" title="困惑的π介子" alt="角色困惑的π介子的对话气泡" src="http://manishearth.github.io/images/pion-nought.png">
            <div></div>
            <div>
             等等，<em>生命周期</em>擦除？
            </div>
        </div>

<p>就像类型擦除一样！“类型擦除”（在Rust中通过<code>dyn Trait</code>完成）让你可以将编译时概念（值的类型）转移到可以在运行时决定的东西中。类似地，<code>yoke</code>的核心价值主张是获取那些背负着生命周期编译时概念的类型，并允许你无论如何在运行时决定它们。</p>

<div>
            <img width="60px" height="60px" title="困惑的π介子" alt="角色困惑的π介子的对话气泡" src="http://manishearth.github.io/images/pion-nought.png">
            <div></div>
            <div>
             <code>Rc&lt;T&gt;</code>不是已经让你能够将生命周期作为运行时决定吗？
            </div>
        </div>

<p>算是吧，<code>Rc&lt;T&gt;</code>本身让你能够<em>避免</em>编译时生命周期，而<code>Yoke</code>则适用于已经存在一个生命周期（例如由于零拷贝反序列化）并且你想要掩盖它的情况。</p>

<div>
            <img width="60px" height="60px" title="困惑的π介子" alt="角色困惑的π介子的对话气泡" src="http://manishearth.github.io/images/pion-nought.png">
            <div></div>
            <div>
             酷！那是什么样子的？
            </div>
        </div>

<p>总体思路是，你可以获取一个零拷贝可反序列化的类型，如<code>Cow&lt;'a, str&gt;</code>（或更复杂的类型），并将其“套索”到它反序列化自的值上，我们称其为“运载车”。</p>

<div>
            <img width="60px" height="60px" title="否定的π介子" alt="角色否定的π介子的对话气泡" src="http://manishearth.github.io/images/pion-minus.png">
            <div></div>
            <div>
             <em>*呻吟*</em> 又一个用双关语命名的crate，Manish。
            </div>
        </div>

<p>我永远不会停止。</p>

<p>无论如何，这就是它的样子。</p>

<div><div><pre><code><span>// 为了清晰起见，显式提及一些类型</span>

<span>// 加载一个文件</span>
<span>let</span> <span>file</span><span>:</span> <span>Rc</span><span>&lt;</span><span>[</span><span>u8</span><span>]</span><span>&gt;</span> <span>=</span> <span>fs</span><span>::</span><span>read</span><span>(</span><span>"data.postcard"</span><span>)</span><span>?</span><span>.into</span><span>();</span>

<span>// 通过克隆它创建一个对文件数据的新Rc引用，</span>
<span>// 然后将其用作Yoke的运载车</span>
<span>let</span> <span>y</span><span>:</span> <span>Yoke</span><span>&lt;</span><span>Cow</span><span>&lt;</span><span>'static</span><span>,</span> <span>str</span><span>&gt;</span><span>,</span> <span>Rc</span><span>&lt;</span><span>[</span><span>u8</span><span>]</span><span>&gt;&gt;</span> <span>=</span> <span>Yoke</span><span>::</span><span>attach_to_cart</span><span>(</span><span>file</span><span>.clone</span><span>(),</span> <span>|</span><span>contents</span><span>|</span> <span>{</span>
    <span>// 从文件反序列化</span>
    <span>let</span> <span>cow</span><span>:</span> <span>Cow</span><span>&lt;</span><span>str</span><span>&gt;</span> <span>=</span>  <span>postcard</span><span>::</span><span>from_bytes</span><span>(</span><span>&amp;</span><span>contents</span><span>);</span>
    <span>cow</span>
<span>})</span>

<span>// 该字符串仍然可以通过 `.get()` 访问</span>
<span>println!</span><span>(</span><span>"{}"</span><span>,</span> <span>y</span><span>.get</span><span>())</span>

<span>drop</span><span>(</span><span>y</span><span>);</span>
<span>// 只有现在文件的引用计数才会减少</span>
</code></pre></div></div>

<div>这里的一些API由于当前的编译器错误可能无法完全正常工作。在这篇博客文章中，我使用这些API的理想版本进行说明，但值得查看Yoke文档以了解是否需要使用备用的解决方法API。截至Rust 1.61，<em>大多数</em>错误已被修复。</div>

<div>
            <img width="60px" height="60px" title="肯定的π介子" alt="角色肯定的π介子的对话气泡" src="http://manishearth.github.io/images/pion-plus.png">
            <div></div>
            <div>
             上面的例子使用了<a href="https://docs.rs/postcard" rel="noopener noreferrer"><code>postcard</code></a>：<code>postcard</code>是一个非常棒的兼容<code>serde</code>的二进制序列化格式，专为资源受限环境设计。它相当快且代码量小，请查看一下！
            </div>
        </div>

<p>类型<code>Yoke&lt;Cow&lt;'static, str&gt;, Rc&lt;[u8]&gt;&gt;</code>是“一个生命周期擦除的<code>Cow&lt;str&gt;</code>，‘套索’到一个作为<code>Rc&lt;[u8]&gt;</code>的支持数据存储‘运载车’上”。这意味着Cow包含了对运载车中数据的引用，然而，<code>Yoke</code>将持有运载车类型直到它完成，这确保了<code>Cow</code>中的引用不再悬垂。</p>

<p><code>Yoke</code>中数据的大多数操作都通过<code>.get()</code>进行，在这种情况下，它将返回一个<code>Cow&lt;'a, str&gt;</code>，其中<code>'a</code>是<code>.get()</code>借用的生命周期。这保持了安全性：在这种情况下，分发<code>Cow&lt;'static, str&gt;</code>实际上并不安全，因为<code>Cow</code>实际上并非借用自静态数据；但只要我们在访问期间将生命周期转换为更短的，这就可以了。</p>

<p>事实证明，<code>Yoke</code>类型中的<code>'static</code>实际上是一个谎言！Rust并不真正允许你使用包含借用内容的类型而不提及<em>某些</em>生命周期，在这里我们希望将编译器从管理生命周期的职责中解脱出来，自己管理它们，所以我们需要给它<em>一些东西</em>以便我们可以命名类型，而<code>'static</code>是Rust中唯一预先存在的命名生命周期。</p>

<p><code>.get()</code>的实际签名<a href="https://docs.rs/yoke/latest/yoke/struct.Yoke.html#method.get" rel="noopener noreferrer">有点奇怪</a>，因为它需要是泛型的，但如果我们的借用类型是<code>Foo&lt;'a&gt;</code>，那么<code>.get()</code>的签名大致如下：</p>

<div><div><pre><code><span>impl</span> <span>Yoke</span><span>&lt;</span><span>Foo</span><span>&lt;</span><span>'static</span><span>&gt;&gt;</span> <span>{</span>
    <span>fn</span> <span>get</span><span>&lt;</span><span>'a</span><span>&gt;</span><span>(</span><span>&amp;</span><span>'a</span> <span>self</span><span>)</span> <span>-&gt;</span> <span>&amp;</span><span>'a</span> <span>Foo</span><span>&lt;</span><span>'a</span><span>&gt;</span> <span>{</span>
        <span>...</span>
    <span>}</span>
<span>}</span>
</code></pre></div></div>

<p>要允许一个类型在<code>Yoke&lt;Y, C&gt;</code>中，它必须实现<code>Yokeable&lt;'a&gt;</code>。这个trait手动实现是unsafe的，在大多数情况下你应该使用<code>#[derive(Yokeable)]</code>自动派生：</p>

<div><div><pre><code><span>#[derive(Yokeable,</span> <span>Serialize,</span> <span>Deserialize)]</span>
<span>struct</span> <span>Person</span><span>&lt;</span><span>'a</span><span>&gt;</span> <span>{</span>
    <span>age</span><span>:</span> <span>u8</span><span>,</span>
    <span>#[serde(borrow)]</span>
    <span>name</span><span>:</span> <span>Cow</span><span>&lt;</span><span>'a</span><span>,</span> <span>str</span><span>&gt;</span><span>,</span>
<span>}</span>

<span>let</span> <span>person</span><span>:</span> <span>Yoke</span><span>&lt;</span><span>Person</span><span>&lt;</span><span>'static</span><span>&gt;</span><span>,</span> <span>Rc</span><span>&lt;</span><span>[</span><span>u8</span><span>]</span><span>&gt;</span> <span>=</span> <span>Yoke</span><span>::</span><span>attach_to_cart</span><span>(</span><span>file</span><span>.clone</span><span>(),</span> <span>|</span><span>contents</span><span>|</span> <span>{</span>
    <span>postcard</span><span>::</span><span>from_bytes</span><span>(</span><span>&amp;</span><span>contents</span><span>)</span>
<span>});</span>
</code></pre></div></div>

<p>与大多数<code>#[derive]</code>不同，<code>Yokeable</code>即使字段尚未实现<code>Yokeable</code>也可以派生，除非生命周期字段同时具有其他泛型参数的情况。在这些情况下，通常只需用<code>#[yoke(prove_covariance_manually)]</code>标记类型并确保任何具有生命周期的字段也实现了<code>Yokeable</code>即可。</p>

<p>你可以用<code>Yoke</code>做更多的事情，例如，你可以“投影”一个套索以获得一个具有初始套索中数据子集的新套索：</p>

<div><div><pre><code><span>let</span> <span>person</span><span>:</span> <span>Yoke</span><span>&lt;</span><span>Person</span><span>&lt;</span><span>'static</span><span>&gt;</span><span>,</span> <span>Rc</span><span>&lt;</span><span>[</span><span>u8</span><span>]</span><span>&gt;&gt;</span> <span>=</span> <span>...</span><span>.</span><span>;</span>

<span>let</span> <span>person_name</span><span>:</span> <span>Yoke</span><span>&lt;</span><span>Cow</span><span>&lt;</span><span>'static</span><span>,</span> <span>str</span><span>&gt;&gt;</span> <span>=</span> <span>person</span><span>.project</span><span>(|</span><span>p</span><span>,</span> <span>_</span><span>|</span> <span>p</span><span>.name</span><span>);</span>

</code></pre></div></div>

<p>这允许将来自不同套索的数据混合使用。</p>

<p><code>Yoke</code>也是<em>可变</em>的，这或许令人惊讶！毕竟，它们主要是为与写时复制数据一起使用而设计的，所以有方法可以修改它们，前提是没有<em>额外的</em>借用数据潜入：</p>

<div><div><pre><code><span>let</span> <span>mut</span> <span>person</span><span>:</span> <span>Yoke</span><span>&lt;</span><span>Person</span><span>&lt;</span><span>'static</span><span>&gt;</span><span>,</span> <span>Rc</span><span>&lt;</span><span>[</span><span>u8</span><span>]</span><span>&gt;&gt;</span> <span>=</span> <span>...</span><span>.</span><span>;</span>

<span>// 让名字听起来更花哨</span>
<span>person</span><span>.with_mut</span><span>(|</span><span>person</span><span>|</span> <span>{</span>
    <span>// 这将把 `Cow` 转换为拥有所有权的</span>
    <span>person</span><span>.name</span><span>.to_mut</span><span>()</span><span>.push</span><span>(</span><span>", Esq."</span><span>)</span>
<span>})</span>
</code></pre></div></div>

<p>总的来说，<code>Yoke</code>是一个相当强大的抽象，适用于涉及零拷贝反序列化以及涉及大量借用的其他情况的各种情况。在ICU4X中，我们用于加载数据的抽象总是使用<code>Yoke</code>，允许混合各种数据加载策略——包括缓存。</p>

<h3 id="how-it-works">它是如何工作的</h3>

<div>
            <img width="60px" height="60px" title="肯定的π介子" alt="角色肯定的π介子的对话气泡" src="http://manishearth.github.io/images/pion-plus.png">
            <div></div>
            <div>
             Manish即将说出“协变”这个词，所以我抢先说：如果您难以理解本节和下一节，请不要担心！这个crate的内部工作依赖于多个小众概念，大多数Rust用户即使在处理其他高级代码时也永远不需要关心。
            </div>
        </div>

<p><code>Yoke</code>通过依赖<em>协变生命周期</em>的概念来工作。<a href="https://docs.rs/yoke/latest/yoke/trait.Yokeable.html" rel="noopener noreferrer"><code>Yokeable</code></a> trait看起来像这样：</p>

<div><div><pre><code><span>pub</span> <span>unsafe</span> <span>trait</span> <span>Yokeable</span><span>&lt;</span><span>'a</span><span>&gt;</span><span>:</span> <span>'static</span> <span>{</span>
    <span>type</span> <span>Output</span><span>:</span> <span>'a</span><span>;</span>
    <span>// 方法省略</span>
<span>}</span>
</code></pre></div></div>

<p>一个典型的实现看起来像这样：</p>

<div><div><pre><code><span>unsafe</span> <span>impl</span><span>&lt;</span><span>'a</span><span>&gt;</span> <span>Yokeable</span><span>&lt;</span><span>'a</span><span>&gt;</span> <span>for</span> <span>Cow</span><span>&lt;</span><span>'static</span><span>,</span> <span>str</span><span>&gt;</span> <span>{</span>
    <span>type</span> <span>Output</span><span>:</span> <span>'a</span> <span>=</span> <span>Cow</span><span>&lt;</span><span>'a</span><span>,</span> <span>str</span><span>&gt;</span><span>;</span>
    <span>// ...</span>
<span>}</span>
</code></pre></div></div>

<p>这个trait的一个实现将位于具有生命周期的类型的<code>'static</code>版本上（我将在本文中称其为<code>Self&lt;'static&gt;</code><sup id="fnref:3"><a href="#fn:3" rel="noopener noreferrer">3</a></sup>），并将其映射到具有生命周期的版本（<code>Self&lt;'a&gt;</code>）。它只能在生命周期<code>'a</code>是<em>协变</em>的类型上实现，也就是说，当<code>'b</code>是更短的生命周期时，将<code>Self&lt;'a&gt;</code>视为<code>Self&lt;'b&gt;</code>是安全的。大多数具有生命周期的类型都属于这一类<sup id="fnref:4"><a href="#fn:4" rel="noopener noreferrer">4</a></sup>，特别是在零拷贝反序列化领域。</p>

<div>
            <img width="60px" height="60px" title="肯定的π介子" alt="角色肯定的π介子的对话气泡" src="http://manishearth.github.io/images/pion-plus.png">
            <div></div>
            <div>
             您可以在<a href="https://doc.rust-lang.org/nomicon/subtyping.html" rel="noopener noreferrer">nomicon</a>中阅读更多关于型变的信息！
            </div>
        </div>

<p>对于任何<code>Yokeable</code>类型<code>Foo&lt;'static&gt;</code>，你可以通过<code>&lt;Foo as Yokeable&lt;'a&gt;&gt;::Output</code>获取该类型的具有生命周期<code>'a</code>的版本。<code>Yokeable</code> trait公开了一些方法，允许人们安全地执行对具有协变生命周期的类型允许的各种转换。</p>

<p><code>#[derive(Yokeable)]</code>在大多数情况下依赖于编译器确定生命周期是否协变的能力，并且实际上并不生成太多代码！在大多数情况下，<code>Yokeable</code>上各种函数的主体都是纯安全的代码，看起来像这样：</p>

<div><div><pre><code><span>impl</span><span>&lt;</span><span>'a</span><span>&gt;</span> <span>Yokeable</span> <span>for</span> <span>Foo</span><span>&lt;</span><span>'static</span><span>&gt;</span> <span>{</span>
    <span>type</span> <span>Output</span><span>:</span> <span>'a</span> <span>=</span> <span>Foo</span><span>&lt;</span><span>'a</span><span>&gt;</span><span>;</span>
    <span>fn</span> <span>transform</span><span>(</span><span>&amp;</span><span>self</span><span>)</span> <span>-&gt;</span> <span>&amp;</span><span>Self</span><span>::</span><span>Output</span> <span>{</span>
        <span>self</span>
    <span>}</span>
    <span>fn</span> <span>transform_owned</span><span>(</span><span>self</span><span>)</span> <span>-&gt;</span> <span>Self</span><span>::</span><span>Output</span> <span>{</span>
        <span>self</span>
    <span>}</span>
    <span>fn</span> <span>transform_mut</span><span>&lt;</span><span>F</span><span>&gt;</span><span>(</span><span>&amp;</span><span>'a</span> <span>mut</span> <span>self</span><span>,</span> <span>f</span><span>:</span> <span>F</span><span>)</span>
    <span>where</span>
        <span>F</span><span>:</span> <span>'static</span> <span>+</span> <span>for</span><span>&lt;</span><span>'b</span><span>&gt;</span> <span>FnOnce</span><span>(</span><span>&amp;</span><span>'b</span> <span>mut</span> <span>Self</span><span>::</span><span>Output</span><span>)</span> <span>{</span>
        <span>f</span><span>(</span><span>self</span><span>)</span>
    <span>}</span>
    <span>// fn make() 省略，因为它不太相关</span>
<span>}</span>
</code></pre></div></div>

<p>编译器知道这些是安全的，因为它知道该类型是协变的，而<code>Yokeable</code> trait允许我们<em>泛型地</em>讨论这些操作安全的类型。</p>

<div>
            <img width="60px" height="60px" title="肯定的π介子" alt="角色肯定的π介子的对话气泡" src="http://manishearth.github.io/images/pion-plus.png">
            <div></div>
            <div>
             换句话说，关于生命周期“可拉伸性”有一个编译器知道的有用的属性，我们可以通过生成如果该属性不适用编译器会拒绝编译的代码来检查该属性是否适用于某个类型。
            </div>
        </div>

<p>使用这个trait，<code>Yoke</code>然后通过存储<code>Self&lt;'static&gt;</code>并将其转换为更短的、更局部的生命周期，然后再传递给任何消费者来工作，使用<code>Yokeable</code>上的方法以各种方式。知道生命周期是协变的使得这种生命周期“挤压”变得安全。<code>'static</code>是一个谎言，但只要该值实际上不是以<code>'static</code>生命周期访问的，做这种事情是安全的，我们非常小心以确保它不会泄漏。</p>

<h2 id="better-conversions-zerofrom">更好的转换：ZeroFrom</h2>

<p>一个与之配合很好的crate是<a href="https://docs.rs/zerofrom" rel="noopener noreferrer"><code>zerofrom</code></a>，主要由<a href="https://github.com/sffc" rel="noopener noreferrer">Shane</a>设计和编写。它附带了<a href="https://docs.rs/zerofrom/latest/zerofrom/trait.ZeroFrom.html" rel="noopener noreferrer"><code>ZeroFrom</code></a> trait：</p>

<div><div><pre><code><span>pub</span> <span>trait</span> <span>ZeroFrom</span><span>&lt;</span><span>'zf</span><span>,</span> <span>C</span><span>:</span> <span>?</span><span>Sized</span><span>&gt;</span><span>:</span> <span>'zf</span> <span>{</span>
    <span>fn</span> <span>zero_from</span><span>(</span><span>other</span><span>:</span> <span>&amp;</span><span>'zf</span> <span>C</span><span>)</span> <span>-&gt;</span> <span>Self</span><span>;</span>
<span>}</span>
</code></pre></div></div>

<p>这个trait的思想是能够泛型地处理可转换为（通常是零拷贝）借用类型。</p>

<p>例如，<code>Cow&lt;'zf, str&gt;</code>既实现了<code>ZeroFrom&lt;'zf, str&gt;</code>也实现了<code>ZeroFrom&lt;'zf, String&gt;</code>，以及<code>ZeroFrom&lt;'zf, Cow&lt;'a, str&gt;&gt;</code>。它类似于<a href="https://doc.rust-lang.org/stable/std/convert/trait.AsRef.html" rel="noopener noreferrer"><code>AsRef</code></a> trait，但允许在发生的借用种类上有更多灵活性，并且实现者应该在转换过程中最小化复制量。例如，当<code>ZeroFrom</code>-从某个其他<code>Cow&lt;'a, str&gt;</code>构造一个<code>Cow&lt;'zf, str&gt;</code>时，它将<em>总是</em>构造一个<code>Cow::Borrowed</code>，即使原始的<code>Cow&lt;'a, str&gt;</code>是拥有的。</p>

<p><code>Yoke</code>有一个方便的构造函数<a href="https://docs.rs/yoke/latest/yoke/struct.Yoke.html#method.attach_to_zero_copy_cart" rel="noopener noreferrer"><code>Yoke::attach_to_zero_copy_cart()</code></a>，如果<code>Y&lt;'zf&gt;</code>对所有生命周期<code>'zf</code>实现了<code>ZeroFrom&lt;'zf, C&gt;</code>，它可以从运载车类型<code>C</code>创建一个<code>Yoke&lt;Y, C&gt;</code>。这对于想要进行基本自引用类型但不进行任何花哨的零拷贝反序列化的情况很有用。</p>

<h2 id="-make-life-rue-the-day-it-thought-it-could-give-you-lifetimes">……让生活后悔它曾认为可以给你生命周期</h2>

<p>使用这个crate的生活并非全是甜美的。我们，呃……不幸地发现了一大堆棘手的编译器错误。很多根源在于<code>Yokeable&lt;'a&gt;</code>在大多数情况下通过<code>for&lt;'a&gt; Yokeable&lt;'a&gt;</code>绑定（“对所有可能的生命周期<code>'a</code>的<code>Yokeable&lt;'a&gt;</code>”）。<code>for&lt;'a&gt;</code>是一个被称为高阶生命周期或trait绑定的 niche 特性（通常称为“HRTB”），虽然它一直是Rust类型系统能够推理函数指针所必需的，但它也一直相当有缺陷，并且经常不鼓励用于此类用途。</p>

<p>我们使用它是为了能够泛型地讨论一个类型的生命周期。幸运的是，有一个正在积极开发的语言特性将更适合此用途：<a href="https://rust-lang.github.io/generic-associated-types-initiative/index.html" rel="noopener noreferrer">泛型关联类型</a>。</p>

<p>这个特性还不稳定，但幸运的是对于<em>我们</em>来说，大多数涉及<code>for&lt;'a&gt;</code>的编译器错误也会影响GAT，所以我们一直从GAT工作中受益，并且我们的很多错误报告帮助加强了GAT代码。非常感谢<a href="https://github.com/jackh726" rel="noopener noreferrer">Jack Huey</a>修复了很多这些错误，<a href="https://github.com/eddyb" rel="noopener noreferrer">eddyb</a>在调试过程中提供了帮助。</p>

<p>截至Rust 1.61，许多主要错误已得到修复，但仍然存在一些涉及trait绑定的错误，为此<code>yoke</code> crate维护了一些<a href="https://docs.rs/yoke/latest/yoke/trait_hack/index.html" rel="noopener noreferrer">解决方法辅助工具</a>。我们的经验是，这里的大多数编译器错误对于你可以使用这个crate做什么<em>没有限制</em>，但它们最终可能导致代码看起来不够理想。总的来说，我们仍然认为它是值得的，我们能够以对外部方便的方式做一些非常棒的零拷贝的事情（即使一些内部代码很乱），而且我们没有到处都是生命周期。</p>

<h2 id="try-it-out">试用一下！</h2>

<p>虽然我不认为<a href="https://docs.rs/yoke" rel="noopener noreferrer"><code>yoke</code></a> crate“完成”了，但它在ICU4X中已经使用了一年，我认为它已经足够成熟可以推荐给其他人。试用一下！让我知道你的想法！</p>

<p><em>感谢<a href="https://twitter.com/plaidfinch" rel="noopener noreferrer">Finch</a>、<a href="https://twitter.com/yaahc_" rel="noopener noreferrer">Jane</a>和<a href="https://github.com/sffc" rel="noopener noreferrer">Shane</a>审阅本文草稿</em></p>

<div>
  <ol>
    <li id="fn:1">
      <p><em>区域设置</em>通常是一个语言和位置，但它可能包含额外的信息，如书写系统甚至使用的日历系统等。<a href="#fnref:1" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn:2">
      <p>请注意，这不仅仅是选择像MM-DD-YYYY这样的格式！仅在美国英语中，日期就可以是<code>4/10/22</code>或<code>4/10/2022</code>或<code>April 10, 2022</code>，或<code>Sunday, April 10, 2022 C.E.</code>，或<code>Sun, Apr 10, 2022</code>，这还不考虑周数、季度或时间！这很快就为每个区域设置增加到相当多的数据。<a href="#fnref:2" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn:3">
      <p>这不是真实的Rust语法；因为<code>Self</code>总是<code>Self</code>，但我们需要能够将<code>Self</code>作为此场景中的高阶类型引用。<a href="#fnref:3" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn:4">
      <p>不包括的类型是那些在生命周期周围涉及可变性（<code>&amp;mut</code>或内部可变性）的类型，以及涉及函数指针和trait对象的类型。<a href="#fnref:4" rel="noopener noreferrer">↩</a></p>
    </li>
  </ol>
</div></code><p><em>由 mimo-v2.5 模型翻译，花费 26972 tokens</em></p>]]></content:encoded>
      <link>http://manishearth.github.io/blog/2022/08/03/zero-copy-1-not-a-yoking-matter/</link>
      <guid isPermaLink="false">http://manishearth.github.io/blog/2022/08/03/zero-copy-1-not-a-yoking-matter</guid>
      <pubDate>Wed, 3 Aug 2022 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Rust中安全追踪式GC设计之旅</title>
      <description>[AI 摘要] 本文探讨了在Rust中实现安全追踪式垃圾回收器的多种设计方案，分析了其挑战、共同点及不同实现案例。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 本文探讨了在Rust中实现安全追踪式垃圾回收器的多种设计方案，分析了其挑战、共同点及不同实现案例。</div><p>自从我开始在 <a href="https://github.com/servo/servo" rel="noopener noreferrer">Servo</a> 的 JavaScript 层工作以来，我就一直在思考 Rust 中的垃圾回收（GC）。我曾 <a href="https://manishearth.github.io/blog/2015/09/01/designing-a-gc-in-rust/" rel="noopener noreferrer">设计过一个 GC 库</a>，<a href="https://manishearth.github.io/blog/2016/08/18/gc-support-in-rust-api-design/" rel="noopener noreferrer">研究过 Rust 本身的 GC 集成方案</a>，参与过 Servo 的 JS GC 集成工作，并帮助过其他一些 Rust GC 项目，如 <a href="https://github.com/asajeffrey/josephine" rel="noopener noreferrer">josephine</a> 和 <a href="https://github.com/kyren/gc-arena" rel="noopener noreferrer">gc-arena</a>。</p>

<p>因此，我经常被卷入 GC 讨论。我喜欢谈论 GC——别误会——但我经常需要重复讲解相同的内容。我比较<a href="https://manishearth.github.io/blog/2018/08/26/why-i-enjoy-blogging/#blogging-lets-me-be-lazy" rel="noopener noreferrer">懒</a>，更希望能有一个地方，让人们可以快速了解 GC 设计的整体领域，然后再深入讨论特定设计的必要权衡。</p>

<p>我需要说明的是，本文中的一些 GC 实现是实验性的或未被维护。本文的目标是将它们作为 <em>设计</em> 示例进行展示，而不一定是你可以直接使用的通用 crate，尽管其中一些也是可用的 crate。</p>

<h3 id="a-note-on-terminology">关于术语的说明</h3>

<p>关于 GC 的讨论常常被混淆的一点是，根据某些“GC”的定义，简单的引用计数（Reference Counting）<em>就是</em>一种 GC。学术界通常使用的 GC 定义广泛指任何形式的自动内存管理。然而，大多数熟悉“GC”一词的程序员通常将其等同于“Java、Go、Haskell 和 C# 的做法”，这可以明确地称为<em>追踪式（tracing）</em>垃圾回收。</p>

<p>追踪式垃圾回收是指跟踪哪些堆对象是直接可达的（“根”），找出所有可达的堆对象集合（“追踪”，也称为“标记”），然后清理它们（“清除”）。</p>

<p>在整个博客文章中，除非另有说明，我将使用“GC”一词来指代追踪式垃圾回收/回收器<sup id="fnref:0"><a href="#fn:0" rel="noopener noreferrer">1</a></sup>。</p>

<h2 id="why-write-gcs-for-rust">为什么要在 Rust 中编写 GC？</h2>

<p>（如果你已经想在 Rust 中编写 GC，并且阅读本文是为了获取<em>如何</em>实现的想法，你可以跳过这一节。你已经知道为什么有人会想为 Rust 编写 GC 了。）</p>

<p>每次这个话题被提起，总会有人说“我以为 Rust 的目的就是避免 GC”或“GC 会毁了 Rust”之类的。一般来说，不要过分在意评论区，但我认为解释一下为什么有人可能希望在 Rust 中获得类 GC 的语义是有用的。</p>

<p>真的有两种不同的使用场景。首先，有时你需要管理带有循环引用的数据，而 <code>Rc&lt;T&gt;</code> 不足以胜任，因为 <code>Rc</code> 循环会导致内存泄漏。<a href="https://docs.rs/petgraph/" rel="noopener noreferrer"><code>petgraph</code></a> 或<a href="https://manishearth.github.io/blog/2021/03/15/arenas-in-rust/" rel="noopener noreferrer">内存池（arena）</a>通常是处理这类模式的可接受方案，但并非总是如此，特别是当你的数据非常异构时。这类问题在处理并发数据结构时经常出现；例如 <a href="https://docs.rs/crossbeam/" rel="noopener noreferrer"><code>crossbeam</code></a> 就有一个 <a href="https://docs.rs/crossbeam/0.8.0/crossbeam/epoch/index.html" rel="noopener noreferrer">基于纪元（epoch）的内存管理系统</a>，虽然不是完整的追踪式 GC，但与 GC 有许多共同特征。</p>

<p>对于这种用例，很少需要设计自定义 GC，你可以寻找像 <a href="https://docs.rs/gc/" rel="noopener noreferrer"><code>gc</code></a> <sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">2</a></sup> 这样的可复用 crate。</p>

<p>第二种情况，根据我的经验，远比第一种更有趣，并且由于无法用现成的解决方案解决，所以更常出现：与（或实现）<em>确实</em>使用垃圾回收器的编程语言进行集成。<a href="https://github.com/servo/servo" rel="noopener noreferrer">Servo</a> 需要这样做来与 Spidermonkey JS 引擎集成，而 <a href="https://github.com/kyren/luster" rel="noopener noreferrer">luster</a> 需要这样做来实现其 Lua VM 的 GC。<a href="https://github.com/jasonwilliams/boa/" rel="noopener noreferrer">boa</a>，一个纯 Rust JS 运行时，使用 <a href="https://docs.rs/gc/" rel="noopener noreferrer"><code>gc</code></a> crate 作为其垃圾回收器的后端。</p>

<p>有时在与使用 GC 的语言集成时，你可以避免实现完整的垃圾回收器：JNI 就是这样做的；虽然 C++ 本身没有原生的垃圾回收，但 JNI 通过简单地将任何跨越到 C++ 边界的对象进行“根化（rooting）”（我们稍后会介绍这是什么意思）来绕过这一点<sup id="fnref:2"><a href="#fn:2" rel="noopener noreferrer">3</a></sup>。这通常没问题！</p>

<p>缺点是每次与 GC 管理的对象交互都必须通过 API 调用；你无法轻松地将高效的 Rust/C++ 对象“嵌入”到 GC 中。例如，在浏览器中，大多数 DOM 类型（例如 <a href="https://doc.servo.org/script/dom/element/struct.Element.html" rel="noopener noreferrer"><code>Element</code></a>）都是用原生代码实现的；并且需要能够包含对其他原生 GC 类型的引用（应该可以在不需要回调 JavaScript 引擎的情况下检查 <a href="https://doc.servo.org/script/dom/node/struct.Node.html#structfield.child_list" rel="noopener noreferrer"><code>Node</code> 的子节点</a>）。</p>

<p>因此，有时你需要能够从运行时与 GC 集成；或者如果你正在编写一个需要 GC 的运行时，甚至需要实现自己的 GC。在这两种情况下，你通常希望能够从 Rust 代码中安全地操作 GC 管理的对象，甚至直接将 Rust 类型放在 GC 堆上。</p>

<h2 id="why-are-gcs-in-rust-hard">为什么 Rust 中的 GC 很难实现？</h2>

<p>用一个词来说：根化（Rooting）。在垃圾回收器中，栈上“直接”使用的对象是“根”，你需要能够识别它们。这里我说“直接”是指“无需通过其他 GC 对象就能访问”，所以将对象放入 <code>Vec&lt;T&gt;</code> 中并不会使其停止成为根，但放入其他 GC 对象中就会。</p>

<p>不幸的是，Rust 没有真正意义上的“直接在栈上”的概念：</p>

<div><div><pre><code><span>struct</span> <span>Foo</span> <span>{</span>
    <span>bar</span><span>:</span> <span>Option</span><span>&lt;</span><span>Gc</span><span>&lt;</span><span>Bar</span><span>&gt;&gt;</span>
<span>}</span>
<span>// 这是一个根</span>
<span>let</span> <span>bar</span> <span>=</span> <span>Gc</span><span>::</span><span>new</span><span>(</span><span>Bar</span><span>::</span><span>new</span><span>());</span>
<span>// 这也是一个根</span>
<span>let</span> <span>foo</span> <span>=</span> <span>Gc</span><span>::</span><span>new</span><span>(</span><span>Foo</span><span>::</span><span>new</span><span>());</span>
<span>// bar 不应该再是根了（但我们无法检测到！）</span>
<span>foo</span><span>.bar</span> <span>=</span> <span>Some</span><span>(</span><span>bar</span><span>);</span>
<span>// 但 foo 在这里应该仍然是一个根，因为它没有在另一个 GC 对象内部</span>
<span>let</span> <span>v</span> <span>=</span> <span>vec!</span><span>[</span><span>foo</span><span>];</span>
</code></pre></div></div>

<p>Rust 的所有权系统实际上使得更容易拥有较少的根，因为它相对容易说明获取 GC 对象的 <code>&amp;T</code> 不需要创建新根，并让 Rust 的所有权系统来处理，但能够区分“直接拥有”和“间接拥有”是非常棘手的。</p>

<p>另一个方面是，垃圾回收实际上是一个全局变更的时刻——垃圾回收器遍历堆并删除其中的一些对象。这是脚底下的地毯被突然抽走的时刻。Rust 的整个设计都基于这种抽地毯行为是<em>非常非常糟糕且不允许发生的</em>，所以这可能会有点问题。这并不像最初听起来那么糟糕，毕竟抽地毯主要是清理不可达对象，但这在组装组件时确实会出现几次，尤其是在析构函数和终结器方面<sup id="fnref:3"><a href="#fn:3" rel="noopener noreferrer">4</a></sup>。如果，例如，你能够声明“不会发生 GC”的代码区域<sup id="fnref:4"><a href="#fn:4" rel="noopener noreferrer">5</a></sup>，那么根化将容易得多，这样你可以紧密地限定抽地毯的范围，而不必过多担心根。</p>

<h3 id="destructors-and-finalizers">析构函数与终结器</h3>

<p>值得特别指出析构函数。在 GC 类型上自定义析构函数的一个巨大问题是，自定义析构函数完全可能在垃圾回收期间将自身存入一个长期存活的引用中，导致悬垂引用：</p>

<div><div><pre><code><span>struct</span> <span>LongLived</span> <span>{</span>
    <span>dangle</span><span>:</span> <span>RefCell</span><span>&lt;</span><span>Option</span><span>&lt;</span><span>Gc</span><span>&lt;</span><span>CantKillMe</span><span>&gt;&gt;&gt;</span>
<span>}</span>

<span>struct</span> <span>CantKillMe</span> <span>{</span>
    <span>// 在构造时设置为指向自身</span>
    <span>self_ref</span><span>:</span> <span>RefCell</span><span>&lt;</span><span>Option</span><span>&lt;</span><span>Gc</span><span>&lt;</span><span>CantKillMe</span><span>&gt;&gt;&gt;</span>
    <span>long_lived</span><span>:</span> <span>Gc</span><span>&lt;</span><span>LongLived</span><span>&gt;</span>
<span>}</span>

<span>impl</span> <span>Drop</span> <span>for</span> <span>CantKillMe</span> <span>{</span>
    <span>fn</span> <span>drop</span><span>(</span><span>&amp;</span><span>mut</span> <span>self</span><span>)</span> <span>{</span>
        <span>// 将自身附加到 long_lived</span>
        <span>*</span><span>self</span><span>.long_lived.dangle</span><span>.borrow_mut</span><span>()</span> <span>=</span> <span>Some</span><span>(</span><span>self</span><span>.self_ref</span><span>.borrow</span><span>()</span><span>.clone</span><span>()</span><span>.unwrap</span><span>());</span>
    <span>}</span>
<span>}</span>

<span>let</span> <span>long</span> <span>=</span> <span>Gc</span><span>::</span><span>new</span><span>(</span><span>LongLived</span><span>::</span><span>new</span><span>());</span>
<span>{</span>
    <span>let</span> <span>cant</span> <span>=</span> <span>Gc</span><span>::</span><span>new</span><span>(</span><span>CantKillMe</span><span>::</span><span>new</span><span>());</span>
    <span>*</span><span>cant</span><span>.self_ref</span><span>.borrow_mut</span><span>()</span> <span>=</span> <span>Some</span><span>(</span><span>cant</span><span>.clone</span><span>());</span>
    <span>// cant 离开作用域，CantKillMe::drop 被运行</span>
    <span>// cant 被附加到 long_lived.dangle 但仍然被清理</span>
<span>}</span>

<span>// 悬垂引用！</span>
<span>let</span> <span>dangling</span> <span>=</span> <span>long</span><span>.dangle</span><span>.borrow</span><span>()</span><span>.unwrap</span><span>();</span>
</code></pre></div></div>

<p>最常见的解决方案是禁止在使用 <code>#[derive(Trace)]</code> 的类型上使用析构函数，这可以通过让自定义 derive 生成一个 <code>Drop</code> 实现来实现，或者生成一些导致类型冲突错误的东西来实现。</p>

<p>你可以另外提供一个具有不同语义的 <code>Finalize</code> 特性：GC 在清理 GC 对象时调用它，但它可能被调用多次，也可能根本不被调用。这类事情在 Rust 之外的 GC 中也很常见。</p>

<h2 id="how-would-you-even-garbage-collect-without-a-runtime">没有运行时你如何进行垃圾回收？</h2>

<p>在大多数带有垃圾回收的语言中，有一个运行时控制所有执行，知道程序中的每个变量，并且能够在任何时候暂停执行来运行 GC。</p>

<p>Rust 的运行时非常小，无法做到像这样，尤其是无法以你的库可以挂接的可插拔方式做到。对于线程本地 GC，你基本上必须编写代码，使得 GC 操作（如修改 GC 字段；基本上是你 GC 库暴露的 API 的某个子集）是唯一可能触发垃圾回收的操作。</p>

<p>并发 GC 可以在单独的线程上触发 GC，但通常需要在线程尝试执行可能被运行的垃圾回收器无效化的 GC 操作时暂停这些线程。</p>

<p>虽然这可能会限制垃圾回收器本身的灵活性，但从 API 设计的角度来看，这实际上对我们相当有利：垃圾回收阶段只能发生在代码的某些已知时刻，这意味着我们只需要确保这些<em>边界</em>上的事情是安全的。我们即将看到的许多设计都建立在这个观察的基础上。</p>

<h2 id="commonalities">共同点</h2>

<p>在介绍实际的 GC 设计示例之前，我想指出它们在设计上的一些共同点，尤其是在它们如何进行追踪方面：</p>

<h3 id="tracing">追踪</h3>

<p>“追踪”是从你的根开始遍历 GC 对象图，查看它们的子对象，以及子对象的子对象，依此类推的操作。</p>

<p>在 Rust 中，实现此操作最简单的方法是通过<a href="https://doc.rust-lang.org/book/ch19-06-macros.html#how-to-write-a-custom-derive-macro" rel="noopener noreferrer">自定义 derive</a>：</p>

<div><div><pre><code><span>// 手动实现是不安全的，因为你可能做错</span>
<span>unsafe</span> <span>trait</span> <span>Trace</span> <span>{</span>
    <span>fn</span> <span>trace</span><span>(</span><span>&amp;</span><span>mut</span> <span>self</span><span>,</span> <span>gc_context</span><span>:</span> <span>&amp;</span><span>mut</span> <span>GcContext</span><span>);</span>
<span>}</span>

<span>#[derive(Trace)]</span>
<span>struct</span> <span>Foo</span> <span>{</span>
    <span>vec</span><span>:</span> <span>Vec</span><span>&lt;</span><span>Gc</span><span>&lt;</span><span>Bar</span><span>&gt;&gt;</span><span>,</span>
    <span>extra_thing</span><span>:</span> <span>Gc</span><span>&lt;</span><span>Baz</span><span>&gt;</span><span>,</span>
    <span>just_a_string</span><span>:</span> <span>String</span>
<span>}</span>
</code></pre></div></div>

<p><code>Trace</code> 的自定义 derive 基本上只是在所有字段上调用 <code>trace()</code>。<code>Vec</code> 的 <code>Trace</code> 实现会调用其所有字段的 <code>trace()</code>，而 <code>String</code> 的 <code>Trace</code> 实现则什么都不做。<code>Gc&lt;T&gt;</code> 可能会有一个 <code>trace()</code> 方法，在 <code>GcContext</code> 中标记其可达性，或类似的操作。</p>

<p>这是一个相当标准的模式，虽然 <code>Trace</code> 特性的具体细节通常会有所不同，但大致思想是相似的。</p>

<p>我不会在这篇文章中深入讨论标记-清除算法如何工作的实际细节；它们有很多潜在的设计，并且从在 Rust 中设计安全的 GC <em>API</em> 的角度来看，它们并不是那么有趣。然而，一般的想法是维护一个最初由根填充的已发现对象队列，追踪它们以找到新对象，并在它们未被追踪过时将它们加入队列。清理任何<em>未被</em>发现的对象。</p>

<h3 id="immutable-by-default">默认不可变</h3>

<p>这些设计的另一个共同点是 <code>Gc&lt;T&gt;</code> 总是潜在共享的，因此需要对可变性进行严格控制以满足 Rust 的所有权不变式。这通常通过内部可变性（interior mutability）来实现，就像 <code>Rc&lt;T&gt;</code> 几乎总是与 <code>RefCell&lt;T&gt;</code> 配对用于可变访问一样，然而一些方法（如 <a href="https://github.com/asajeffrey/josephine" rel="noopener noreferrer">josephine</a> 中的方法）确实允许在没有运行时检查的情况下进行可变访问。</p>

<h3 id="threading">线程</h3>

<p>有些 GC 是单线程的，有些是多线程的。单线程的 GC 通常有一个不是 <code>Send</code> 的 <code>Gc&lt;T&gt;</code> 类型，所以虽然你可以在不同的线程上设置多个 GC 类型的图，但它们本质上是独立的。垃圾回收只影响为其执行的线程，所有其他线程可以不受阻碍地继续运行。</p>

<p>多线程 GC 将有一个 <code>Send</code> 的 <code>Gc&lt;T&gt;</code> 类型。垃圾回收通常（但并非总是）会在该时间段内阻塞任何试图访问 GC 管理数据的线程。在某些语言中，有“停止世界（stop the world）”的垃圾回收器，它们在编译器插入的“安全点（safepoints）”阻塞所有线程；Rust 没有能力插入此类安全点，在 GC 上阻塞线程是在库层面完成的。</p>

<p>下面的大多数示例是单线程的，但它们的 API 设计不难扩展到假想的多线程 GC。</p>

<h2 id="rust-gc">rust-gc</h2>

<p><a href="https://docs.rs/gc/" rel="noopener noreferrer"><code>gc</code></a> crate 是我与 <a href="https://twitter.com/kneecaw/" rel="noopener noreferrer">Nika Layzell</a> 一起编写的，主要是作为一个有趣的练习，以确定安全的 GC API 是否<em>可能</em>实现。我之前<a href="https://manishearth.github.io/blog/2015/09/01/designing-a-gc-in-rust/" rel="noopener noreferrer">深入写过其设计</a>，但其设计的本质是，它做了一些类似于引用计数的事情来跟踪根，并强制所有 GC 变更通过特殊的 <code>GcCell</code> 类型，以便它们可以更新根计数。基本上，每当某物成为根或停止成为根时，都会更新“根计数”：</p>

<div><div><pre><code><span>struct</span> <span>Foo</span> <span>{</span>
    <span>bar</span><span>:</span> <span>GcCell</span><span>&lt;</span><span>Option</span><span>&lt;</span><span>Gc</span><span>&lt;</span><span>Bar</span><span>&gt;&gt;&gt;</span>
<span>}</span>
<span>// 这是一个根（根计数 = 1）</span>
<span>let</span> <span>bar</span> <span>=</span> <span>Gc</span><span>::</span><span>new</span><span>(</span><span>Bar</span><span>::</span><span>new</span><span>());</span>
<span>// 这也是一个根（根计数 = 1）</span>
<span>let</span> <span>foo</span> <span>=</span> <span>Gc</span><span>::</span><span>new</span><span>(</span><span>Foo</span><span>::</span><span>new</span><span>());</span>
<span>// .borrow_mut() 的 RAII guard 将 bar 根化（设置其根计数为 0）</span>
<span>*</span><span>foo</span><span>.bar</span><span>.borrow_mut</span><span>()</span> <span>=</span> <span>Some</span><span>(</span><span>bar</span><span>);</span>
<span>// foo 在这里仍然是一个根，没有调用 .set()</span>
<span>let</span> <span>v</span> <span>=</span> <span>vec!</span><span>[</span><span>foo</span><span>];</span>

<span>// 在析构时，foo 的根计数被设置为 0</span>
</code></pre></div></div>

<p>实际的垃圾回收阶段会在根据某些启发式方法认为堆已经变得足够大时，执行某些 GC 操作时发生。</p>

<p>虽然这对读取来说本质上是“免费”的，但这在任何类型的写操作上都产生了相当大的引用计数流量，这可能不是期望的；使用 GC 的目标通常是为了<em>避免</em>类似引用计数模式的性能特征。最终这是一种混合方法，结合了追踪和引用计数<sup id="fnref:10"><a href="#fn:10" rel="noopener noreferrer">6</a></sup>。</p>

<p><a href="https://docs.rs/gc/" rel="noopener noreferrer"><code>gc</code></a> 作为一个通用的 GC 是有用的，如果你只是希望一些东西参与循环而不需要思考太多。整体设计可以应用于与其他语言运行时集成的专用 GC，因为它提供了一种清晰的方式来跟踪根；但它可能不一定具有所需的性能特征。</p>

<h2 id="servos-dom-integration">Servo 的 DOM 集成</h2>

<p><a href="https://github.com/servo/servo" rel="noopener noreferrer">Servo</a> 是一个用 Rust 编写的浏览器引擎，我曾经全职参与过。如前所述，浏览器引擎通常用原生代码（即 Rust 或 C++，而不是 JS）实现其大部分 DOM 类型，所以例如 <a href="https://doc.servo.org/script/dom/element/struct.Element.html" rel="noopener noreferrer"><code>Node</code></a> 是一个纯 Rust 对象，并且它<a href="https://doc.servo.org/script/dom/node/struct.Node.html#structfield.child_list" rel="noopener noreferrer">包含对其子节点的直接引用</a>，这样 Rust 代码就可以进行遍历树等操作，而无需在 JS 和 Rust 之间来回切换。</p>

<p>Servo 的模型有点奇怪：根是<em>不同的类型</em>，并且 lint 会强制执行未根化的堆引用永远不会放在栈上：</p>

<div><div><pre><code><span>#[dom_struct]</span> <span>// 这是 #[derive(JSTraceable)] 加上一些用于 lint 的标记</span>
<span>pub</span> <span>struct</span> <span>Node</span> <span>{</span>
    <span>// 父类型，用于继承</span>
    <span>eventtarget</span><span>:</span> <span>EventTarget</span><span>,</span>
    <span>// 在实际代码中，这是一个组合了 RefCell、Option 和 Dom 的不同辅助类型，但我为简化示例使用了 stdlib 类型</span>
    <span>prev_sibling</span><span>:</span> <span>RefCell</span><span>&lt;</span><span>Option</span><span>&lt;</span><span>Dom</span><span>&lt;</span><span>Node</span><span>&gt;&gt;&gt;</span><span>,</span>
    <span>next_sibling</span><span>:</span> <span>RefCell</span><span>&lt;</span><span>Option</span><span>&lt;</span><span>Dom</span><span>&lt;</span><span>Node</span><span>&gt;&gt;&gt;</span><span>,</span>
    <span>// ...</span>
<span>}</span>

<span>impl</span> <span>Node</span> <span>{</span>
    <span>fn</span> <span>frob_next_sibling</span><span>(</span><span>&amp;</span><span>self</span><span>)</span> <span>{</span>
        <span>// 字段可以作为借用访问，无需任何根化</span>
        <span>if</span> <span>let</span> <span>Some</span><span>(</span><span>next</span><span>)</span> <span>=</span> <span>self</span><span>.next_sibling</span><span>.borrow</span><span>()</span><span>.as_ref</span><span>()</span> <span>{</span>
            <span>next</span><span>.frob</span><span>();</span>
        <span>}</span>
    <span>}</span>

    <span>fn</span> <span>get_next_sibling</span><span>(</span><span>&amp;</span><span>self</span><span>)</span> <span>-&gt;</span> <span>Option</span><span>&lt;</span><span>DomRoot</span><span>&lt;</span><span>Node</span><span>&gt;&gt;</span> <span>{</span>
        <span>// 但你需要将它们根化，以便它们能逃逸借用</span>
        <span>// .root() 将 Dom&lt;T&gt; 转换为 DomRoot&lt;T&gt;</span>
        <span>self</span><span>.next_sibling</span><span>.borrow</span><span>()</span><span>.as_ref</span><span>()</span><span>.map</span><span>(|</span><span>x</span><span>|</span> <span>n</span><span>.root</span><span>())</span>
    <span>}</span>

    <span>fn</span> <span>illegal</span><span>(</span><span>&amp;</span><span>self</span><span>)</span> <span>{</span>
        <span>// 这行代码会被一个名为 unrooted_must_root 的自定义 lint 检查</span>
        <span>// （它的工作方式类似于 Rust 的 must_use）</span>
        <span>let</span> <span>ohno</span><span>:</span> <span>Dom</span><span>&lt;</span><span>Node</span><span>&gt;</span> <span>=</span> <span>self</span><span>.next_sibling</span><span>.borrow_mut</span><span>()</span><span>.take</span><span>();</span>
    <span>}</span>
<span>}</span>
</code></pre></div></div>

<p><code>Dom&lt;T&gt;</code> 基本上是一个智能指针，行为像 <code>&amp;T</code> 但没有生命周期，而 <code>DomRoot&lt;T&gt;</code> 具有创建时根化（并在 <code>Drop</code> 时取消根化）的附加行为。自定义 lint 插件本质上强制执行 <code>Dom&lt;T&gt;</code>，以及任何 DOM 结构体（用 <code>#[dom_struct]</code> 标记），除了通过 <code>DomRoot&lt;T&gt;</code> 或 <code>&amp;T</code>，否则永远无法在栈上访问。</p>

<p>我不推荐这种方法；它工作得还好，但我们早就想摆脱它了，因为它依赖于自定义插件 lint 来保证健全性。但为了完整性，值得一提。</p>

<h2 id="josephine-servos-experimental-gc-plans">Josephine（Servo 的实验性 GC 方案）</h2>

<p>鉴于 Servo 现有的 GC 解决方案依赖于编译器进行额外的静态分析，我们想要更好的方案。因此 <a href="https://github.com/asajeffrey/" rel="noopener noreferrer">Alan</a> 设计了 <a href="https://github.com/asajeffrey/josephine" rel="noopener noreferrer">Josephine</a>（“JS 仿射”），它更干净地使用 Rust 的仿射类型和借用（borrowing）来提供一个安全的 GC 系统。</p>

<p>Josephine 是专门为 Servo 的用例设计的，因此它围绕“区域（compartments）”等做了很多巧妙的事情，除非你特别希望你的 GC 与 JS 引擎集成，否则这些可能无关紧要。</p>

<p>我前面提到过，垃圾回收阶段只能发生在代码的某些已知时刻，这实际上可以使 GC 设计更容易，而 Josephine 就是这样一个例子。</p>

<p>Josephine 有一个“JS 上下文（JS context）”，它需要在各处传递，本质上代表 GC 本身。当执行可能触发 GC 的操作时，你必须可变地借用上下文，而当访问堆对象时，你需要不可变地借用上下文。你可以根化堆对象以消除这个要求：</p>

<div><div><pre><code><span>// cx 是一个 `JSContext`，`node` 是一个 `JSManaged&lt;'a, C, Node&gt;`</span>
<span>// 为简化起见，假设 next_sibling 和 prev_sibling 不是 Option</span>

<span>// 为 `'b` 借用 cx</span>
<span>let</span> <span>next_sibling</span><span>:</span> <span>&amp;</span><span>'b</span> <span>Node</span> <span>=</span> <span>node</span><span>.next_sibling</span><span>.borrow</span><span>(</span><span>cx</span><span>);</span>
<span>println!</span><span>(</span><span>"Name: {:?}"</span><span>,</span> <span>next_sibling</span><span>.name</span><span>);</span>
<span>// 非法，因为 cx 被 next_sibling 不可变地借用了</span>
<span>// node.prev_sibling.borrow_mut(cx).frob();</span>

<span>// 从 next_sibling 读取以确保它存活足够长</span>
<span>println!</span><span>(</span><span>"{:?}"</span><span>,</span> <span>next_sibling</span><span>.name</span><span>);</span>

<span>let</span> <span>ref</span> <span>mut</span> <span>root</span> <span>=</span> <span>cx</span><span>.new_root</span><span>();</span>
<span>// 不再需要借用 cx，而是借用 root 持续时间为 'root</span>
<span>let</span> <span>next_sibling</span><span>:</span> <span>JSManaged</span><span>&lt;</span><span>'root</span><span>,</span> <span>C</span><span>,</span> <span>Node</span><span>&gt;</span> <span>=</span> <span>node</span><span>.next_sibling</span><span>.in_root</span><span>(</span><span>root</span><span>);</span>
<span>// 现在没问题了，`cx` 没有未解决的借用</span>
<span>node</span><span>.prev_sibling</span><span>.borrow_mut</span><span>(</span><span>cx</span><span>)</span><span>.frob</span><span>();</span>

<span>// 从 next_sibling 读取以确保它存活足够长</span>
<span>println!</span><span>(</span><span>"{:?}"</span><span>,</span> <span>next_sibling</span><span>.name</span><span>);</span>
</code></pre></div></div>

<p><code>new_root()</code> 创建一个新的根，而 <code>in_root</code> 将 JS 托管类型的生命周期绑定到根而不是 <code>JSContext</code> 借用，释放了 <code>JSContext</code> 的借用，允许在未来 <code>.borrow_mut()</code> 调用中可变借用。</p>

<p>请注意，这里的 <code>.borrow()</code> 和 <code>.borrow_mut()</code> 尽管与 <code>RefCell::borrow()</code> 相似，但没有运行时借用检查成本；它们反而进行了一些生命周期变换（juggling）以确保安全。创建根通常确实有运行时成本。有时你<em>可能</em>需要使用 <code>RefCell&lt;T&gt;</code>，原因与在 <code>Rc</code> 中使用相同，但大多仅用于非 GC 字段。</p>

<p>自定义类型通常这样定义为两部分：</p>

<div><div><pre><code><span>#[derive(Copy,</span> <span>Clone,</span> <span>Debug,</span> <span>Eq,</span> <span>PartialEq,</span> <span>JSTraceable,</span> <span>JSLifetime,</span> <span>JSCompartmental)]</span>
<span>pub</span> <span>struct</span> <span>Element</span><span>&lt;</span><span>'a</span><span>,</span> <span>C</span><span>&gt;</span> <span>(</span><span>pub</span> <span>JSManaged</span><span>&lt;</span><span>'a</span><span>,</span> <span>C</span><span>,</span> <span>NativeElement</span><span>&lt;</span><span>'a</span><span>,</span> <span>C</span><span>&gt;&gt;</span><span>);</span>

<span>#[derive(JSTraceable,</span> <span>JSLifetime,</span> <span>JSCompartmental)]</span>
<span>pub</span> <span>struct</span> <span>NativeElement</span><span>&lt;</span><span>'a</span><span>,</span> <span>C</span><span>&gt;</span> <span>{</span>
    <span>name</span><span>:</span> <span>JSString</span><span>&lt;</span><span>'a</span><span>,</span> <span>C</span><span>&gt;</span><span>,</span>
    <span>parent</span><span>:</span> <span>Option</span><span>&lt;</span><span>Element</span><span>&lt;</span><span>'a</span><span>,</span> <span>C</span><span>&gt;&gt;</span><span>,</span>
    <span>children</span><span>:</span> <span>Vec</span><span>&lt;</span><span>Element</span><span>&lt;</span><span>'a</span><span>,</span> <span>C</span><span>&gt;&gt;</span><span>,</span>
<span>}</span>
</code></pre></div></div>

<p>其中 <code>Element&lt;'a&gt;</code> 是一个方便的可复制引用，用于其他 GC 类型内部，而 <code>NativeElement&lt;'a&gt;</code> 是其后备存储。<code>C</code> 参数与区域有关，现在可以忽略。</p>

<p>值得一提的一个巧妙之处是，即使根允许你持有对同一对象的多个引用，操作其他 GC 引用也<em>不需要</em>运行时借用检查！</p>

<div><div><pre><code><span>let</span> <span>parent_root</span> <span>=</span> <span>cx</span><span>.new_root</span><span>();</span>
<span>let</span> <span>parent</span> <span>=</span> <span>element</span><span>.borrow</span><span>(</span><span>cx</span><span>)</span><span>.parent</span><span>.in_root</span><span>(</span><span>parent_root</span><span>);</span>
<span>let</span> <span>ref</span> <span>mut</span> <span>child_root</span> <span>=</span> <span>cx</span><span>.new_root</span><span>();</span>

<span>// 可能是 `element` 的第二个引用，如果它是第一个子节点的话</span>
<span>let</span> <span>first_child</span> <span>=</span> <span>parent</span><span>.children</span><span>[</span><span>0</span><span>]</span><span>.in_root</span><span>(</span><span>child_root</span><span>);</span>

<span>// 这没问题，即使我们通过 element.parent 持有对 `parent` 的引用</span>
<span>// 因为我们已经根化了该引用，所以它现在独立于 `element.parent` 是否更改！</span>
<span>first_child</span><span>.borrow_mut</span><span>(</span><span>cx</span><span>)</span><span>.parent</span> <span>=</span> <span>None</span><span>;</span>
</code></pre></div></div>

<p>本质上，当修改字段时，你必须获得对上下文的可变访问权，因此字段本身不会有仍然存在的引用（例如 <code>element.borrow(cx).parent</code>），只有对其内部的 GC 数据的引用，所以你可以更改字段引用的内容而不会使其他对字段引用的<em>内容</em>的引用失效。这是一个非常巧妙的技巧，实现了<em>没有运行时检查的内部可变性</em>的 GC，这在类似的设计中相对罕见。</p>

<h2 id="unfinished-design-for-a-builtin-rust-gc">Rust 内置 GC 的未完成设计</h2>

<p>有一段时间，我们几个人研究了一种使 Rust <em>本身</em> 可扩展并支持可插拔 GC 的方法，利用 LLVM 栈映射（stack map）支持来查找根。毕竟，如果我们知道哪些类型是 GC 类的，我们就可以为每个函数包含如何查找根的元数据，类似于 Rust 函数当前包含的展开（unwinding）钩子，以便在 panic 期间干净地运行析构函数。</p>

<p>我们从未完成设计，但你可以在<a href="https://manishearth.github.io/blog/2016/08/18/gc-support-in-rust-api-design/" rel="noopener noreferrer">我</a>和<a href="http://blog.pnkfx.org/blog/categories/gc/" rel="noopener noreferrer">Felix</a>关于这个主题的文章中找到更多信息。本质上，它涉及一个具有更通用 <code>trace</code> 方法的 <code>Trace</code> 特性，一个自动实现的 <code>Root</code> 特性（其工作方式类似于 <code>Send</code>），以及编译器机制来跟踪哪些 <code>Root</code> 类型在栈上。</p>

<p>这对于试图实现 GC 的人可能不太有用，但我为了完整性而提及它。</p>

<p>请注意，1.0 之前的 Rust 确实有一个内置的 GC（<code>@T</code>，被称为“托管指针”），但据我回忆，实际上循环管理部分从未被实现，所以它表现得完全像 <code>Rc&lt;T&gt;</code>。我相信它本意是要有一个循环收集器（我将在下一节讨论更多）。</p>

<h2 id="bacon-rajan-cc-and-cycle-collectors-in-general">bacon-rajan-cc（以及一般的循环收集器）</h2>

<p><a href="https://fitzgeraldnick.com/" rel="noopener noreferrer">Nick Fitzgerald</a> 编写了 <a href="https://github.com/fitzgen/bacon-rajan-cc" rel="noopener noreferrer"><code>bacon-rajan-cc</code></a> 来实现 David F. Bacon 和 V.T. Rajan 的论文 <a href="https://researcher.watson.ibm.com/researcher/files/us-bacon/Bacon01Concurrent.pdf" rel="noopener noreferrer">"Concurrent Cycle Collection in Reference Counted Systems"</a>。</p>

<p>这就是俗称的<em>循环收集器（cycle collector）</em>；一种垃圾回收器，本质上可以理解为“如果我们拿了 <code>Rc&lt;T&gt;</code> 但让它能检测循环引用会怎样”。有些人不认为这些是<em>追踪式</em>垃圾回收器，但它们有很多类似的特征（并且它们确实仍然“追踪”类型）。它们通常被归类为“混合”方法，就像 <a href="https://docs.rs/gc/" rel="noopener noreferrer"><code>gc</code></a> 一样。</p>

<p>其思想是，如果你维护引用计数，你实际上不需要<em>知道</em>根是什么：如果一个堆对象的引用计数多于引用它的堆对象数量，那么它一定是一个根。实际上，遍历整个堆效率很低，因此会应用优化，通常是通过给节点分配不同的“颜色”，并且只查看最近其引用计数递减的对象集合。</p>

<p>这里一个关键的观察是，如果你<em>只关注潜在的垃圾</em>，你可以稍微调整你对“根”的定义，在寻找循环引用时，你不需要寻找来自栈的引用，你可以满足于来自<em>你确切知道可以从非潜在垃圾对象可达的堆的任何部分</em>的引用。</p>

<p>循环收集器的一个巧妙特性是，虽然标记-清除追踪式 GC 的性能随整个堆的大小而变化，但循环收集器的性能随<em>你拥有的实际垃圾</em>的大小而变化<sup id="fnref:5"><a href="#fn:5" rel="noopener noreferrer">7</a></sup>。当然还有其他权衡：在追踪式 GC 中，释放通常更便宜或“免费”（通过在清除阶段摊销这些成本），而循环收集器在引用计数归零时清理对象涉及恒定的分配器流量。</p>

<p><a href="https://github.com/fitzgen/bacon-rajan-cc" rel="noopener noreferrer">bacon-rajan-cc</a> 的工作方式是，每次引用计数递减时，该对象都会被添加到“潜在循环根”列表中，除非引用计数递减到 0（在这种情况下，对象会立即被清理，就像 <code>Rc</code> 一样）。然后它追踪这个列表；对于它跟随的每个引用递减引用计数，并清理任何引用计数达到 0 的元素。然后它<em>再次</em>遍历这个列表，并对它跟随的每个引用递增引用计数，以恢复原始的引用计数。这基本上将任何不能从这个“潜在循环根”列表可达的元素视为“非垃圾”，并且不去访问它。</p>

<p>循环收集器需要对垃圾回收算法进行更紧密的控制，并且具有不同的性能特征，因此它们不一定适用于 Rust 中 GC 集成的所有用例，但绝对值得考虑！</p>

<h2 id="cell-gc">cell-gc</h2>

<p><a href="https://twitter.com/jorendorff/" rel="noopener noreferrer">Jason Orendorff</a> 的 <a href="https://github.com/jorendorff/cell-gc" rel="noopener noreferrer">cell-gc</a> crate 很有趣，它有一个“堆会话（heap sessions）”的概念。这是自定义 readme 的一个修改示例：</p>

<div><div><pre><code><span>use</span> <span>cell_gc</span><span>::</span><span>Heap</span><span>;</span>

<span>// 实现 IntoHeap，并生成一个 IntListRef 类型和访问器</span>
<span>#[derive(cell_gc_derive::IntoHeap)]</span>
<span>struct</span> <span>IntList</span><span>&lt;</span><span>'h</span><span>&gt;</span> <span>{</span>
    <span>head</span><span>:</span> <span>i64</span><span>,</span>
    <span>tail</span><span>:</span> <span>Option</span><span>&lt;</span><span>IntListRef</span><span>&lt;</span><span>'h</span><span>&gt;&gt;</span>
<span>}</span>

<span>fn</span> <span>main</span><span>()</span> <span>{</span>
    <span>// 创建一个堆（你整个程序只做一次）</span>
    <span>let</span> <span>mut</span> <span>heap</span> <span>=</span> <span>Heap</span><span>::</span><span>new</span><span>();</span>

    <span>heap</span><span>.enter</span><span>(|</span><span>hs</span><span>|</span> <span>{</span>
        <span>// 分配一个对象（返回一个 IntListRef）</span>
        <span>let</span> <span>obj1</span> <span>=</span> <span>hs</span><span>.alloc</span><span>(</span><span>IntList</span> <span>{</span> <span>head</span><span>:</span> <span>17</span><span>,</span> <span>tail</span><span>:</span> <span>None</span> <span>});</span>
        <span>assert_eq!</span><span>(</span><span>obj1</span><span>.head</span><span>(),</span> <span>17</span><span>);</span>
        <span>assert_eq!</span><span>(</span><span>obj1</span><span>.tail</span><span>(),</span> <span>None</span><span>);</span>

        <span>// 分配另一个对象</span>
        <span>let</span> <span>obj2</span> <span>=</span> <span>hs</span><span>.alloc</span><span>(</span><span>IntList</span> <span>{</span> <span>head</span><span>:</span> <span>33</span><span>,</span> <span>tail</span><span>:</span> <span>Some</span><span>(</span><span>obj1</span><span>)</span> <span>});</span>
        <span>assert_eq!</span><span>(</span><span>obj2</span><span>.head</span><span>(),</span> <span>33</span><span>);</span>
        <span>assert_eq!</span><span>(</span><span>obj2</span><span>.tail</span><span>()</span><span>.unwrap</span><span>()</span><span>.head</span><span>(),</span> <span>17</span><span>);</span>

        <span>// 修改 `tail`</span>
        <span>obj2</span><span>.set_tail</span><span>(</span><span>None</span><span>);</span>
    <span>});</span>
<span>}</span>
</code></pre></div></div>

<p>所有修改都通过自动生成的访问器进行，因此该 crate 对通过 GC 的流量有更多控制。这些访问器通过类似于 <a href="https://docs.rs/gc/" rel="noopener noreferrer"><code>gc</code></a> 所做方案的方案帮助跟踪根；其中使用 <code>IntoHeap</code> 特性在引用通过访问器放入和取出堆时修改根引用计数。</p>

<p>堆会话允许堆被移动，甚至发送到其他线程，它们的生命周期防止堆对象在会话之间混合。这使用了一个称为<em>世代性（generativity）</em>的概念；你可以在<a href="https://github.com/Gankra" rel="noopener noreferrer">Aria Beingessner</a> 的<a href="https://raw.githubusercontent.com/Gankra/thesis/master/thesis.pdf" rel="noopener noreferrer">《You Can't Spell Trust Without Rust》</a>第 6.3 章，或通过查看 <a href="https://github.com/bluss/indexing" rel="noopener noreferrer"><code>indexing</code></a> crate 来了解更多关于世代性的信息。</p>

<h2 id="interlude-the-similarities-between-async-and-gcs">幕间：async 与 GC 的相似性</h2>

<p>接下来的两个示例使用 Rust 的 <code>async</code> 功能的机制，尽管与 async I/O 无关，我认为解释一下为什么这是合理的很重要。我<a href="https://twitter.com/ManishEarth/status/1073651552768819200" rel="noopener noreferrer">之前发过推文</a>：<a href="https://github.com/kyren" rel="noopener noreferrer">Catherine West</a> 和我是在讨论她基于 <code>async</code> 的<a href="https://github.com/kyren/gc-arena" rel="noopener noreferrer">GC 想法</a>时发现这一点的。</p>

<p>你可以在 Go 中看到这种对应性：Go 是一种同时具有垃圾回收和 async I/O 的语言，两者都使用相同的“安全点”让出给垃圾回收器或调度器。在 Go 中，编译器需要自动插入代码来检查堆的“脉搏”，并可能运行垃圾回收。它还需要自动插入代码来告诉调度器“嘿，现在是让我安全中断的好时机，如果另一个 goroutine 想要运行的话”。这些在原理上非常相似——它们本质上都是编译器插入的“现在可以中断我”的检查点，有时称为“中断点”或“让出点（yield points）”。</p>

<p>现在，Rust 编译器不会自动插入中断点。然而，Rust 中 <code>async</code> 的设计本质上是一种向 Rust 添加<em>显式</em>中断点的方式。Rust 中的 <code>foo().await</code> 是运行 <code>foo()</code> 并期望调度器<em>可能</em>在两者之间中断代码的方式。<a href="https://doc.rust-lang.org/nightly/std/future/trait.Future.html" rel="noopener noreferrer"><code>Future</code></a> 和 <a href="https://doc.rust-lang.org/nightly/std/pin/struct.Pin.html" rel="noopener noreferrer"><code>Pin&lt;P&gt;</code></a> 的设计是为了使这既安全又令人愉快。</p>

<p>正如我们将看到的，相同的机制可用于在 Rust 中为 GC 创建安全的中断点。</p>

<h2 id="shifgrethor">Shifgrethor</h2>

<p><a href="https://github.com/withoutboats/shifgrethor" rel="noopener noreferrer">shifgrethor</a> 是 <a href="https://github.com/withoutboats/" rel="noopener noreferrer">Saoirse</a> 的一个实验，尝试构建一个使用 <a href="https://doc.rust-lang.org/nightly/std/pin/struct.Pin.html" rel="noopener noreferrer"><code>Pin&lt;P&gt;</code></a> 来管理根的 GC。他们已经写了大量关于 <a href="https://github.com/withoutboats/shifgrethor" rel="noopener noreferrer">shifgrethor</a> 设计的<a href="https://without.boats/tags/shifgrethor/" rel="noopener noreferrer">博客文章</a>。特别是，<a href="https://without.boats/blog/shifgrethor-iii/" rel="noopener noreferrer">关于根化的文章</a>详细介绍了根化是如何工作的。</p>

<p>基本设计是有一个 <code>Root&lt;'root&gt;</code> 类型，其中包含一个 <code>Pin&lt;P&gt;</code></p><p><em>由 mimo-v2.5 模型翻译，花费 38191 tokens</em></p>]]></content:encoded>
      <link>http://manishearth.github.io/blog/2021/04/05/a-tour-of-safe-tracing-gc-designs-in-rust/</link>
      <guid isPermaLink="false">http://manishearth.github.io/blog/2021/04/05/a-tour-of-safe-tracing-gc-designs-in-rust</guid>
      <pubDate>Mon, 5 Apr 2021 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Rust中的Arena</title>
      <description>[AI 摘要] 本文介绍了Rust中Arena的用法、现有crate实现，并深入探讨了自引用Arena中涉及的酷炫生命周期效应。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 本文介绍了Rust中Arena的用法、现有crate实现，并深入探讨了自引用Arena中涉及的酷炫生命周期效应。</div><p>最近关于Rust中的Arena有一些讨论，我想就这个话题写点东西。</p>

<p>Arena并不是Rust中你通常会首先想到的东西，因此了解它的人较少；你通常只会在某些特定应用场景中看到它。通常，你可以通过引入一个crate来使用Arena，而无需额外使用<code>unsafe</code>，所以在Rust中不必对它特别担忧，而且了解它似乎很有用，特别是对于从Arena更常见的领域转向Rust的人来说。</p>

<p>此外，当实现自引用Arena时，涉及一系列<em>非常酷的</em>生命周期效应，我认为之前还没有人写过相关内容。</p>

<p>我写这篇文章主要是为了探讨那些酷的生命周期效应，但我觉得值得写一个对所有Rust程序员都有价值的通用介绍。如果你知道Arena是什么，只想了解酷的生命周期效应，可以直接跳到<a href="#implementing-a-self-referential-arena" rel="noopener noreferrer">实现自引用Arena的部分</a>。否则，请继续阅读。</p>

<h2 id="whats-an-arena">什么是Arena？</h2>

<p>Arena本质上是一种将预期具有相同生命周期的分配分组的方法。有时你需要为一个事件的生命周期分配一批对象，之后可以将它们全部丢弃。每次都调用系统分配器效率低下，更可取的做法是为你的对象<em>预分配</em>一批内存，在完成后一次性清理。</p>

<p>广泛来说，你可能希望使用Arena有两个原因：</p>

<p>首先，如前所述，你的主要目标可能是减少分配压力。例如，在游戏或应用程序中，可能有大量每帧需要分配然后丢弃的每帧对象。这在游戏开发中尤为常见，游戏开发者往往很关心分配器压力。使用Arena，可以轻松分配一个Arena，在每帧填充它，然后在帧结束后清理它。这还有缓存局部性的额外好处：你可以确保大多数每帧对象（可能比其他对象更频繁使用）在帧期间通常位于缓存中，因为它们是相邻分配的。</p>

<p>另一个目标可能是你想编写自引用数据，比如一个可以一次性清理的复杂带循环图。例如，在编写编译器时，类型信息可能需要引用其他类型和其他数据，导致一个复杂的、可能是循环的类型图。一旦你计算了一个类型，你可能不需要单独丢弃它，所以你可以使用一个Arena来存储所有计算出的类型信息，在类型不再重要的阶段一次性清理所有内容。使用这种模式可以让你的代码不必担心自引用部分是否会被“提前”释放，它让你可以假设如果你有一个<code>Ty</code>，它的生命周期与所有其他<code>Ty</code>相同，并且可以直接引用它们。</p>

<p>这两个目标不一定互斥：你可能希望使用Arena同时实现这两个目标。但你也可以有一个不允许自引用类型的Arena（但具有其他良好特性）。在本文的后面，我将实现一个允许自引用类型但在分配压力方面不理想的Arena，主要是为了便于实现。<em>通常</em>如果你为自引用类型编写Arena，你可以让它同时减少分配器压力，但也可能存在权衡。</p>

<h2 id="how-can-i-use-an-arena-in-rust">如何在Rust中使用Arena？</h2>

<p>通常，要<em>使用</em>Arena，你只需引入一个实现了正确类型Arena的crate。我知道有两个，下面会讨论，不过<a href="https://crates.io/search?q=arena" rel="noopener noreferrer">在crates.io上快速搜索“arena”</a>会发现许多其他有前途的候选者。</p>

<p>我要指出的是，如果你只需要循环图结构，你不一定<em>必须</em>使用Arena，优秀的<a href="https://docs.rs/petgraph/" rel="noopener noreferrer"><code>petgraph</code></a> crate通常就足够了。<a href="https://docs.rs/slotmap/" rel="noopener noreferrer"><code>slotmap</code></a>也很有用；它是一个基于代际索引的映射式数据结构，适用于自引用数据。</p>

<h3 id="bumpalo">Bumpalo</h3>

<p><a href="https://docs.rs/bumpalo" rel="noopener noreferrer"><code>Bumpalo</code></a>是一个快速的“凸分配器”，允许异构内容，只有在你不在乎析构函数运行的情况下才允许循环。</p>

<div><div><pre><code><span>use</span> <span>bumpalo</span><span>::</span><span>Bump</span><span>;</span>

<span>// (示例略作修改自 `bumpalo` 文档)</span>

<span>// 创建一个用于凸分配的新 Arena。</span>
<span>let</span> <span>bump</span> <span>=</span> <span>Bump</span><span>::</span><span>new</span><span>();</span>

<span>// 在 Arena 中分配值。</span>
<span>let</span> <span>scooter</span> <span>=</span> <span>bump</span><span>.alloc</span><span>(</span><span>Doggo</span> <span>{</span>
    <span>cuteness</span><span>:</span> <span>u64</span><span>::</span><span>max_value</span><span>(),</span>
    <span>age</span><span>:</span> <span>8</span><span>,</span>
    <span>scritches_required</span><span>:</span> <span>true</span><span>,</span>
<span>});</span>

<span>// 生日快乐，Scooter！</span>
<span>scooter</span><span>.age</span> <span>+=</span> <span>1</span><span>;</span>
</code></pre></div></div>

<p>每次调用<a href="https://docs.rs/bumpalo/3.6.1/bumpalo/struct.Bump.html#method.alloc" rel="noopener noreferrer"><code>Bump::alloc()</code></a>都会返回一个指向已分配对象的可变引用。你可以分配不同的对象，它们甚至可以相互引用<sup id="fnref:0"><a href="#fn:0" rel="noopener noreferrer">1</a></sup>。默认情况下，它不会对其内容调用析构函数；但是，你可以使用<a href="https://docs.rs/bumpalo/3.6.1/bumpalo/boxed/index.html" rel="noopener noreferrer"><code>bumpalo::boxed</code></a>（或Nightly版的自定义分配器）来获得此行为。类似地，你可以使用<a href="https://docs.rs/bumpalo/3.6.1/bumpalo/collections/index.html" rel="noopener noreferrer"><code>bumpalo::collections</code></a>来获得由<a href="https://docs.rs/bumpalo" rel="noopener noreferrer"><code>bumpalo</code></a>支持的向量和字符串。<a href="https://docs.rs/bumpalo/3.6.1/bumpalo/boxed/index.html" rel="noopener noreferrer"><code>bumpalo::boxed</code></a>将不被允许参与循环。</p>

<h3 id="typed-arena"><code>typed-arena</code></h3>

<p><a href="https://docs.rs/typed-arena/" rel="noopener noreferrer"><code>typed-arena</code></a>是一个只能存储单一类型对象的Arena分配器，但它允许建立循环引用：</p>

<div><div><pre><code><span>// 来自 typed-arena 文档的示例</span>

<span>use</span> <span>std</span><span>::</span><span>cell</span><span>::</span><span>Cell</span><span>;</span>
<span>use</span> <span>typed_arena</span><span>::</span><span>Arena</span><span>;</span>

<span>struct</span> <span>CycleParticipant</span><span>&lt;</span><span>'a</span><span>&gt;</span> <span>{</span>
    <span>other</span><span>:</span> <span>Cell</span><span>&lt;</span><span>Option</span><span>&lt;&amp;</span><span>'a</span> <span>CycleParticipant</span><span>&lt;</span><span>'a</span><span>&gt;&gt;&gt;</span><span>,</span>
<span>}</span>

<span>let</span> <span>arena</span> <span>=</span> <span>Arena</span><span>::</span><span>new</span><span>();</span>

<span>let</span> <span>a</span> <span>=</span> <span>arena</span><span>.alloc</span><span>(</span><span>CycleParticipant</span> <span>{</span> <span>other</span><span>:</span> <span>Cell</span><span>::</span><span>new</span><span>(</span><span>None</span><span>)</span> <span>});</span>
<span>let</span> <span>b</span> <span>=</span> <span>arena</span><span>.alloc</span><span>(</span><span>CycleParticipant</span> <span>{</span> <span>other</span><span>:</span> <span>Cell</span><span>::</span><span>new</span><span>(</span><span>None</span><span>)</span> <span>});</span>

<span>// 事后进行变异以设置循环</span>
<span>a</span><span>.other</span><span>.set</span><span>(</span><span>Some</span><span>(</span><span>b</span><span>));</span>
<span>b</span><span>.other</span><span>.set</span><span>(</span><span>Some</span><span>(</span><span>a</span><span>));</span>
</code></pre></div></div>

<p>与<a href="https://docs.rs/bumpalo" rel="noopener noreferrer"><code>bumpalo</code></a>不同，<a href="https://docs.rs/typed-arena/" rel="noopener noreferrer"><code>typed-arena</code></a>在Arena本身离开作用域时总会对其内容运行析构函数<sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">2</a></sup>。</p>

<h2 id="implementing-a-self-referential-arena">实现一个自引用Arena</h2>

<p>自引用Arena之所以有趣，是因为通常Rust对自引用数据非常非常警惕。但Arena允许你清晰地分离“我不关心这个对象”和“这个对象可以被删除”这两个步骤，这足以允许自引用和循环类型。</p>

<p>需要自己实现Arena的情况相当罕见——<a href="https://docs.rs/bumpalo" rel="noopener noreferrer"><code>bumpalo</code></a>和<a href="https://docs.rs/typed-arena/" rel="noopener noreferrer"><code>typed-arena</code></a>涵盖了大多数用例，如果它们没有覆盖你的用例，你很可能在<a href="https://crates.io/search?q=arena" rel="noopener noreferrer">crates.io</a>上找到合适的东西。但如果你确实需要，或者你对底层的生命周期细节感兴趣，这一节适合你。</p>

<div>对于不太熟悉生命周期的人来说：语法<code>&amp;'a Foo</code>和<code>Foo&lt;'b&gt;</code>中的生命周期含义不同。<code>&amp;'a Foo</code>中的<code>'a</code>是<code>Foo</code><em>本身</em>的生命周期，或者至少是<em>这个</em>对<code>Foo</code>的引用的生命周期。<code>Foo&lt;'b&gt;</code>中的<code>'b</code>是<code>Foo</code>的一个<em>参数化</em>生命周期，通常意味着类似“<code>Foo</code>被允许引用的数据的生命周期”。</div>

<p>实现一个条目类型为<code>Entry</code>的Arena <code>Arena</code>的关键在于以下规则：</p>

<ul>
  <li><code>Arena</code>和<code>Entry</code>都应该有一个生命周期参数：<code>Arena&lt;'arena&gt;</code>和<code>Entry&lt;'arena&gt;</code></li>
  <li><code>Arena</code>的所有方法都应该接收<code>Arena&lt;'arena&gt;</code>作为<code>&amp;'arena self</code>，即它们的<code>self</code>类型是<code>&amp;'arena Arena&lt;'arena&gt;</code></li>
  <li><code>Entry</code>几乎总是应该作为<code>&amp;'arena Entry&lt;'arena&gt;</code>传递（为此定义一个别名很有用）</li>
  <li>使用内部可变性；<code>Arena</code>上的<code>&amp;mut self</code>会使所有东西停止编译。如果使用<code>unsafe</code>进行可变操作，请确保在某个地方有<code>PhantomData</code>用于<code>RefCell&lt;Entry&lt;'arena&gt;&gt;</code>。</li>
</ul>

<p>以上基本上是生命周期方面的全部要求，其余工作都在确定你想要的API和实现后备存储。有了上述规则，你应该能够让你的自定义Arena按你需要的保证工作，而不必理解底层生命周期发生了什么。</p>

<p>让我们通过一个实现示例，然后剖析<em>为什么</em>它有效。</p>

<h3 id="implementation">实现</h3>

<p>我的crate <a href="https://docs.rs/elsa" rel="noopener noreferrer"><code>elsa</code></a>在其<a href="https://github.com/Manishearth/elsa/blob/915d26008d8bae069927c551da506dba05d2755b/examples/mutable_arena.rs" rel="noopener noreferrer">一个示例</a>中以100%安全代码实现了一个Arena。这个Arena<em>并不</em>节省分配，因为<a href="https://docs.rs/elsa/1.4.0/elsa/vec/struct.FrozenVec.html" rel="noopener noreferrer"><code>elsa::FrozenVec</code></a>要求其内容位于某种间接引用之后，并且它不是泛型的，但它是说明生命周期如何工作的一种合理方式，而不会陷入实现一个<em>非常优秀</em>的Arena所需的<code>unsafe</code>细节。</p>

<p>该示例实现了一个<code>Person&lt;'arena&gt;</code>类型的Arena，<code>Arena&lt;'arena&gt;</code>。目标是实现某种有向社交图，它可能有循环。</p>

<div><div><pre><code><span>use</span> <span>elsa</span><span>::</span><span>FrozenVec</span><span>;</span>

<span>struct</span> <span>Arena</span><span>&lt;</span><span>'arena</span><span>&gt;</span> <span>{</span>
    <span>people</span><span>:</span> <span>FrozenVec</span><span>&lt;</span><span>Box</span><span>&lt;</span><span>Person</span><span>&lt;</span><span>'arena</span><span>&gt;&gt;&gt;</span><span>,</span>
<span>}</span>
</code></pre></div></div>

<p><a href="https://docs.rs/elsa/1.4.0/elsa/vec/struct.FrozenVec.html" rel="noopener noreferrer"><code>elsa::FrozenVec</code></a>是一个仅追加的<code>Vec</code>式抽象，允许你在不需要可变引用的情况下调用<code>.push()</code>，这就是我们能够在安全代码中实现这个Arena的原因。</p>

<p>每个<code>Person&lt;'arena&gt;</code>都有一个他们关注的人列表，但也跟踪关注他们的人：</p>

<div><div><pre><code><span>struct</span> <span>Person</span><span>&lt;</span><span>'arena</span><span>&gt;</span> <span>{</span>
    <span>pub</span> <span>follows</span><span>:</span> <span>FrozenVec</span><span>&lt;</span><span>PersonRef</span><span>&lt;</span><span>'arena</span><span>&gt;&gt;</span><span>,</span>
    <span>pub</span> <span>reverse_follows</span><span>:</span> <span>FrozenVec</span><span>&lt;</span><span>PersonRef</span><span>&lt;</span><span>'arena</span><span>&gt;&gt;</span><span>,</span>
    <span>pub</span> <span>name</span><span>:</span> <span>&amp;</span><span>'static</span> <span>str</span><span>,</span>
<span>}</span>

<span>// 遵循上面关于条目类型引用的规则</span>
<span>type</span> <span>PersonRef</span><span>&lt;</span><span>'arena</span><span>&gt;</span> <span>=</span> <span>&amp;</span><span>'arena</span> <span>Person</span><span>&lt;</span><span>'arena</span><span>&gt;</span><span>;</span>
</code></pre></div></div>

<p>生命周期<code>'arena</code>本质上是“Arena本身的生命周期”。这就是奇怪的地方开始出现：通常，如果你的类型有一个生命周期<em>参数</em>，调用者可以选择填入什么。你不能仅仅说“这是对象本身的生命周期”，调用者通常能够实例化一个<code>Arena&lt;'static&gt;</code>，或者一个<code>Arena&lt;'a&gt;</code>（某个<code>'a</code>）。但这里我们声明<code>'arena</code>是Arena本身的生命周期；显然这里有些不对劲。</p>

<p>以下是我们实际实现Arena的地方：</p>

<div><div><pre><code><span>impl</span><span>&lt;</span><span>'arena</span><span>&gt;</span> <span>Arena</span><span>&lt;</span><span>'arena</span><span>&gt;</span> <span>{</span>
    <span>fn</span> <span>new</span><span>()</span> <span>-&gt;</span> <span>Arena</span><span>&lt;</span><span>'arena</span><span>&gt;</span> <span>{</span>
        <span>Arena</span> <span>{</span>
            <span>people</span><span>:</span> <span>FrozenVec</span><span>::</span><span>new</span><span>(),</span>
        <span>}</span>
    <span>}</span>
    
    <span>fn</span> <span>add_person</span><span>(</span><span>&amp;</span><span>'arena</span> <span>self</span><span>,</span> <span>name</span><span>:</span> <span>&amp;</span><span>'static</span> <span>str</span><span>,</span>
                  <span>follows</span><span>:</span> <span>Vec</span><span>&lt;</span><span>PersonRef</span><span>&lt;</span><span>'arena</span><span>&gt;&gt;</span><span>)</span> <span>-&gt;</span> <span>PersonRef</span><span>&lt;</span><span>'arena</span><span>&gt;</span> <span>{</span>
        <span>let</span> <span>idx</span> <span>=</span> <span>self</span><span>.people</span><span>.len</span><span>();</span>
        <span>self</span><span>.people</span><span>.push</span><span>(</span><span>Box</span><span>::</span><span>new</span><span>(</span><span>Person</span> <span>{</span>
            <span>name</span><span>,</span>
            <span>follows</span><span>:</span> <span>follows</span><span>.into</span><span>(),</span>
            <span>reverse_follows</span><span>:</span> <span>Default</span><span>::</span><span>default</span><span>(),</span>
        <span>}));</span>
        <span>let</span> <span>me</span> <span>=</span> <span>&amp;</span><span>self</span><span>.people</span><span>[</span><span>idx</span><span>];</span>
        <span>for</span> <span>friend</span> <span>in</span> <span>&amp;</span><span>me</span><span>.follows</span> <span>{</span>
            <span>// 我们正在变异现有的 Arena 条目以添加引用，</span>
            <span>// 可能创建循环！</span>
            <span>friend</span><span>.reverse_follows</span><span>.push</span><span>(</span><span>me</span><span>)</span>
        <span>}</span>
        <span>me</span>
    <span>}</span>

    <span>fn</span> <span>dump</span><span>(</span><span>&amp;</span><span>'arena</span> <span>self</span><span>)</span> <span>{</span>
        <span>// 打印每个 Person、他们的关注者以及关注他们的人的代码</span>
    <span>}</span>
<span>}</span>
</code></pre></div></div>

<p>注意<code>add_person</code>中的<code>&amp;'arena self</code>。</p>

<p>这里一个好的实现通常会分离出处理“如果A<code>follows</code> B，那么B<code>reverse_follows</code> A”这个高层不变量的代码，但这只是一个示例。</p>

<p>最后，我们可以这样使用Arena：</p>

<div><div><pre><code><span>fn</span> <span>main</span><span>()</span> <span>{</span>
    <span>let</span> <span>arena</span> <span>=</span> <span>Arena</span><span>::</span><span>new</span><span>();</span>
    <span>let</span> <span>lonely</span> <span>=</span> <span>arena</span><span>.add_person</span><span>(</span><span>"lonely"</span><span>,</span> <span>vec!</span><span>[]);</span>
    <span>let</span> <span>best_friend</span> <span>=</span> <span>arena</span><span>.add_person</span><span>(</span><span>"best friend"</span><span>,</span> <span>vec!</span><span>[</span><span>lonely</span><span>]);</span>
    <span>let</span> <span>threes_a_crowd</span> <span>=</span> <span>arena</span><span>.add_person</span><span>(</span><span>"threes a crowd"</span><span>,</span> <span>vec!</span><span>[</span><span>lonely</span><span>,</span> <span>best_friend</span><span>]);</span>
    <span>let</span> <span>rando</span> <span>=</span> <span>arena</span><span>.add_person</span><span>(</span><span>"rando"</span><span>,</span> <span>vec!</span><span>[]);</span>
    <span>let</span> <span>_everyone</span> <span>=</span> <span>arena</span><span>.add_person</span><span>(</span><span>"follows everyone"</span><span>,</span> <span>vec!</span><span>[</span><span>rando</span><span>,</span> <span>threes_a_crowd</span><span>,</span> <span>lonely</span><span>,</span> <span>best_friend</span><span>]);</span>
    <span>arena</span><span>.dump</span><span>();</span>
<span>}</span>
</code></pre></div></div>

<p>在这种情况下，所有“可变性”都发生在Arena本身的实现中，但此代码直接向<code>follows</code>/<code>reverse_follows</code>列表添加条目是可能的，或者<code>Person</code>可以为其他类型的链接拥有<code>RefCell</code>，或者任何其他方式。</p>

<h3 id="how-the-lifetimes-work">生命周期如何工作</h3>

<p>那么这是如何工作的呢？正如我之前所说，对于Rust中的此类抽象，调用者通常可以自由地根据他们如何使用它来设置生命周期。例如，如果你有一个<code>HashMap&lt;K, &amp;'a str&gt;</code>，<code>'a</code>将基于你尝试插入的内容的生命周期来设置。</p>

<p>当你构造<code>Arena</code>时，其生命周期参数确实是无约束的，我们可以通过检查以下代码（它强制约束了生命周期）仍然编译来测试这一点。</p>

<div><div><pre><code><span>let</span> <span>arena</span><span>:</span> <span>Arena</span><span>&lt;</span><span>static</span><span>&gt;</span> <span>=</span> <span>Arena</span><span>::</span><span>new</span><span>();</span>
</code></pre></div></div>

<p>但一旦你尝试对Arena做任何事情，这就行不通了：</p>

<div><div><pre><code><span>let</span> <span>arena</span><span>:</span> <span>Arena</span><span>&lt;</span><span>static</span><span>&gt;</span> <span>=</span> <span>Arena</span><span>::</span><span>new</span><span>();</span>
<span>let</span> <span>lonely</span> <span>=</span> <span>arena</span><span>.add_person</span><span>(</span><span>"lonely"</span><span>,</span> <span>vec!</span><span>[]);</span>
</code></pre></div></div>

<div><div><pre><code>error[E0597]: `arena` does not live long enough
  --&gt; examples/mutable_arena.rs:5:18
   |
4  |     let arena: Arena&lt;'static&gt; = Arena::new();
   |                -------------- type annotation requires that `arena` is borrowed for `'static`
5  |     let lonely = arena.add_person("lonely", vec![]);
   |                  ^^^^^ borrowed value does not live long enough
...
11 | }
   | - `arena` dropped here while still borrowed
</code></pre></div></div>

<p><code>add_person</code>方法不知何故突然强制<code>Arena</code>的<code>'arena</code>参数设置为它<em>自己</em>的生命周期，约束它（并且使得用类型注解强制将其约束为其他值变得不可能）。</p>

<p>这里发生的事情是<code>add_person</code>的<code>&amp;'arena self</code>签名（即<code>self</code>是<code>&amp;'arena Arena&lt;'self&gt;</code>）与<code>Arena&lt;'arena&gt;</code>中<code>'arena</code>是一个<a href="https://doc.rust-lang.org/nomicon/subtyping.html#variance" rel="noopener noreferrer"><em>不变生命周期</em></a>的事实之间的巧妙交互。</p>

<p>通常在你的Rust程序中，生命周期有点可伸可缩。以下代码编译得很好：</p>

<div><div><pre><code><span>// 请求两个具有*相同生命周期*的字符串</span>
<span>fn</span> <span>take_strings</span><span>&lt;</span><span>'a</span><span>&gt;</span><span>(</span><span>x</span><span>:</span> <span>&amp;</span><span>'a</span> <span>str</span><span>,</span> <span>y</span><span>:</span> <span>&amp;</span><span>'a</span> <span>str</span><span>)</span> <span>{}</span>

<span>// 具有生命周期 'static 的字符串字面量</span>
<span>let</span> <span>lives_forever</span> <span>=</span> <span>"foo"</span><span>;</span>
<span>// 具有较短局部生命周期的所有权字符串</span>
<span>let</span> <span>short_lived</span> <span>=</span> <span>String</span><span>::</span><span>from</span><span>(</span><span>"bar"</span><span>);</span>

<span>// 仍然有效！</span>
<span>take_strings</span><span>(</span><span>lives_forever</span><span>,</span> <span>&amp;*</span><span>short_lived</span><span>);</span>
</code></pre></div></div>

<p>在这段代码中，Rust很乐意注意到虽然<code>lives_forever</code>和<code>&amp;*short_lived</code>具有不同的生命周期，但在<code>take_strings</code>函数的持续时间内，完全<em>可以假装</em><code>lives_forever</code>具有较短的生命周期。它只是一个引用，一个具有较长生命周期的引用<em>也</em>对较短生命周期有效。</p>

<p>问题是，这种可伸可缩性并非对所有生命周期都相同！<a href="https://doc.rust-lang.org/nomicon/subtyping.html" rel="noopener noreferrer">nomicon中关于子类型和变体的章节</a>详细解释了<em>为什么</em>是这种情况，但一个通用经验法则是，大多数生命周期是“可缩的”<sup id="fnref:2"><a href="#fn:2" rel="noopener noreferrer">3</a></sup>，如上面<code>&amp;'a str</code>中的那个，但如果涉及某种形式的可变性，它们就是刚性的，也称为“不变”。如果你使用函数类型，也可以有“可伸的”<sup id="fnref:3"><a href="#fn:3" rel="noopener noreferrer">4</a></sup>生命周期，但它们很少见。</p>

<p>我们的<code>Arena&lt;'arena&gt;</code>以一种使<code>'arena</code>不变的方式使用内部可变性（通过<code>FrozenVec</code>）。</p>

<p>让我们再次看看我们的两行代码。当编译器看到下面代码的第一行时，它构造了<code>arena</code>，我们将其生命周期称为<code>'a</code>。此时，<code>arena</code>的类型是<code>Arena&lt;'?&gt;</code>，其中<code>'?</code>是我们为尚未约束的生命周期编造的表示法。</p>

<div><div><pre><code><span>let</span> <span>arena</span> <span>=</span> <span>Arena</span><span>::</span><span>new</span><span>();</span> 
<span>let</span> <span>lonely</span> <span>=</span> <span>arena</span><span>.add_person</span><span>(</span><span>"lonely"</span><span>,</span> <span>vec!</span><span>[]);</span>
</code></pre></div></div>

<p>让我们实际上重写它以更清楚地说明生命周期是什么。</p>

<div><div><pre><code><span>let</span> <span>arena</span> <span>=</span> <span>Arena</span><span>::</span><span>new</span><span>();</span> <span>// 类型 Arena&lt;'?&gt;，生命周期为 'a</span>

<span>// 显式写出调用 add_person 时构造的 `self`</span>
<span>let</span> <span>ref_to_arena</span> <span>=</span> <span>&amp;</span><span>arena</span><span>;</span> <span>// 类型 &amp;'a Arena&lt;'?&gt;</span>
<span>let</span> <span>lonely</span> <span>=</span> <span>Arena</span><span>::</span><span>add_person</span><span>(</span><span>ref_to_arena</span><span>,</span> <span>"lonely"</span><span>,</span> <span>vec!</span><span>[]);</span>

</code></pre></div></div>

<p>还记得我之前列出的第二条规则吗？</p>

<blockquote>
  <p><code>Arena</code>的所有方法都应该接收<code>Arena&lt;'arena&gt;</code>作为<code>&amp;'arena self</code>，即它们的<code>self</code>类型是<code>&amp;'arena Arena&lt;'arena&gt;</code></p>
</blockquote>

<p>我们遵循了这个规则；<code>add_person</code>的签名是<code>fn add_person(&amp;'arena self)</code>。这意味着<code>ref_to_arena</code>被<em>强制</em>具有匹配模式<code>&amp;'arena Arena&lt;'arena&gt;</code>的生命周期。目前它的生命周期是<code>&amp;'a Arena&lt;'?&gt;</code>，这意味着<code>'?</code>被<em>强制</em>与<code>'a</code>相同，即<code>arena</code>变量本身的生命周期。如果生命周期不是不变的，编译器将能够挤压其他生命周期以适应，但它是不变的，并且无约束的生命周期被强制恰好是一个生命周期。</p>

<p>通过这个相当微妙的戏法，我们能够强制编译器将<code>Arena&lt;'arena&gt;</code>的<em>参数化</em>生命周期设置为其<em>实例</em>的生命周期。</p>

<p>此后，其余部分就相当简单了。<code>Arena&lt;'arena&gt;</code>持有<code>Person&lt;'arena&gt;</code>类型的条目，这基本上是一种说法，即“一个被允许引用生命周期为<code>'arena</code>的项目的<code>Person</code>，即<code>Arena</code>中的项目”。<code>type PersonRef&lt;'arena&gt; = &amp;'arena Person&lt;'arena&gt;</code>是一个便捷的简写，表示“一个存在于<code>Arena</code>中并被允许引用其对象的<code>Person</code>的引用”。</p>

<h3 id="what-about-destructors">析构函数怎么办？</h3>

<p>到目前为止，我还没有涉及的一件事是，在存在析构函数的情况下这如何能是安全的。如果你的Arena被允许有循环引用，并且你编写一个从这些循环引用读取的析构函数，那么在循环中稍后删除的任何一个参与者都会具有悬垂引用。</p>

<p>这涉及到Rust中一个<em>非常</em>晦涩的部分，甚至比变体更晦涩。你几乎不需要真正理解这一点，除了“显式析构函数会微妙地改变借用检查行为”。但了解它对于更好地理解这里发生的事情很有用。</p>

<p>如果我们向Arena示例添加以下代码：</p>

<div><div><pre><code><span>impl</span><span>&lt;</span><span>'arena</span><span>&gt;</span> <span>Drop</span> <span>for</span> <span>Person</span><span>&lt;</span><span>'arena</span><span>&gt;</span> <span>{</span>
    <span>fn</span> <span>drop</span><span>(</span><span>&amp;</span><span>mut</span> <span>self</span><span>)</span> <span>{</span>
        <span>println!</span><span>(</span><span>"goodbye {:?}"</span><span>,</span> <span>self</span><span>.name</span><span>);</span>
        <span>for</span> <span>friend</span> <span>in</span> <span>&amp;</span><span>self</span><span>.reverse_follows</span> <span>{</span>
            <span>// 可能悬垂！</span>
            <span>println!</span><span>(</span><span>"</span><span>\t\t</span><span>{}"</span><span>,</span> <span>friend</span><span>.name</span><span>);</span>
        <span>}</span>
    <span>}</span>
<span>}</span>
</code></pre></div></div>

<p>我们实际上会得到这个错误：</p>

<div><div><pre><code><span>error</span><span>[</span><span>E0597</span><span>]:</span> <span>`</span><span>arena</span><span>`</span> <span>does</span> <span>not</span> <span>live</span> <span>long</span> <span>enough</span>
  <span>-</span><span>-&gt;</span> <span>examples</span><span>/</span><span>mutable_arena</span><span>.rs</span><span>:</span><span>5</span><span>:</span><span>18</span>
   <span>|</span>
<span>5</span>  <span>|</span>     <span>let</span> <span>lonely</span> <span>=</span> <span>arena</span><span>.add_person</span><span>(</span><span>"lonely"</span><span>,</span> <span>vec!</span><span>[]);</span>
   <span>|</span>                  <span>^^^^^</span> <span>borrowed</span> <span>value</span> <span>does</span> <span>not</span> <span>live</span> <span>long</span> <span>enough</span>
<span>...</span>
<span>11</span> <span>|</span> <span>}</span>
   <span>|</span> <span>-</span>
   <span>|</span> <span>|</span>
   <span>|</span> <span>`</span><span>arena</span><span>`</span> <span>dropped</span> <span>here</span> <span>while</span> <span>still</span> <span>borrowed</span>
   <span>|</span> <span>borrow</span> <span>might</span> <span>be</span> <span>used</span> <span>here</span><span>,</span> <span>when</span> <span>`</span><span>arena</span><span>`</span> <span>is</span> <span>dropped</span> <span>and</span> <span>runs</span> <span>the</span> <span>destructor</span> <span>for</span> <span>type</span> <span>`</span><span>Arena</span><span>&lt;</span><span>'_</span><span>&gt;</span><span>`</span>
</code></pre></div></div>

<p>析构函数的存在微妙地改变了借用检查器在自引用生命周期周围的行为。确切的规则很棘手，<a href="https://doc.rust-lang.org/nomicon/dropck.html" rel="noopener noreferrer">在nomicon中有解释</a>，但<em>本质上</em>发生的是，<code>Person&lt;'arena&gt;</code>上的自定义析构函数的存在使得<code>Person</code>（以及<code>Arena</code>）中的<code>'arena</code>成为一个“在析构过程中被观察到”的生命周期。这在借用检查中被考虑在内——突然间，作用域末尾的隐式<code>drop()</code>被知道能够读取<code>'arena</code>数据，Rust得出了适当的结论：在内容被清理后，<code>drop()</code>将能够读取事物，因为析构本身是一个可变操作，而<code>drop()</code>是在其中交错运行的。</p>

<p>当然，一个合理的问题是，如果析构函数不被允许“包装”带有<code>'arena</code>的类型，我们如何存储像<code>Box</code>和<code>FrozenVec</code>这样的东西。原因是Rust知道<code>Box</code>上的<code>Drop</code><em>不能</em>检查<code>person.follows</code>，因为<code>Box</code>甚至不知道什么是<code>Person</code>，并且已经承诺永远不会试图去了解。如果我们有一个随机泛型类型，这不一定是真的，因为析构函数可以调用特征方法（或特化的覆盖方法），这些方法<em>确实</em>知道如何读取<code>Person</code>的内容，但在这种情况下，微妙改变的借用检查器规则会再次发挥作用。标准库类型和其他自定义数据结构通过一个逃生舱口实现这一点，<a href="https://doc.rust-lang.org/nomicon/dropck.html#an-escape-hatch" rel="noopener noreferrer"><code>#[may_dangle]</code></a>（也称为“眼罩”<sup id="fnref:4"><a href="#fn:4" rel="noopener noreferrer">5</a></sup>），它允许你保证不会在自定义析构函数中读取生命周期或泛型参数。</p>

<p>这同样适用于<a href="https://docs.rs/typed-arena/" rel="noopener noreferrer"><code>typed-arena</code></a>等crate；如果你正在创建循环，你将无法在放入Arena的类型上编写自定义析构函数。只要你不以可以创建循环的方式进行变异，你<em>可以</em>使用<a href="https://docs.rs/typed-arena/" rel="noopener noreferrer"><code>typed-arena</code></a>编写自定义析构函数；所以你将无法使用内部可变性让一个Arena条目指向另一个。</p>

<p><em>感谢<a href="https://mpc.sh" rel="noopener noreferrer">Mark Cohen</a>和<a href="https://twitter.com/kneecaw/" rel="noopener noreferrer">Nika Layzell</a>审阅了本文的草稿。</em></p>
<div>
  <ol>
    <li id="fn:0">
      <p>但不是以循环的方式；借用检查器会强制执行这一点！ <a href="#fnref:0" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn:1">
      <p>你可能想知道，对于循环引用，析构函数如何能安全地运行——毕竟，第二个被销毁的条目的析构函数将能够读取一个悬垂引用。我们将在本文后面讨论这一点，但这与drop检查有关，特别是如果你尝试建立循环，那么只允许在适当标记的类型上有显式析构函数。 <a href="#fnref:1" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn:2">
      <p>技术术语是“协变生命周期” <a href="#fnref:2" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn_3">
      <p>技术术语是“逆变生命周期” <a href="#fnref:3" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn_4">
      <p>因为你声称析构函数“看不到”该类型或生命周期，明白吗？ <a href="#fnref:4" rel="noopener noreferrer">↩</a></p>
    </li>
  </ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 28341 tokens</em></p>]]></content:encoded>
      <link>http://manishearth.github.io/blog/2021/03/15/arenas-in-rust/</link>
      <guid isPermaLink="false">http://manishearth.github.io/blog/2021/03/15/arenas-in-rust</guid>
      <pubDate>Mon, 15 Mar 2021 00:00:00 +0000</pubDate>
    </item>
  </channel>
</rss>
