<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>性能优化 on 搬砖程序员带你飞</title>
        <link>https://lpflpf.cn/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/</link>
        <description>Recent content in 性能优化 on 搬砖程序员带你飞</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-CN</language>
        <managingEditor>lipengfei634626165@163.com (lpflpf)</managingEditor>
        <webMaster>lipengfei634626165@163.com (lpflpf)</webMaster>
        <lastBuildDate>Wed, 26 Feb 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://lpflpf.cn/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>使用 Swiss Tables 实现更快的 Go maps</title>
            <link>https://lpflpf.cn/posts/go-swisstable/</link>
            <pubDate>Wed, 26 Feb 2025 00:00:00 +0000</pubDate><author>lipengfei634626165@163.com (lpflpf)</author>
            <guid>https://lpflpf.cn/posts/go-swisstable/</guid>
            <description>&lt;p&gt;在现代编程语言中，map 是一个不可或缺的数据结构，而哈希表正是其背后的核心实现技术。作为计算机科学中最基础的数据结构之一，哈希表为 Go 等众多编程语言提供了高效的键值存储方案。&lt;/p&gt;&#xA;&lt;p&gt;回溯历史，哈希表的故事要从 1953 年说起。那一年，IBM 的 Hans Peter Luhn 在一份内部备忘录中首次提出了这个开创性的想法：通过将数据项分散到不同的&amp;quot;桶&amp;quot;中，并用链表处理冲突来加快查找速度。这种方法后来被称为&amp;quot;链接法&amp;quot;，成为了哈希表最经典的实现方式之一。&lt;/p&gt;&#xA;&lt;p&gt;一年之后的 1954 年，Gene M. Amdahl、Elaine M. McGraw 和 Arthur L. Samuel 在开发 IBM 701 时提出了另一种巧妙的方案：当一个位置已被占用时，就尝试放入下一个空位。这种被称为&amp;quot;开放寻址&amp;quot;的技术在 1957 年由 W. Wesley Peterson 在其论文《随机访问存储的寻址》中得到了理论化和系统化的阐述。这就是我们今天所说的线性探测法。&#xA;·&#xA;面对这些已有数十年历史的经典数据结构，人们很容易认为它们已经发展到了极致，没有什么可以改进的空间了。但事实恰恰相反！计算机科学的研究仍在不断推进，无论是在算法复杂度的优化上，还是在充分利用现代 CPU 特性方面，都在持续取得突破。比如 Go 1.19 就用 Orson R. L. Peters 在 2015 年提出的模式消除快速排序取代了传统的快速排序算法，这是排序算法演进的最新例证。&lt;/p&gt;&#xA;&lt;p&gt;哈希表的创新并未止步。2017 年，Google 的工程师团队 —— Sam Benzaquen、Alkis Evlogimenos、Matt Kulukundis 和 Roman Perepelitsa —— 带来了一个令人耳目一新的设计：Swiss Tables。这个巧妙的名字暗示了它的特点：就像瑞士军刀一样，既精巧又高效。这个设计在 2018 年通过 Abseil C++ 库开源，让更多开发者得以一睹其风采。&lt;/p&gt;&#xA;&lt;p&gt;而现在，Go 1.24 版本迎来了一次重大更新：内置的 map 类型完全采用了 Swiss Table 的设计理念。这次革新不仅带来了性能的提升，更重要的是展示了如何将一个出色的想法改造成符合 Go 语言特性的实现。让我们一起深入了解这个精妙的设计，以及 Go 团队是如何应对实现过程中的各种挑战的。&lt;/p&gt;&#xA;&lt;h2 id=&#34;开放寻址哈希表基础知识&#34;&gt;开放寻址哈希表：基础知识&#xA;&lt;/h2&gt;&lt;p&gt;在深入 Swiss Tables 的细节之前，我们需要先理解它的基础 —— 开放寻址哈希表。想象一个巨大的停车场，每个车位就像数组中的一个槽位。当你想停车（存储一个键）时，你会根据车牌号（通过哈希函数）计算出一个理想的车位。如果这个车位已经被占用了（发生冲突），你就需要找下一个空位 —— 这就是开放寻址的核心思想。&lt;/p&gt;&#xA;&lt;p&gt;在技术实现上，所有数据都存储在一个连续的数组中，每个位置我们称之为槽位。通过哈希函数，我们可以将任意键转换为一个整数，用这个整数来确定数据应该存储在哪个槽位。当出现冲突时，我们就按照某种固定的模式（探测序列）寻找下一个空槽位。这种设计的优雅之处在于它的简单性和内存局部性。让我们通过一个具体的例子来看看它是如何工作的。&lt;/p&gt;&#xA;&lt;h3 id=&#34;示例&#34;&gt;示例&#xA;&lt;/h3&gt;&lt;p&gt;下面你可以看到一个具有 16 个槽位的哈希表后备数组，以及存储在每个槽位中的键（如果有）。值未显示，因为它们与此示例无关。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;&#xA;&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;5&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;+------+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;| 槽位 | 0  | 1  | 2  | 3  | 4  | 5  | 6  | 7  | 8  | 9  | 10 | 11 | 12 | 13 | 14 | 15 |&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;+------+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;| 键   |    |    |    | 56 | 32 | 21 | 78 |    |    |    |    |    |    |    |    |    |&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;+------+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;要插入新键，我们使用哈希函数来选择一个槽位。由于只有 16 个槽位，我们需要限制在这个范围内，所以我们将使用 hash(key) % 16 作为目标槽位。假设我们要插入键 98，且 hash(98) % 16 = 7。槽位 7 是空的，所以我们只需在那里插入 98。另一方面，假设我们要插入键 25，且 hash(25) % 16 = 3。槽位 3 是一个冲突，因为它已经包含键 56。因此我们不能在这里插入。&lt;/p&gt;&#xA;&lt;p&gt;我们使用探测序列来找到另一个槽位。有各种众所周知的探测序列。最原始和最直接的探测序列是线性探测，它只是按顺序尝试连续的槽位。&lt;/p&gt;&#xA;&lt;p&gt;所以，在 hash(25) % 16 = 3 的例子中，由于槽位 3 正在使用中，我们会接下来考虑槽位 4，它也在使用中。槽位 5 也是如此。最后，我们会到达空槽位 6，我们会在那里存储键 25。&lt;/p&gt;&#xA;&lt;p&gt;查找遵循相同的方法。查找键 25 会从槽位 3 开始，检查它是否包含键 25（不包含），然后继续线性探测，直到在槽位 6 中找到键 25。&lt;/p&gt;&#xA;&lt;p&gt;这个示例使用一个具有 16 个槽位的后备数组。如果我们插入超过 16 个元素会发生什么？如果哈希表用完空间，它将增长，通常是将后备数组的大小加倍。所有现有条目都会重新插入到新的后备数组中。&lt;/p&gt;&#xA;&lt;p&gt;开放寻址哈希表实际上不会等到后备数组完全填满才增长，因为随着数组变得更满，每个探测序列的平均长度增加。在上面使用键 25 的示例中，我们必须探测 4 个不同的槽位才能找到一个空槽位。如果数组只有一个空槽位，最坏情况下的探测长度将是 O(n)。也就是说，你可能需要扫描整个数组。已使用槽位的比例称为负载因子，大多数哈希表定义了一个最大负载因子（通常为 70-90%），在此时它们将增长以避免非常满的哈希表的极长探测序列。&lt;/p&gt;&#xA;&lt;h2 id=&#34;swiss-table并行探测的艺术&#34;&gt;Swiss Table：并行探测的艺术&#xA;&lt;/h2&gt;&lt;p&gt;Swiss Table 也是一种开放寻址哈希表，但它引入了一个巧妙的创新：分组处理和并行探测。想象一下，如果我们能同时检查多个槽位，而不是一个接一个地查找，那会有多高效！这正是 Swiss Table 的核心思想。&lt;/p&gt;&#xA;&lt;p&gt;在 Swiss Table 中，我们将整个数组划分为多个逻辑组，每组包含 8 个槽位。这个数字并非随意选择 —— 它与现代 CPU 的 SIMD（单指令多数据）指令集完美匹配，允许我们一次处理 8 个字节的数据。&lt;/p&gt;&#xA;&lt;p&gt;除了存储实际数据的槽位外，每个组还配备了一个 64 位的&amp;quot;控制字&amp;quot;作为元数据。这个控制字中的每个字节对应组中的一个槽位，记录着该槽位的状态（空闲、已删除或使用中）以及一个额外的信息：如果槽位正在使用，该字节还会存储键的哈希值的低 7 位（我们称之为 h2）。这个看似简单的设计，却是 Swiss Table 高效性能的关键所在。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;&#xA;&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;5&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;6&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;7&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;+-------+----+----+----+----+----+----+----+----+  +-------+----+----+----+----+----+----+----+----+&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;| 组 0  | 0  | 1  | 2  | 3  | 4  | 5  | 6  | 7  |  | 组 1  | 0  | 1  | 2  | 3  | 4  | 5  | 6  | 7  |&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;+-------+----+----+----+----+----+----+----+----+  +-------+----+----+----+----+----+----+----+----+&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;| 键    | 56 | 32 | 21 |    |    |    |    |    |  | 键    | 78 |    |    |    |    |    |    |    |&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;+-------+----+----+----+----+----+----+----+----+  +-------+----+----+----+----+----+----+----+----+&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;| h2    | 23 | 89 | 50 |    |    |    |    |    |  | h2    | 47 |    |    |    |    |    |    |    |&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;+-------+----+----+----+----+----+----+----+----+  +-------+----+----+----+----+----+----+----+----+&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;插入的工作方式如下：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;计算 hash(key) 并将哈希分成两部分：上部 57 位（称为 h1）和下部 7 位（称为 h2）。&lt;/li&gt;&#xA;&lt;li&gt;上部位（h1）用于选择要考虑的第一个组：在这种情况下为 h1 % 2，因为只有 2 个组。&lt;/li&gt;&#xA;&lt;li&gt;在一个组内，所有槽位都同样有资格容纳键。我们必须首先确定是否有任何槽位已经包含这个键，在这种情况下，这是一个更新而不是新插入。&lt;/li&gt;&#xA;&lt;li&gt;如果没有槽位包含该键，那么我们寻找一个空槽位来放置这个键&lt;/li&gt;&#xA;&lt;li&gt;如果没有空槽位，那么我们通过搜索下一个组来继续探测序列。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;查找遵循相同的基本过程。如果我们在步骤 4 中找到一个空槽位，那么我们知道插入会使用这个槽位，可以停止搜索。&lt;/p&gt;&#xA;&lt;p&gt;步骤 3 是 Swiss Table 魔法发生的地方。我们需要检查组中是否有任何槽位包含所需的键。简单的方法是进行线性扫描并比较所有 8 个键。然而，控制字让我们可以更有效地做到这一点。每个字节包含该槽位的哈希的低 7 位（h2）。如果我们确定控制字中哪些字节包含我们正在寻找的 h2，我们就会有一组候选匹配。&lt;/p&gt;&#xA;&lt;p&gt;换句话说，我们想要在控制字内进行逐字节的相等比较。例如，如果我们正在寻找键 32，其中 h2 = 89，我们想要的操作如下所示：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;&#xA;&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;5&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;6&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;7&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;8&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;9&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;+--------+----+----+----+----+----+----+----+----+&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;| 测试字 | 89 | 89 | 89 | 89 | 89 | 89 | 89 | 89 |&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;+--------+----+----+----+----+----+----+----+----+&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;| 比较   | == | == | == | == | == | == | == | == |&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;+--------+----+----+----+----+----+----+----+----+&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;| 控制字 | 23 | 89 | 50 | -  | -  | -  | -  | -  |&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;+--------+----+----+----+----+----+----+----+----+&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;| 结果   | 0  | 1  | 0  | 0  | 0  | 0  | 0  | 0  |&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;+--------+----+----+----+----+----+----+----+----+&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;这是 SIMD（单指令多数据）硬件支持的操作，其中单个指令对较大值（向量）内的独立值执行并行操作。在这种情况下，当特殊的 SIMD 硬件不可用时，我们可以使用一组标准的算术和位运算来实现这个操作。&lt;/p&gt;&#xA;&lt;p&gt;结果是一组候选槽位。h2 不匹配的槽位不会有匹配的键，因此可以跳过。h2 匹配的槽位是潜在的匹配，但我们仍然必须检查整个键，因为存在冲突的可能性（7 位哈希的冲突概率为 1/128，因此仍然相当低）。&lt;/p&gt;&#xA;&lt;p&gt;这个操作非常强大，因为我们实际上并行执行了探测序列的 8 个步骤。这通过减少我们需要执行的平均比较次数来加速查找和插入。这种探测行为的改进使得 Abseil 和 Go 实现都能够增加 Swiss Table maps 相比之前的 maps 的最大负载因子，这降低了平均内存占用。&lt;/p&gt;&#xA;&lt;h2 id=&#34;go-的挑战让-swiss-table-适应-go-的特性&#34;&gt;Go 的挑战：让 Swiss Table 适应 Go 的特性&#xA;&lt;/h2&gt;&lt;p&gt;将 Swiss Table 引入 Go 并非易事。Go 语言的 map 类型有着一些独特的特性和保证，这些都是 Go 开发者所熟悉和依赖的。要让 Swiss Table 在 Go 中发挥作用，我们需要解决一些关键挑战。&lt;/p&gt;&#xA;&lt;h3 id=&#34;增量增长平衡性能与延迟&#34;&gt;增量增长：平衡性能与延迟&#xA;&lt;/h3&gt;&lt;p&gt;想象一下这样的场景：你正在运行一个关键的网络服务，突然间，一个简单的 map 操作导致整个系统暂停了几百毫秒。这对于实时系统来说可能是灾难性的。&lt;/p&gt;&#xA;&lt;p&gt;传统哈希表在增长时通常采用&amp;quot;一次性扩容&amp;quot;的策略：当空间不足时，创建一个两倍大的新数组，然后将所有现有元素重新哈希并复制过去。对于一个包含百万级条目的大型 map，这个过程可能需要相当长的时间。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;&#xA;&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;// 传统哈希表增长示意&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;1GB map → 需要扩容 → 暂停服务 → 复制全部数据到2GB新空间 → 恢复服务&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                      ↑_______可能需要几百毫秒_______↑&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;Go 作为一种注重实用性的语言，特别关注低延迟场景。因此，Go 的 map 采用了增量增长策略，将大型扩容操作分散到多次插入中，确保每次操作的延迟都在可控范围内。&lt;/p&gt;&#xA;&lt;p&gt;然而，Swiss Table 的设计本身就假设一次性增长，因为它的探测序列依赖于表的总大小。这看似是一个难以调和的矛盾。&lt;/p&gt;&#xA;&lt;p&gt;Go 团队的解决方案既优雅又实用：将一个大的 map 拆分成多个较小的 Swiss Tables。具体来说：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;每个 Go map 内部由多个独立的 Swiss Tables 组成&lt;/li&gt;&#xA;&lt;li&gt;每个表负责键空间的一个子集，最多存储 1024 个条目&lt;/li&gt;&#xA;&lt;li&gt;使用哈希值的高位来决定键属于哪个表&lt;/li&gt;&#xA;&lt;li&gt;当单个表需要增长时，只影响该表的条目，其他表不受影响&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;这种设计确保了即使在最坏的情况下，一次插入操作也只需要重新哈希 1024 个条目，而不是整个 map 的所有条目。这有效地将最大延迟限制在一个可接受的范围内，同时保留了 Swiss Table 的性能优势。&lt;/p&gt;&#xA;&lt;h3 id=&#34;迭代期间的修改保持一致性的艺术&#34;&gt;迭代期间的修改：保持一致性的艺术&#xA;&lt;/h3&gt;&lt;p&gt;想象你正在遍历一个装满书的书架，同时有人在添加、移除或更换书籍。这就是 map 迭代期间修改的场景。大多数哈希表实现，包括 Abseil 的 Swiss Tables，都采取了最简单的方案：完全禁止在迭代过程中修改 map。这就像在整理书架时贴上&amp;quot;请勿触摸&amp;quot;的标签。&lt;/p&gt;&#xA;&lt;p&gt;但 Go 选择了一个更具挑战性的路径。Go 语言规范明确允许在迭代期间修改 map，并定义了一组精确的规则：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;&#xA;&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;5&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;6&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;k&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;v&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;range&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 在这个循环中：&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 1. 如果某个元素在被遍历到之前被删除，就不会被看到&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 2. 如果某个元素在被遍历到之前被更新，会看到新值&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 3. 新添加的元素可能会也可能不会被看到&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;这些规则看似简单，实现起来却颇具挑战性。传统的哈希表迭代方式是按照内存布局顺序遍历数组。但这种方法在 Go 的场景下会出现问题，特别是当 map 在迭代过程中发生增长时，内存布局会发生变化。&lt;/p&gt;&#xA;&lt;p&gt;Go 团队提出了一个巧妙的解决方案：让迭代器保持对正在迭代的表的快照引用。这就像在整理书架时拍了一张照片，用照片来决定访问书籍的顺序。即使书架布局发生变化，我们仍然按照照片中的顺序进行。&lt;/p&gt;&#xA;&lt;p&gt;但这个方案也带来了新的挑战：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;新添加的元素：它们只存在于新的布局中，不在我们的&amp;quot;照片&amp;quot;里。这符合规范，因为规范允许新元素可能不被遍历到。&lt;/li&gt;&#xA;&lt;li&gt;更新和删除：如果只看&amp;quot;照片&amp;quot;，我们可能会看到已经不存在的书，或者错过了书的新版本。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;解决方案是：使用旧表（我们的&amp;quot;照片&amp;quot;）仅来决定遍历顺序，但在实际返回数据之前，我们会检查新表以确认元素是否仍然存在，并获取最新的值。这就像按照照片的顺序去找书，但每次都要确认书架上的实际情况。&lt;/p&gt;&#xA;&lt;p&gt;这种设计确保了：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;遍历顺序保持稳定&lt;/li&gt;&#xA;&lt;li&gt;删除的元素不会被看到&lt;/li&gt;&#xA;&lt;li&gt;更新的元素会显示新值&lt;/li&gt;&#xA;&lt;li&gt;新添加的元素可能会被看到（如果它们恰好被添加到我们即将访问的位置）&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;虽然这种方案增加了实现的复杂性，但它为 Go 开发者提供了一个直观且可预测的 map 迭代行为。这再次展示了 Go 团队在设计语言特性时对实用性的重视：宁可在实现层面承担复杂性，也要为开发者提供简单直观的使用体验。&lt;/p&gt;&#xA;&lt;h2 id=&#34;未来展望swiss-table-的进化之路&#34;&gt;未来展望：Swiss Table 的进化之路&#xA;&lt;/h2&gt;&lt;p&gt;Swiss Table 的引入只是 Go map 优化旅程的一个里程碑，而非终点。在微基准测试中，新的 map 实现展现出了令人印象深刻的性能提升——某些操作比 Go 1.23 快了高达 60%。当然，由于 map 的使用场景极其多样化，性能改进的幅度也因情况而异，甚至在某些边缘场景下可能会有轻微的退步。但从整体来看，在真实应用程序的基准测试中，我们观察到了约 1.5% 的 CPU 时间平均改进。这个数字看似不大，但考虑到 map 操作在 Go 程序中的普遍性，这意味着几乎所有 Go 程序都能从中受益。&lt;/p&gt;&#xA;&lt;p&gt;展望未来，我们看到了多个可能的优化方向：&lt;/p&gt;&#xA;&lt;h3 id=&#34;1-提升缓存局部性&#34;&gt;1. 提升缓存局部性&#xA;&lt;/h3&gt;&lt;p&gt;现代计算机架构中，内存访问速度远远落后于 CPU 处理速度。当数据不在 CPU 缓存中时，获取这些数据会导致显著的延迟。我们计划研究如何重新组织 map 的内存布局，使相关数据更可能同时位于缓存中，从而减少缓存未命中的情况。这对于大型 map（无法完全放入 CPU 缓存的 map）尤为重要。&lt;/p&gt;&#xA;&lt;h3 id=&#34;2-扩展-simd-指令的使用&#34;&gt;2. 扩展 SIMD 指令的使用&#xA;&lt;/h3&gt;&lt;p&gt;Swiss Table 的一个核心优势是能够利用 SIMD 指令并行处理多个槽位。目前，Go 1.24 已经为 amd64 架构实现了使用 8 字节 SIMD 指令的优化，但这只是开始：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;扩展架构支持&lt;/strong&gt;：将 SIMD 优化扩展到其他流行的处理器架构，如 ARM64&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;增加组大小&lt;/strong&gt;：现代 SIMD 指令集通常支持至少 16 字节（有些甚至支持 32 或 64 字节）的并行操作。通过将组大小从 8 增加到 16 或更多，我们可以在一次操作中比较更多的槽位，进一步减少平均探测次数&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;&#xA;&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;5&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;6&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;7&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;// 当前的 8 槽位组与潜在的 16 槽位组比较&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;┌─────────────────────────────┐  ┌─────────────────────────────────────────────────────┐&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;│ 8 槽位组 (64位控制字)         │  │ 16 槽位组 (128位控制字)                                │&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;├─┬─┬─┬─┬─┬─┬─┬─┬─────────────┤  ├─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─────────────────────┤&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;│0│1│2│3│4│5│6│7│ 数据槽位...  │  │0│1│2│3│4│5│6│7│8│9│A│B│C│D│E│F│ 数据槽位...         │&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;└─┴─┴─┴─┴─┴─┴─┴─┴─────────────┘  └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─────────────────────┘&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  1次SIMD操作检查8个槽位          1次SIMD操作检查16个槽位&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;这些优化不仅会进一步提高 map 操作的性能，还可能降低内存使用量，因为更高效的探测意味着我们可以安全地使用更高的负载因子。&lt;/p&gt;&#xA;&lt;h2 id=&#34;致谢&#34;&gt;致谢&#xA;&lt;/h2&gt;&lt;p&gt;基于 Swiss Table 的 Go map 实现已经酝酿很久，并涉及许多贡献者。我要感谢 YunHao Zhang (@zhangyunhao116)、PJ Malloy (@thepudds) 和 @andy-wm-arthur 构建了 Go Swiss Table 实现的初始版本。Peter Mattis (@petermattis) 将这些想法与上述 Go 挑战的解决方案结合起来，构建了 github.com/cockroachdb/swiss，这是一个符合 Go 规范的 Swiss Table 实现。Go 1.24 内置 map 实现很大程度上基于 Peter 的工作。感谢社区中所有做出贡献的人！&lt;/p&gt;&#xA;</description>
        </item></channel>
</rss>
