<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Don&apos;t Panic</title><description>Learn, think, and write.</description><link>https://tangdh.life/</link><item><title>我的第一个小生意</title><link>https://tangdh.life/posts/interesting/first-business/</link><guid isPermaLink="true">https://tangdh.life/posts/interesting/first-business/</guid><description>这周我解锁了一个新的人生成就——开启自己的小生意</description><pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这周我解锁了一个新的人生成就——做一个属于自己的小生意。虽然这个生意很短暂，仅仅维持到周末就结束了。但在这过程中，我还是经历了一个完整的商业流程，所以我觉得还是很有必要写一篇博客记录一下的。&lt;/p&gt;
&lt;h2&gt;起因&lt;/h2&gt;
&lt;p&gt;就像大多数生意的起因一样，我这个生意也是来源于自身的需求——AI 订阅。在 Opus 4.5 之前，我每个月会在 OpenAI 和 Anthropic 身上各花 20 美金，订阅他们的 AI 服务。而我的要求就是&lt;strong&gt;稳定且没有溢价&lt;/strong&gt;。因此那时苹果内购可以完美满足我的需求，我每个月只需要购买礼品卡即可。&lt;/p&gt;
&lt;p&gt;但是当 Opus 4.5 出来之后，一切变了，我抛弃了古法编程转而完全用 AI 来写代码。这就出现一个问题，最初级的订阅档位，无法满足我的需求。我需要去订阅更高的档位，对于 Claude 来说，就是 MAX 5X 100 美金的这一档位。但如果依旧使用苹果内购的方式，那么需要交 25% 的苹果税，也就是用 125 美金购买一个价值 100 美金的产品。这显然违反了我的订阅准则。&lt;/p&gt;
&lt;h2&gt;穷极思变&lt;/h2&gt;
&lt;p&gt;既然我还没有富到完全不把 25 美金当回事，那自然就要想办法怎么解决这个问题。我在网上大量搜索原价订阅的方案，包括但不限于以下各种方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过 meiguo 购买信用卡进行订阅&lt;/li&gt;
&lt;li&gt;购买 Claude 礼品卡进行订阅&lt;/li&gt;
&lt;li&gt;他人信用卡代充&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这其中，要做到除了订阅费绝不多花一分钱的方式，只有购买礼品卡。但礼品卡有一个致命的缺陷，Anthropic 封号不给退款。正当我决定冒险一试的时候，我鬼使神差的打开了国内最大的黑市 APP——闲鱼。我在闲鱼上搜索 Claude 礼品卡，发现 Claude MAX 5X 在这上面竟然只卖 400 多块钱，我咨询店主，店主和我说是有封号风险的，而且没有任何质保。我手上刚好有一个 Claude 小号，抱着试一试的心态，我花了 475 买了一张礼品卡，成功地订阅上 MAX 5X。&lt;/p&gt;
&lt;p&gt;当时我想着可能过几天就会被封号，所以我开始高强度使用，希望可以在封号前用回本。后来过了一周，我发现我的账号居然还可以正常使用。在兴奋之余，我也不禁思索到底为什么他可以做到这么便宜？&lt;/p&gt;
&lt;h2&gt;深挖渠道&lt;/h2&gt;
&lt;p&gt;为了研究他们为什么可以把礼品卡卖得这么便宜，我又开始高强度搜索。我发现各个论坛的确有很多人，也在问同样的事情，他们为什么可以卖得这么便宜？最多的回答是黑卡盗刷，看上去这是个显而易见的答案，而且也符合奥卡姆剃刀原理。&lt;/p&gt;
&lt;p&gt;但如果再仔细想一下就会发现，这个答案其实不是那么成立。原因在于，如果是黑卡盗刷，为什么是最近才出现的呢？如果你可以盗刷别人的卡，你要买的肯定是只要这一刻花出去，后续无法溯源，无法追回的东西，比如加密货币这类。礼品卡明显不符合这种要求，你用黑卡买了礼品卡，可能在你卖出之前，你礼品卡就被官方吊销了。我认为没有任何黑客会做这种事。&lt;/p&gt;
&lt;p&gt;所以我们还是要继续探索这批礼品卡到底是怎么来的？随着我在各个论坛高强度搜索，在电报上各种关键字搜索公开频道，依旧一无所获。其实这也很合理，人怎么会把自己挣钱的秘诀轻易告诉你呢？但我不想放弃，这时候我想到我目前的搜索方式完全是以一个白嫖者的身份进行的，那有价值的信息一定是对我严防死堵的。&lt;strong&gt;除了开源作者，我想没有人会喜欢一个白嫖者，并且愿意把信息和他免费分享的。&lt;/strong&gt; 转变思路后，我把自己包装成收货的，我去各种找卖家，问他们我要大量购买，底价是多少？大量购买要怎么交货？包装身份后，很快我就拿到了一些交易的网站，在这些网站上，有些网站上面就贴着电报群，我一个个加进去。到这一步，我算是一只脚踏进了门槛。至于这些礼品卡到底是怎么来的，这篇博客就不透露了。但至少我摸清了渠道和价格，这就够了。&lt;/p&gt;
&lt;h2&gt;开始第一次卖货&lt;/h2&gt;
&lt;p&gt;在加进第一个电报群后，其实你会很容易地加 4-5 个群，因为电报群的群主会允许别人在自己的频道打广告，所以你只要一个个加过去就完事了。在加了许多群后，我逐渐知道去哪里拿货，底价一般是多少，在抹平信息差后，我很快就去闲鱼上支起了自己的小铺子。&lt;/p&gt;
&lt;p&gt;但我很快就学会了做生意的第一课，&lt;strong&gt;你的客户从哪里来&lt;/strong&gt;？即使你搞定了货源，你有一大堆潜在客户，但如果他们不知道你，你也一单都卖不出去。就算他们知道了你，他们为什么要信任你这么一个从来没有成交过一单的商家？&lt;/p&gt;
&lt;p&gt;第一次卖货很快就宣告了失败，在闲鱼上挂了 4-5 天的商品，只有一个客户询问了一下，成交量为 0。&lt;/p&gt;
&lt;h2&gt;成功开启小生意&lt;/h2&gt;
&lt;p&gt;尽管一单都没成交，但我觉得探索的经历还是挺有趣的，于是和我朋友分享了一下。他听我说了以后，给我建议说可以去校园论坛发帖，这样更容易获取信任，并且可以做私域流量。我觉得这简直是天才的 idea，但我不可能给学生卖 Claude 礼品卡，他们一个月的生活费才多少，不可能花 4-500 块钱去买一个 AI 订阅。于是我选择了一个新的选品——低价 GPT Plus。只需要学校食堂的一顿饭钱，就可以给自己的账号充值一个月的 Plus，对于学生群体而言，这绝对是一个可行的生意。因为卖的是学生，所以我特地查了货源使用的技术手段，确定没问题后，我们很快开始了第二次的卖货尝试。&lt;/p&gt;
&lt;p&gt;这次我们解决了&lt;strong&gt;客户从哪里来&lt;/strong&gt;的问题，所以第二次卖货相较于第一次卖货而言，成功了不少。当天晚上的营业额就达到了 500 多。后面几天其实都很不错，营业额几乎每天都是 100% 的增长。甚至有客户咨询，问我们这个会卖多久，体验了 GPT plus 以后再也回不到豆包和 deepseek 了。&lt;/p&gt;
&lt;h2&gt;停不下来&lt;/h2&gt;
&lt;p&gt;但随之而来的就是焦虑，我开始完全定不下心，不停地刷群消息，不停地看后台订单量。精神高度紧张。尽管明知深夜不会有新的订单，但还是不想睡觉，就是想不停地刷新。只要看到后台有了新的订单，销售金额增加，我体内的多巴胺直接爆表，我第一次切身体会到为什么老板可以把自己全部精力投入到自己的公司中。这并不是他有什么异于常人的特质，这不过是人性罢了，按照赵本山的说法就是，你跺你也麻！&lt;/p&gt;
&lt;h2&gt;小生意的结束和感悟&lt;/h2&gt;
&lt;p&gt;从上面的描述，应该也能看出来，这样的状态是无法长久的。我整个人的精神状态已经不对了，所以我女朋友勒令我把这门小生意给结束了。我的宗旨就是家和万事兴，如果一个事情会让家里鸡飞狗跳，那我相信这个事情往往会以失败告终。于是这个生意也来到了尽头。&lt;/p&gt;
&lt;p&gt;尽管这门生意很短暂，但我觉得其实我还是学到了很重要的一课，商业的本质就是了解你的客户，提供给他想要的东西。如果你不知道你客户是谁，你客户不知道你是谁，那其实很难做成一笔好的生意。也许这就是为什么明星的代言费可以这么高，因为如果没有明星的代言，你的客户不知道你，那你这个生意根本就开始不了。一笔代言费，换来商业上的成功，还是很划算的。&lt;/p&gt;
&lt;p&gt;另外严格意义上来说，我这个的确算作是赚信息差。很多人觉得这不道德，因为我&quot;什么都没付出&quot;。但事实上这个说法并不成立，我花了大量时间研究渠道，伪装身份混进电报群，用自己的钱去试错，承担封号的风险。这些都是真实的投入，只不过它不是体力劳动，所以容易被忽视。&lt;/p&gt;
&lt;p&gt;而且如果你仔细想想，赚信息差其实是商业世界里最普遍的事情。代购是信息差，中间商是信息差，甚至你去超市买瓶水，超市从厂家进货加价卖给你，本质上也是信息差。没有人会觉得超市不道德，因为大家都明白一个道理：你省下了自己去厂家进货的时间和精力，这本身就值那个差价。&lt;/p&gt;
&lt;p&gt;我做的事情也是一样的。大多数人不愿意花时间去找货源，他们宁愿多花一点钱来获得便利。而那些愿意自己花时间消除信息差的人，自然也不需要从我这里买。各取所需，没有人被强迫。从我最粗浅的理解来看，商业的道德就是&lt;strong&gt;明码标价，不卖假货，不坑不骗&lt;/strong&gt;。满足了这几点，我觉得就是一门可做的生意。&lt;/p&gt;
</content:encoded><category>Interesting</category><category>Business</category><category>Thoughts</category><author>tang-hi</author></item><item><title>2025年度回顾</title><link>https://tangdh.life/posts/interesting/rewind-2025/</link><guid isPermaLink="true">https://tangdh.life/posts/interesting/rewind-2025/</guid><description>2025年我依旧没有暴富, 但我觉得这是暴富的开始</description><pubDate>Fri, 02 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;2025 年，我依旧没有暴富，也没有按照我年初设想的那样发展。但是 2025 年终究是结束了，所以我还是按照工作/生活这两个维度来回顾一下我的 2025。&lt;/p&gt;
&lt;h2&gt;工作&lt;/h2&gt;
&lt;h3&gt;公司&lt;/h3&gt;
&lt;p&gt;6 月份从阿里的智能信息离职去了 DolphinDB。离职的原因也很简单，我不想再加班了，而且我对在阿里做的事情也逐渐感到厌烦，陈旧的技术栈，意义不明的需求和任务，每天早上的日会，都让我身心俱疲。虽然我对数据库的兴趣也不是很大了，但是能够六点下班对我而言就是最大的兴趣所在。&lt;/p&gt;
&lt;p&gt;换了新工作以后，每天晚上有大把的时间，我可以自己做饭，陪女友看看剧，探索一些兴趣爱好，研究一些赚钱门路，写点想写的代码。作息也调整到 11 点睡觉，7 点多起床。从结果来看，换工作的目标是完全达成了。&lt;/p&gt;
&lt;p&gt;唯一不好的点就是，当时阿里加班加的我完全 burnout 了，所以为了不加班，薪资降的太多了。当时应该再 argue 一下的。不过瑕不掩瑜，这次工作换的我觉得还挺成功的。&lt;/p&gt;
&lt;h3&gt;技术&lt;/h3&gt;
&lt;p&gt;毕业以后主要从事的就是搜索引擎的相关开发，但我自问我对整个搜索引擎虽然有个整体的 overview ，但其实我对很大内容的细节并不是很了解。感觉也不知道之前打工的时候是怎么回事，稀里糊涂的就混过去了。所以今年四季度开始，决定自己写一个搜索引擎，彻底弄懂每个细节。目前还在开发/学习中。&lt;/p&gt;
&lt;p&gt;换了工作以后，虽说去了一家数据库公司，但做的事情相关性倒不是很高，更像是给上游加一些功能。但我其实也不想主动去深入整个架构。原因在于，我现在把技术分为两类，一类是基本盘技术，意思就是我学这类技术的目的就是为了确保我可以有个工作保住生活基本盘，这一类的东西，我希望我可以知道每一个细节。&lt;/p&gt;
&lt;p&gt;另一类是高杠杆技术，这类其实也不一定是技术，而是我可以拿去变现/未来有可能可以变现的东西，这类东西，我不求深入理解，而是够用就好。对于搜索引擎/数据库/AI infra 等等，我都统一把他们归于基本盘技术，而这类的要求是深度，所以我只能从其中选择一个来深入学习，毕竟人的精力有限。在这中间我选择了搜索引擎，一方面是因为我之前有基础，另一方面是因为搜索的应用面相对来说更广一点，无论怎么发展，人类总是需要搜索信息的。&lt;/p&gt;
&lt;h2&gt;生活&lt;/h2&gt;
&lt;h3&gt;感情&lt;/h3&gt;
&lt;p&gt;今年见了女朋友的家长，也带她回上海看了我爸妈。明年应该就要结婚了。感情上总体还是比较稳定的，虽然平时偶尔会有一些争吵，但是在人生的大方向上，我们是一致的。&lt;/p&gt;
&lt;p&gt;其实，我之前一直觉得我自己一个人的生活就足够充实有趣了，甚至觉得有一个伴侣可能会让我分心，我需要花时间和精力去照顾她的情绪。事实上，的确如此。但另一方面，和女朋友在一起的时光，也让我发现了我之前很多的缺陷，比如我很以自我为中心，有时候自以为是，同时很多事情其实考虑的不是很成熟。而这些问题，如果不是伴侣，可能不会有第二个人单独指出来。&lt;/p&gt;
&lt;p&gt;所以总体来说，和女友的这段关系，让我成为了一个更好的人。&lt;/p&gt;
&lt;h3&gt;习惯&lt;/h3&gt;
&lt;p&gt;今年依旧保持了跑步的习惯，从一开始的每两天一次5公里，改为每天一次3公里。&lt;a href=&quot;https://running-page-tang.vercel.app/&quot;&gt;战绩可查&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我女朋友说，她很佩服我可以坚持跑步这么久，但其实她不知道的是，跑步对我来说其实根本不是一件需要坚持的事情。跑步已经成为了我生活的一部分，我需要跑步，就像我需要吃饭睡觉一样。跑步带给我的不仅是身体上的健康，更是精神上的愉悦。&lt;/p&gt;
&lt;p&gt;更重要的是，跑步可以给我带来难得的独处时间。并且在跑步的时候，我不需要考虑输赢，不需要考虑别人的看法，我只需要专注于自己的呼吸和步伐。在这个动不动就是融资几十个亿，年纪轻轻就出任副总裁的时代，有一个可以只专注于自己的时间，对我来说难能可贵。&lt;/p&gt;
&lt;p&gt;另外，还有一个今年养成的习惯是每天写很简短的日记，然后和 AI 一起复盘，我觉得对我个人来说帮助还挺大的，有效的帮我逐渐减少了手机的使用时间。&lt;/p&gt;
&lt;h3&gt;投资&lt;/h3&gt;
&lt;p&gt;今年在投资上，我觉得我进步了不少，相比于去年总期望寻找 10 倍股，期望一夜暴富。今年我彻底放弃了这个想法，转而去寻找自己可以理解的投资标的，并且长期持有。尽管，我最初的理解可能出现了偏差，但我相信通过这种方式，我可以逐渐加深对投资/商业模式的理解，而不是形成赌一把的思维官宣。&lt;/p&gt;
&lt;p&gt;今年的整体收益率是 22%，但我投入的本金并不多，所以实际收益也就是几万块钱。但是本金不多的本质原因在于，我对于投资的认识还不够，我没有足够的信心去投入更多的资金。这并不是说，早知道多投点就好了，而是我的水平今年只能赚这么多了。&lt;/p&gt;
&lt;p&gt;我认为从人生的长度来看，每一个人都会碰到几次 10 倍股的机会。但问题在于，你那个时候有多大把握相信你的判断是正确的，并且你有没有勇气去押注它。我希望在未来的几年里，我可以逐渐锻炼自己的心态，可以把握住一两次这样的机会。&lt;/p&gt;
&lt;h3&gt;阅读&lt;/h3&gt;
&lt;p&gt;今年我读了 16 本书，给我印象最深的两本书是《国富论》和《对赌》。&lt;/p&gt;
&lt;p&gt;在读《国富论》之前，我一直以为这会是一本十分难懂的经济学著作，但实际看下来却发现这本书其实写的十分通俗易懂，而且内容也十分有趣。里面很多内容让我耳目一新，也让我意识到，经济学其实是一个多变量系统的学科，每一个经济现象的背后，往往有很多因素在起作用，而不是简单的单一因素决定论。光是这个点，就让我省下了很多时间去读网络上一些看似很有道理，但其实根本是情绪性的经济学文章。&lt;/p&gt;
&lt;p&gt;《对赌》这本书，让我印象最深的是以下几个思维模型：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一件事的成功与失败，有两个因素，一个是策略一个是运气。一定要把这两个因素认清楚，不能把运气的成分也算到策略里去&lt;/li&gt;
&lt;li&gt;一个事情，你会有一个先入为主的观念，但其实你可能是错的。应该为自己的置信度设一个百分比，这样你可以更好的衡量它。在否定别人的观点前，先自己问自己你愿意为这个信念去赌吗？这可以很好的减少认知偏差。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;杂项&lt;/h3&gt;
&lt;p&gt;在 AI 时代，搞点乱七八糟的事情变得尤为重要，而且也变得相当容易。反正有什么不懂的，问 AI 就好了。&lt;/p&gt;
&lt;p&gt;今年我尝试了去做 CSGO 的量化交易，尝试去写 Chrome 插件，尝试去玩玩 AI 绘画，尝试去写写小游戏。虽然这些事，按照我女友的说法，天天瞎折腾，也没弄出啥名堂，但我觉得这些事对我来说还是挺有意义的。毕竟打发了我不少的时间，也让我学到了一些东西。&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;2025 年我依旧没有暴富，但是过得还算愉快。希望 2026 年我可以继续过得愉快，彻底弄懂搜索引擎的所有细节，好好学学英语，折腾点有意思的项目。&lt;/p&gt;
&lt;p&gt;当然最重要的还是，希望我可以暴富！&lt;/p&gt;
</content:encoded><category>Rewind</category><author>tang-hi</author></item><item><title>经典的倒排索引 - Finite State Transducers (实现篇)</title><link>https://tangdh.life/posts/ir/fst-implement/</link><guid isPermaLink="true">https://tangdh.life/posts/ir/fst-implement/</guid><description>倒排索引是搜索引擎中最核心的数据结构之一，也是搜索引擎区别于其他数据库系统的关键所在。FST 则是倒排索引的一个经典实现方式。这次我们讲实现。</description><pubDate>Mon, 17 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;在&lt;a href=&quot;https://tangdh.life/posts/ir/fst-overview/&quot;&gt;前一篇文章中&lt;/a&gt;，我们介绍了 FST 作为倒排索引的优势，同时也介绍了 FST 的基本概念以及相应的算法流程。
在这篇文章中，我们会详细介绍如何用 C++ 实现一个 FST 以及相关的实现细节。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;我们会先介绍构建阶段 FST 所需要用到的数据结构。&lt;/li&gt;
&lt;li&gt;再实现 &lt;code&gt;FstBuilder&lt;/code&gt;，参照上一篇的四个步骤一步步进行开发。&lt;/li&gt;
&lt;li&gt;最后实现一个 &lt;code&gt;FstSearcher&lt;/code&gt;，用于在构建好的 FST 上进行搜索。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;0. 最终目标&lt;/h2&gt;
&lt;p&gt;在开始之前，我们先来看一下最终目标。我们希望实现两个类：&lt;code&gt;FstBuilder&lt;/code&gt; 和 &lt;code&gt;FstSearcher&lt;/code&gt;。它们的接口如下所示:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FstBuilder builder;

// add must be called with inputs in lexicographical order
builder.add(&quot;a&quot;, 1);
builder.add(&quot;ab&quot;, 2);
builder.add(&quot;cap&quot;, 1);
builder.add(&quot;tap&quot;, 1);

builder.finish(); // after this no more inputs will be added

// immutable FST is built
FstSearcher searcher = builder.buildFst();

std::optional&amp;lt;int&amp;gt; v1 = searcher.search(&quot;ab&quot;); // v1 == 2
std::optional&amp;lt;int&amp;gt; v2 = searcher.search(&quot;cad&quot;);  // v2 == std::nullopt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了便于理解，我们在这里实现一个最朴素的 &lt;code&gt;std::string -&amp;gt; int&lt;/code&gt; 映射的 FST。&lt;/p&gt;
&lt;h2&gt;1. 构建时所用到的数据结构&lt;/h2&gt;
&lt;p&gt;构建时我们需要用到一些数据结构来表示 FST 的节点和边。我们对照上一篇文章中的定义来实现这些数据结构。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Node;
struct Arc {
  char label; // edge label
  Node* target; // target node
  int output; // output value

  bool targetCompiled; // whether target node is compiled
  uint64_t targetAddress; // address of target node in serialized FST
  bool isFinal; // whether target node is final
  int finalOutput; // final output value of target node
};

struct Node {
  std::vector&amp;lt;Arc&amp;gt; arcs; // outgoing edges
  bool isFinal; // is final node
  int finalOutput; // final output value

  bool compiled; // whether this node has been compiled
  uint64_t address; // address in the serialized FST
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了加深印象，我们再介绍一下 FST 边（Arc）和节点（Node）的定义。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Arc 的定义如下&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;label&lt;/strong&gt;: 边的标签，表示这条边所代表的字符&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;target&lt;/strong&gt;: 这条边所指向的目标节点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;output&lt;/strong&gt;: 这条边所携带的输出值（值 &amp;gt;= 0）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;targetCompiled&lt;/strong&gt;: 一个布尔值，表示目标节点是否已经被序列化&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;targetAddress&lt;/strong&gt;: 如果目标节点已经被序列化，则表示该节点在文件中的偏移地址&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;isFinal&lt;/strong&gt;: 一个布尔值，表示目标节点是否为终止节点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;finalOutput&lt;/strong&gt;: 如果目标节点是终止节点，则表示该节点的最终输出值（详细作用见上一篇文章）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Node 的定义如下&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;arcs&lt;/strong&gt;: 该节点的所有出边&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;isFinal&lt;/strong&gt;: 一个布尔值，表示该节点是否为终止节点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;finalOutput&lt;/strong&gt;: 如果该节点是终止节点，则表示该节点的最终输出值（详细作用见上一篇文章）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;compiled&lt;/strong&gt;: 一个布尔值，表示该节点是否已经被序列化&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;address&lt;/strong&gt;: 如果该节点已经被序列化，则表示该节点在文件中的偏移地址&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在明确了 FST 的基本数据结构后，我们就可以给出 &lt;code&gt;FstBuilder&lt;/code&gt; 的定义了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class FstBuilder {
  public:
    // add a new key-output pair
    bool add(const std::string &amp;amp;input, int out);

    // finish the building process
    void finish();

    // convert to FST
    FstSearcher buildFst();

  private:
    Meta meta_; // FST metadata
    std::string lastInput_; // last input added
    std::vector&amp;lt;std::unique_ptr&amp;lt;Node&amp;gt;&amp;gt; frontier_; // nodes waiting for processing
    std::unordered_map&amp;lt;std::vector&amp;lt;std::byte&amp;gt;, uint64_t&amp;gt; nodeCache_; // nodes could be reused
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数的定义就不说了，语义都很清晰。我们重点介绍一下 &lt;code&gt;FstBuilder&lt;/code&gt; 中维护的变量:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;meta_&lt;/code&gt;: FST 的元数据，包含根节点地址和字节大小&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lastInput_&lt;/code&gt;: 上一个被添加的输入字符串，用于寻找前缀&lt;/li&gt;
&lt;li&gt;&lt;code&gt;frontier_&lt;/code&gt;: 一个节点数组，用于存储当前待处理（待冻结）的节点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nodeCache_&lt;/code&gt;: 一个哈希表，用于存储已经处理过的节点，以便进行节点复用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;看了前一篇文章的话，你对这些变量应该都不陌生。不过即使你印象不深，我们在后续的实现过程中也会一一解释它们的作用。&lt;/p&gt;
&lt;h2&gt;2. 构建主流程&lt;/h2&gt;
&lt;p&gt;我们依旧先给出整个构建流程的代码框架，然后再聚焦每一个步骤的实现细节。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool FstBuilder::add(const std::string &amp;amp;input, int out) {

    if (input &amp;lt; lastInput_) {
        throw std::invalid_argument(&quot;Inputs must be added in lexicographical order&quot;);
    }
    
    // step 1: find common prefix length
    uint64_t prefixLen = commonPrefixLength(lastInput_, input);

    // step 2: process suffix of last input
    freezeTail(prefixLen + 1);

    // step 3: insert new input
    insertNewInput(input, prefixLen + 1);

    // step 4: adjust outputs
    adjustOutputs(input, out, prefixLen + 1);

    // update last input
    lastInput_ = input;
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后续我们详细介绍每一个步骤的实现细节。&lt;/p&gt;
&lt;h3&gt;2.1 寻找前缀：commonPrefixLength&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;commonPrefixLength&lt;/code&gt; 用于寻找两个字符串的最长公共前缀长度。通过这个长度，我们可以确定哪些节点在后续构建的过程中不会再新增出边，从而可以进行冻结操作（写到磁盘中）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uint64_t commonPrefixLength(const std::string &amp;amp;a, const std::string &amp;amp;b) {
    uint64_t minLength = std::min(a.size(), b.size());
    for (uint64_t i = 0; i &amp;lt; minLength; ++i) {
        if (a[i] != b[i]) {
            return i;
        }
    }
    return minLength;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 处理后缀：freezeTail&lt;/h3&gt;
&lt;p&gt;在第一步中，我们找到了最长公共前缀的长度，所以在这个长度之后的节点都不会再有变化了（因为输入是按照字典序添加的，读者可以自己思考一下）。我们可以对这些节点进行冻结处理。&lt;/p&gt;
&lt;p&gt;这是整个构建流程中较为复杂的一步，我们仍旧先给出代码框架，然后对照代码进行解释。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void FstBuilder::freezeTail(uint64_t startIndex) {
  startIndex = std::max(startIndex, 1UL); ---------------------- a
  for (auto i = lastInput_.size(); i &amp;gt;= startIndex; --i) {
    auto &amp;amp;current = frontier_[i];
    auto &amp;amp;parent = frontier_[i - 1];
    bool isFinal = current-&amp;gt;isFinal;  
    auto finalOutput = current-&amp;gt;finalOutput;
    auto compiledAddress = compileNode(current.get());  -------- b

    // update the arc in parent pointing to current
    Arc &amp;amp;pointingArc = parent-&amp;gt;arcs.back();     ---------------|
    pointingArc.targetCompiled = true;                         |
    pointingArc.targetAddress = compiledAddress;               | c
    pointingArc.target = nullptr;                              |
    pointingArc.isFinal = isFinal;                             |
    pointingArc.finalOutput = finalOutput; --------------------| 

    current-&amp;gt;reset(); ------------------------------------------ d
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们首先看代码 &lt;code&gt;a&lt;/code&gt; 处，这里我们确保 &lt;code&gt;startIndex&lt;/code&gt; 至少为 1，因为根节点在构建时是不会被冻结的。只有当我们所有的输入全部处理完毕后，根节点才会被处理。&lt;/p&gt;
&lt;p&gt;接下来我们进入循环，从上一个输入的最后一个节点开始，一直到公共前缀的下一个节点为止。对于每一个节点，我们都需要进行冻结处理。&lt;/p&gt;
&lt;p&gt;在代码 &lt;code&gt;b&lt;/code&gt; 处，我们调用 &lt;code&gt;compileNode&lt;/code&gt; 函数对当前处理的节点进行序列化，并将其写入磁盘中。然后将序列化后的节点在文件中的偏移作为返回值返回。&lt;/p&gt;
&lt;p&gt;在完成节点序列化后，我们需要更新其父节点中指向该节点的边（Arc）。
这部分代码在 &lt;code&gt;c&lt;/code&gt; 处。我们通过 &lt;code&gt;parent-&amp;gt;arcs.back()&lt;/code&gt; 获取到指向当前节点的边，然后将其目标节点信息更新为刚才序列化后的地址，并把目标节点指针置为空。
同时，我们还需要把当前节点的终止状态和 &lt;code&gt;finalOutput&lt;/code&gt; 也更新到边上。原因后面会讲到。&lt;/p&gt;
&lt;p&gt;最后，在代码 &lt;code&gt;d&lt;/code&gt; 处，我们调用 &lt;code&gt;current-&amp;gt;reset()&lt;/code&gt; 函数来重置当前节点。这是因为构建过程中 &lt;code&gt;frontier_&lt;/code&gt; 中的节点是会复用的，当该节点的所有信息都已经
保存完毕后，我们就可以将其重置，方便后续进行复用。&lt;/p&gt;
&lt;p&gt;现在我们再来看一下 &lt;code&gt;compileNode&lt;/code&gt; 函数的实现:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uint64_t FstBuilder::compileNode(Node* node) {
    // if already compiled, return its address
    if (node-&amp;gt;compiled) {
        return node-&amp;gt;address;
    }

    // if node has no arcs and is not final, return SPECIAL_ADDRESS 
    if (node-&amp;gt;arcs.empty()) {  -----------------------------------------                                        
        node-&amp;gt;reset();                                                 |
        node-&amp;gt;compiled = true;                                         | a
        node-&amp;gt;address = detail::SPECIAL_ADDRESS;                       |
        return node-&amp;gt;address;                                          |
    } -----------------------------------------------------------------|

    meta_.arcCount += node-&amp;gt;arcs.size();

    // serialize node into a temporary buffer
    storage::BufferWriter compiledBuffer;
    serializeNode(node, compiledBuffer); ------------------------------- b

    // write to buffer if not found in cache                                      
    uint64_t nodeAddress = buffer_.size(); -------------------------------
    if (auto iter = nodeCache_.find(compiledBuffer.data());             |
        iter != nodeCache_.end()) {                                     |
        // found in cache                                               |
        nodeAddress = iter-&amp;gt;second;                                     |
    } else {                                                            |
        // not found in cache, append to buffer                         |
        buffer_.insert(buffer_.end(), compiledBuffer.rawPtr(),          |c
                       compiledBuffer.rawPtr()                          |
                       + compiledBuffer.size());                        |
        nodeCache_.emplace(compiledBuffer.data(), nodeAddress);         |
        meta_.nodeCount += 1;                                           |
    }-------------------------------------------------------------------|

    node-&amp;gt;reset();
    node-&amp;gt;compiled = true;
    node-&amp;gt;address = nodeAddress;
    return node-&amp;gt;address;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;compileNode&lt;/code&gt; 函数中，我们首先检查该节点是否已经被编译过，如果是的话直接返回其地址。&lt;/p&gt;
&lt;p&gt;接下来在代码 &lt;code&gt;a&lt;/code&gt; 处我们检查当前节点是不是没有出边（arcs）。在这种情况下，我们不需要将其序列化后写入磁盘，可以直接返回一个特殊地址 &lt;code&gt;SPECIAL_ADDRESS&lt;/code&gt;，从而节省存储空间。&lt;/p&gt;
&lt;p&gt;然后我们在代码 &lt;code&gt;b&lt;/code&gt; 处调用 &lt;code&gt;serializeNode&lt;/code&gt; 函数，将节点序列化到一个临时缓冲区中。&lt;/p&gt;
&lt;p&gt;代码 &lt;code&gt;c&lt;/code&gt; 处，则是 FST 的&lt;strong&gt;精华所在&lt;/strong&gt;。我们通过一个哈希表（&lt;code&gt;nodeCache_&lt;/code&gt;）来缓存已经序列化过的节点。如果当前节点的序列化结果已经存在于缓存中，我们就直接复用其地址，这就完成了共享后缀。
通过这种方式，我们可以大幅度减少 FST 的存储空间。&lt;/p&gt;
&lt;p&gt;这里我用一个大的 &lt;code&gt;buffer_&lt;/code&gt; 模拟“磁盘文件”，&lt;code&gt;nodeAddress&lt;/code&gt; 实际上就是节点在这个 &lt;code&gt;buffer_&lt;/code&gt; 中的偏移。 真正工程里可以把这个 &lt;code&gt;buffer_&lt;/code&gt; 写到文件里，或者用 mmap 映射进来。&lt;/p&gt;
&lt;p&gt;至此，我们还剩下 &lt;code&gt;serializeNode&lt;/code&gt; 函数的实现没有介绍:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void FstBuilder::serializeNode(Node* node, storage::BufferWriter &amp;amp;writer) {
    for (size_t i = 0; i &amp;lt; node-&amp;gt;arcs.size(); ++i) {
        auto &amp;amp;arc = node-&amp;gt;arcs[i];
        serializeArc(arc, writer, i == node-&amp;gt;arcs.size() - 1);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从 &lt;code&gt;serializeNode&lt;/code&gt; 函数中我们可以看到，虽然说是序列化节点，但实际上我们只是将节点的所有出边依次序列化而已。&lt;/p&gt;
&lt;p&gt;这也解释了之前为什么我们要把节点的终止状态和 &lt;code&gt;finalOutput&lt;/code&gt; 信息存储到边上，因为节点本身并没有被序列化，只有边被序列化了。
所以我们需要把这些信息存储到边上，以便在搜索时能够获取这些信息。&lt;/p&gt;
&lt;p&gt;至于 &lt;code&gt;serializeArc&lt;/code&gt; 函数的实现，这里就不展开讲了，你怎么实现边的序列化都可以，只要你能反序列化回来就行。&lt;/p&gt;
&lt;h3&gt;2.3 插入新的输入：insertNewInput&lt;/h3&gt;
&lt;p&gt;在处理完上一个输入的后缀后，我们需要将当前输入的新增部分插入到 FST 中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void FstBuilder::insertNewInput(const std::string &amp;amp;input, uint64_t fromIndex) {
    // ensure frontier_ has enough nodes
    while (frontier_.size() &amp;lt; input.size() + 1) {
        frontier_.emplace_back(std::make_unique&amp;lt;Node&amp;gt;());
    }

    for (uint64_t i = fromIndex; i &amp;lt;= input.size(); ++i) { ------
        auto &amp;amp;current = frontier_[i];                           |
        auto &amp;amp;parent = frontier_[i - 1];                        |
        Label label = input[i - 1];                             |
                                                                | a
        Arc arc;                                                | 
        arc.label = label;                                      |
        arc.target = current.get();                             |
        parent-&amp;gt;arcs.push_back(arc);                            |
    }-----------------------------------------------------------|

    std::unique_ptr&amp;lt;Node&amp;gt; &amp;amp;lastNode = frontier_[input.size()];
    lastNode-&amp;gt;isFinal = true;
    lastNode-&amp;gt;finalOutput = 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这部分代码比较简单，我们首先确保 &lt;code&gt;frontier_&lt;/code&gt; 中有足够的节点来存储当前输入的所有节点。&lt;/p&gt;
&lt;p&gt;然后在代码 &lt;code&gt;a&lt;/code&gt; 处，我们从 &lt;code&gt;fromIndex&lt;/code&gt; 开始，依次将当前输入的新增字符插入到 FST 中。
同时父节点需要增加一条出边，指向当前节点。&lt;/p&gt;
&lt;p&gt;在完成上述步骤后，我们需要把最后一个节点标记为终止节点，并把它的 &lt;code&gt;finalOutput&lt;/code&gt; 设为 0。&lt;/p&gt;
&lt;h3&gt;2.4 调整输出：adjustOutputs&lt;/h3&gt;
&lt;p&gt;终于到了构建的最后一步，但这也是较为复杂的一步。在增加了新的输入后，我们需要重新调整路径上的输出值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void FstBuilder::adjustOutputs(const std::string &amp;amp;input, int out, uint64_t toIndex) {
  int residual = out;          ------------------------------- a
  for (size_t i = 1; i &amp;lt; toIndex; ++i) { 
    auto &amp;amp;parent = frontier_[i - 1];
    auto &amp;amp;current = frontier_[i];

    Arc &amp;amp;arc = parent-&amp;gt;arcs.back();

    int lastArcOutput = arc.output;--------------------------|
    int commonOutput = std::min(residual, lastArcOutput);    |b
    int suffixOutput = lastArcOutput - commonOutput;         |
    arc.output = commonOutput;  -----------------------------|
    prependOutput(current.get(), suffixOutput);-------------- c
    residual = residual - commonOutput;
  }

  Arc &amp;amp;arc = frontier_[toIndex - 1]-&amp;gt;arcs.back();
  arc.output = residual;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在代码 &lt;code&gt;a&lt;/code&gt; 处，我们首先将输入值赋给 &lt;code&gt;residual&lt;/code&gt;，这个变量用于记录当前还有多少输出值需要分配到边上。&lt;/p&gt;
&lt;p&gt;不同于之前的步骤，这次我们是从根节点开始，顺序调整边上的输出值，直到公共前缀的下一个节点为止。
我们在代码 &lt;code&gt;b&lt;/code&gt; 处，对 &lt;code&gt;residual&lt;/code&gt; 和 &lt;code&gt;lastArcOutput&lt;/code&gt; 取最小值，并使用该值作为当前边的输出值。&lt;/p&gt;
&lt;p&gt;为什么这么做呢？假设我们现在的 &lt;code&gt;residual&lt;/code&gt; 是 10，而当前边的输出值是 15。为了确保我们不会破坏之前路径的输出值，同时又可以让当前路径的累加值等于输入值，我们只能把
当前边的输出值设为 10，即 &lt;code&gt;min(residual, lastArcOutput)&lt;/code&gt; 的值。&lt;/p&gt;
&lt;p&gt;接下来，我们要将当前边剩余的输出值 &lt;code&gt;suffixOutput&lt;/code&gt; 通过 &lt;code&gt;prependOutput&lt;/code&gt; 函数下推到下一个节点上。这部分代码在 &lt;code&gt;c&lt;/code&gt; 处。然后我们更新 &lt;code&gt;residual&lt;/code&gt;，减去当前边的输出值。&lt;/p&gt;
&lt;p&gt;最后，在循环结束后，我们需要将 &lt;code&gt;residual&lt;/code&gt; 的值赋给公共前缀下一个节点的边的输出值。这样就完成了输出值的调整。&lt;/p&gt;
&lt;p&gt;下面我们再简单介绍一下 &lt;code&gt;prependOutput&lt;/code&gt; 函数的实现:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void FstBuilder::prependOutput(Node* node, int output) {
  if (output == 0) {
      return;
    }

  for (auto &amp;amp;arc : node-&amp;gt;arcs) {
      arc.output = arc.output + output;
    }

  if (node-&amp;gt;isFinal) {
      node-&amp;gt;finalOutput = node-&amp;gt;finalOutput + output;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;逻辑相当简单，就是把传入的 &lt;code&gt;output&lt;/code&gt; 累加到该节点的所有出边上。如果该节点恰好是终止节点，那么我们还需要把 &lt;code&gt;output&lt;/code&gt; 累加到 &lt;code&gt;finalOutput&lt;/code&gt; 上。&lt;/p&gt;
&lt;p&gt;至此，整个 FST 的构建流程就算完成了，只要我们最后调用一下 &lt;code&gt;finish&lt;/code&gt; 函数就可以了:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void FstBuilder::finish() {
    // freeze all remaining nodes
    freezeTail(0);
    auto &amp;amp;rootNode = frontier_[0];
    uint64_t rootAddress = compileNode(rootNode.get());
    meta_.rootAddress = rootAddress; // record root address
    meta_.byteSize = buffer_.size();
    isFrozen_ = true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们冻结所有剩余的节点，然后编译根节点，并记录根节点的地址。最后将构建状态标记为“已冻结”。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;调整输出的逻辑理解起来可能比较吃力，可以结合上一篇文章中的例子来配合理解。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;3. 搜索的实现&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;FstSearcher&lt;/code&gt; 的实现相对简单，我们只需要沿着输入字符串对应的路径遍历 FST 即可。我们先给出 &lt;code&gt;FstSearcher&lt;/code&gt; 的定义:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class FstSearcher {
  public:
    FstSearcher(const storage::BufferReader &amp;amp;buffer, const Meta &amp;amp;meta);

    // search for input string, return output if found
    std::optional&amp;lt;int&amp;gt; search(const std::string &amp;amp;input) const;

  private:
    storage::BufferReader reader_;
    Meta meta_;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;FstSearcher&lt;/code&gt; 中维护了两个变量:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;reader_&lt;/code&gt;: 用于读取 FST 数据的缓冲区&lt;/li&gt;
&lt;li&gt;&lt;code&gt;meta_&lt;/code&gt;: FST 的元数据，包含根节点地址和字节大小&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来我们来看一下 &lt;code&gt;search&lt;/code&gt; 函数的实现:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::optional&amp;lt;int&amp;gt; FstSearcher::search(const std::string &amp;amp;input) const {
    uint64_t currentAddress = meta_.rootAddress;

    int accumulatedOutput = 0;
    Arc currentArc;

    for (size_t i = 0; i &amp;lt; input.size(); ++i) {           
        reader_.seek(currentAddress);                     
        char currentLabel = input[i];                     
        currentArc.reset();                               
        if (!findLabel(currentAddress, currentLabel, currentArc)) {       
            return std::nullopt;                                          
        }                                                                 
        accumulatedOutput = accumulatedOutput + currentArc.output;
        currentAddress = currentArc.targetAddress;
    }

    if (currentArc.isFinal()) {
        if (currentArc.hasFinalOutput()) {
            accumulatedOutput = accumulatedOutput + currentArc.finalOutput;
        }
        return accumulatedOutput;
    }
    return std::nullopt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，我们从 &lt;code&gt;meta_&lt;/code&gt; 中获取根节点的地址，并将输出值 &lt;code&gt;accumulatedOutput&lt;/code&gt; 初始化为 0。&lt;/p&gt;
&lt;p&gt;随后，我们遍历输入字符串的每一个字符，通过 &lt;code&gt;findLabel&lt;/code&gt; 函数在当前节点中查找对应的边。如果找不到对应的边，则说明输入字符串不存在于 FST 中，我们返回 &lt;code&gt;std::nullopt&lt;/code&gt;。
如果找到了对应的边，我们将该边的输出值累加到 &lt;code&gt;accumulatedOutput&lt;/code&gt; 上，并将当前节点地址更新为该边的目标节点地址。&lt;/p&gt;
&lt;p&gt;在遍历完所有字符后，我们检查当前边是否为终止边。如果是的话，我们还需要将其 &lt;code&gt;finalOutput&lt;/code&gt; 累加到 &lt;code&gt;accumulatedOutput&lt;/code&gt; 上，然后返回最终的输出值。
如果当前边不是终止边，则说明输入字符串不存在于 FST 中，我们返回 &lt;code&gt;std::nullopt&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我们接下来再看一下 &lt;code&gt;findLabel&lt;/code&gt; 函数的实现:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool FstSearcher::findLabel(uint64_t nodeAddress, char label, Arc &amp;amp;outArc) const {
    if (nodeAddress == SPECIAL_ADDRESS) {
        return false;
    }

    reader_.seek(nodeAddress);
    while (true) {
        outArc.flag = static_cast&amp;lt;ArcFlag&amp;gt;(reader_.read&amp;lt;uint8_t&amp;gt;());
        outArc.label = reader_.read&amp;lt;char&amp;gt;();

        outArc.output = 0;
        if ((outArc.flag &amp;amp; ArcFlag::VALUE_ARC) != ArcFlag::EMPTY) {
            outArc.output = reader_.read&amp;lt;int&amp;gt;();
        }

        outArc.finalOutput = 0;
        if ((outArc.flag &amp;amp; ArcFlag::FINAL_VALUE_ARC) != ArcFlag::EMPTY) {
            outArc.finalOutput = reader_.read&amp;lt;int&amp;gt;();
        }

        outArc.targetAddress = detail::SPECIAL_ADDRESS;
        if ((outArc.flag &amp;amp; ArcFlag::STOP_NODE) == ArcFlag::EMPTY) {
            outArc.targetAddress = reader_.read&amp;lt;uint64_t&amp;gt;();
        }

        if (outArc.label == label) {
            return true;
        }

        if ((outArc.flag &amp;amp; ArcFlag::LAST) != ArcFlag::EMPTY) {
            break;
        }
    }
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;findLabel&lt;/code&gt; 函数中，我们首先检查当前节点地址是否为特殊地址 &lt;code&gt;SPECIAL_ADDRESS&lt;/code&gt;，如果是的话，说明该节点不存在，我们直接返回 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;然后我们定位到当前节点地址，并开始遍历该节点的所有出边。
这实际上就是反序列化的过程，我们依次读取每一条边的信息，并检查其标签是否与目标标签匹配。如果匹配，我们将该边的信息存储到 &lt;code&gt;outArc&lt;/code&gt; 中，并返回 &lt;code&gt;true&lt;/code&gt;。如果遍历完所有边都没有找到匹配的标签，则返回 &lt;code&gt;false&lt;/code&gt;。
因为序列化时我们是依次序列化每一条边的，它们都顺序存储在一起，所以我们只需要顺序读取，直至遇到最后一条边即可。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;在这篇文章中，我们详细介绍了如何用 C++ 实现一个 FST 以及相关的实现细节。不过我们实现的 FST 是最简单的版本，在实际应用中，你可能还需要考虑更多的优化。但是
对于初学者理解和掌握 FST 的基本原理，这个实现已经足够了。完整的代码可以在 &lt;a href=&quot;https://github.com/tang-hi/simplefst&quot;&gt;这里&lt;/a&gt; 找到。&lt;/p&gt;
</content:encoded><category>Full Text Search</category><category>Inverted Index</category><category>倒排索引</category><category>Lucene</category><author>tang-hi</author></item><item><title>经典的倒排索引 - Finite State Transducers (描述篇)</title><link>https://tangdh.life/posts/ir/fst-overview/</link><guid isPermaLink="true">https://tangdh.life/posts/ir/fst-overview/</guid><description>倒排索引是搜索引擎中最核心的数据结构之一，也是搜索引擎区别于其他数据库系统的关键所在。FST 则是倒排索引的一个经典实现方式.</description><pubDate>Sun, 02 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;倒排索引是搜索引擎中最核心的数据结构之一，正因为有了倒排索引，搜索引擎才能高效地处理海量文本数据。目前工程里常见的实现大致有三类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基于哈希表的倒排索引&lt;/li&gt;
&lt;li&gt;基于FST的倒排索引&lt;/li&gt;
&lt;li&gt;基于跳表的倒排索引&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本文将首先介绍倒排索引不同实现方式的优缺点，然后重点介绍基于 FST 的倒排索引的原理及实现方式。&lt;/p&gt;
&lt;h2&gt;倒排索引的对比&lt;/h2&gt;
&lt;p&gt;首先我们先明确一下本文所说的倒排索引，它存储的键值对是什么?  在教科书中，我们会将&lt;code&gt;term&lt;/code&gt;作为键，将包含该&lt;code&gt;term&lt;/code&gt;的文档ID列表(posting list)作为值。如下图所示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20251102135046.png&quot; alt=&quot;Inverted Index&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但是在具体实现中，我们往往并不会将整个文档ID列表存储在倒排索引中，因为文档数量非常庞大，如果把整个文档ID列表作为值存储在倒排索引中，
会导致倒排索引所需要的存储空间过大，无法常驻内存中，从而影响检索效率。因此倒排索引通常设计为两层结构。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一层: 倒排索引, 存储&lt;code&gt;term&lt;/code&gt;以及对应的文档ID列表在磁盘上的偏移位置(offset)&lt;/li&gt;
&lt;li&gt;第二层: 磁盘上的文档ID列表, 存储实际的文档ID列表, 可以由倒排索引中的offset定位&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大概的结构如下图所示
&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20251102140327.png&quot; alt=&quot;Inverted Index Structure&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因此本文所说的倒排索引，实际上是指倒排索引的第一层结构。它的键值对为&lt;code&gt;term&lt;/code&gt;以及对应的文档ID列表在磁盘上的偏移位置(offset)。
为了方便理解，我们假设倒排索引的数据结构，即为存储&lt;code&gt;&amp;lt;string, int&amp;gt;&lt;/code&gt;的键值对.&lt;/p&gt;
&lt;p&gt;在明确了这一点后，我们来对比一下不同实现方式的优缺点。&lt;/p&gt;
&lt;h3&gt;基于哈希表的倒排索引&lt;/h3&gt;
&lt;p&gt;使用哈希表实现倒排索引几乎是每个人的第一反应，因为这个使用场景完美契合哈希表的特点。 实际上，很多搜索引擎也的确是使用哈希表来实现倒排索引的。
但这种方式有一个天然短板，那就是哈希表无法高效的进行&lt;code&gt;前缀搜索&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在搜索的场景中，&lt;code&gt;前缀搜索&lt;/code&gt;是一个非常常见的需求。 例如，当用户在搜索框中输入&quot;app&quot;时，我们希望不仅能够匹配到&quot;app&quot;, 还希望搜索框中可以联想出&quot;apple&quot;, &quot;application&quot;等单词,
这样可以提升用户的搜索体验。可是由于哈希表并不是有序的，我们没有好的办法来进行&lt;code&gt;前缀搜索&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;不过哈希表的优势也明显：查找和构建都很快。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20251102144313.png&quot; alt=&quot;hash&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;基于跳表的倒排索引&lt;/h3&gt;
&lt;p&gt;跳表有序，天然支持前缀/范围查询，因此，如果我们需要一个支持增删改的倒排索引，那么跳表是一个不错的选择。很多搜索引擎都会用跳表作为增量数据的倒排索引。
但是跳表相较于哈希表，查找速度会慢一些。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20251102145708.png&quot; alt=&quot;skiplist&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;基于FST的倒排索引&lt;/h3&gt;
&lt;p&gt;FST 是Lucene中使用的倒排索引实现，但国内许多大厂自研的搜索引擎往往不使用 FST 作为倒排索引的实现方式，主要原因有以下几点:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;认为FST速度不够快, 不符合高性能搜索引擎的要求&lt;/li&gt;
&lt;li&gt;FST对插入的顺序有要求，不支持实时的修改 (后面会详细说明)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但 FST 的优势也非常明显:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;占用的内存非常小&lt;/li&gt;
&lt;li&gt;支持高效的前缀搜索&lt;/li&gt;
&lt;li&gt;速度适中, 它的时间复杂度为 O(m), m为term的最大长度, 与term的数量无关.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此 FST 非常适合作为全量数据的倒排索引实现方式，尤其是在内存资源有限的场景下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20251102150805.png&quot; alt=&quot;FST&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;FST的概述与实现&lt;/h2&gt;
&lt;p&gt;下面我们来详细介绍 FST 的原理及实现方式.
FST 可以被认为是一个压缩的 Trie 树。区别在于Trie树是通过复用前缀来节省空间，而FST在Trie树的基础上，更进一步，通过复用后缀进一步节省空间。&lt;/p&gt;
&lt;p&gt;我们可以通过下面的图片来直观感受一下FST和Trie的区别.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20251102152200.png&quot; alt=&quot;FST vs Trie&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从图中我们可以看到，FST通过复用后缀，显著减少了节点的数量，从而节省了内存空间。但是从本质上来讲，FST和Trie是类似的，都是通过有向无环图(DAG)来存储字符串集合。
下面将分别从搜索和构建两个方面来介绍 FST 的原理。FST 的搜索显著简单于构建，因此我们先介绍搜索。&lt;/p&gt;
&lt;h3&gt;FST的搜索&lt;/h3&gt;
&lt;p&gt;在讲述FST的搜索之前，我们首先介绍一下组成FST的各个部分。&lt;/p&gt;
&lt;h4&gt;边&lt;/h4&gt;
&lt;p&gt;FST的边是有向的，从一个节点指向另一个节点。每条边都包含以下信息:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;label: 边的标签，表示这条边所代表的字符&lt;/li&gt;
&lt;li&gt;target: 这条边所指向的目标节点&lt;/li&gt;
&lt;li&gt;output: 这条边所携带的输出值(值 &amp;gt;= 0)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果我们有一个 term &lt;code&gt;cat&lt;/code&gt;，它对应的值为 5，那么它会变为三条边, 如下图所示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20251102155716.png&quot; alt=&quot;label&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;label&lt;/code&gt; 和 &lt;code&gt;target&lt;/code&gt;, 这几个概念和Trie树是类似的，也比较直观。但 &lt;code&gt;output&lt;/code&gt; 是 FST 所独有的，不同于Trie树将值存储在叶子节点上，FST将值分散的存储在各个边上。
需要将&lt;code&gt;term&lt;/code&gt;对应的所有边的&lt;code&gt;output&lt;/code&gt;进行累加，才能得到最终的值。&lt;/p&gt;
&lt;h4&gt;节点&lt;/h4&gt;
&lt;p&gt;FST的节点包含以下信息:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;arcs: 该节点的所有出边&lt;/li&gt;
&lt;li&gt;isFinal: 该节点是否为终止节点&lt;/li&gt;
&lt;li&gt;finalOutput: 如果该节点为终止节点，则在最终的输出中需要加上该值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于&lt;code&gt;arcs&lt;/code&gt;，它是一个边的有序列表，按照边的&lt;code&gt;label&lt;/code&gt; 进行排序。 &lt;code&gt;isFinal&lt;/code&gt; 则和Trie树的含义一样，表示该节点是否为一个完整的term的终止节点。可以用于判断一个term是否存在于FST中。&lt;/p&gt;
&lt;p&gt;但 &lt;code&gt;finalOutput&lt;/code&gt; 则看起来不直观，因为我们在前面介绍边的时候，已经说了FST会把&lt;code&gt;term&lt;/code&gt; 对应的值分散存储在各个边上，那么为什么还需要在终止节点上存储一个 &lt;code&gt;finalOutput&lt;/code&gt; 呢？&lt;/p&gt;
&lt;p&gt;我们可以从下图的例子中看到我们为什么需要&lt;code&gt;finalOutput&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20251102161427.png&quot; alt=&quot;finalOutput&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因为边的&lt;code&gt;output&lt;/code&gt; 只能存储非负值，因此当我们遇到上述情况的时候发现，我们无法在满足 &lt;code&gt;mon&lt;/code&gt; 的所有边上的 &lt;code&gt;output&lt;/code&gt; 加起来等于 5，
同时又满足&lt;code&gt;monz&lt;/code&gt; 的对应边上的&lt;code&gt;output&lt;/code&gt; 加起来等于3。 因此我们需要在&lt;code&gt;mon&lt;/code&gt; 终止节点上存储一个&lt;code&gt;finalOutput&lt;/code&gt;，来解决这个问题。
如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20251102162125.png&quot; alt=&quot;finalOutput2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;看到这里，大家可能会有疑惑，为什么&lt;code&gt;output&lt;/code&gt; 不能是负数呢? 从实现上面来说，这完全是可以的。 在实现上，我们完全可以在边上存储一个负数。
这个非负数的限制，主要是为了减少存储空间。在实际的应用中，我们会将&lt;code&gt;output&lt;/code&gt; 存储为变长整数(&lt;code&gt;varint&lt;/code&gt;), 但变长整数对于负数的存储效率非常低，
因此选择将 &lt;code&gt;output&lt;/code&gt; 限制为非负数，从而提升存储效率。&lt;/p&gt;
&lt;p&gt;在介绍完FST的边和节点之后，其实搜索的算法过程也变得非常直观了。我们只需从根节点开始，依次查找每个字符对应的边，累加边上的&lt;code&gt;output&lt;/code&gt;，直到遍历完整个term。如果最终停留的节点是一个终止节点，那么我们还需要加上该节点的&lt;code&gt;finalOutput&lt;/code&gt;，最终得到的值即为该term对应的值。&lt;/p&gt;
&lt;p&gt;整个搜索流程的伪代码/流程如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def search(fst, term):
    node = fst.root
    output = 0
    for char in term:
        arc = node.find_arc(char)
        if arc is None:
            return None
        output += arc.output
        node = arc.target
    if node.isFinal:
        output += node.finalOutput
        return output
    return None
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/excalidraw-claymate.gif&quot; alt=&quot;search&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;FST的构建&lt;/h3&gt;
&lt;p&gt;这一章节，我们讲述 FST 的构建过程。相较于搜索，构建复杂一些。本文不一上来给出完整算法，而是通过一个精心设计的小例子，逐步引入构建中涉及的关键概念，最后再回到整体流程。&lt;/p&gt;
&lt;p&gt;假设我们有如下的 term 集合（&lt;strong&gt;已排序&lt;/strong&gt;）及对应的值:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a     -&amp;gt; 5
ab    -&amp;gt; 2
cap   -&amp;gt; 1
tap   -&amp;gt; 1
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么要求有序？
有序能保证“上一个 term 的后缀部分不会再被修改”，因此可安全冻结（freeze）并参与后续复用。也正因为这一点，FST 不适合“实时逐条改写”，而适合批量构建。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们从空的 FST 开始，依次插入每个 term。&lt;/p&gt;
&lt;h4&gt;1. 插入 &lt;code&gt;a -&amp;gt; 5&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;第一条最简单，因为当前FST是空的，因此我们只需要创建一条边，连接根节点和一个终止节点即可，如下图所示:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20251102183154.png&quot; alt=&quot;step1&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：虽然语义上只要“路径上弧输出相加 = term 值”即可，实现上会尽量把值放在开头的弧，实现更为简单。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2. 插入 &lt;code&gt;ab -&amp;gt; 2&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;从这步开始进入完整流程。每次插入分为四步：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;寻找前缀&lt;/strong&gt;: 与上一条输入的最长公共前缀。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;处理后缀&lt;/strong&gt;: 把上一条在前缀之后的后缀冻结（以便复用）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;插入新的输入&lt;/strong&gt;: 接下来，我们需要插入当前的输入。因为我们已经得到了前缀，所以我们只需要插入后缀部分即可。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调整输出&lt;/strong&gt;: 对边的&lt;code&gt;output&lt;/code&gt;进行调整，从而确保路径上的&lt;code&gt;output&lt;/code&gt; 累加起来等于&lt;code&gt;term&lt;/code&gt;对应的值。同时不会影响之前插入的term的值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们先看前三个步骤，然后再看最后一个步骤。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20251102191551.png&quot; alt=&quot;step2-1&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2.1 寻找前缀&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在这个例子中，插入的 term 是 &lt;code&gt;ab&lt;/code&gt;, 它与上一个插入的 term &lt;code&gt;a&lt;/code&gt; 共有的前缀为 &lt;code&gt;a&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2.2 处理后缀&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为找到的前缀 &quot;a&quot; 就是上一个插入的 term &quot;a&quot; 的全部内容，因此后缀为空，不需要进行冻结，可以直接跳过这一步。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2.3 插入新的输入&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在前缀节点（图中 node 1）之后，插入后缀 b 与新的终止节点 node 2。新弧的 &lt;code&gt;label&lt;/code&gt;为b、&lt;code&gt;output&lt;/code&gt;为0。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2.4 调整输出&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们发现当把路径上边的 &lt;code&gt;output&lt;/code&gt; 累加起来，对于&quot;ab&quot; 而言， 它的值并不等于2. 因此需要调整边的 &lt;code&gt;output&lt;/code&gt;，使结果正确。&lt;/p&gt;
&lt;p&gt;FST 的调整输出的算法直接描述比较复杂，这里提供一份伪代码，配合上面的例子，方便读者理解。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def prepend_output(child, prefix, outputs):
    &quot;&quot;&quot;
    把 prefix加到 child 的所有弧上；
    若 child 在此为终止状态，还要再把 prefix 加到 child 的finalOutput上。
    - child.out_arc_outputs: 该子节点的所有出弧
    - child.is_final:        bool，child 是否为终止节点(final 为True)
    - child.final_output:    节点的finalOutput(前文已经介绍过这个概念)
    &quot;&quot;&quot;
    for i in range(len(child.out_arc_outputs)):
        child.out_arc_outputs[i] = outputs.add(prefix, child.out_arc_outputs[i])
    if child.is_final:
        child.final_output = outputs.add(prefix, child.final_output)


def adjust_output_once(parent, child, residual):
    &quot;&quot;&quot;
    在共享前缀的 parent child 节点之间调整输出
    - parent:     父节点
    - residual:   新插入的term剩余output
    - child:      子节点
    &quot;&quot;&quot;
    # 1) 取父节点最后一条弧的 output 与 residual 的公共output， 对于整数而言，就是取最小值
    parent_last_arc_output = parent.arcs[-1].output
    common = outputs.common(residual, parent_last_arc_output)
    parent.arcs[-1].output = common

    # 2) 旧弧多出来的那部分下沉到 child（影响其所有弧与终止）
    word_suffix = outputs.subtract(parent_last_arc_output, common)
    if word_suffix != outputs.NO_OUTPUT:
        prepend_output(child, word_suffix, outputs)

    # 3) 新键自己的剩余输出也去掉公共部分，带去下一台阶
    new_residual = outputs.subtract(residual, common)

    return new_residual

def adjust_output(parent, child, residual, prefix):
    &quot;&quot;&quot;
    递归地在共享前缀的 parent child 节点之间调整输出
    - parent:     父节点, 初次调用时为根节点
    - residual:   新插入的term剩余output
    - child:      子节点
    - prefix:     共享前缀
    &quot;&quot;&quot;
    for i in range(len(prefix)):
        residual = adjust_output_once(parent, child, residual)
        if residual == outputs.NO_OUTPUT:
            break
        # 继续往下调整
        parent = child
        child = child.arcs[-1].target
    child.arcs[-1].output = residual
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据上面的伪代码，在这个例子中，我们先处理共享前缀的部分。&lt;/p&gt;
&lt;p&gt;调整 &lt;code&gt;node 0&lt;/code&gt; 和 &lt;code&gt;node 1&lt;/code&gt; 之间边的 &lt;code&gt;output&lt;/code&gt;。我们发现 &lt;code&gt;node 0&lt;/code&gt; 的最后一条边的 &lt;code&gt;output&lt;/code&gt; 为 5, 而需要插入的 term &quot;ab&quot; 的 &lt;code&gt;output&lt;/code&gt; 为 2,
因此它们的公共部分 (min) 为 2， 因此我们将 &lt;code&gt;node 0&lt;/code&gt; 和 &lt;code&gt;node 1&lt;/code&gt; 之间边的 &lt;code&gt;output&lt;/code&gt; 调整为 2。&lt;/p&gt;
&lt;p&gt;然后将多出来的部分 3 下沉到 &lt;code&gt;node 1&lt;/code&gt; 上，因为 &lt;code&gt;node 1&lt;/code&gt; 是终止节点，因此需要将 3 加到 &lt;code&gt;node 1&lt;/code&gt; 的 &lt;code&gt;finalOutput&lt;/code&gt; 上。
同时 &lt;code&gt;node 1&lt;/code&gt; 的所有出边的 &lt;code&gt;output&lt;/code&gt; 也需要加上 3。 如下图所示:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20251102211642.png&quot; alt=&quot;step2-2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在处理完共享前缀的部分后，直接将分叉后最后一条边的 &lt;code&gt;output&lt;/code&gt; 设置为新的 &lt;code&gt;residual&lt;/code&gt; 即可。如下图所示:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20251102212336.png&quot; alt=&quot;step2-3&quot; /&gt;&lt;/p&gt;
&lt;p&gt;完成上述四个步骤后，我们成功地将 &lt;code&gt;ab -&amp;gt; 2&lt;/code&gt; 插入到了 FST 中。&lt;/p&gt;
&lt;h4&gt;3. 插入 &lt;code&gt;cap -&amp;gt; 1&lt;/code&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;3.1 寻找前缀&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;插入的 term 是 &lt;code&gt;cap&lt;/code&gt;, 它与上一个插入的 term &lt;code&gt;ab&lt;/code&gt; 共有的前缀为空。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;3.2 处理后缀&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为找到的前缀为空，因此上一个term的后缀部分 &quot;ab&quot; 仍然存在，需要将其进行冻结 (freeze)。 冻结的过程为，将
需要冻结的节点所包含的信息进行哈希，然后检查是否已经存储了相同的节点，如果存在，就复用该节点，否则就将该节点存下来(称之为&lt;code&gt;compiledNode&lt;/code&gt;)。&lt;/p&gt;
&lt;p&gt;举一个例子，假设需要冻结的节点包含以下信息:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;arcs:
&lt;ul&gt;
&lt;li&gt;label: b, target: end, output: 0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;isFinal: True&lt;/li&gt;
&lt;li&gt;finalOutput: 0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些信息哈希后, 得到一个 hash 值, &lt;code&gt;hash1&lt;/code&gt;。 后续构建过程中，如果发现一个节点，它的 hash 值也是 &lt;code&gt;hash1&lt;/code&gt;, 那么就可以复用之前存储的节点.
这样就实现了节点的复用，从而节省了内存空间。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;3.3 插入新的输入&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来，我们需要插入term 的后缀部分 &quot;cap&quot;。因为找不到任何前缀，因此需要从根节点开始，依次插入三条边以及一个终止节点。 如下图所示:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20251102214546.png&quot; alt=&quot;step3-1&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;3.4 调整输出&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个例子中，没有相同的前缀，因此只需将分叉节点（根节点）的最后一条边的 &lt;code&gt;output&lt;/code&gt; 设置为新的 &lt;code&gt;residual&lt;/code&gt; 即可。如下图所示:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20251102215035.png&quot; alt=&quot;step3-2&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;4. 插入 &lt;code&gt;tap -&amp;gt; 1&lt;/code&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;4.1 寻找前缀&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;插入的 term 是 &lt;code&gt;tap&lt;/code&gt;, 它与上一个term &lt;code&gt;cap&lt;/code&gt; 共有的前缀为空。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;4.2 处理后缀&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为共享前缀为空，因此上一个term的后缀部分为 &quot;cap&quot;，需要将其进行冻结 (freeze)。 冻结的过程与上一个例子类似.
这里我们发现，&lt;code&gt;node 5&lt;/code&gt; 的信息与之前已经冻结的 &lt;code&gt;node 2&lt;/code&gt; 一样，因此它们的 hash 值也相同，可以对该节点复用。&lt;/p&gt;
&lt;p&gt;将 &lt;code&gt;node 5&lt;/code&gt; 父节点（&lt;code&gt;node 4&lt;/code&gt;）对应边的指向改为已冻结的节点(&lt;code&gt;compiledNode&lt;/code&gt;)即可。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;4.3 插入新的输入&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们需要插入当前term的后缀部分 &quot;tap&quot;。 与上一个例子类似，需要从根节点开始，依次插入三条边以及一个终止节点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;4.4 调整输出&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;仍与上一个例子一样，只需将分叉节点（根节点）最后一条边的 &lt;code&gt;output&lt;/code&gt; 设置为新的 &lt;code&gt;residual&lt;/code&gt; 即可。&lt;/p&gt;
&lt;p&gt;因为这个例子与上一个例子非常类似，所以直接给出最终结果，如下图所示:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20251102220303.png&quot; alt=&quot;step4&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;5. 冻结剩余节点&lt;/h4&gt;
&lt;p&gt;当所有 term 插入完毕，还需把剩余未冻结的节点自底向上依次冻结。在本例中，依次冻结 node 8 → node 7 → node 6 → root。过程中会不断命中等价节点并复用其地址：&lt;/p&gt;
&lt;p&gt;首先是 &lt;code&gt;node 8&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这个节点什么都没有，仅仅是一个终止节点。与之前的 &lt;code&gt;node 2&lt;/code&gt; 一样，因此可以复用&lt;code&gt;node A&lt;/code&gt;(冻结后的&lt;code&gt;node 2&lt;/code&gt;)。随后
让 &lt;code&gt;node 7&lt;/code&gt; 对应边指向 &lt;code&gt;node A&lt;/code&gt;。如下图所示:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20251102221533.png&quot; alt=&quot;step5-1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接下来是 &lt;code&gt;node 7&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这个节点的出边的 label 为 &lt;code&gt;p&lt;/code&gt;, 指向&lt;code&gt;node A&lt;/code&gt;，&lt;code&gt;output&lt;/code&gt; 为 0。 它与已冻结的 &lt;code&gt;node C&lt;/code&gt; 完全一样，因此可以复用&lt;code&gt;node C&lt;/code&gt;。
让 &lt;code&gt;node 6&lt;/code&gt; 对应边指向&lt;code&gt;node C&lt;/code&gt;。如下图所示:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20251102221711.png&quot; alt=&quot;step5-2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后是 &lt;code&gt;node 6&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这个节点的出边的 label 为 &lt;code&gt;a&lt;/code&gt;, 指向&lt;code&gt;node C&lt;/code&gt;，&lt;code&gt;output&lt;/code&gt; 为 0。 它与已冻结的 &lt;code&gt;node D&lt;/code&gt; 一样，因此可以复用&lt;code&gt;node D&lt;/code&gt;。
让根节点的对应边指向&lt;code&gt;node D&lt;/code&gt;。如下图所示:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20251102221818.png&quot; alt=&quot;step5-3&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后是根节点，因为根节点不会与其他节点重复，因此直接将其存储下来即可。&lt;/p&gt;
&lt;p&gt;最后的FST结构如下图所示:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20251102222526.png&quot; alt=&quot;final&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本文介绍了 FST 的基本结构与查找/构建算法中的具体流程与细节。提供了一个对FST较为宏观的视角，方便读者理解 FST 的整体原理。
但是具体如何用代码将FST实现出来，本文并没有涉及。 在后续文章中，将介绍 FST 的具体实现代码，帮助读者建立起从理论到实践的完整认知。&lt;/p&gt;
</content:encoded><category>Full Text Search</category><category>Inverted Index</category><category>倒排索引</category><category>Lucene</category><author>tang-hi</author></item><item><title>揭秘高维向量：巧用“反直觉”特性，让向量搜索飞起来！</title><link>https://tangdh.life/posts/vector-search/high-dimension/</link><guid isPermaLink="true">https://tangdh.life/posts/vector-search/high-dimension/</guid><description>随着向量搜索的广泛使用，我们需要计算的向量维数也越来越高，由此带来的计算/存储压力也越来越大。那么在高维向量中，有什么特性是可以被我们所利用，从而减少距离计算时间/存储成本? 本文将试图回答这一问题。</description><pubDate>Thu, 22 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;随着向量搜索的广泛应用，向量维度已从几十增长到上千，带来了显著的计算和存储压力。本文将探讨如何利用高维向量的特性来应对这些挑战，主要包含以下内容：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;高维向量的独有特性&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AdSampling&lt;/strong&gt; 算法的原理与实现&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RaBitQ&lt;/strong&gt; 算法的原理与实现&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;高维向量的独有特性&lt;/h2&gt;
&lt;p&gt;为了提升文章的可读性，本文&lt;strong&gt;非必要不进行数学推导&lt;/strong&gt;，而是给读者一个感性的认识，并通过代码实验验证结论。&lt;/p&gt;
&lt;h3&gt;1. 在高维空间中，任意两个向量几乎都是正交的&lt;/h3&gt;
&lt;p&gt;如果提问我们在二维空间中，随机生成两个向量，它们之间的夹角是多少？我们可能会理所当然的认为，在[$0, 180$]之间任意的角度都有可能。
但是当这个问题来到高维空间时，答案就不是这样了。我们可以通过下面的代码来验证这个结论。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gist.github.com/tang-hi/3ef51ddab75961a3a80304aeec348bd9&quot;&gt;Github Gist&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;从运行结果可以看到，&lt;strong&gt;随着维度的增加，任意两个随机向量的夹角趋近于 $90^\circ$，即几乎正交&lt;/strong&gt;。这说明在高维空间中，任意两个向量几乎都是垂直的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/high-angles.png&quot; alt=&quot;高维向量的夹角分布&quot; /&gt;&lt;/p&gt;
&lt;p&gt;详细证明可参考&lt;a href=&quot;https://www.spaces.ac.cn/archives/7076&quot;&gt;这里&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;2. 存储 $N$ 个向量，只需要 $\mathcal{O}(\log N)$ 维空间&lt;/h3&gt;
&lt;p&gt;这个特性实际上是 &lt;strong&gt;JL引理&lt;/strong&gt;（Johnson-Lindenstrauss Lemma）的一个应用。&lt;strong&gt;JL引理&lt;/strong&gt; 是一个非常重要的结论，它表明对高维向量进行降维时，在保持相对距离基本不变的情况下，降维后的维数只与&lt;strong&gt;向量的个数&lt;/strong&gt;有关，而与原始维数无关。&lt;/p&gt;
&lt;p&gt;也就是说，&lt;strong&gt;我们只需要 $\mathcal{O}(\log N)$ 维的空间就可以存储 $N$ 个向量&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这个结论也回答了一个常见问题：&lt;strong&gt;向量的维数是否会随着业务/时间的推移而无限增加？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;答案是：&lt;strong&gt;不会&lt;/strong&gt;。因为我们需要处理的向量个数是有限的，所以只需要 $\mathcal{O}(\log N)$ 维即可保证较高的召回率。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;之前了解到有团队在调研如何使用软硬件结合的方式处理4096维的向量，但也许他们永远不会遇到4096维的向量。:)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;有兴趣的读者可以参考&lt;a href=&quot;https://www.spaces.ac.cn/archives/8679/comment-page-1&quot;&gt;这篇文章&lt;/a&gt;，里面详细介绍了 &lt;strong&gt;JL引理&lt;/strong&gt; 的证明过程。事实上，&lt;strong&gt;JL引理&lt;/strong&gt; 还有许多有用的推论，后文也会用到。&lt;/p&gt;
&lt;p&gt;接下来介绍两篇论文，分别是 &lt;a href=&quot;https://arxiv.org/abs/2303.09855&quot;&gt;&lt;code&gt;AdSampling&lt;/code&gt;&lt;/a&gt; 和 &lt;a href=&quot;https://arxiv.org/abs/2405.12497&quot;&gt;&lt;code&gt;RaBitQ&lt;/code&gt;&lt;/a&gt; 算法，这两者都利用了高维向量的特性来减少计算和存储压力。&lt;/p&gt;
&lt;h2&gt;AdSampling 算法的原理与实现&lt;/h2&gt;
&lt;p&gt;在介绍 &lt;code&gt;AdSampling&lt;/code&gt; 之前，我们先考虑一个场景。假设我们需要判断两个1024维的向量，它们的 &lt;strong&gt;&lt;code&gt;L2&lt;/code&gt;距离&lt;/strong&gt; 是否小于某个 &lt;strong&gt;&lt;code&gt;阈值&lt;/code&gt;&lt;/strong&gt; ？通常的做法是计算&lt;strong&gt;每个维度的差值平方和&lt;/strong&gt;。这意味着需要进行1024次乘法和加法运算。那么，有没有办法在&lt;strong&gt;未计算完所有维度&lt;/strong&gt;的情况下，就判断出这两个向量的&lt;code&gt;L2&lt;/code&gt;距离小于该&lt;code&gt;阈值&lt;/code&gt;呢？答案是肯定的。&lt;/p&gt;
&lt;p&gt;通过 &lt;strong&gt;&lt;code&gt;JL引理&lt;/code&gt;&lt;/strong&gt;（Johnson-Lindenstrauss Lemma），我们有如下公式：&lt;/p&gt;
&lt;p&gt;$$
P\left( \left| \sqrt{\frac{D}{d}} | P x | - | x | \right| \leq \epsilon | x | \right) \geq 1 - 2 e^{-c_0 d \epsilon^2}
$$&lt;/p&gt;
&lt;p&gt;这个公式的含义是：如果我们使用一个随机的 $d \times D$ 矩阵 $P$ 对向量 $x$ 进行投影（其中 $d \le D$），那么投影后向量 $Px$ 的 $d$ 维 &lt;code&gt;L2&lt;/code&gt;距离与原始向量 $x$ 的 &lt;code&gt;L2&lt;/code&gt;距离之间的&lt;strong&gt;误差可以被控制在一定范围内&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这能带来什么好处呢？
首先，我们简述向量检索的典型步骤：给定一个&lt;strong&gt;候选集&lt;/strong&gt; $S$，一个&lt;strong&gt;查询向量&lt;/strong&gt; $Q$，以及一个&lt;strong&gt;结果集&lt;/strong&gt; $R$。我们需要遍历 $S$ 中的每个向量 $s$，计算其与 $Q$ 的距离 $dis = |Q - s|$。由于结果集的大小通常限定为 $K$，我们需要判断 $dis$ 是否小于某个&lt;code&gt;阈值&lt;/code&gt;（即当前结果集 $R$ 中最大的距离）。如果小于，则将 $s$ 加入结果集 $R$。&lt;/p&gt;
&lt;p&gt;基于上述描述和&lt;code&gt;JL引理&lt;/code&gt;，我们可以设计一个更高效的向量检索算法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;随机生成一个 $D \times D$ 的&lt;strong&gt;投影矩阵&lt;/strong&gt; $P$。&lt;/li&gt;
&lt;li&gt;对候选集 $S$ 中的每个向量 $s$ 进行投影，得到 $s&apos; = Ps$。&lt;/li&gt;
&lt;li&gt;对查询向量 $Q$ 进行投影，得到 $Q&apos; = PQ$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逐步计算距离&lt;/strong&gt; $dis$：我们不一次性计算完整的距离，而是采用&lt;strong&gt;分批（batch）&lt;strong&gt;的方式。例如，设 &lt;code&gt;batch size&lt;/code&gt; 为 $b$，每次我们仅计算 $dis = |Q&apos; - s&apos;|$ 的前 $b$ 个维度的部分距离，即 $dis_{partial} = \sum_{i=1}^b (Q&apos;_i - s&apos;_i)^2$。如果这个&lt;/strong&gt;部分距离已经大于设定的阈值&lt;/strong&gt;，我们就可以&lt;strong&gt;提前终止&lt;/strong&gt;对该向量的计算，直接跳过。否则，继续计算下一批维度的距离，直至计算完所有维度或提前终止。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过这种&lt;strong&gt;提前终止&lt;/strong&gt;的策略，我们可以显著减少不必要的维度计算，从而提升向量检索的效率。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AdSampling&lt;/code&gt; 是一个相对容易实现的算法，对现有代码改动较小（其难点主要在于数学推导，而原论文作者已完成这部分工作）。然而，需要注意的一点是，由于在计算距离时，每个 &lt;code&gt;batch&lt;/code&gt; 都需要进行&lt;code&gt;阈值&lt;/code&gt;判断，其&lt;strong&gt;单次距离计算时间可能会慢于直接使用 &lt;code&gt;SIMD&lt;/code&gt; 指令进行全维度计算的方式&lt;/strong&gt;。因此，若想在如图搜索算法 &lt;code&gt;HNSW&lt;/code&gt; 中应用 &lt;code&gt;AdSampling&lt;/code&gt; 并获得显著性能提升，更推荐直接实现论文中提出的 &lt;code&gt;AKNN++&lt;/code&gt; 方案，通过&lt;strong&gt;大量被跳过的维度计算来弥补距离计算本身的时间开销&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;详细实现可参考&lt;a href=&quot;https://github.com/gaoj0017/ADSampling&quot;&gt;原作者的 GitHub 仓库&lt;/a&gt;或我的&lt;a href=&quot;https://github.com/tang-hi/bnsw&quot;&gt;复现代码&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;RaBitQ 算法的原理与实现&lt;/h2&gt;
&lt;p&gt;首先介绍 &lt;strong&gt;RaBitQ&lt;/strong&gt;，这是一种量化算法，可以将 $D$ 维向量压缩成 $D$ 个比特（对于 &lt;code&gt;float&lt;/code&gt; 而言，相当于 $32\times$ 的压缩比例），同时保证 &lt;code&gt;L2&lt;/code&gt; 距离计算的精度误差，从而保证较高的召回率。&lt;/p&gt;
&lt;p&gt;由于 &lt;code&gt;L2&lt;/code&gt; 距离是无界的（取值范围为 $[0, +\infty)$），而 &lt;strong&gt;RaBitQ&lt;/strong&gt; 需要保证距离计算的精度误差，因此在构建索引时，首先需要对原始向量进行归一化，即&lt;/p&gt;
&lt;p&gt;$$ o := \frac{o_r - c}{|o_r - c|} $$&lt;/p&gt;
&lt;p&gt;其中，$$o_r$$ 是原始向量，$$o$$ 是归一化后的向量，$$c$$ 是中心向量。&lt;/p&gt;
&lt;h3&gt;1. RaBitQ 索引构建&lt;/h3&gt;
&lt;p&gt;所有向量量化都可分为两步：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;确定 &lt;strong&gt;codebook&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;根据 &lt;strong&gt;codebook&lt;/strong&gt; 对向量进行量化&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1.1 确定 &lt;code&gt;codebook&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;由于所有向量都做了归一化处理，数据点大致均匀分布在单位超球面上。因此，直观上 &lt;code&gt;codebook&lt;/code&gt; 应具备以下特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;均匀分布在单位超球面上&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每个维度只有两个可能的取值（便于压缩为 1 个比特）&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一种自然的 &lt;code&gt;codebook&lt;/code&gt; 构造方式是：每个维度只取两个值 $+\frac{1}{\sqrt{D}}$ 和 $-\frac{1}{\sqrt{D}}$，所有组合构成 $2^D$ 个单位向量。即：&lt;/p&gt;
&lt;p&gt;$$
C := \left{ \left( \pm \frac{1}{\sqrt{D}}, \pm \frac{1}{\sqrt{D}}, \ldots, \pm \frac{1}{\sqrt{D}} \right) \right}
$$&lt;/p&gt;
&lt;p&gt;这种构造方式保证了 &lt;code&gt;codebook&lt;/code&gt; 均匀分布在单位超球面上，并且每个维度只有两种可能的取值。&lt;/p&gt;
&lt;p&gt;但这种方式仍有缺陷：对某些单位向量的量化误差非常小（如 $(\frac{1}{\sqrt{D}}, ..., \frac{1}{\sqrt{D}})$），但对另一些（如 $(1, 0, ..., 0)$）则误差较大。&lt;/p&gt;
&lt;p&gt;为解决此问题，可以给 &lt;code&gt;codebook&lt;/code&gt; 加入“随机旋转”：先随机生成一个正交矩阵 $P$，用 $P$ 对 &lt;code&gt;codebook&lt;/code&gt; 里的每个向量做一次旋转，得到最终的 &lt;code&gt;codebook&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;$$
C_{\text{rand}} := { P x \mid x \in C }
$$&lt;/p&gt;
&lt;p&gt;这样，所有 &lt;code&gt;codebook&lt;/code&gt; 向量依然是单位向量，且经过随机旋转后，原本偏向某些方向的“偏好”被消除了，所有方向都一视同仁。&lt;/p&gt;
&lt;h4&gt;1.2 根据 &lt;code&gt;codebook&lt;/code&gt; 对向量进行量化&lt;/h4&gt;
&lt;p&gt;确定 &lt;code&gt;codebook&lt;/code&gt; 后，对向量量化的目标是：&lt;strong&gt;找到与待量化向量距离最近的 &lt;code&gt;codebook&lt;/code&gt; 向量，并用它替代原始向量&lt;/strong&gt;。即：&lt;/p&gt;
&lt;p&gt;$$
\mathop{\arg\min}_{x \in C} | o - P x |^2
$$&lt;/p&gt;
&lt;p&gt;推导后可得：&lt;/p&gt;
&lt;p&gt;$$
= \mathop{\arg\max}_{x \in C} \langle o, P x \rangle
$$&lt;/p&gt;
&lt;p&gt;利用正交矩阵的性质，有：&lt;/p&gt;
&lt;p&gt;$$
\langle o, P x \rangle = \langle P^{-1} o, x \rangle
$$&lt;/p&gt;
&lt;p&gt;其中 $P^{-1}$ 是正交矩阵 $P$ 的逆（等于其转置）。$P^{-1}$ 和 $o$ 都已知，可以直接计算。要使 $\langle o, P x \rangle$ 最大，只需找到与 $P^{-1} o$ 每一维正负号相同的 &lt;code&gt;codebook&lt;/code&gt; 向量即可。&lt;/p&gt;
&lt;p&gt;因此，对向量 $o$ 进行量化的步骤为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;保存构建 &lt;code&gt;codebook&lt;/code&gt; 时所用的 $P$&lt;/li&gt;
&lt;li&gt;计算 $P^{-1} o$&lt;/li&gt;
&lt;li&gt;取 $P^{-1} o$ 每一维的正负号
&lt;ul&gt;
&lt;li&gt;正：该维取 $+\frac{1}{\sqrt{D}}$&lt;/li&gt;
&lt;li&gt;负：该维取 $-\frac{1}{\sqrt{D}}$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;用 $P$ 对量化后的向量反变换，得到最终量化结果&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样，我们实际上只需要存储 $P^{-1} o$ 每一维的正负号(因为每一维的绝对值都是固定的 $\frac{1}{\sqrt{D}}$，正负号决定了该维度是取 $+\frac{1}{\sqrt{D}}$ 还是 $-\frac{1}{\sqrt{D}}$)，即 $D$ 个比特，就可以表示原始的 $D$ 维浮点数向量（通常每个维度需要32个比特），实现了惊人的压缩效果.&lt;/p&gt;
&lt;h3&gt;2. RaBitQ 索引查询&lt;/h3&gt;
&lt;p&gt;量化后，可以对向量进行查询。我们需要考虑原始向量和查询向量的 &lt;code&gt;L2&lt;/code&gt; 距离：&lt;/p&gt;
&lt;p&gt;$$
| o_r - q_r |^2 = | (o_r - c) - (q_r - c) |^2
$$&lt;/p&gt;
&lt;p&gt;$$
= | o_r - c |^2 + | q_r - c |^2 - 2 \cdot | o_r - c | \cdot | q_r - c | \cdot \langle q, o \rangle
$$&lt;/p&gt;
&lt;p&gt;其中 $o_r$ 和 $q_r$ 分别是原始和查询向量，$c$ 是中心向量。$| o_r - c |$ 可在索引构建时计算，$| q_r - c |$ 在查询时仅需计算一次。因此，&lt;strong&gt;只需计算 $\langle q, o \rangle$&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;现在有了量化后的向量 $\bar{o}$ 和归一化向量 $o$，如何用 $\langle \bar{o}, o \rangle$ 表示 $\langle q, o \rangle$？&lt;/p&gt;
&lt;p&gt;直接给出公式(推导见附录)：&lt;/p&gt;
&lt;p&gt;$$
\langle \bar{o}, q \rangle = \langle \bar{o}, o \rangle \cdot \langle o, q \rangle + \langle \bar{o}, e_1 \rangle \cdot \sqrt{1 - \langle o, q \rangle^2}
$$&lt;/p&gt;
&lt;p&gt;其中 $e_1$ 是垂直于 $o$ 的单位向量。&lt;/p&gt;
&lt;p&gt;还记得前文提到的高维特性吗？&lt;strong&gt;在高维空间中，任意两个向量几乎都是正交的&lt;/strong&gt;。$\bar{o}$ 为经过随机旋转的向量，因此可以近似认为 $\bar{o}$ 和 $e_1$ 是两个随机向量，即 $\langle \bar{o}, e_1 \rangle$ 近似为 $0$。&lt;/p&gt;
&lt;p&gt;因此有：&lt;/p&gt;
&lt;p&gt;$$
\langle \bar{o}, q \rangle \approx \langle \bar{o}, o \rangle \cdot \langle o, q \rangle
$$&lt;/p&gt;
&lt;p&gt;这样，计算误差也有了较为确定的界限，即 $\langle \bar{o}, e_1 \rangle \cdot \sqrt{1 - \langle o, q \rangle^2}$。&lt;/p&gt;
&lt;p&gt;现在要计算 $\langle q, o \rangle$，只需计算 $\langle \bar{o}, o \rangle$ 和 $\langle o, q \rangle$。其中 $\langle \bar{o}, o \rangle$ 可在索引构建时计算好。如何高效计算 $\langle \bar{o}, q \rangle$？&lt;/p&gt;
&lt;p&gt;为方便计算，可将 $\bar{o}$ 转为 0/1 的二进制格式，即乘以 $P^{-1}$（内积乘以正交矩阵不改变内积值）：&lt;/p&gt;
&lt;p&gt;$$
\langle \bar{o}, q \rangle = \langle P \bar{x}, q \rangle = \langle \bar{x}, P^{-1} q \rangle
$$&lt;/p&gt;
&lt;p&gt;其中 $\bar{x}$ 是 $\pm \frac{1}{\sqrt{D}}$。只需对输入的查询向量做一次正交变换，得到 $P^{-1} q$，然后根据量化向量的正负号取对应符号，最后结果除以 $\sqrt{D}$ 即可。&lt;/p&gt;
&lt;p&gt;当然，为提升效率，论文中还有许多工程优化，详细代码可参考&lt;a href=&quot;https://github.com/gaoj0017/RaBitQ&quot;&gt;原作者 Github&lt;/a&gt; 或我的&lt;a href=&quot;https://github.com/tang-hi/rabitQ&quot;&gt;复现&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;3. RaBitQ 算法总结&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;RaBitQ&lt;/strong&gt; 算法的核心在于利用正交矩阵的各种特性完成高维向量的压缩与查询，同时利用高维向量“几乎正交”的特性给误差确定了上下界。这两点结合，使得 &lt;strong&gt;RaBitQ&lt;/strong&gt; 在高达 $32\times$ 压缩率下，仍能保持 95% 以上的召回率。&lt;/p&gt;
&lt;p&gt;本文未能覆盖 RaBitQ 的所有细节，更多内容建议读者参考&lt;a href=&quot;https://arxiv.org/abs/2405.12497&quot;&gt;原文&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;这两篇论文都是同一个&lt;a href=&quot;https://personal.ntu.edu.sg/c.long/pages/team.html&quot;&gt;团队&lt;/a&gt;所做出的工作。相较于其他论文从工程角度上出发对向量检索进行优化。
他们从数学的角度上出发，利用高维向量的特性，优化距离计算，这一最基础也是最耗时的部分，并取得了不错的效果。
这无疑给向量检索带来新的思路。尽管论文仅仅只对&lt;code&gt;L2&lt;/code&gt;距离进行了研究，但是对于其他的距离度量，应该也是可以获得差不多的效果。&lt;/p&gt;
&lt;h3&gt;附录&lt;/h3&gt;
&lt;p&gt;推导
$$
\langle \bar{o}, q \rangle = \langle \bar{o}, o \rangle \cdot \langle o, q \rangle + \langle \bar{o}, e_1 \rangle \cdot \sqrt{1 - \langle o, q \rangle^2}
$$&lt;/p&gt;
&lt;p&gt;我们将 $q$ 拆为两个单位向量 $o$ 和 $e_1$ （$e_1$ 是垂直于 $o$ 的单位向量）的组合,即
$$
q = o cos\theta + e_1 sin\theta
$$&lt;/p&gt;
&lt;p&gt;其中的 $\theta$ 表示 $q$ 和 $o$ 的夹角, 对于单位向量而言，它们之间的点积等于 $cos\theta$
所以我们可以把上述公式写为
$$
q = \langle q, o \rangle \cdot o + \sqrt{1 - \langle q, o \rangle^2} e_1
$$&lt;/p&gt;
&lt;p&gt;我们把上述公式代入到 $\langle \bar{o}, q \rangle$ 中
$$
\langle \bar{o}, q \rangle = \langle \bar{o}, \langle q, o \rangle \cdot o + \sqrt{1 - \langle q, o \rangle^2} e_1 \rangle
$$&lt;/p&gt;
&lt;p&gt;根据点积的结合律，我们可以得到
$$
\langle \bar{o}, q \rangle = \langle \bar{o}, \langle q, o \rangle \cdot o \rangle + \langle \bar{o}, \sqrt{1 - \langle q, o \rangle^2} e_1 \rangle
$$&lt;/p&gt;
&lt;p&gt;我们再将标量提取出来, 即可完成推导
$$
\langle \bar{o}, q \rangle = \langle \bar{o}, o \rangle \cdot \langle o, q \rangle + \langle \bar{o}, e_1 \rangle \cdot \sqrt{1 - \langle o, q \rangle^2}
$$&lt;/p&gt;
</content:encoded><category>vector search</category><category>database</category><category>interesting</category><author>tang-hi</author></item><item><title>为什么ncdu这么快？</title><link>https://tangdh.life/posts/interesting/ncdu/</link><guid isPermaLink="true">https://tangdh.life/posts/interesting/ncdu/</guid><description>上周被同事发现我就是导致组里磁盘满的罪魁祸首后，由此引发的一系列有趣调试历程</description><pubDate>Mon, 10 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我们组的开发模式是多人共用一台开发机，因此开发机的磁盘空间成了常见问题，三天两头就会出现&lt;code&gt;No space left on device&lt;/code&gt;的错误。
上周磁盘再次爆满，同事使用&lt;code&gt;ncdu&lt;/code&gt;查看磁盘占用情况，发现我就是导致磁盘满的罪魁祸首。他分享的截图类似于下面这样：
&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20250310225708.png&quot; alt=&quot;ncdu&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我也登上机器，在根目录执行了&lt;code&gt;ncdu&lt;/code&gt;。令我惊讶的是，&lt;code&gt;ncdu&lt;/code&gt;的速度相当快，几乎10秒钟就扫描完了1.3T的磁盘空间。
而平时如果在根目录执行&lt;code&gt;du -sh /&lt;/code&gt;，则需要将近一分钟的时间。这不禁让我好奇：&lt;code&gt;ncdu&lt;/code&gt;为什么这么快？&lt;/p&gt;
&lt;h2&gt;探索过程&lt;/h2&gt;
&lt;p&gt;最初我猜想&lt;code&gt;ncdu&lt;/code&gt;是否使用了多线程进行扫描，于是我查看了&lt;code&gt;ncdu&lt;/code&gt;的&lt;code&gt;Release Note&lt;/code&gt;。发现&lt;code&gt;ncdu&lt;/code&gt;直到2.0版本用Zig重写
后才支持多线程扫描。而我使用的是1.20版本，所以并非多线程扫描。随后我又阅读了&lt;code&gt;ncdu&lt;/code&gt;的源码，想看看它是否用了什么神奇的
算法。它扫描文件的核心代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Walks through the directory that we&apos;re currently chdir&apos;ed to. *dir contains
 * the filenames as returned by dir_read(), and will be freed automatically by
 * this function. */
static int dir_walk(char *dir) {
  int fail = 0;
  char *cur;

  fail = 0;
  for(cur=dir; !fail&amp;amp;&amp;amp;cur&amp;amp;&amp;amp;*cur; cur+=strlen(cur)+1) {
    dir_curpath_enter(cur);
    memset(buf_dir, 0, offsetof(struct dir, name));
    memset(buf_ext, 0, sizeof(struct dir_ext));
    fail = dir_scan_item(cur);
    dir_curpath_leave();
  }

  free(dir);
  return fail;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码看起来也平平无奇，就是遍历目录下的文件，然后调用&lt;code&gt;dir_scan_item&lt;/code&gt;扫描文件，无法解释&lt;code&gt;ncdu&lt;/code&gt;为什么这么快。&lt;/p&gt;
&lt;p&gt;最后我在Google上搜索了&quot;Why ncdu is fast&quot;，发现有人提出了类似问题：&lt;a href=&quot;https://superuser.com/questions/1531603/why-is-du-sh-faster-after-running-ncdu&quot;&gt;Why is du -sh faster after running ncdu?&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;看到这个问题时，答案已经呼之欲出了——这是因为&lt;code&gt;Page Cache&lt;/code&gt;的作用。&lt;strong&gt;&lt;code&gt;ncdu&lt;/code&gt;本身并不特别快&lt;/strong&gt;，但当我同事预先使用&lt;code&gt;ncdu&lt;/code&gt;扫描了磁盘后，&lt;code&gt;Page Cache&lt;/code&gt;中已经缓存了部分文件信息，所以当我
第二次执行&lt;code&gt;ncdu&lt;/code&gt;时，只需10秒钟就能完成扫描。&lt;/p&gt;
&lt;p&gt;为了验证我的猜想，我使用&lt;code&gt;echo 3 &amp;gt; /proc/sys/vm/drop_caches&lt;/code&gt;清空了&lt;code&gt;Page Cache&lt;/code&gt;，然后再次执行&lt;code&gt;ncdu&lt;/code&gt;。果然，&lt;code&gt;ncdu&lt;/code&gt;的速度和&lt;code&gt;du -sh /&lt;/code&gt;相近，都需要将近一分钟的时间。
在此基础上，我进一步分别测试了：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;echo 1 &amp;gt; /proc/sys/vm/drop_caches&lt;/code&gt;（清除页面缓存）&lt;/p&gt;
&lt;p&gt;&lt;code&gt;echo 2 &amp;gt; /proc/sys/vm/drop_caches&lt;/code&gt;（清除目录项和inode缓存）&lt;/p&gt;
&lt;p&gt;它们的执行时间如下所示：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;echo 3 &amp;gt; /proc/sys/vm/drop_caches (60秒)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;echo 1 &amp;gt; /proc/sys/vm/drop_caches (60秒)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;echo 2 &amp;gt; /proc/sys/vm/drop_caches (16秒)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这是因为&lt;code&gt;ncdu&lt;/code&gt;扫描文件系统时，需要完成两项工作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;读取文件夹的内容，获取该文件夹下的文件和inode信息（此时使用page cache，因为文件夹本质上是一种特殊文件）&lt;/li&gt;
&lt;li&gt;根据文件名，获取inode信息以及文件大小等信息（此时使用dentries和inodes缓存，获取文件大小不需要读取文件内容）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以当&lt;code&gt;Page Cache&lt;/code&gt;被清空后，&lt;code&gt;ncdu&lt;/code&gt;每次扫描文件都需要重新发起磁盘I/O操作。
而仅清空&lt;code&gt;dentries and inodes cache&lt;/code&gt;后，&lt;code&gt;ncdu&lt;/code&gt;只需要重新获取文件的inode信息，读取量相较于完全清空&lt;code&gt;Page Cache&lt;/code&gt;时少了很多，所以速度会快很多。&lt;/p&gt;
&lt;p&gt;为了进一步验证我的猜想，我使用&lt;code&gt;perf&lt;/code&gt;工具对&lt;code&gt;du&lt;/code&gt;进行了性能分析：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo perf stat -e major-faults -e minor-faults sudo du -sh /
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;出乎意料的是，无论&lt;code&gt;Page Cache&lt;/code&gt;是否被清空，&lt;code&gt;du&lt;/code&gt;的&lt;code&gt;major-faults&lt;/code&gt;都是0，而&lt;code&gt;minor-faults&lt;/code&gt;的数量也大体一致。
这似乎表明并没有发生磁盘I/O操作。但是执行速度确实有很大差异，而且CPU的利用率也证实了这一点：缓存被清空后的&lt;code&gt;du&lt;/code&gt;的CPU利用率仅为
20%，而有缓存时的&lt;code&gt;du&lt;/code&gt;的CPU利用率几乎达到100%。这表明当缓存被清空后，CPU的时间主要花在了等待磁盘I/O上。&lt;/p&gt;
&lt;p&gt;这个问题困扰了我很久，直到我回顾之前编写&lt;code&gt;xv6&lt;/code&gt;的经验，才意识到&lt;code&gt;page faults&lt;/code&gt;仅仅指的是访问内存时发生的缺页中断次数，只有当使用&lt;code&gt;mmap&lt;/code&gt;时，
&lt;code&gt;Page Cache Miss&lt;/code&gt;和&lt;code&gt;Page faults&lt;/code&gt;才有直接关系。这意味着我花了很长时间，却在测量一个与问题无关的指标。:(&lt;/p&gt;
&lt;h3&gt;衡量Page Cache的命中率&lt;/h3&gt;
&lt;p&gt;那么如何正确地衡量&lt;code&gt;Page Cache&lt;/code&gt;的命中率呢？perf工具似乎并没有提供相关的观测手段。此时，我想这是一个绝佳的机会来学习&lt;code&gt;eBPF&lt;/code&gt;——通过hook相关的系统调用，统计&lt;code&gt;Page Cache&lt;/code&gt;的命中率。在DeepSeek的帮助下，我很快就得到了一个&lt;code&gt;eBPF&lt;/code&gt;程序（实现原理类似于&lt;code&gt;cachestat&lt;/code&gt;工具）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3

from bcc import BPF
from time import sleep, strftime
import signal

# signal handler
def signal_ignore(signal, frame):
    print()

# define BPF program
bpf_text = &quot;&quot;&quot;
#include &amp;lt;uapi/linux/ptrace.h&amp;gt;

struct key_t {
    u32 nf;
};

enum {
    NF_APCL,
    NF_MPA,
    NF_MBD,
    NF_APD,
};

BPF_HASH(counts, struct key_t);

static int __do_count(void *ctx, u32 nf) {
    struct key_t key = {};
    key.nf = nf;
    counts.atomic_increment(key);
    return 0;
}

int do_count_apcl(struct pt_regs *ctx) {
    return __do_count(ctx, NF_APCL);
}

int do_count_mpa(struct pt_regs *ctx) {
    return __do_count(ctx, NF_MPA);
}

int do_count_mbd(struct pt_regs *ctx) {
    return __do_count(ctx, NF_MBD);
}

int do_count_apd(struct pt_regs *ctx) {
    return __do_count(ctx, NF_APD);
}

int do_count_apd_tp(void *ctx) {
    return __do_count(ctx, NF_APD);
}
&quot;&quot;&quot;

# load BPF program
b = BPF(text=bpf_text)

if BPF.get_kprobe_functions(b&apos;filemap_add_folio&apos;):
    b.attach_kprobe(event=&quot;filemap_add_folio&quot;, fn_name=&quot;do_count_apcl&quot;)
else:
    b.attach_kprobe(event=&quot;add_to_page_cache_lru&quot;, fn_name=&quot;do_count_apcl&quot;)

if BPF.get_kprobe_functions(b&apos;folio_mark_accessed&apos;):
    b.attach_kprobe(event=&quot;folio_mark_accessed&quot;, fn_name=&quot;do_count_mpa&quot;)
else:
    b.attach_kprobe(event=&quot;mark_page_accessed&quot;, fn_name=&quot;do_count_mpa&quot;)

# Function account_page_dirtied() is changed to folio_account_dirtied() in 5.15.
# Both folio_account_dirtied() and account_page_dirtied() are
# static functions and they may be gone during compilation and this may
# introduce some inaccuracy, use tracepoint writeback_dirty_{page,folio},
# instead when attaching kprobe fails, and report the running
# error in time.
if BPF.get_kprobe_functions(b&apos;folio_account_dirtied&apos;):
    b.attach_kprobe(event=&quot;folio_account_dirtied&quot;, fn_name=&quot;do_count_apd&quot;)
elif BPF.get_kprobe_functions(b&apos;account_page_dirtied&apos;):
    b.attach_kprobe(event=&quot;account_page_dirtied&quot;, fn_name=&quot;do_count_apd&quot;)
elif BPF.tracepoint_exists(&quot;writeback&quot;, &quot;writeback_dirty_folio&quot;):
    b.attach_tracepoint(tp=&quot;writeback:writeback_dirty_folio&quot;, fn_name=&quot;do_count_apd_tp&quot;)
elif BPF.tracepoint_exists(&quot;writeback&quot;, &quot;writeback_dirty_page&quot;):
    b.attach_tracepoint(tp=&quot;writeback:writeback_dirty_page&quot;, fn_name=&quot;do_count_apd_tp&quot;)
else:
    raise Exception(&quot;Failed to attach kprobe %s or %s or any tracepoint&quot; %
                    (&quot;folio_account_dirtied&quot;, &quot;account_page_dirtied&quot;))
b.attach_kprobe(event=&quot;mark_buffer_dirty&quot;, fn_name=&quot;do_count_mbd&quot;)

# check whether hash table batch ops is supported
htab_batch_ops = True if BPF.kernel_struct_has_field(b&apos;bpf_map_ops&apos;,
                                                    b&apos;map_lookup_and_delete_batch&apos;) == 1 else False

# header
print(&quot;%-8s %8s %8s %8s %8s&quot; % (&quot;TIME&quot;, &quot;HITS&quot;, &quot;MISSES&quot;, &quot;DIRTIES&quot;, &quot;BUFFDIRTIES&quot;))

exiting = 0
while 1:
    try:
        sleep(1)
    except KeyboardInterrupt:
        exiting = 1
        signal.signal(signal.SIGINT, signal_ignore)

    counts = b[&quot;counts&quot;]
    apcl = 0
    mpa = 0
    mbd = 0
    apd = 0
    for k, v in counts.items():
        if k.nf == 0:  # NF_APCL
            apcl = max(0, v.value)
        if k.nf == 1:  # NF_MPA
            mpa = max(0, v.value)
        if k.nf == 2:  # NF_MBD
            mbd = max(0, v.value)
        if k.nf == 3:  # NF_APD
            apd = max(0, v.value)

    misses = apcl - apd
    total = mpa - mbd
    hits = total - misses

    if misses &amp;lt; 0:
        misses = 0
    if total &amp;lt; 0:
        total = 0

    if hits &amp;lt; 0:
        misses = total
        hits = 0

    if total &amp;gt; 0:
        ratio = float(hits) / total
    else:
        ratio = 0

    print(&quot;%-8s %8d %8d %8d %8d&quot; % (strftime(&quot;%H:%M:%S&quot;), hits, misses, apd, mbd))

    counts.clear()

    if exiting:
        print(&quot;Detaching...&quot;)
        exit()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这个程序，我清楚地观察到：当冷启动&lt;code&gt;ncdu&lt;/code&gt;时，会有大量的Cache Miss；而当&lt;code&gt;Page Cache&lt;/code&gt;被预热后，几乎所有的访问都是Hit。这也完美解释了为什么&lt;code&gt;ncdu&lt;/code&gt;第二次扫描会如此快速。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;正如jyy在南大操作系统课上所说，计算机没有魔法，它本质上就是一个状态机，一切都可以找到一个解释。&lt;/p&gt;
&lt;p&gt;Don&apos;t panic, just Keep calm and Debug on.&lt;/p&gt;
</content:encoded><category>Interesting</category><category>Work</category><category>Thoughts</category><author>tang-hi</author></item><item><title>我的第一款Chrome插件以及AI时代的思考</title><link>https://tangdh.life/posts/interesting/first-plugin/</link><guid isPermaLink="true">https://tangdh.life/posts/interesting/first-plugin/</guid><description>我开发了我的第一款Chrome插件 smartTab，以及我在开发过程中的一些思考</description><pubDate>Sat, 08 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在这几周的业余时间里，我开发了我人生中的第一个Chrome插件 —— &lt;a href=&quot;https://chromewebstore.google.com/detail/smarttab/ffddpdidlmbeleejbllbimfhlmahkkln?pli=1&quot;&gt;&lt;strong&gt;smartTab&lt;/strong&gt;&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;这个插件的功能其实很简单：&lt;strong&gt;对你的标签页，根据大模型的返回结果进行智能分组&lt;/strong&gt;。详细功能可以参考这个&lt;a href=&quot;https://github.com/tang-hi/smarTab?tab=readme-ov-file&quot;&gt;GitHub 仓库&lt;/a&gt;。首先，欢迎大家下载试用！&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=&quot;center&quot;&amp;gt;
&amp;lt;a href=&quot;https://chromewebstore.google.com/detail/smarttab/ffddpdidlmbeleejbllbimfhlmahkkln&quot;&amp;gt;
&amp;lt;img style=&quot;height:100px&quot; src=&quot;https://user-images.githubusercontent.com/53124886/111952712-34f12300-8aee-11eb-9fdd-ad579a1eb235.png&quot;&amp;gt;&amp;lt;/img&amp;gt;
&amp;lt;/a&amp;gt;
&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;h2&gt;开发过程&lt;/h2&gt;
&lt;p&gt;在开发这个插件前，我只是通过 &lt;a href=&quot;https://web.stanford.edu/class/cs142/lectures.html&quot;&gt;CS142&lt;/a&gt; 课程简单学习了前端的基础知识。然而，&lt;strong&gt;借助大模型和 &lt;code&gt;GitHub Copilot&lt;/code&gt; 的帮助&lt;/strong&gt;，我只需要能够理解 JavaScript 代码并查阅 Chrome 插件的 API 文档，就能够快速将我的想法实现出来。&lt;/p&gt;
&lt;p&gt;整个开发过程与我之前的开发经历有很大不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;大模型时代之前&lt;/strong&gt;：开发功能需要官方文档、StackOverflow、Google 三管齐下&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大模型时代&lt;/strong&gt;：只需一个大模型，就能快速实现想要的功能&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;只有在大模型输出的代码不符合预期时，我才会去查阅官方文档。这种开发方式大大提高了我的效率。&lt;/p&gt;
&lt;p&gt;但这也带来了一个有趣的副作用 —— 尽管我开发了一个插件，但对于 Chrome 插件的原理我依旧是&lt;strong&gt;一知半解&lt;/strong&gt;。以前程序员常说，学习新技术最好的方式就是动手实现它。但现在，在大模型的帮助下，即使你实现了功能，对技术的理解可能并不深刻。我认为，这是 AI 时代的一个典型特征。&lt;/p&gt;
&lt;p&gt;在开发过程中，我对 LLM（大语言模型）有了两点重要观察：&lt;/p&gt;
&lt;h3&gt;1️⃣ LLM 需要考虑开发者友好性&lt;/h3&gt;
&lt;p&gt;起初，smartTab 的大模型选型是 &lt;code&gt;Qwen&lt;/code&gt;，一方面是因为听说 &lt;code&gt;Qwen&lt;/code&gt; 是开源模型的第一梯队，另一方面是希望国内用户可以直接使用，无需翻墙。&lt;/p&gt;
&lt;p&gt;但在开发过程中，我发现 &lt;code&gt;Qwen&lt;/code&gt; 的结构化输出并不友好 —— &lt;strong&gt;它无法准确按照我给的 schema 输出对应的 JSON&lt;/strong&gt;。这个问题导致我不得不不断调整解析代码，严重影响了开发体验。同时，&lt;code&gt;Qwen&lt;/code&gt; 的响应速度也十分慢，有一次调试时我等了将近 1 分钟才得到结果。&lt;/p&gt;
&lt;p&gt;这些问题最终让我放弃了 &lt;code&gt;Qwen&lt;/code&gt;，转而使用 &lt;code&gt;Gemini-2.0-flash&lt;/code&gt;。在我的测试中，它的输出始终符合我给的 schema，而且基本都是&quot;秒出&quot;结果。&lt;/p&gt;
&lt;p&gt;我认为，如果大模型厂商想要让开发者基于他们的模型进行开发，构建有趣的应用，那么他们应该&lt;strong&gt;更多考虑开发者的体验&lt;/strong&gt;，而不仅仅是去刷榜，追求一些可能无关紧要的分数。&lt;/p&gt;
&lt;h3&gt;2️⃣ LLM 不擅长处理 &quot;dirty work&quot;&lt;/h3&gt;
&lt;p&gt;这里的 &quot;dirty work&quot; 指的是那些不复杂也不困难，但琐碎且需要人集中注意力处理的工作。比如经典的&quot;9.11 和 9.9 哪个大？&quot;或者简单的乘法计算。&lt;/p&gt;
&lt;p&gt;这类问题的特点是：不需要复杂逻辑，只需要集中注意力按规则处理即可。但 LLM 在这类问题上表现得并不好。&lt;/p&gt;
&lt;p&gt;在开发 smartTab 的过程中，我提供给 LLM 的输入是一个数组，每个元素都是一个标签页的 title 及其对应的 ID。LLM 的任务是将这些 title 进行分组，然后按照&quot;组名: [id1, id2, ...]&quot;的格式输出。&lt;/p&gt;
&lt;p&gt;然而，只要标签页数量超过 30 个，LLM 就会出现问题：要么漏掉一些标签页，要么记错标签页和 ID 的对应关系。这导致组名很合理，但组内的标签页却是错的。&lt;/p&gt;
&lt;p&gt;为了避免这个问题，我改变了策略：&lt;strong&gt;让 LLM 直接输出 title&lt;/strong&gt;，然后我自己根据 title 进行 ID 匹配。采用这种方式后，LLM 的输出基本正确了。&lt;/p&gt;
&lt;p&gt;打个比方，LLM 就像职场中的 Leader —— 能够高屋建瓴地给出方向或对大项目进行拆分，但当涉及到具体的落地实施，就不那么可靠了。&lt;/p&gt;
&lt;p&gt;LLM 的这一特点与编程恰好互补：&lt;strong&gt;编程的本质就是通过规则将琐碎工作交给计算机处理&lt;/strong&gt;。有人说 LLM 会取代程序员，但我认为 LLM 和编程的结合才能产生真正的价值。&lt;/p&gt;
&lt;h2&gt;AI 时代的思考&lt;/h2&gt;
&lt;h3&gt;技术方面&lt;/h3&gt;
&lt;p&gt;在 DeepSeek 出现之前，我一直认为 AI 时代只不过是给程序员提供了更好的代码补全工具。我曾拿 LeetCode 周赛题目测试大模型的解题能力，除了 OpenAI 的 o1 模型可以解决一些中等题目外，其他模型基本都不行。&lt;/p&gt;
&lt;p&gt;但在 DeepSeek 出现后，我发现已经几乎没有 LeetCode 周赛题目能够难倒大模型了。虽然解题能力与实际工作能力有很大差距，但这表明&lt;strong&gt;我们应该思考在 AI 时代如何适应这个新环境&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;让我们先想想：在大模型出现之前，成为一名优秀的程序员需要具备什么条件？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;能够使用 Google 检索自己想要的信息&lt;/li&gt;
&lt;li&gt;可以在 StackOverflow 寻找答案&lt;/li&gt;
&lt;li&gt;有能力阅读官方文档&lt;/li&gt;
&lt;li&gt;熟练掌握一门编程语言&lt;/li&gt;
&lt;li&gt;了解操作系统、网络、数据结构、算法等基础知识&lt;/li&gt;
&lt;li&gt;熟悉自己所在领域的专业知识&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以看到，成为优秀程序员的门槛相当高，这也许是程序员薪水一直较高的原因。&lt;/p&gt;
&lt;p&gt;但在 AI 时代，这些条件似乎被简化为了一条：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;能够正确提问，让大模型给出准确答案&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为门槛降低，我们看到许多从未接触过编程的人通过大模型的帮助，也能写出有趣的应用。那这是否意味着程序员的未来会变得暗淡？&lt;/p&gt;
&lt;p&gt;我对程序员的未来持乐观态度。这种情况意味着&lt;strong&gt;我们获取信息的效率更高了&lt;/strong&gt;，可以更快地实现想法。同时，好的答案源于好的问题，而提出好问题需要一定的领域知识 —— 你能更清晰准确地提出需求，大模型给出的答案也会更加准确。&lt;/p&gt;
&lt;p&gt;这也就意味着，&lt;strong&gt;你的技术能力越强，大模型对你的帮助也越大&lt;/strong&gt;。就像《终极一班》里的龙纹鏊一样：&lt;em&gt;遇强则强&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;但毕竟大模型是一个跨时代的工具，它确实对程序员带来了影响。例如，在大模型出现前，成为程序员的门槛很高，也许你只要精通其中一两项，就能找到不错的工作，比如成为一名 C++ &quot;语言律师&quot;。&lt;/p&gt;
&lt;p&gt;但在大模型出现后，很多技能的边际效益在下降，对编程语言的熟练度就是其中之一。正如 Bjarne Stroustrup 所说：&quot;你不需要了解 C++ 的每一个细节，就能写出优秀的 C++ 代码。&quot;&lt;/p&gt;
&lt;p&gt;在大模型时代，相较于成为&quot;语言律师&quot;，&lt;strong&gt;问题的拆分和项目的合理组织变得更加重要&lt;/strong&gt;。只要你能将复杂问题拆分到大模型能处理的粒度，就能创造出优秀的产品。而问题拆分和项目组织，需要的是你对用户需求的理解，以及计算机相关领域的基础知识。&lt;/p&gt;
&lt;p&gt;这影响不仅限于程序员，对所有领域都是如此 —— 卷成绩、卷绩点、卷一些数学解题技巧的意义正在降低。&lt;/p&gt;
&lt;p&gt;相比于学习新技术，思考如何用这些技术创造有价值的产品更加重要。在 AI 时代，Jonathan Gillette 的名言依然闪耀着光芒，在此与大家共勉：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;When you don&apos;t create things, you become defined by your tastes rather than ability. Your tastes only narrow and exclude people. So create.&quot;&lt;/p&gt;
&lt;p&gt;&quot;当你不创造东西时，你会被自己的品味而非能力所定义。而品味只会让你变得狭隘，排除他人。所以，去创造吧。&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;职场方面&lt;/h3&gt;
&lt;p&gt;由于我尚未找到合适的创业机会，也没有特别好的想法，所以我仍需要向市场出售我的时间来换取报酬。既然如此，我也不禁思考：AI 对我的职业发展有何影响？&lt;/p&gt;
&lt;p&gt;最大的影响应该是：&lt;strong&gt;对职业规划可以更加大胆一些&lt;/strong&gt;。因为有大模型的帮助，你能更快地学习新知识，掌握新技术。这意味着你可以更迅速地切换领域，适应市场需求。我们知道，长远来看，你的薪资水平取决于你在市场上所能提供的价值的稀缺程度。&lt;/p&gt;
&lt;p&gt;因为我在毕业后第一家公司的经历，我对所谓的&quot;职场忠诚度&quot;、&quot;与老板的关系&quot;、&quot;绩效/汇报&quot;，以及&quot;做的活是否是老板关注的&quot;、&quot;是否有成长空间&quot;、&quot;是否有学习空间&quot;等问题都持相对淡然的态度。这些因素存在固然很好，但如果没有，我也不会太在意。&lt;/p&gt;
&lt;p&gt;因为我看到那些所谓的 leader、老员工，上午还在和大家一起吃饭，下午就被通知收拾东西走人了。我的第一个 Leader 在部门里技术实力很强，其他组的人一提到他都会说：&quot;哦，你们组的 Leader 技术很厉害。&quot;他与老板的关系也不错，毕竟他本身也是把原来的 Leader 挤下去的，并非那种一心搞技术、不懂职场政治的类型。&lt;/p&gt;
&lt;p&gt;就是这样的员工，只要组织架构一变，分分钟被裁员走人。临走时，他获得的&quot;优秀员工&quot;奖杯还放在桌子上。&lt;/p&gt;
&lt;p&gt;说远了些，我的观点是：在大模型时代，职场中更应该关注的是&lt;strong&gt;你想做什么&lt;/strong&gt;。现在的岗位能否提供你想要的？如果可以，那很好，继续努力。如果不行，是选择跳槽还是去开源社区贡献代码？&lt;/p&gt;
&lt;p&gt;我们都知道国内互联网的工作环境 —— 不加班几乎是不可能的。但这不过是一种代价罢了，你是选择牺牲自己未来的可能性来取悦老板，让年度绩效好看一点？还是选择增加自己未来的可能性？至于老板的感受，大多数情况下，你们之间的联系不过是在背调时需要他的联系方式罢了。&lt;/p&gt;
&lt;p&gt;如今我们有能力，也有条件快速学习和掌握知识。&lt;strong&gt;请充分利用这一优势，让自己真正掌握自己的职业生涯&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当然，加班是容易的，从零开始更换职场赛道是困难的。周末加班加点完成老板临时交代的任务是舒适的，而鼓起勇气说&quot;不&quot;，专注于自己的长期目标则更具挑战性。&lt;/p&gt;
&lt;p&gt;但有了大模型，不妨再勇敢一点。没有那么多可怕的事情，适度的&quot;任性&quot;也不会带来严重后果。&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;我无比庆幸生活在这样一个时代。虽然我也曾想过，如果没有大模型，大家一起按部就班，慢慢查阅文档，做做&quot;语言律师&quot;该多好。但大模型毕竟已经诞生，我也有了可以快速学习的工具。&lt;/p&gt;
&lt;p&gt;既然如此，我想&lt;strong&gt;持续创造，不断变化&lt;/strong&gt;才是真正让我感到快乐的事情。对于人生，我愿意更加大胆一些！&lt;/p&gt;
</content:encoded><category>Interesting</category><category>Chrome Extension</category><category>Thoughts</category><author>tang-hi</author></item><item><title>从 double free 再熟悉链接器</title><link>https://tangdh.life/posts/interesting/double-free/</link><guid isPermaLink="true">https://tangdh.life/posts/interesting/double-free/</guid><description>在工作中遇到了一个因为链接导致的double free bug，这篇文章将会从这个bug出发，再次熟悉链接器的工作原理。</description><pubDate>Sat, 22 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在上上周的工作中，我发现部门的一个C++程序在退出时会出现&lt;code&gt;double free&lt;/code&gt;的问题，而之所以之前一直没有发现这个问题
是因为GCC版本太低，在高版本的GCC中，编译器对这种&lt;code&gt;double free&lt;/code&gt;的问题进行了检查，因此暴露了这个问题。
尽管这个问题可以继续隐藏，但是我因为比较喜欢解密，所以我还是决定去排查了一下这个问题（因为不能把公司的代码写在这里，所以我自己写了一个简单的例子， 下面分析的也都是我这个例子）&lt;/p&gt;
&lt;h2&gt;快速排查与解决&lt;/h2&gt;
&lt;p&gt;对于这种问题，第一反应就是使用&lt;code&gt;valgrind&lt;/code&gt;来检查内存的问题. 但使用&lt;code&gt;valgrind&lt;/code&gt;检查时会报错，可能还是GCC版本的问题，所以依旧使用&lt;code&gt;gdb&lt;/code&gt;来查看这个问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gdb ./your_program
r
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;gdb 的结果如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/250222_22h29m04s_screenshot.png&quot; alt=&quot;gdb&quot; /&gt;&lt;/p&gt;
&lt;p&gt;看到上面的报错信息和堆栈时，我当时有点慌张，没有任何业务代码，只有一堆系统和C++标准库的符号，但是就像
博客名称&lt;code&gt;Don&apos;t Panic&lt;/code&gt;一样，计算机没有魔法，一切东西都可以找到解释。从这个堆栈中，我们至少可以看到
&lt;code&gt;delete&lt;/code&gt; 的地址&lt;code&gt;0x55555556b2d0&lt;/code&gt;，那我们可以通过&lt;code&gt;gdb&lt;/code&gt;来查看这个地址的内存情况&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gdb ./your_program
watch *0x55555556b2d0
r
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/250222_22h30m45s_screenshot.png&quot; alt=&quot;delete&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们发现delete的是一个 &lt;code&gt;std::string&lt;/code&gt; 对象，检查代码可以发现这个对象是一个静态成员变量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Foo.hpp
#pragma once
#include &amp;lt;string&amp;gt;

class Foo {
public:
  static std::string getFoo();
  static std::string foo;
};

// Foo.cpp
#include &quot;foo.hpp&quot;
#include &amp;lt;string&amp;gt;

std::string Foo::foo = &quot;dwjdiawjdiawjdiawjdawidjawijdwai&quot;;

std::string Foo::getFoo() {
  return foo;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了查看这个地址在哪里被&lt;code&gt;delete&lt;/code&gt;，我们可以使用&lt;code&gt;gdb&lt;/code&gt;的&lt;code&gt;break&lt;/code&gt;命令来查看这个地址在哪里被&lt;code&gt;delete&lt;/code&gt;($rdi是第一个参数所在的寄存器)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;break operator delete if $rdi = 0x55555556b2d0
r
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/250222_22h42m07s_screenshot.png&quot; alt=&quot;break&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们可以看到在程序依赖的两个so析构时，这个地址被&lt;code&gt;delete&lt;/code&gt;了两次，这就是&lt;code&gt;double free&lt;/code&gt;的原因。当时我猜测应该就是链接导致两个so都认为自己拥有这个符号。
因为我们这个程序的链接关系相当错综复杂，极易出错。因此我猜测式的修改了一下链接的方式，暂时解决了这个问题。&lt;/p&gt;
&lt;h2&gt;复现问题以及解释BUG&lt;/h2&gt;
&lt;p&gt;这个问题解决后，我没有细究它。直到这周周会，为了不浪费我的个人时间，我决定来细究这个问题。了解一个问题的最好方式就是复现这个问题。
下面是我们程序的依赖关系。简单来说就是，&lt;strong&gt;我们编译了两个so，其中一个so依赖另一个的静态库，然后我们的main程序依赖这两个so&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;foo.h foo.cpp -&amp;gt; libfoo.so &amp;amp; libfoo.a&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;bar.cpp libfoo.a -&amp;gt; libbar.so&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;main.cpp libfoo.so libbar.so -&amp;gt; main&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// foo.hpp
#pragma once
#include &amp;lt;string&amp;gt;

class Foo {
public:
   static std::string getFoo();
   static std::string foo;
};

// foo.cpp
#include &quot;foo.hpp&quot;
#include &amp;lt;string&amp;gt;

std::string Foo::foo = &quot;dwjdiawjdiawjdiawjdawidjawijdwai&quot;;

std::string Foo::getFoo() {
  return foo;
}

// bar.cpp
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;ostream&amp;gt;

#include &quot;foo.hpp&quot;

void global_function() {
    std::cout &amp;lt;&amp;lt; &quot;global_function&quot; &amp;lt;&amp;lt; std::endl;
    std::cout &amp;lt;&amp;lt; Foo::getFoo() &amp;lt;&amp;lt; std::endl;
}

// main.cpp
#include &amp;lt;iostream&amp;gt;
#include &quot;foo.hpp&quot;
#include &amp;lt;ostream&amp;gt;

int main(int argc, char *argv[]) {
  std::cout &amp;lt;&amp;lt; &quot;Hello, double free!&quot; &amp;lt;&amp;lt; std::endl;
  std::cout &amp;lt;&amp;lt; Foo::foo &amp;lt;&amp;lt; std::endl;
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译脚本如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;g++ -fPIC -c -g foo.cpp -o foo.o
g++ -shared -g foo.o -o libfoo.so

ar rcs foo.a foo.o

g++ -fPIC -c -g bar.cpp -o bar.o

g++ -shared -g bar.o foo.a -o libbar.so

g++ main.cpp -g -o main -L. -lfoo -lbar -Wl,-rpath=.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那&lt;code&gt;double free&lt;/code&gt;的问题是如何产生的呢？我们来分析一下这个编译脚本&lt;/p&gt;
&lt;p&gt;首先我们编译了&lt;code&gt;foo.cpp&lt;/code&gt;生成了&lt;code&gt;libfoo.so&lt;/code&gt; 和 &lt;code&gt;foo.a&lt;/code&gt;. 我们通过&lt;code&gt;nm&lt;/code&gt;命令查看&lt;code&gt;so&lt;/code&gt;的符号表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nm -DC libfoo.so

// output
0000000000005100 B Foo::foo[abi:cxx11]
000000000000227a T Foo::getFoo[abi:cxx11]()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到&lt;code&gt;libfoo.so&lt;/code&gt;中有一个&lt;code&gt;Foo::foo&lt;/code&gt;的符号，这个符号是一个全局变量，且是强符号。&lt;/p&gt;
&lt;p&gt;然后我们编译了&lt;code&gt;bar.cpp&lt;/code&gt;生成了&lt;code&gt;libbar.so&lt;/code&gt;，这个so依赖了&lt;code&gt;libfoo.a&lt;/code&gt;。因为&lt;code&gt;bar.cpp&lt;/code&gt;中调用了&lt;code&gt;Foo::getFoo&lt;/code&gt;，所以&lt;code&gt;libbar.so&lt;/code&gt;中会有一个对&lt;code&gt;Foo::getFoo&lt;/code&gt;的引用。
根据链接器的规则，链接时会把&lt;code&gt;foo.a&lt;/code&gt; 中的&lt;code&gt;Foo::getFoo&lt;/code&gt;符号放到&lt;code&gt;libbar.so&lt;/code&gt;中，这样&lt;code&gt;libbar.so&lt;/code&gt;中就有了&lt;code&gt;Foo::foo&lt;/code&gt;和&lt;code&gt;Foo::getFoo&lt;/code&gt;两个符号。而且应该是两个强符号。
我们可以通过&lt;code&gt;nm&lt;/code&gt;命令查看&lt;code&gt;libbar.so&lt;/code&gt;的符号表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nm -DC libbar.so

// output
0000000000005120 B Foo::foo[abi:cxx11]
0000000000002398 T Foo::getFoo[abi:cxx11]()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;CSAPP 对链接静态库的规则有详细的介绍，这里简单介绍一下。链接器会按照从左到右的顺序查找符号，
如果找到了就会使用这个符号，如果没有找到就会继续查找下一个静态库。如果找到了一个强符号，那么就会使用这个符号，如果找到了一个弱符号，那么就会使用这个符号。如果找到了两个强符号，那么就会报错。
最后阶段，链接器会检查是否有未定义的符号，如果有未定义的符号，那么就会报错。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;然后我们编译了&lt;code&gt;main.cpp&lt;/code&gt;生成了&lt;code&gt;main&lt;/code&gt;，这个程序依赖了&lt;code&gt;libfoo.so&lt;/code&gt;和&lt;code&gt;libbar.so&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所以到这一步就很清晰了，当&lt;code&gt;main&lt;/code&gt; 被执行时，&lt;code&gt;libfoo.so&lt;/code&gt;和&lt;code&gt;libbar.so&lt;/code&gt;都会被加载到内存中，&lt;code&gt;libfoo.so&lt;/code&gt;中的&lt;code&gt;Foo::foo&lt;/code&gt;会被初始化，而准备初始化&lt;code&gt;libbar.so&lt;/code&gt;中的&lt;code&gt;Foo:foo&lt;/code&gt;时会发现&lt;code&gt;Foo::foo&lt;/code&gt;在符号表中已经存在了，所以会直接将
&lt;code&gt;libbar.so&lt;/code&gt;中的&lt;code&gt;Foo::foo&lt;/code&gt;重定向到&lt;code&gt;libfoo.so&lt;/code&gt;中的&lt;code&gt;Foo::foo&lt;/code&gt;。这样就会导致两个&lt;code&gt;so&lt;/code&gt;中这个符号的内存地址是一样的，所以在程序结束时，两个&lt;code&gt;so&lt;/code&gt;都分别会对这个符号进行析构，导致&lt;code&gt;double free&lt;/code&gt;的问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是等一下！&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;链接时有两个强符号会报错，为什么这里没有报错呢？&lt;/h3&gt;
&lt;p&gt;这是因为当链接动态库时，静态链接器的职责为确保所有符号（函数、变量等）在目标文件和动态库中有明确定义，同时记录动态库的依赖关系。
而在执行的过程时，动态链接器的符号介入(Interposition)机制为先加载的符号优先。&lt;/p&gt;
&lt;p&gt;会检查符号冲突的情况仅在链接&lt;strong&gt;静态库以及可重定向目标文件&lt;/strong&gt;时。&lt;/p&gt;
&lt;h3&gt;但是我平时明明会出现multiple definition的问题？&lt;/h3&gt;
&lt;p&gt;这是因为代码在编译时，也会检查你是否有多次定义的符号，如果有多次定义的符号，那么就会报错。此时还没有链接。&lt;/p&gt;
&lt;h2&gt;完整的故事&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;符号重复定义：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;libfoo.so和libbar.so均包含Foo::foo的定义。&lt;/li&gt;
&lt;li&gt;在链接libbar.so时，静态库foo.a被合并到libbar.so中，导致libbar.so内部包含独立的Foo::foo实例。&lt;/li&gt;
&lt;li&gt;主程序同时链接了libfoo.so和libbar.so，二者均导出Foo::foo符号。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;动态链接器的符号解析：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;动态链接器默认采用“全局符号介入”策略。若多个库定义同名全局符号，先加载的库中的符号会被优先使用，后续库中对同名符号的引用会绑定到第一个符号的地址。&lt;/li&gt;
&lt;li&gt;因此，libfoo.so和libbar.so中的Foo::foo最终指向同一个内存地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;双重析构：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;程序退出时，共享库的析构函数会按加载顺序的反序执行。&lt;/li&gt;
&lt;li&gt;Foo::foo作为全局对象，其析构函数会被两个库各自调用一次，导致std::string的内存被释放两次（double free）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;考虑到依赖关系错综复杂，直接隐藏符号可能导致更加混乱的依赖关系，所以在编译&lt;code&gt;so&lt;/code&gt;时，我们可以使用&lt;code&gt;-Bsymbolic&lt;/code&gt;要求每个动态库使用自己的符号，这样就可以避免这个问题。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;在排查这个问题时，重新熟悉了链接器的工作原理倒是其次，更重要的是，我对项目管理有了更深的理解&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;永远要选择一门工业级的语言&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;C++已经不是一门工业级的语言了，它没有包管理器，意味着你没法方便的复用别人的成果。我很难说出第二门语言需要像C++那样，代码复用度底，程序员大部分时间不是专注
业务代码本身，而是解决一些乱七八糟的编译，链接问题。更关键的是，你想要从市场招聘一个厉害的C++程序员，基本等同于抽奖，不是说你开的工资高，就能招到。更关键的是
C++程序员往往技术ego较大，而讽刺的是令他ego大的正是他从来没有弄清楚的东西。至少对我来说，如果我得知我的竞争对手使用C++开发，我有很大的自信在迭代上会比他快。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;项目管理永远要选择最简单的方式&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个问题你很容易blame到个人身上(这应该也是互联网的大部分做法)，例如你依赖做的太烂了，你代码写的太烂了。这是最不动脑子也是最符合人性的做法。但这是最于事无补的做法。
项目管理应该尽可能做到从市场上招来任何水平的人，都有一套最简单的方式让他们不出错。比如这个问题，完全可以通过统一代码仓库，统一编译脚本，统一编译器版本，统一编译选项，
完全源码编译来解决。而且这是对个人心智负担最小的方式。&lt;/p&gt;
&lt;p&gt;你用各个模块链接的方式，不可避免有人在编译失败时就会想要不静态链接试试？这样就会不可避免导致这个问题。比如说，之前我们项目头文件的排序方式是按照和当前文件的相关度排序，
这种方式甚至没有直接按照字母排序好。因为这完全无法客观衡量，而且给人带来的心智负担是巨大的。最后的结果一定是头文件的排序乱七八糟。&lt;/p&gt;
</content:encoded><category>Interesting</category><category>Linker</category><category>Bug</category><author>tang-hi</author></item><item><title>Back to Basic - 文档打分及检索优化</title><link>https://tangdh.life/posts/ir/doc-similarity/</link><guid isPermaLink="true">https://tangdh.life/posts/ir/doc-similarity/</guid><description>当用户在搜索框中输入`Query`后，我们应该如何基于用户的`Query`来对文档进行排序， 又该如何快速的返回用户所需的结果？ 这篇文章会解释相关的基础知识。</description><pubDate>Mon, 13 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;当用户在进行检索时，他往往希望搜索引擎可以迅速返回他最需要的文档信息。在这个使用场景中，我们需要解决两个问题。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在确定&lt;code&gt;Query&lt;/code&gt;的情况下，如何定量的给文档进行打分？&lt;/li&gt;
&lt;li&gt;如何在上亿乃至上百亿的文档中快速的找到与用户&lt;code&gt;Query&lt;/code&gt;最相关的文档？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面我们会从这两个问题出发，描述基于文本检索的搜索引擎是如何处理这两个问题(本文只做基础知识的介绍)。&lt;/p&gt;
&lt;h2&gt;从头开始发明TF-IDF和BM25&lt;/h2&gt;
&lt;p&gt;大多数人看到&lt;code&gt;BM25&lt;/code&gt;的公式后，会选择放弃理解公式的含义，随后将其作为一个黑盒模型使用。这是因为这个公式考虑了多方面的因素，使得人们难以
一下子理解它的含义。
为了方便读者理解该公式, 我们通过再次&quot;发明&quot;的方式来理解这两个公式。&lt;/p&gt;
&lt;h3&gt;TF-IDF&lt;/h3&gt;
&lt;p&gt;在给定&lt;code&gt;Query&lt;/code&gt;的情况下，我们如何衡量一篇文档比另一篇文档更加相关呢？&lt;/p&gt;
&lt;p&gt;一个显而易见的做法是，对给定的term，我们统计文档中该term出现的次数。
出现的次数越多，那这篇文档的相关性越高。&lt;/p&gt;
&lt;p&gt;$$
\text{score}(t,D) = \text{occurance}(t,D)
$$&lt;/p&gt;
&lt;p&gt;那我们如何衡量&lt;code&gt;Query&lt;/code&gt;与文档的相关性呢？我们可以将&lt;code&gt;Query&lt;/code&gt;中每个term的相关性加起来，即&lt;/p&gt;
&lt;p&gt;$$
\text{score}(Q,D) = \sum_{t \in Q} \text{score}(t,D)
$$&lt;/p&gt;
&lt;p&gt;单独使用term出现次数来衡量文档的相关性，有两个很明显的问题&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对于&lt;code&gt;Query&lt;/code&gt;中的每个term都是平等对待的，没有考虑哪些term更加重要。
例如当&lt;code&gt;Query&lt;/code&gt;是&quot;猫和老鼠&quot;，检索出来的文档中很有可能是 “和” 出现次数最多的文档，这显然不是我们想要的。&lt;/li&gt;
&lt;li&gt;文档所包含的term越多，它的相关性高的可能性就越大，这显然是不合理的。
例如你的&lt;code&gt;Query&lt;/code&gt;是&quot;猫和老鼠&quot;，你可能搜出来一个完全无关的文档，仅仅因为这个文档很长，而它又包含了大量&quot;猫&quot;和&quot;老鼠&quot;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为了解决第一个问题，我们需要给予&lt;code&gt;Query&lt;/code&gt;中的term一个权重，而term的权重应该与它在整个文档集合中的稀缺性成正比。
如果一个term在很多文档中都出现，说明它是一个常用词，比如&quot;这&quot;, &quot;那&quot;。 反之，如果一个term很少在文档中出现，说明它在文档集合中的稀缺性很高，
那么它在整个&lt;code&gt;Query&lt;/code&gt;中的权重就应该更高。&lt;/p&gt;
&lt;p&gt;我们如何衡量一个term在整个文档集合中的稀缺性呢？我们可以用包含这个term的文档&lt;strong&gt;倒数&lt;/strong&gt;来衡量，即&lt;/p&gt;
&lt;p&gt;$$
\text{importance}(t) = \frac{1}{\text{doc_count}(t)}
$$&lt;/p&gt;
&lt;p&gt;为了便于后续处理，我们可以将$$\text{importance}(t)$$ 乘以文档总数进行归一化，即&lt;/p&gt;
&lt;p&gt;$$
\text{importance}(t) = \text{(number of documents)} \times \frac{1}{\text{doc_count}(t)}
$$&lt;/p&gt;
&lt;p&gt;至此，我们&quot;发明&quot;了IDF(Inverse Document Frequency)的概念。&lt;/p&gt;
&lt;p&gt;那我们如何将IDF和TF结合起来呢？我们可以简单将两者相乘，即&lt;/p&gt;
&lt;p&gt;$$
\text{score}(t,D) = \text{occurance}(t,D) \times \text{importance}(t)
= \text{tf}(t,D) \cdot \text{idf}(t,D)
$$&lt;/p&gt;
&lt;p&gt;至此，TF-IDF的基本概念基本就讲完了。但我们还需要对上述公式进行一些调整，以便更好的适应实际场景。
具体调整方式为对IDF取log。公式如下&lt;/p&gt;
&lt;p&gt;$$
\text{idf}(t,D) = \log{\frac{N}{|{d \in D : t \in d}|}}
$$&lt;/p&gt;
&lt;p&gt;为什么要取&lt;code&gt;log&lt;/code&gt;呢？ 我们可以从下图中看到，当DF较小时，DF轻微的变化会导致IDF剧烈变化，这显然是不合理的。&lt;/p&gt;
&lt;p&gt;例如，当DF从1变到2时，IDF的数值减少了一半，这显然是不符合常识的，
在一个较大的文档集合中，term在两个文档中出现和在一个文档中出现, 这两者的重要性应该是差不多的。但我们的IDF公式并没有考虑这一点，
因此我们需要取&lt;code&gt;log&lt;/code&gt;使得IDF的变化更加平滑。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20250113224742.png&quot; alt=&quot;IDF&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;BM25&lt;/h3&gt;
&lt;p&gt;在TF-IDF这一节中，我们介绍了它的两个缺点，即会偏向长文档，以及对&lt;code&gt;Query&lt;/code&gt;中的term没有进行加权的处理。
我们已经解决了&lt;code&gt;Query&lt;/code&gt;中term的权重问题，那么如何解决文档长度的问题呢？&lt;/p&gt;
&lt;h4&gt;文档长度的问题&lt;/h4&gt;
&lt;p&gt;检索过程中，当两篇文档的term出现的次数相同时，我们应该更加青睐于较短的文档。 因此我们需要一个公式来对文档的长度进行惩罚。&lt;/p&gt;
&lt;p&gt;在得到这个公式之前，我们先量化一下文档的长度：用文档中term的个数来量化文档的长度，即&lt;/p&gt;
&lt;p&gt;$$
\text{doc_length}(D) = \sum_{t \in D} \text{occurance}(t,D)
$$&lt;/p&gt;
&lt;p&gt;但是这个公式只考虑了文档的长度，而没有考虑整个文档集合。因此我们需要把文档的长度与整个文档集合的平均长度进行比较，从而评估在文档集合中，这个文档的长度是长还是短。即&lt;/p&gt;
&lt;p&gt;$$
\text{doc_length_ratio}(D) = \frac{\text{doc_length}(D)}{\text{avg_doc_length}}
$$&lt;/p&gt;
&lt;p&gt;在得到可以量化文档长度的公式之后，我们先暂缓一下，思考一下TF-IDF的其他问题。&lt;/p&gt;
&lt;h4&gt;Refine TF&lt;/h4&gt;
&lt;p&gt;在TF-IDF中，我们假设TF和相关性是成正比的，即TF越大，相关性越高。但这显然是不合理的。&lt;/p&gt;
&lt;p&gt;例如，猫在一篇文档中出现了&lt;strong&gt;1000&lt;/strong&gt;次，这并不意味该文档的相关性比猫只出现了100次的文档高10倍。恰恰相反,
在实际搜索中，我们会倾向于认为，当term在文档中出现的次数较多时，它相关性的边际效益会递减。&lt;/p&gt;
&lt;p&gt;同时如果仅使用TF来衡量文档的相关性，那么对于&lt;code&gt;Query&lt;/code&gt;: &quot;猫和老鼠&quot;，出现了&quot;猫&quot;和&quot;老鼠&quot;各一次的文档与出现了&quot;猫&quot;2次的文档的相关性是一样的，
这显然也是不合理的。&lt;/p&gt;
&lt;p&gt;因此，我们需要对TF进行调整，使得TF较小时，相关性的增长较快，而TF较大时，相关性的增长较慢。并对于&lt;code&gt;Query&lt;/code&gt;中的term命中数相同，不同的term越多，相关性越高。
新的TF公式如下&lt;/p&gt;
&lt;p&gt;$$
\text{new_tf}(t,D) = \frac{\text{occurance}(t,D) }{\text{occurance}(t,D) + K}
$$&lt;/p&gt;
&lt;p&gt;从图中我们可以看到，当我们加上K之后，TF的增长速度会随着TF的增加而放缓。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20250113232106.png&quot; alt=&quot;TF&quot; /&gt;&lt;/p&gt;
&lt;p&gt;同时如果&lt;code&gt;Query&lt;/code&gt;为&quot;猫和老鼠&quot;，那么出现了&quot;猫&quot;和&quot;老鼠&quot;各一次的文档, 它的相关性为 $$\frac{1}{2} + \frac{1}{2} = 1$$ (假设K=1)，
而出现了&quot;猫&quot;2次的文档，它的相关性为 $$\frac{2}{3} &amp;lt; 1$$，&lt;/p&gt;
&lt;p&gt;到了这里还记得，我们上一节暂缓的文档长度问题吗？我们可以将文档长度的问题与TF的问题结合起来，
即将我们在上一节得到的量化文档长度的公式与新的TF公式结合起来，即&lt;/p&gt;
&lt;p&gt;$$
\text{new_tf}(t,D) = \frac{\text{occurance}(t,D) }{\text{occurance}(t,D) + K \cdot \frac{\text{doc_length}(D)}{\text{avg_doc_length}} }
$$&lt;/p&gt;
&lt;p&gt;为什么这个公式是合理的呢？如果文档的长度等于平均长度，那么这个公式就等价于我们上一节得到的新的TF公式。
而当文档的长度远大于平均长度时，这个公式会增大K值，从而减小相关性，即对文档的长度进行了惩罚。而当文档的长度远小于平均长度时，这个公式会减小K值，从而增大相关性，即对文档的长度进行了奖励。&lt;/p&gt;
&lt;h4&gt;自定义文档长度的惩罚&lt;/h4&gt;
&lt;p&gt;在实际检索的场景中，我们并不总是希望文档的长度越短越好，有时候我们希望文档的长度越长越好。例如在搜索引擎中，我们希望返回的文档尽可能的丰富，这时我们可以自定义一个参数&lt;code&gt;b&lt;/code&gt;，来调整文档长度的惩罚。即&lt;/p&gt;
&lt;p&gt;$$
(1 - b + b \cdot \frac{\text{doc_length}(D)}{\text{avg_doc_length}}) (0 \leq b \leq 1)
$$&lt;/p&gt;
&lt;p&gt;当&lt;code&gt;b=1&lt;/code&gt;时，公式如下所示，与之前的公式等价&lt;/p&gt;
&lt;p&gt;$$
(\frac{\text{doc_length}(D)}{\text{avg_doc_length}})
$$&lt;/p&gt;
&lt;p&gt;当&lt;code&gt;b=0&lt;/code&gt;时，文档的长度不再对相关性产生影响。&lt;/p&gt;
&lt;p&gt;$$
(1 - 0 + 0 \cdot \frac{\text{doc_length}(D)}{\text{avg_doc_length}}) = 1
$$&lt;/p&gt;
&lt;p&gt;因此用户可以通过调整&lt;code&gt;b&lt;/code&gt;的值来调整文档长度对相关性的影响。至此将我们的公式整合一下，即&lt;/p&gt;
&lt;p&gt;$$
\text{score}(t,D) = \frac{tf(t,D)}{tf(t,D) + k_1 \cdot (1 - b + b \cdot \frac{\text{doc_length}(D)}{\text{avg_doc_length}})} \cdot \text{idf}(t,D)
$$&lt;/p&gt;
&lt;h4&gt;Refine IDF&lt;/h4&gt;
&lt;p&gt;BM25中定义的IDF与我们之前定义的IDF有所不同，具体的公式如下&lt;/p&gt;
&lt;p&gt;$$
\text{idf}(t,D) = \log{\frac{N - n(t) + 0.5}{n(t) + 0.5}}
$$&lt;/p&gt;
&lt;p&gt;这是数学家出于理论上的考虑，对IDF进行了一些调整，使得IDF的变化更加平滑，同时避免了一些极端情况的发生。但是在实际情况中，Lucene对IDF进行了一些调整，具体的公式如下&lt;/p&gt;
&lt;p&gt;$$
\text{idf}(t,D) = \log{\frac{ 1 + (N - DF + 0.5)}{DF + 0.5}}
\approx \log{1 + \frac{N - DF}{DF}}
= \log{\frac{N}{DF}}
$$&lt;/p&gt;
&lt;p&gt;即我们之前定义的IDF公式。&lt;/p&gt;
&lt;h4&gt;Warp up&lt;/h4&gt;
&lt;p&gt;将我们之前定义的TF，IDF，文档长度的惩罚，文档长度的奖励整合起来，即&lt;/p&gt;
&lt;p&gt;$$
\text{BM25}(t,D) = \frac{tf(t,D)}{tf(t,D) + k_1 \cdot (1 - b + b \cdot \frac{\text{doc_length}(D)}{\text{avg_doc_length}})} \cdot \text{idf}(t,D)
$$&lt;/p&gt;
&lt;p&gt;我们可以发现，一个简单的BM25的公式，通过巧妙的数学，成为了文档相关性打分的事实标准。即使在深度学习的时代，BM25仍然是搜索引擎中最常用的打分公式之一。&lt;/p&gt;
&lt;h2&gt;快速求解TopK&lt;/h2&gt;
&lt;p&gt;在实际的搜索引擎中, 我们最常处理的&lt;code&gt;Query&lt;/code&gt;就是TopK，即给定一个&lt;code&gt;Query&lt;/code&gt;，我们需要返回与&lt;code&gt;Query&lt;/code&gt;最相关的K个文档。&lt;/p&gt;
&lt;p&gt;但是在几十上百亿的文档中，如何快速的找到与&lt;code&gt;Query&lt;/code&gt;最相关的K个文档呢？我们当然可以对所有被召回的文档进行打分，然后排序，取TopK。&lt;/p&gt;
&lt;p&gt;但是这样的延迟显然是不可接受的。因此我们需要一些技巧来加速这个过程。
这一章会介绍当前最常用的几种技术(无损检索，即检索的TopK文档是准确的)。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们主要关注&lt;code&gt;OR Query&lt;/code&gt;， 即&lt;code&gt;Query&lt;/code&gt;中的term之间是或的关系。 &lt;code&gt;AND Query&lt;/code&gt; 因为求交的特性，往往计算要求不高&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;DAAT(Document At A Time)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;DAAT&lt;/code&gt;是一种朴素的方法，我们会使用一个堆用来维护当前的TopK文档。
它的特点是，对每一篇文档，我们都会计算它的得分，然后与堆顶的文档比较，如果新文档得分更高，那我们就更新堆顶的文档。&lt;/p&gt;
&lt;p&gt;具体步骤如下&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对&lt;code&gt;Query&lt;/code&gt;中每一个term，我们检索出该term对应的倒排拉链&lt;/li&gt;
&lt;li&gt;我们将这些倒排链表合并，得到一个包含所有文档ID的列表&lt;/li&gt;
&lt;li&gt;对每一个文档，我们计算它的得分，然后与堆顶的文档进行比较，如果得分更高，那么我们将堆顶的文档替换为当前文档。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们通过如下的伪代码来描述这个过程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def DAAT(Query, document_collection):
  &quot;&quot;&quot;
  Performs Document-At-A-Time (DAAT) retrieval.

  Args:
    Query: A list of Query terms (strings).
    document_collection: collection of documents to be searched.

  Returns:
    A list of (documentID, score) tuples, ranked in descending order of score.
  &quot;&quot;&quot;

  inverted_index = document_collection.get_inverted_index()
  postings_lists = [inverted_index[term] for term in Query]

  merged_postings = merge_postings_lists(postings_lists)
  for doc_id in merged_postings:
    score = compute_score(Query, doc_id, document_collection)
    # Update the heap with the new score.
    if len(heap) &amp;lt; k:
      heapq.heappush(heap, (score, doc_id))
    elif score &amp;gt; heap[0][0]:
      heapq.heappushpop(heap, (score, doc_id))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;TAAT(Term At A Time)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;TAAT&lt;/code&gt; 与 &lt;code&gt;DAAT&lt;/code&gt;一样，也是一种朴素的方法。它的特点是，我们不会像&lt;code&gt;DAAT&lt;/code&gt;一样，使用堆来维护TopK文档，而是会遍历每一个倒排链，计算出文档的部分得分(partial score)。
同时内存中维护每一篇文档的部分得分，当我们遍历完所有的倒排链之后，我们再对文档按照得分进行排序，取TopK。它的具体步骤如下&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对&lt;code&gt;Query&lt;/code&gt;中的每一个term，我们检索出该term对应的倒排链表&lt;/li&gt;
&lt;li&gt;对每一个倒排链表，我们遍历其中的每一篇文档，计算它的部分得分&lt;/li&gt;
&lt;li&gt;对每一个文档，我们将它的部分得分加到它的总得分上&lt;/li&gt;
&lt;li&gt;对所有的文档，将其按照得分进行排序，取TopK&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们通过如下的伪代码来描述这个过程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def TAAT(Query, document_collection):
  &quot;&quot;&quot;
  Performs Term-At-A-Time (TAAT) retrieval.

  Args:
    Query: A list of Query terms (strings).
    document_collection: collection of documents to be searched.

  Returns:
    A list of (documentID, score) tuples, ranked in descending order of score.
  &quot;&quot;&quot;

  inverted_index = document_collection.get_inverted_index()
  postings_lists = [inverted_index[term] for term in Query]

  scores = {}
  for postings_list in postings_lists:
    for doc_id in postings_list:
      score = compute_score(Query, doc_id, document_collection)
      scores[doc_id] += score

  top_k = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:k]
  return top_k
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Compare DAAT and TAAT&lt;/h3&gt;
&lt;p&gt;在介绍完最简单的两个朴素方法之后，我们来比较一下两种方法的优缺点。
在实际检索的场景下，我们往往会使用&lt;code&gt;DAAT&lt;/code&gt;，不仅因为它的延迟更低，而且它的内存占用也更低。&lt;code&gt;TAAT&lt;/code&gt;的优势在于， 他对倒排链的顺序读取，有利于CPU的cache命中以及磁盘的预读取。
但是随着文档集合的增大，&lt;code&gt;TAAT&lt;/code&gt;的内存占用会迅速增加，而他顺序读取的优势也会因为更新文档的得分而减弱。
相反&lt;code&gt;DAAT&lt;/code&gt;的内存占用是固定的，且在大规模文档集合中，它的延迟会比&lt;code&gt;TAAT&lt;/code&gt;更低。因此在实际的搜索引擎中，我们往往会使用&lt;code&gt;DAAT&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;WAND(Weak AND)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;WAND&lt;/code&gt;是一种基于&lt;code&gt;DAAT&lt;/code&gt;的优化方法，它的特点是，我们会使用一个阈值来剪枝，当文档的得分低于阈值时，我们就不再计算它的得分。&lt;/p&gt;
&lt;p&gt;他的具体步骤如下:&lt;/p&gt;
&lt;p&gt;索引构建阶段：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在索引构建阶段，我们会记录每一个term所能得到的最高得分(upper bound)，即&lt;/p&gt;
&lt;p&gt;$$\text{upperbound}(t) = max(\text{score}(t, \forall d \in D))$$&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;索引检索阶段:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对&lt;code&gt;Query&lt;/code&gt;中的每一个term，我们检索出该term对应的倒排链表,设置阈值为0&lt;/li&gt;
&lt;li&gt;我们将倒排链表按照&lt;code&gt;Doc ID&lt;/code&gt;进行排序&lt;/li&gt;
&lt;li&gt;我们按顺序遍历每一个倒排链表，将term对应的&lt;code&gt;upperbound&lt;/code&gt;进行累加，直到得分超过当前维护的阈值。
&lt;ol&gt;
&lt;li&gt;如果遍历完了所有的倒排链表，得分仍然没有超过阈值，那么检索结束，返回结果&lt;/li&gt;
&lt;li&gt;如果得分超过了阈值，那么我们获取这条倒排链的&lt;code&gt;Doc ID&lt;/code&gt;
&lt;ol&gt;
&lt;li&gt;如果这个&lt;code&gt;Doc ID&lt;/code&gt; ≤ 当前记录的&lt;code&gt;Doc ID&lt;/code&gt;，那么我们任意选择一个之前的倒排链，调用&lt;code&gt;next(docid)&lt;/code&gt;方法, 跳转到步骤1&lt;/li&gt;
&lt;li&gt;检查第一条倒排链的&lt;code&gt;Doc ID&lt;/code&gt;是否等于当前记录的&lt;code&gt;Doc ID&lt;/code&gt;，如果是，那么我们就找到了一个文档，我们计算它的得分，然后与堆顶的文档进行比较，如果得分更高，那么我们就将堆顶的文档替换为当前文档。&lt;/li&gt;
&lt;li&gt;如果第一条倒排链的&lt;code&gt;Doc ID&lt;/code&gt;不等于当前记录的&lt;code&gt;Doc ID&lt;/code&gt;，那么我们任意选择一个之前的倒排链，调用&lt;code&gt;next(docid)&lt;/code&gt;方法, 跳转到步骤1&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们通过如下的伪代码来描述这个过程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def find_pivot_term(term, threshold, postings_lists):
  &quot;&quot;&quot;
  Find the pivot term for WAND retrieval.

  Args:
    term: A Query term (string).
    threshold: The threshold score.

  Returns:
    A tuple (posting_idx, term) of the pivot term.
  &quot;&quot;&quot;
  score = 0
  for i, postings_list in enumerate(postings_lists):
    term = postings_list.term
    score += term.upperbound
    if score &amp;gt; threshold:
      return i, term

def WAND(Query, document_collection):
  &quot;&quot;&quot;
  Performs Weak AND (WAND) retrieval.

  Args:
    Query: A list of Query terms (strings).
    document_collection: collection of documents to be searched.

  Returns:
    A list of (documentID, score) tuples, ranked in descending order of score.
  &quot;&quot;&quot;

  inverted_index = document_collection.get_inverted_index()
  postings_lists = [inverted_index[term] for term in `Query`]
  top_k = []
  threshold = 0
  current_doc_id = None
  while True:
    sort(postings_lists, key=lambda x: x.current_doc_id())
    pivot_idx, pivot_term = find_pivot_term(postings_lists, threshold)
    pivot_doc_id = postings_lists[pivot_idx].current_doc_id()
    if pivot_term is None:
      break
    if pivot_doc_id &amp;lt;= current_doc_id:
      random_postings_list = random.choice(postings_lists[:pivot_idx])
      random_postings_list.next(current_doc_id + 1)
      continue
    else:
      if postings_list[0].current_doc_id() == pivot_doc_id:
        current_doc_id = pivot_doc_id
        score = compute_score(Query, current_doc_id, document_collection)
        if len(top_k) &amp;lt; k:
          heapq.heappush(top_k, (score, current_doc_id))
        elif score &amp;gt; top_k[0][0]:
          heapq.heappushpop(top_k, (score, current_doc_id))
          threshold = top_k[0][0]
      else:
        random_postings_list = random.choice(postings_lists[:pivot_idx])
        random_postings_list.next(pivot_doc_id)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面我们再用一个例子来详细说明&lt;code&gt;WAND&lt;/code&gt;的过程&lt;/p&gt;
&lt;p&gt;假设我们的&lt;code&gt;Query&lt;/code&gt;是&quot;猫 狗 老鼠&quot;，求解Top2，&lt;/p&gt;
&lt;p&gt;在最初的情况下，因为堆为空，所以我们的阈值为0。&lt;/p&gt;
&lt;p&gt;假定&lt;/p&gt;
&lt;p&gt;&lt;code&gt;猫&lt;/code&gt;在整个文档集合中最大的得分为0.1&lt;/p&gt;
&lt;p&gt;&lt;code&gt;狗&lt;/code&gt;在整个文档集合中最大的得分为0.4&lt;/p&gt;
&lt;p&gt;&lt;code&gt;老鼠&lt;/code&gt;在整个文档集合中最大的得分为0.9。&lt;/p&gt;
&lt;p&gt;初始状态如下图所示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20250115232956.png&quot; alt=&quot;WAND&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下面我们开始检索，对倒排链表按照&lt;code&gt;Doc ID&lt;/code&gt;进行排序。从第一条倒排链开始，我们把对应term的&lt;code&gt;upperbound&lt;/code&gt; 加到当前的得分上，得分为0.9。因为0.9 大于阈值0，
所以我们计算这个Doc真正的得分(0.6)，将其放入堆中。并将对应的倒排链移动到下一个文档。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20250114221922.png&quot; alt=&quot;WAND1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后我们继续对倒排链表进行排序，继续从第一条倒排链开始。我们把&lt;code&gt;老鼠&lt;/code&gt;的&lt;code&gt;upperbound&lt;/code&gt; 加到当前的得分上，得分为0.9。因为0.9 大于阈值0，
所以我们计算这个Doc真正的得分(0.7)，将其放入堆中。 并将对应的倒排链移动到下一个文档。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20250114221819.png&quot; alt=&quot;WAND2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们仍然对倒排链表进行排序, 然后从第一条倒排链开始，此时因为我们堆中已经有两个文档，所以我们的阈值为0.6。我们
从&lt;code&gt;猫&lt;/code&gt;的倒排链开始，把&lt;code&gt;猫&lt;/code&gt;的&lt;code&gt;upperbound&lt;/code&gt; 加到当前的得分上，因为0.1 小于阈值0.6，所以我们需要继续累加&lt;code&gt;狗&lt;/code&gt;的&lt;code&gt;upperbound&lt;/code&gt;(0.4)。
直到当我们累加&lt;code&gt;老鼠&lt;/code&gt;的&lt;code&gt;upperbound&lt;/code&gt;时，我们的得分超过阈值。此时我们需要对比当前倒排链的&lt;code&gt;Doc ID&lt;/code&gt;与第一条倒排链(猫)的&lt;code&gt;Doc ID&lt;/code&gt;，发现当前倒排链的&lt;code&gt;Doc ID&lt;/code&gt;大于第一条倒排链的&lt;code&gt;Doc ID&lt;/code&gt;，
因此我们可以将&lt;strong&gt;当前倒排链&lt;/strong&gt;之前的所有倒排链的&lt;code&gt;Doc ID&lt;/code&gt;都移动到大于等于&lt;strong&gt;当前倒排链&lt;/strong&gt;的&lt;code&gt;Doc ID&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20250114221700.png&quot; alt=&quot;WAND3&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;尽管&lt;code&gt;WAND&lt;/code&gt;论文中提到的是在当前倒排链之前的所有倒排链中任选一个，将其&lt;code&gt;Doc ID&lt;/code&gt;移动到大于等于当前倒排链的&lt;code&gt;Doc ID&lt;/code&gt;，
这主要是出于担心所有倒排链一起移动会触发多次磁盘读取，但是实际上，我们可以将所有倒排链一起移动，这并不会影响正确性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们继续重复之前的步骤，这一次我们会发现&lt;strong&gt;当前倒排链&lt;/strong&gt;的&lt;code&gt;Doc ID&lt;/code&gt;与第一条倒排链(猫)的&lt;code&gt;Doc ID&lt;/code&gt;相等，此时我们就找到了一个文档，我们计算它的得分(0.5)，然后与堆顶的文档进行比较，如果得分更高，那我们就将堆顶的文档替换为当前文档。
同时将对应的倒排链移动到下一个文档。下一轮我们会发现所有倒排链&lt;code&gt;upperbound&lt;/code&gt;的和都小于阈值，因此我们可以结束检索，返回结果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20250115233425.png&quot; alt=&quot;WAND4&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从这个例子中我们可以看到，&lt;code&gt;WAND&lt;/code&gt;的优势在于，它可以在检索的过程中，根据当前的得分，动态的调整阈值，从而减少不必要的计算以及磁盘读取。(跳过了文档3，4，6，9，12，15，17)&lt;/p&gt;
&lt;h3&gt;MaxScore&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MaxScore&lt;/code&gt;是一种基于&lt;code&gt;DAAT&lt;/code&gt;的优化方法，它的特点是，我们会将倒排链分成两部分，一部分用来做OR检索，另一部分仅仅用来计算得分,
从而减少计算次数。它的具体步骤如下:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对&lt;code&gt;Query&lt;/code&gt;中的每一个term，我们检索出term对应的倒排链表&lt;/li&gt;
&lt;li&gt;我们将这些倒排链表按照&lt;code&gt;upperbound&lt;/code&gt;倒序排序&lt;/li&gt;
&lt;li&gt;根据当前TopK的得分，我们将倒排链分成两部分：&lt;code&gt;essential&lt;/code&gt;和&lt;code&gt;non-essential&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;对&lt;code&gt;essential&lt;/code&gt;倒排链进行OR检索，得到一个文档ID, 再检查&lt;code&gt;non-essential&lt;/code&gt;倒排链中是否包含这个文档ID，如果包含，将对应的term的得分加到当前文档的得分上
&lt;ol&gt;
&lt;li&gt;将文档的得分与堆顶的文档进行比较，如果得分更高，那我们将堆顶的文档替换为当前文档，并更新阈值，重复步骤3&lt;/li&gt;
&lt;li&gt;如果得分没有超过阈值，重复步骤4&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;步骤3中的划分依据为，我们自下而上的累加&lt;code&gt;upperbound&lt;/code&gt;，直到得分超过阈值。&lt;/p&gt;
&lt;p&gt;假设此时的倒排链为&lt;code&gt;i&lt;/code&gt;，那么我们将前&lt;code&gt;i&lt;/code&gt;个倒排链作为&lt;code&gt;essential&lt;/code&gt;倒排链，剩下的作为&lt;code&gt;non-essential&lt;/code&gt;倒排链。
这样划分的好处是，一个Doc哪怕包含了&lt;code&gt;non-essential&lt;/code&gt;倒排链中所有term，也不会成为TopK文档，因此我们只需考虑&lt;code&gt;essential&lt;/code&gt;倒排链中的term。
从而减少计算次数。&lt;/p&gt;
&lt;p&gt;伪代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def split_postings_lists(postings_lists, threshold):
  &quot;&quot;&quot;
  Split the postings lists into essential and non-essential parts.

  Args:
    postings_lists: A list of postings lists.
    threshold: The threshold score.

  Returns:
    A tuple (essential_postings_lists, non_essential_postings_lists) of the split postings lists.
  &quot;&quot;&quot;
  essential_postings_lists = []
  non_essential_postings_lists = []
  score = 0
  # reverse for descending order
  i = len(postings_lists) - 1
  while i &amp;gt;= 0:
    score += postings_lists[i].upperbound
    if score &amp;gt; threshold:
      break
    non_essential_postings_lists.append(postings_lists[i])
  essential_postings_lists = postings_lists[:i] if i &amp;gt; 0 else []
  return essential_postings_lists, non_essential_postings_lists
def MaxScore(Query, document_collection):
  &quot;&quot;&quot;
  Performs MaxScore retrieval.

  Args:
    Query: A list of Query terms (strings).
    document_collection: collection of documents to be searched.

  Returns:
    A list of (documentID, score) tuples, ranked in descending order of score.
  &quot;&quot;&quot;

  inverted_index = document_collection.get_inverted_index()
  postings_lists = [inverted_index[term] for term in `Query`]
  top_k = []
  threshold = 0
  current_doc_id = None
  sort(postings_lists, key=lambda x: x.upperbound, reverse=True)
  while True:
    es_posting_list, unes_posting_list = split_postings_lists(postings_lists, threshold)
    doc_id = OR(essential_postings_lists)
    if doc_id in non_essential_postings_lists:
      score = compute_score(Query, doc_id, document_collection)
      if len(top_k) &amp;lt; k:
        heapq.heappush(top_k, (score, doc_id))
      elif score &amp;gt; top_k[0][0]:
        heapq.heappushpop(top_k, (score, doc_id))
        threshold = top_k[0][0]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面我们再从一个例子来详细说明&lt;code&gt;MaxScore&lt;/code&gt;的过程&lt;/p&gt;
&lt;p&gt;假设我们的&lt;code&gt;Query&lt;/code&gt;是&quot;猫 狗 老鼠&quot;，求解Top2，
假定&lt;/p&gt;
&lt;p&gt;&lt;code&gt;猫&lt;/code&gt;在整个文档集合中最大的得分为0.1&lt;/p&gt;
&lt;p&gt;&lt;code&gt;狗&lt;/code&gt;在整个文档集合中最大的得分为0.4&lt;/p&gt;
&lt;p&gt;&lt;code&gt;老鼠&lt;/code&gt;在整个文档集合中最大的得分为0.9。&lt;/p&gt;
&lt;p&gt;下图为初始状态&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20250115232125.png&quot; alt=&quot;MaxScore_init&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在最开始的情况下，我们的阈值为0，因此所有倒排链都是&lt;code&gt;essential&lt;/code&gt;倒排链。
我们只需对&lt;code&gt;Doc ID&lt;/code&gt;最小的文档进行算分，将其放入堆中。将对应的倒排链移动到下一个文档。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20250115232212.png&quot; alt=&quot;MaxScore&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因为堆中的文档数小于2，我们的阈值依旧为0，因此我们继续对&lt;code&gt;Doc ID&lt;/code&gt;最小的文档进行算分，将其放入堆中。然后我们将对应的倒排链移动到下一个文档。
此时堆的文档数等于2，我们更新阈值为0.3。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20250115232350.png&quot; alt=&quot;MaxScore2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此时我们划分倒排链，我们发现&lt;code&gt;猫&lt;/code&gt;的upperbound(0.1) + &lt;code&gt;狗&lt;/code&gt;的upperbound(0.4) = 0.5 &amp;gt; 0.3，因此我们将&lt;code&gt;猫&lt;/code&gt;的倒排链划分为&lt;code&gt;non-essential&lt;/code&gt;倒排链，&lt;code&gt;狗&lt;/code&gt;和&lt;code&gt;老鼠&lt;/code&gt;的倒排链作为&lt;code&gt;essential&lt;/code&gt;倒排链。
我们对&lt;code&gt;essential&lt;/code&gt;倒排链进行OR检索，得到文档9，因为&lt;code&gt;non-essential&lt;/code&gt;倒排链中不包含文档9，因此我们计算文档9的得分(0.8), 更新TopK文档，更新阈值为0.7。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20250115232503.png&quot; alt=&quot;MaxScore3&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们继续划分倒排链，此时需要将所有的倒排链全部加起来才能超过阈值，因此我们将&lt;code&gt;猫&lt;/code&gt;和&lt;code&gt;狗&lt;/code&gt;的倒排链划分为&lt;code&gt;non-essential&lt;/code&gt;倒排链。
&lt;code&gt;老鼠&lt;/code&gt;的倒排链作为&lt;code&gt;essential&lt;/code&gt;倒排链。因为&lt;code&gt;essential&lt;/code&gt;倒排链只有一条，我们直接遍历这条倒排链，得到文档18，
因为&lt;code&gt;non-essential&lt;/code&gt;倒排链中包含文档18，计算文档18的得分(0.4), 无需更新TopK文档。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20250115232629.png&quot; alt=&quot;MaxScore4&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因为&lt;code&gt;essential&lt;/code&gt;倒排链全部计算完毕，所以我们结束检索，返回结果。&lt;/p&gt;
&lt;h3&gt;Compare WAND and MaxScore&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;WAND&lt;/code&gt;和&lt;code&gt;MaxScore&lt;/code&gt;都是基于&lt;code&gt;DAAT&lt;/code&gt;的优化方法，它们的优势在于，可以根据当前的得分，动态的调整阈值，从而减少不必要的计算以及磁盘读取。
他们之间的优劣难以比较，在实际应用中，也无法说某一个方法一定比另一个方法好。
&lt;code&gt;WAND&lt;/code&gt;相较于&lt;code&gt;MaxScore&lt;/code&gt;的优势在于，它可以更激进的剪枝，从而减少计算次数。而它的劣势在于每一次检索时，都需要对倒排链进行排序
在&lt;code&gt;Query&lt;/code&gt;较长时，排序的开销会变得很大。因此在&lt;code&gt;Query&lt;/code&gt;较长时，&lt;code&gt;MaxScore&lt;/code&gt;往往会比&lt;code&gt;WAND&lt;/code&gt;更快。而在文档数量较大时，&lt;code&gt;WAND&lt;/code&gt;往往会比&lt;code&gt;MaxScore&lt;/code&gt;更快。&lt;/p&gt;
&lt;h3&gt;What&apos;s Next&lt;/h3&gt;
&lt;p&gt;我们在这里仅仅简单介绍了检索中算分的一些简单方法，而更加进阶的&lt;code&gt;BlockMaxScore&lt;/code&gt;，&lt;code&gt;BlockMaxWAND&lt;/code&gt;等方法，留待以后的文章中介绍。&lt;/p&gt;
</content:encoded><category>Full Text Search</category><category>tf-idf</category><category>bm25</category><category>文本相关性</category><author>tang-hi</author></item><item><title>fsync is Costly, But Don&apos;t Avoid It</title><link>https://tangdh.life/posts/interesting/fsync/</link><guid isPermaLink="true">https://tangdh.life/posts/interesting/fsync/</guid><description>Is fsync slow, And If So, What Can You Do About It?</description><pubDate>Tue, 20 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Last week, our newly developed service experienced slow performance when processing real-time data. It took an unreasonably long time—up to 12 hours—to process just 8 million records. To simplify the process code for better readability, I&apos;ve included a simplified version below.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int
process() {
  // .........

  // open many files, store the file descriptor in an array
  int *fd = create_files(FILE_NUMBER, O_RDWR | O_CREAT);

  // each file write some bytes and fsync
  for (int i = 0; i &amp;lt; FILE_NUMBER; i++) {
    if (write(fd[i], Buffer, 200) != 200) {
      printf(&quot;Failed to write to file\n&quot;);
      return -1;
    }
    if (fsync(fd[i]) == -1) {
      printf(&quot;Failed to fsync file\n&quot;);
      return -1;
    }
  }
  

  if (close_files(fd, FILE_NUMBER) != 0) {
    printf(&quot;Failed to close files\n&quot;);
    return -1;
  }
  // ......
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At first glance, this code pattern is commonly seen in data-oriented systems and shouldn&apos;t cause any performance degradation. However, after a thorough investigation, I discovered that the root cause of the slow performance was the &lt;code&gt;fsync&lt;/code&gt; system call, which is essential for guaranteeing data integrity but can significantly slow down system processes.&lt;/p&gt;
&lt;p&gt;Why is this the case? &lt;code&gt;fsync&lt;/code&gt; is used so widely in storage systems that you&apos;ll find it in almost any codebase. But why is it a bottleneck for our service? To answer this question, I conducted a survey of &lt;code&gt;fsync&lt;/code&gt; and wrote this blog to delve into the details.&lt;/p&gt;
&lt;p&gt;This blog will cover the following topics:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What is &lt;code&gt;fsync&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;How slow is &lt;code&gt;fsync&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Why does it impact our system so much?&lt;/li&gt;
&lt;li&gt;What can we do to alleviate its impact?&lt;/li&gt;
&lt;li&gt;Lessons Learned&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Please note: All experiments were conducted on my home PC. The measured numbers may vary due to differences in hardware and operating systems. However, the results and conclusions should remain consistent.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;What is &lt;code&gt;fsync&lt;/code&gt;?&lt;/h2&gt;
&lt;p&gt;The Linux fsync(2) man page provides a clear explanation:&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;fsync() transfers (&quot;flushes&quot;) all modified in-core data of 
(i.e.,modified buffer cache pages for) the file referred to
by the filedescriptor fd to the disk device (or other permanent
storage device) so that all changed information can be retrieved
even if the system crashes or is rebooted. This includes writing
through or flushing a disk cache if present. The call blocks until
the device reports that the transfer has completed.
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;In short, &lt;code&gt;fsync&lt;/code&gt; ensures data written to a file is safely stored on the storage device.  We can delve deeper by examining the code itself. Here&apos;s a snippet from the Linux kernel&apos;s &lt;a href=&quot;https://github.com/torvalds/linux/blob/master/fs/ext4/fsync.c&quot;&gt;ext4/fsync.c&lt;/a&gt; file. This code demonstrates how &lt;code&gt;fsync&lt;/code&gt; works at a lower level.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int ext4_sync_file(struct file *file, loff_t start, loff_t end, int datasync)
{
	int ret = 0, err;
	bool needs_barrier = false;
	struct inode *inode = file-&amp;gt;f_mapping-&amp;gt;host;
  // ......

	ret = file_write_and_wait_range(file, start, end);
	if (ret)
		goto out;

	/*
	 *  The caller&apos;s filemap_fdatawrite()/wait will sync the data.
	 *  Metadata is in the journal, we wait for proper transaction to
	 *  commit here.
	 */
	ret = ext4_fsync_journal(inode, datasync, &amp;amp;needs_barrier);

issue_flush:
	if (needs_barrier) {
		err = blkdev_issue_flush(inode-&amp;gt;i_sb-&amp;gt;s_bdev);
		if (!ret)
			ret = err;
	}
out:
	err = file_check_and_advance_wb_err(file);
	if (ret == 0)
		ret = err;
	trace_ext4_sync_file_exit(inode, ret);
	return ret;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ext4_sync_file&lt;/code&gt; is the function will invoked when you call &lt;code&gt;fsync&lt;/code&gt;. It involves 3 steps.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;file_write_and_wait_range&lt;/code&gt; will write all dirty pages belonging to the file to the disk.Then the dirty page will sit in the disk volatile cache (still will lost when power outrage)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ext4_fsync_journal&lt;/code&gt; will write the inode&apos;s metadata to the disk. After this step, This file will still exist even when OS kernel is crash.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;blkdev_issue_flush&lt;/code&gt; will issue a flush operation to the block device and waits until it&apos;s finished. This operation will tell block device it should flush its volatile cache to the persistent storage.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;As long as the block device manufacturer adheres to the Linux contract, &lt;code&gt;fsync&lt;/code&gt; will reliably ensure your data is safe and sound&lt;/p&gt;
&lt;h2&gt;How Slow is &lt;code&gt;fsync&lt;/code&gt;?&lt;/h2&gt;
&lt;p&gt;In this section, I’ll create a simple C program to measure the speed of &lt;code&gt;fsync&lt;/code&gt;. The program will open a file, call &lt;code&gt;fsync&lt;/code&gt; 10,000 times, and calculate the average &lt;code&gt;fsync&lt;/code&gt; time. We’ll also compare the time it takes to write 200 bytes to files with and without using &lt;code&gt;fsync&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void
fsync_speed() {
  clr_pgcache();
  int fd = open(&quot;file&quot;, O_RDWR | O_CREAT, 0666);
  if (fd == -1) {
    printf(&quot;Failed to open file\n&quot;);
    return;
  }

  MEASURE_TIME_START();
  for (int i = 0; i &amp;lt; 10000; i++) {
    if (fsync(fd) == -1) {
      printf(&quot;Failed to fsync file\n&quot;);
      return;
    }
  }
  MEASURE_TIME_END();
  if (close(fd) == -1) {
    printf(&quot;Failed to close file\n&quot;);
    return;
  }
  if (remove(&quot;file&quot;) == -1) {
    printf(&quot;Failed to remove file\n&quot;);
    return;
  }
  unsigned long cost_ms = MEASURE_TIME_MS();
  printf(&quot;fsync avg time: %ld ms\n&quot;, cost_ms);
}

int
without_fsync() {
  if (clr_pgcache() != 0) {
    printf(&quot;Failed to clear page cache\n&quot;);
    return -1;
  }

  // open many files, store the file descriptor in an array
  int *fd = create_files(FILE_NUMBER, O_RDWR | O_CREAT);

  MEASURE_TIME_START();
  // each file write some bytes
  for (int i = 0; i &amp;lt; FILE_NUMBER; i++) {
    if (write(fd[i], Buffer, 200) != 200) {
      printf(&quot;Failed to write to file\n&quot;);
      return -1;
    }
  }
  MEASURE_TIME_END();

  if (close_files(fd, FILE_NUMBER) != 0) {
    printf(&quot;Failed to close files\n&quot;);
    return -1;
  }

  return MEASURE_TIME_US();
}

int
with_fsync() {
  if (clr_pgcache() != 0) {
    printf(&quot;Failed to clear page cache\n&quot;);
    return -1;
  }

  // open many files, store the file descriptor in an array
  int *fd = create_files(FILE_NUMBER, O_RDWR | O_CREAT);

  MEASURE_TIME_START();
  // each file write some bytes
  for (int i = 0; i &amp;lt; FILE_NUMBER; i++) {
    if (write(fd[i], Buffer, 200) != 200) {
      printf(&quot;Failed to write to file\n&quot;);
      return -1;
    }
    if (fsync(fd[i]) == -1) {
      printf(&quot;Failed to fsync file\n&quot;);
      return -1;
    }
  }
  MEASURE_TIME_END();

  if (close_files(fd, FILE_NUMBER) != 0) {
    printf(&quot;Failed to close files\n&quot;);
    return -1;
  }

  return MEASURE_TIME_MS();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The measurement results are shown below.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Fsync&lt;/th&gt;
&lt;th&gt;Write&lt;/th&gt;
&lt;th&gt;Write(fsync)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Average time&lt;/td&gt;
&lt;td&gt;2 ms&lt;/td&gt;
&lt;td&gt;430 us&lt;/td&gt;
&lt;td&gt;154 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;As Jeff Dean suggested in his famous article &apos;Latency Numbers Every Programmer Should Know,&apos; we can consider &lt;code&gt;fsync&lt;/code&gt; equivalent to &lt;a href=&quot;https://colin-scott.github.io/personal_website/research/interactive_latency.html&quot;&gt;a disk seek&lt;/a&gt;. While &lt;code&gt;fsync&lt;/code&gt; is relatively slow, it&apos;s the only way to guarantee our data is safely stored on the storage device.&lt;/p&gt;
&lt;h2&gt;Why does it impact our system so much?&lt;/h2&gt;
&lt;p&gt;&quot;As we saw in the previous section, &lt;code&gt;fsync&lt;/code&gt; is relatively slow. However, nearly all storage systems, including databases, distributed file systems, and object storage, use &lt;code&gt;fsync&lt;/code&gt; to ensure data integrity. How can they achieve high performance while using &lt;code&gt;fsync&lt;/code&gt;? After careful investigation, I discovered that the issue lies not with &lt;code&gt;fsync&lt;/code&gt; itself but with our design flaws.&lt;/p&gt;
&lt;h3&gt;1. Too many files&lt;/h3&gt;
&lt;p&gt;To achieve high concurrency, we divided the entire dataset into multiple Index Files called &lt;code&gt;Atomic Indexes&lt;/code&gt;. We initially believed this would reduce thread contention, allowing multiple threads to read their respective &lt;code&gt;Atomic Indexes&lt;/code&gt; without interfering with each other. However, this approach introduced a significant problem: we needed to call &lt;code&gt;fsync&lt;/code&gt; for each file when dumping in-memory data to disk. A typical service we maintain contains hundreds of &lt;code&gt;Atomic Indexes&lt;/code&gt;, so the time spent waiting for &lt;code&gt;fsync&lt;/code&gt; to complete can easily outweigh the benefits of high concurrency.&lt;/p&gt;
&lt;h3&gt;2. Misuse of  &lt;code&gt;fsync&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;fsync&lt;/code&gt; is a resource-intensive system call that should only be used for critical files that you cannot afford to lose. According to our design, the service will reply with real-time data even if the server crashes. Therefore, we can treat &lt;code&gt;Atomic Indexes&lt;/code&gt; as files that can be regenerated, making it unnecessary to call &lt;code&gt;fsync&lt;/code&gt; for them.&lt;/p&gt;
&lt;h2&gt;What can we do to alleviate its impact?&lt;/h2&gt;
&lt;p&gt;Although in my case simply removing the &lt;code&gt;fsync&lt;/code&gt; call provided significant performance improvements, I&apos;m curious about other strategies for mitigating the expensive cost of &lt;code&gt;fsync&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Direct I/O&lt;/h3&gt;
&lt;p&gt;One potential approach is to use Direct I/O, which bypasses the page cache and directly transfers data between the application and the block device. This could potentially reduce the overhead of &lt;code&gt;fsync&lt;/code&gt;, as there would be no need to flush dirty pages. To validate this idea, I wrote the following code&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int
direct_write_fsync() {
  if (clr_pgcache() != 0) {
    printf(&quot;Failed to clear page cache\n&quot;);
    return -1;
  }

  // open many files, store the file descriptor in an array
  int *fd = create_files(FILE_NUMBER, O_RDWR | O_CREAT | O_DIRECT);

  MEASURE_TIME_START();
  // each file write some bytes
  for (int i = 0; i &amp;lt; FILE_NUMBER; i++) {
    if (write(fd[i], Buffer, 200) != 200) {
      printf(&quot;Failed to write to file\n&quot;);
      return -1;
    }
    if (fsync(fd[i]) == -1) {
      printf(&quot;Failed to fsync file\n&quot;);
      return -1;
    }
  }
  MEASURE_TIME_END();

  if (close_files(fd, FILE_NUMBER) != 0) {
    printf(&quot;Failed to close files\n&quot;);
    return -1;
  }

  return MEASURE_TIME_MS();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Direct I/O&lt;/th&gt;
&lt;th&gt;Write(fsync)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Average Time&lt;/td&gt;
&lt;td&gt;153 ms&lt;/td&gt;
&lt;td&gt;154 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Unfortunately, our experiments showed that Direct I/O didn&apos;t provide significant performance improvements. While it&apos;s true that &lt;code&gt;fsync&lt;/code&gt; wouldn&apos;t need to flush the page cache in this scenario, Direct I/O itself has increased overhead and may not fully utilize &lt;a href=&quot;https://en.wikipedia.org/wiki/Allocate-on-flush&quot;&gt;delayed allocation&lt;/a&gt;. As a result, the time saved by skipping page cache flushing was offset by the slower Direct I/O writes, leading to negligible overall performance gains&lt;/p&gt;
&lt;h3&gt;io_uring&lt;/h3&gt;
&lt;p&gt;Since &lt;code&gt;fsync&lt;/code&gt; involves synchronizing data from the system cache to the disk, which can be a bottleneck, we can explore using io_uring to potentially improve performance. Introduced in Linux kernel version 5.1, io_uring allows submitting asynchronous I/O operations. The kernel worker threads handle these operations and notify the user space when they complete. I won&apos;t delve into the details of io_uring here, but you can refer to the &lt;a href=&quot;https://kernel.dk/io_uring.pdf&quot;&gt;Kernel Documentation&lt;/a&gt;for further information. Below, I&apos;ll showcase code that tests performance improvements using io_uring&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int
io_uring() {
  if (clr_pgcache() != 0) {
    printf(&quot;Failed to clear page cache\n&quot;);
    return -1;
  }

  int *fd = create_files(FILE_NUMBER, O_RDWR | O_CREAT);

  struct io_uring ring;
  if (setup_io_uring(&amp;amp;ring) != 0) {
    printf(&quot;Failed to setup io_uring\n&quot;);
    return -1;
  }

  MEASURE_TIME_START();
  for (int i = 0; i &amp;lt; FILE_NUMBER; i++) {
    submit_write_request(&amp;amp;ring, fd[i], 0, Buffer, BYTES_NUMBER);
    submit_fsync_request(&amp;amp;ring, fd[i]);
  }
  wait_for_all_operations(&amp;amp;ring, 2 * FILE_NUMBER);
  MEASURE_TIME_END();
  close_files(fd, FILE_NUMBER);
  return MEASURE_TIME_MS();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Direct I/O&lt;/th&gt;
&lt;th&gt;Write(fsync)&lt;/th&gt;
&lt;th&gt;Io_uring&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Average Time&lt;/td&gt;
&lt;td&gt;153 ms&lt;/td&gt;
&lt;td&gt;154 ms&lt;/td&gt;
&lt;td&gt;30 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Our experimental results demonstrate that io_uring significantly improved performance. This is primarily due to the asynchronous nature of io_uring. Instead of waiting for each &lt;code&gt;fsync&lt;/code&gt; call to return, we can submit I/O requests and then process them all at once when there are no other tasks to handle&lt;/p&gt;
&lt;h2&gt;Lessons Learned&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Avoid excessive file fragmentation.&lt;/p&gt;
&lt;p&gt;Having too many files can quickly consume your operating system&apos;s file descriptors and introduce higher latency compared to reading from or writing to a single file. You can find the experimental code &lt;a href=&quot;https://gist.github.com/tang-hi/0fbf5ca3087beed9c501668caffd2e52&quot;&gt;here&lt;/a&gt;. If your &lt;strong&gt;critical&lt;/strong&gt; data is split across multiple files, the situation worsens. You&apos;ll spend significant time waiting for &lt;code&gt;fsync&lt;/code&gt; to complete. Compared to the various problems caused by dividing data into multiple files, the minor advantage of multithreading is insignificant.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use &lt;code&gt;fsync&lt;/code&gt; judiciously.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;fsync&lt;/code&gt; is a resource-intensive system call. Use it sparingly but strategically. If you can&apos;t recover your data file once it&apos;s corrupted, &lt;code&gt;fsync&lt;/code&gt; is essential to ensure its integrity. Otherwise, avoid using it and implement a mechanism to regenerate the file if necessary.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Harness the power of &lt;code&gt;io_uring&lt;/code&gt;.
&lt;code&gt;io_uring&lt;/code&gt; is a powerful and user-friendly tool. Consider incorporating it into your next project.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><category>Interesting</category><author>tang-hi</author></item><item><title>设计一款自己的代码配色</title><link>https://tangdh.life/posts/design/code-color-theme/</link><guid isPermaLink="true">https://tangdh.life/posts/design/code-color-theme/</guid><description>从色彩的基本知识开始, 设计出自己专属的代码配色</description><pubDate>Sat, 29 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近看了一本设计相关的科普读物&lt;a href=&quot;https://book.douban.com/subject/26657933/&quot;&gt;写给大家看的设计书&lt;/a&gt;,
因此萌生了利用所学的知识, 设计一款代码配色。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style = &quot;text align: left&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/color-scheme.jpg&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;整个过程相当简单，如果目标只是设计出一款不丑的配色。那么在了解基本原理后, 大概10分钟就可以完成。&lt;/p&gt;
&lt;h2&gt;前置知识&lt;/h2&gt;
&lt;h3&gt;色轮的概念&lt;/h3&gt;
&lt;p&gt;色轮是由三原色(红，绿，蓝)组成的。假定我们在一个圆环上只放置这三种颜色, 我们可以得到如下的色轮。 我们再对这三种颜色其进行两两组合，我们就有了六种颜色。
我们使用上述的方法不断重复下去，我们就得到了完整的色轮。
&amp;lt;div style=&quot;display: flex; justify-content: center; align-items: center&quot;&amp;gt;
&amp;lt;div style = &quot;text align: left&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/color_wheel_1.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;div style = &quot;text align: left&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/color_wheel_2.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;div style = &quot;text align: left&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/color_wheel_4.jpg&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h3&gt;暗色和亮色&lt;/h3&gt;
&lt;p&gt;上述色轮的概念仅仅只是色调，我们可以通过往色调的基础上添加黑色或者白色，来得到暗色和亮色。这样我们就可以得到更多的颜色。
下面则是一个简单的例子(从左至右依次是，原色调，暗色，亮色)。
&amp;lt;div style = &quot;text align: left&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/dark-light.jpg&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h3&gt;互补色(complementary)&lt;/h3&gt;
&lt;p&gt;色轮上相对的颜色即为互补色。在设计中我们往往采用一种作为主色，而另一种颜色用于强调。我们从下面的例子中，可以直观感受到互补色的对比。
&amp;lt;div style = &quot;text align: left&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/complementary-color-banner.webp&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h3&gt;三色组(Triadic)&lt;/h3&gt;
&lt;p&gt;在色轮上，我们可以找到三种颜色，它们之间的角度相差120度，这三种颜色就是三色组。三色组的颜色搭配会显得和谐, 看上去令人愉悦。
比如红，黄，蓝就是一个三色组。儿童产品往往就会采用这种组合，最经典的例子就是超人。
&amp;lt;div style = &quot;text align: left&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/superman.avif&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h3&gt;分裂互补三色组(Split)&lt;/h3&gt;
&lt;p&gt;分裂互补三色组，是从色轮的一边选择一种颜色，再在色轮上找到他的互补色，但是并不直接使用这个互补色，而是使用这个互补色两侧的颜色。
这样的组合往往会有一种更为细致的颜色边界。可以通过下面的例子，更直观的感受这一点。
&amp;lt;div style = &quot;text align: left&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/split-complementry.jpg&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h3&gt;类似色(Analogous)&lt;/h3&gt;
&lt;p&gt;类似色是指在色轮上相邻的颜色。因为这种颜色组合有相同的基础色，所以这种颜色组合看上去会很和谐，但是缺少对比。
因此在设计中，我们往往会加入一些对比色，来增加视觉效果。下面的这幅画就是类似色的组合。
&amp;lt;div style = &quot;text align: left&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/analogous.jpg&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;开始设计&lt;/h2&gt;
&lt;p&gt;在介绍了上述的基本知识后，我们就可以开始设计我们的代码配色了。&lt;/p&gt;
&lt;h3&gt;颜色配置文件&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;VSCode&lt;/strong&gt; 的颜色配置在&lt;code&gt;setting.json&lt;/code&gt;中, 我们可以通过 &lt;code&gt;cmd + shift + P&lt;/code&gt; 唤起&lt;strong&gt;VSCode&lt;/strong&gt;的命令面板, 然后输入setting, 找到我们的配置文件。
&amp;lt;div style = &quot;text align: left&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20240629163617.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;然后在&lt;code&gt;setting.json&lt;/code&gt;中增加一个配置选项&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;editor.tokenColorCustomizations&quot;: {

    &quot;[Default Light+]&quot;: {  // 这里的Default Light+ 是你当前使用的主题
        &quot;keywords&quot;: &quot;#C752A5&quot;, // 关键字的配色
        &quot;comments&quot;: &quot;#A6B3E2&quot;, // 注释的配色
        &quot;variables&quot;: &quot;#3E4D19&quot;, // 变量的配色
        &quot;functions&quot;: &quot;#52A5C7&quot;, // 函数的配色
        &quot;types&quot;: &quot;#C46E4A&quot;,  // 类型的配色
        &quot;strings&quot;: &quot;#C7526A&quot;, // 字符串的配色
        &quot;numbers&quot;: &quot;#7F6D29&quot;, // 数字的配色
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配色选择&lt;/h3&gt;
&lt;p&gt;推荐使用Figma的色轮工具，可以很方便的找到你需要的颜色。&lt;a href=&quot;https://www.figma.com/color-wheel/&quot;&gt;Figma色轮工具&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我们先将所有的颜色都设置为黑色。
&amp;lt;div style = &quot;text align: left; width:auto; height:20%&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20240629164926.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h4&gt;函数，关键字，变量&lt;/h4&gt;
&lt;p&gt;因为我认为代码是由函数组成的，所以我决定将函数的颜色作为主色，同时我不想让整个配色显得太热烈，因此我选择了蓝色作为主色。最终选择了&lt;code&gt;#52A5C7&lt;/code&gt;作为主色。
这个颜色的三色组为&lt;code&gt;#C752A5&lt;/code&gt;和&lt;code&gt;#A5C752&lt;/code&gt;，我们可以将这两种颜色作为&lt;strong&gt;关键字&lt;/strong&gt;和&lt;strong&gt;变量&lt;/strong&gt;的配色。&lt;br /&gt;
&amp;lt;div style = &quot;text align: left&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20240629170833.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;呈现出的效果如下图所示
&amp;lt;div style = &quot;text align: left&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20240629171135.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;可以看到变量的颜色过于亮了，我们可以将其调暗一些。最终选择了&lt;code&gt;#3E4D19&lt;/code&gt;这一暗色作为变量的配色。&lt;/p&gt;
&lt;h4&gt;类型，注释&lt;/h4&gt;
&lt;p&gt;函数和类型因为往往是在一起的，因此我们选择主色的补色，同时为了让注释不是那么显眼，我们选择了主色的相似色。
从左至右依次为主色，补色，相似色。
&amp;lt;div style=&quot;display: flex; justify-content: center; align-items: center&quot;&amp;gt;
&amp;lt;div style = &quot;text align: left&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20240629172449.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;div style = &quot;text align: left&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20240629172515.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;div style = &quot;text align: left&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20240629172532.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;呈现出的效果如下图所示&lt;/p&gt;
&lt;p&gt;&amp;lt;div style = &quot;text align: left&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20240629172712.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;为了让效果更加和谐，我们修改了对应颜色的亮度，最终选择了&lt;code&gt;#A6B3E2&lt;/code&gt;作为注释的配色，&lt;code&gt;#C46E4A&lt;/code&gt;作为类型的配色。&lt;/p&gt;
&lt;h4&gt;字符串，数字&lt;/h4&gt;
&lt;p&gt;为了让字符串, 数字和函数可以和谐共处，我们采用了主色的分裂互补三色组,并相应的修改了亮度。最终呈现的效果为&lt;/p&gt;
&lt;p&gt;&amp;lt;div style = &quot;text align: left&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20240629173225.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h4&gt;完整配置文件&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&quot;keywords&quot;: &quot;#C752A5&quot;,
&quot;comments&quot;: &quot;#A6B3E2&quot;,
&quot;variables&quot;: &quot;#3E4D19&quot;,
&quot;functions&quot;: &quot;#52A5C7&quot;,
&quot;types&quot;: &quot;#C46E4A&quot;,
&quot;strings&quot;: &quot;#C7526A&quot;,
&quot;numbers&quot;: &quot;#7F6D29&quot;,
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;当然色彩领域的知识还有很多, 我学到的也只是皮毛，但是能通过学到的知识创造出一款还看得过去的代码配色，也是一件令人开心的事情。
希望这篇文章能够帮助到你，也希望你能够设计出一款属于自己的代码配色。&lt;/p&gt;
</content:encoded><category>Design</category><category>Color</category><category>Tools</category><category>VsCode</category><author>tang-hi</author></item><item><title>期权学习笔记</title><link>https://tangdh.life/posts/invest/option/</link><guid isPermaLink="true">https://tangdh.life/posts/invest/option/</guid><description>期权是一种衍生证券，它的价值取决于或衍生于其他证券的价格. 本文是对期权的学习笔记</description><pubDate>Sun, 19 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;h2&gt;什么是期权?&lt;/h2&gt;
&lt;p&gt;期权可以认为是一种权力。它赋予你，可以在某个时间段以特定价格买入或者卖出股票的权利。&lt;/p&gt;
&lt;p&gt;这么说可能不是很好懂，我们通过几个例子来解释这件事。&lt;/p&gt;
&lt;p&gt;假设有这么一个人(甲)，他的意见总是和你相反。你觉得阿里巴巴的股票会涨，他就觉得会跌。你觉得阿里巴巴的股票会跌，他就觉得会涨。同时，你们都希望对方为自己的认识付出代价。你们由此设计出了一套协议叫做&lt;code&gt;期权 (option)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;假设现在阿里巴巴的股价是80美金&lt;/p&gt;
&lt;h3&gt;1. 你认为会涨，甲认为会跌&lt;/h3&gt;
&lt;p&gt;在这种情况下，甲对你提议说，既然你认为阿里巴巴的股价仍旧会涨，那我就卖给你&lt;code&gt;看涨期权 (call option)&lt;/code&gt;，行权价为82，有效期为一个月。拥有这份期权就意味着在这一个月的时间里，无论阿里巴巴的股价如何变化，你都可以用一股82美金的价格来向甲&lt;strong&gt;买入&lt;/strong&gt;阿里的股票。当然，购买这份期权，意味着你需要先支付少许的&lt;code&gt;权力金 (premium)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;那么在完成交易后会发生什么事呢？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;a. 阿里的股价超过了82美金&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在这种情况下，你可以使用便宜的价格来购买阿里股票&lt;code&gt;(你可以用82美金向甲买入阿里股票，然后马上在市场上卖掉，从而赚取利润)&lt;/code&gt;。
假设目前阿里的股价为&lt;code&gt;T&lt;/code&gt;美元, 那么你的收益则为 $$ T - 82 $$。 同时别忘了，为了购买这份期权，我们预先支付了一笔&lt;code&gt;权利金&lt;/code&gt;。 因此你的实际收益为 $$ T - 82 - premium $$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;b. 阿里的股价没有超过82美金.&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在这种情况下，你并不会选择以82美金的价格向甲购买阿里的股票(放弃行权)，因为这完全是一笔亏本买卖。
所以你会亏损你之前购买期权所付出的&lt;code&gt;权利金&lt;/code&gt;。 而甲则什么都没有损失，反而白赚了一笔你所付的&lt;code&gt;权利金&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2. 你认为会跌，甲认为会涨&lt;/h3&gt;
&lt;p&gt;在这种情况下，甲对你提议说，既然你认为阿里巴巴的股价会下跌，那我就卖给你&lt;code&gt;看跌期权 (put option)&lt;/code&gt;，行权价为78，有效期为一个月。
拥有这份期权就意味着在这一个月的时间里，无论阿里巴巴的股价如何变化，你都可以用一股78美金的价格向甲&lt;strong&gt;卖出&lt;/strong&gt;阿里的股票。同样的，购买这份期权，你也需要先支付少许的&lt;code&gt;权力金&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;那么在完成交易后会发生什么事呢？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;a. 阿里的股价超跌破78美金&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在这种情况下，你可以用高于市场的价格来卖出阿里股票&lt;code&gt;(在股票市场上买入阿里股票，然后再高价卖给甲，从而赚取利润)&lt;/code&gt;。
假设目前阿里的股价为&lt;code&gt;T&lt;/code&gt;美元, 那么你的收益则为 $$ 78 - T $$。 同时别忘了，为了购买这份期权，我们预先支付了一笔&lt;code&gt;权利金&lt;/code&gt;。 因此你的实际收益为 $$ 78 - T - premium $$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;b. 阿里的股价高于78美金.&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在这种情况下，你并不会选择以78美金的价格卖给甲(放弃行权)，因为这也是一笔亏本买卖。
所以你会亏损你之前购买期权所付出的&lt;code&gt;权利金&lt;/code&gt;。 而甲则白赚了一笔你所付的&lt;code&gt;权利金&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上述的&lt;code&gt;看涨期权(call option)&lt;/code&gt; 和 &lt;code&gt;看跌期权(put option)&lt;/code&gt; 就是我们一直所说的的期权。
在现实生活中，期权的购买单位为张(100股为一张), 也就是说一张期权等于100股股票&lt;code&gt;购买/卖出&lt;/code&gt;的权利。&lt;/p&gt;
&lt;p&gt;值得注意的是, 期权的卖出并不需要你有相应的股票，你可以直接在市场上卖出期权，并立刻获得买方所付的权利金，只要你的保证金足够。&lt;/p&gt;
&lt;h2&gt;期权有什么用?&lt;/h2&gt;
&lt;h3&gt;投机&lt;/h3&gt;
&lt;p&gt;期权相对于股票而言，它对价格的波动更加敏感。
假设，阿里巴巴当前股价为&lt;code&gt;80美金&lt;/code&gt;， 你购买了一张行权价为&lt;code&gt;81美金&lt;/code&gt;的期权。期权的价格为&lt;code&gt;1.2美金&lt;/code&gt;一股。那么当阿里巴巴的股价涨到&lt;code&gt;83美金&lt;/code&gt;时, 期权的价格至少为&lt;strong&gt;2&lt;/strong&gt;美金(83 - 81)。&lt;/p&gt;
&lt;p&gt;期权与股票的利润对比，可参考如下表格。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;期权/股票&lt;/th&gt;
&lt;th&gt;本金&lt;/th&gt;
&lt;th&gt;利润&lt;/th&gt;
&lt;th&gt;浮盈&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;期权&lt;/td&gt;
&lt;td&gt;120$&lt;/td&gt;
&lt;td&gt;200$&lt;/td&gt;
&lt;td&gt;66.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;股票&lt;/td&gt;
&lt;td&gt;8000$&lt;/td&gt;
&lt;td&gt;8300$&lt;/td&gt;
&lt;td&gt;3.75%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;可以看到在股价相同的波动下，期权可以获得更高的利润率。&lt;/p&gt;
&lt;h3&gt;对冲&lt;/h3&gt;
&lt;p&gt;期权因为其高杠杆，它不仅可以进行投机，还可以用来对冲风险, 在股票市场上我们一般有如下策略。&lt;/p&gt;
&lt;h4&gt;保护性看跌期权(protective put)&lt;/h4&gt;
&lt;p&gt;如果你想要持有一只股票，但是你不想在它下跌时蒙受太大的损失，那么你可以在购买这只股票的同时，买入它的看跌期权。&lt;/p&gt;
&lt;p&gt;假定你在阿里巴巴股价为&lt;code&gt;80美金&lt;/code&gt;时，买入了100股，并且买入了行权价为&lt;code&gt;78美金&lt;/code&gt;的&lt;code&gt;看跌期权(put)&lt;/code&gt;。那么你的最大损失为每股&lt;code&gt;2美金&lt;/code&gt;加&lt;code&gt;权利金&lt;/code&gt;。这样你就给你的损失设置了一个下限。利润与股价的关系如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/protective-put.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h4&gt;抛补看涨期权(covered call)&lt;/h4&gt;
&lt;p&gt;如果你在购买股票时，设置了一个止盈价位，那么你可以在买入股票的同时，同时卖出等量的&lt;code&gt;看涨期权(call)&lt;/code&gt;。这种策略可以进一步的增加你的利润。&lt;/p&gt;
&lt;p&gt;假定你在阿里巴巴股价为&lt;code&gt;80美金&lt;/code&gt;时，买入了100股，且你的目标价位为&lt;code&gt;85美金&lt;/code&gt;。那么你可以直接卖出100股&lt;code&gt;85美金&lt;/code&gt;的看涨期权，这样子当股价到达了&lt;code&gt;85美金&lt;/code&gt;时。你每股不仅赚到了&lt;code&gt;5美金&lt;/code&gt;的差额，同时还赚到了出售期权获得的&lt;code&gt;权利金&lt;/code&gt;。当然这种策略的缺点就是，当股价继续上涨时，超出85美金的部分就和你没有关系了。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/covered-call.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h4&gt;跨式期权(straddle)&lt;/h4&gt;
&lt;p&gt;当你觉得一家公司的后续股价，要么大涨，要么大跌(比如面临一个重要的官司)。你可以通过同时买入行权价相同，过期日期相同的&lt;code&gt;看涨期权(call)&lt;/code&gt;和&lt;code&gt;看跌期权(put)&lt;/code&gt;, 来赚取利润.&lt;/p&gt;
&lt;p&gt;假定你在阿里巴巴股价为&lt;code&gt;80美金&lt;/code&gt;时, 买入了行权价为&lt;code&gt;81美金&lt;/code&gt;的&lt;code&gt;看涨期权&lt;/code&gt;和&lt;code&gt;看跌期权&lt;/code&gt;。当阿里巴巴的股价变为&lt;code&gt;70美金时&lt;/code&gt;, 你的看跌期权每股将价值&lt;code&gt;11美金&lt;/code&gt;，完全可以覆盖你付出的&lt;code&gt;权利金&lt;/code&gt;。同样的当阿里巴巴的股价变为&lt;code&gt;90美金&lt;/code&gt;时，看涨期权每股将价值&lt;code&gt;9美金&lt;/code&gt;, 同样可以覆盖你付出的&lt;code&gt;权利金&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;但是当股价不变或者小幅波动时，你将会损失你购买期权所付出的&lt;code&gt;权利金&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/straddle.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h4&gt;双限期权(collar)&lt;/h4&gt;
&lt;p&gt;这种策略会对你的资产组合设置一个上限和下限。&lt;/p&gt;
&lt;p&gt;假设你持有100股阿里巴巴的股票。假定每股价格为&lt;code&gt;80美金&lt;/code&gt;, 你通过买入行权价为&lt;code&gt;70美金&lt;/code&gt;的保护性看跌期权，给你的股票价值设置了一个下限。但是购买期权需要付出&lt;code&gt;权利金&lt;/code&gt;，如果你不想出这笔钱，你可以卖出和&lt;code&gt;看跌期权(put)&lt;/code&gt;价值差不多的&lt;code&gt;看涨期权&lt;/code&gt;。 这样你相当于零成本的，为你的股票设置了上下限。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/collar.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h4&gt;价差套利(spread)&lt;/h4&gt;
&lt;p&gt;这种策略是当你发现了，不同价格的两个期权的定价有错误(一个比另一个便宜), 你可以通过买入相对便宜的期权，同时卖出相对贵的期权，来进行套利。&lt;/p&gt;
&lt;p&gt;假设，你觉得行权价为&lt;code&gt;90美金&lt;/code&gt;的看涨期权比行权价&lt;code&gt;100美金&lt;/code&gt;的看涨期权更便宜。那么你可以买入行权价为&lt;code&gt;90美金&lt;/code&gt;的看涨期权,同时卖出行权价为&lt;code&gt;100美金&lt;/code&gt;的看涨期权。这样只要股价超过&lt;code&gt;90美金&lt;/code&gt;, 你就是有利可图的。注意，这里说的便宜是指购买期权的权利金。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/spread.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;如何对期权定价?&lt;/h2&gt;
&lt;p&gt;那么一张行权价为&lt;code&gt;90美金&lt;/code&gt;的&lt;code&gt;看涨期权(call)&lt;/code&gt;该如何定价呢？如果当前股价是&lt;code&gt;100美金&lt;/code&gt;, 毫无疑问这个期权是有价值的，每一股可以赚到&lt;code&gt;10美金&lt;/code&gt;的差价。但如果当前股价是&lt;code&gt;80美金&lt;/code&gt;呢？这个期权的价值就一文不值吗？答案当然是否定的。因为期权除了&lt;code&gt;内在价值(intrinstic value)&lt;/code&gt;, 还有其&lt;code&gt;时间价值(time value)&lt;/code&gt;。 毕竟只要还没到期，股价就有可能一飞冲天。&lt;/p&gt;
&lt;p&gt;那么有没有一个合理的方式对期权进行定价？想要绝对客观的定价，是不可能的。我们只能基于一些假设来对期权进行定价。常用的方法有&lt;code&gt;二项式期权定价&lt;/code&gt;，以及&lt;code&gt;布莱克-斯科尔斯公式(Black-Scholes pricing formula)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;这里主要介绍一下二项式期权定价&lt;/p&gt;
&lt;p&gt;假定一个股票的价格为100美元，到年底这个股票有两种可能升到120，以及跌到90。当前无风险年利率为10%。我们现在计算一股行权价格为110美金，到期时间一年的期权应该价值(权利金)多少？&lt;/p&gt;
&lt;p&gt;我们首先可以得到，这个期权在到期后可能得到的收益，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/option-price.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;我们如果可以构建出一笔资产组合，使得其年底收益与该期权的收益相等，那么我们就可以对该期权进行定价。
假设我们现在有这么一个资产组合，价值100美元的股票和81.82元的借款。因为我们购买股票的钱，有81.82是借来的，因此我们个人出资18.18。同时因为利率为10%， 到期后我们需要还90元。这个资产组合的收益如何呢？如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/stock-price.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;可以看到这个资产组合的收益是期权的三倍。而我们的个人出资为18.18, 因此我们可以得到该期权的价格为$$18.18 / 3 = 6.06$$&lt;/p&gt;
&lt;p&gt;这里介绍的定价方法，仅仅是简化版的方法，现实生活中的定价方法复杂得多，要考虑的因素也更多，有兴趣的读者可以自己探索。&lt;/p&gt;
</content:encoded><category>option</category><category>invest</category><author>tang-hi</author></item><item><title>[译] Binary quantization</title><link>https://tangdh.life/posts/vector-search/bq/</link><guid isPermaLink="true">https://tangdh.life/posts/vector-search/bq/</guid><description>Binary quantization 是一种向量压缩技术, 在Weaviate中该技术被用来减少HNSW索引以及Flat Index的内存占用</description><pubDate>Sat, 13 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://weaviate.io/blog/binary-quantization&quot;&gt;原文链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;什么是 binary quantization?&lt;/h2&gt;
&lt;p&gt;目前的向量数据库会构建大规模的向量索引，并将向量索引放在内存中进行搜索。这样可以实现实时查询，但相应的成本也相应增加。Binary quantization(BQ) 是一种向量压缩算法，可以在内存占用和查询准确性之间做出权衡。&lt;/p&gt;
&lt;p&gt;我们可以通过类比的方式来了解这一技术。假设每个要存储的向量都像是一个家庭地址。这个地址可以精确地定位某人的居住位置，包括国家、州、城市、街道号甚至门牌号。但为了获得这种精确性，需要占用大量内存来存储、搜索和读取每个地址（详细的地址占用的内存更多）。同样地，在多维空间中定位一个向量，可以将向量中每个维度的数字视为沿该维度指定的方向移动的距离。&lt;/p&gt;
&lt;p&gt;Binary quantization(BQ) 的压缩过程是根据每个数字的符号将向量中的每一维转换为0（负数）或1（正数）。这听起来似乎有些不可思议，因为丢失了每个维度上的具体数字，那么如何精确定位该向量呢？尽管BQ听起来似乎不太可靠，但在高维度矢量上却能取得不错的效果。接下来让我们来看看原因！&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/bq_vecs1.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
二值化不仅适用于向量的压缩，我们还可以从其他领域理解其用途，比如在计算机视觉中。如果对一幅图像进行二值化，即对每个像素，如果大于某个阈值，则替换为1；否则，替换为0。这样生成的图像是黑白二进制图像。虽然会丢失图像细节，但显著地压缩了图像大小。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;display: flex; justify-content: space-between;&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/bp_vecs2.png&quot; alt=&quot;原始图片&quot; style=&quot;max-width: 45%;&quot;/&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/bp_vecs3.png&quot; alt=&quot;二值化后的图片&quot; style=&quot;max-width: 45%;&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;现在，让我们考虑一下将一个句子向量embedding进行二值化后会呈现怎样的形式。在下面的图示中，我们将句子：“All your vector embeddings belong to you!”转化为一个384维的向量。第一张图展示了向量的所有384个数字，每个数字都是一个32位的浮点数，在热力图上用颜色渐变的方式显示。每个向量维度上的数字决定了热力图上颜色渐变的程度。下面的图展示了相同的向量，但我们对向量进行了阈值处理，使得所有正值维度转换为1（白色），而负值维度转换为0（黑色）。因此，我们得到了一个黑白相间的热力图，看起来有点像条形码。这就是对向量进行Binary quantization(BQ)的效果！得到的向量大小要小得多，但也丢失了很多细节。&lt;/p&gt;
&lt;p&gt;Binary quantization(BQ)通过仅保留向量的方向来简化向量的编码。每个向量维度都用一个比特位编码，表示它是正还是负。例如，像 [12, 1, -100, 0.003, -0.001, 128, -1000, 0.0001] 这样的向量将被压缩为单个字节，结果是二进制序列[1,1,0,1,0,1,0,1]。通过将每个维度存储的数字从float32转换为1-bit，将每个向量占用的空间减少了32倍。然而，我们无法从BQ后的向量还原出原始向量——这使得这成为一种有损压缩技术。&lt;/p&gt;
&lt;h2&gt;使用二值化向量的细节&lt;/h2&gt;
&lt;h3&gt;二值化向量的距离计算&lt;/h3&gt;
&lt;p&gt;首先，我们考虑如何计算两个二值化向量之间的距离。计算方法很简单：因为我们仅关注它们的方向性，我们只需要评估它们每个维度的bit是否一致。即计算两个向量不同比特位的数量。在这里，因为可以利用位操作，会比计算非二值化向量之间的距离要快得多。&lt;/p&gt;
&lt;p&gt;例如，将向量&lt;code&gt;[12, 1, -100, 0.03, -0.01, 128, -100, 0.01]&lt;/code&gt;压缩为&lt;code&gt;11010101&lt;/code&gt;，以及将第二个向量&lt;code&gt;[11, 4, -99, -0.01, 0.02, 130, -150, 0.02]&lt;/code&gt;压缩为&lt;code&gt;11001101&lt;/code&gt;，它们之间的距离由不同比特位的数量决定 (距离为2)。这实际上也就是两个向量之间的汉明距离。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/bp-vecs6.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h3&gt;BQ下数据分布的重要性&lt;/h3&gt;
&lt;p&gt;与我们介绍的&lt;a href=&quot;https://weaviate.io/blog/pq-rescoring&quot;&gt;product quantization&lt;/a&gt;不同，&lt;code&gt;BQ&lt;/code&gt;并不适用于所有类型的数据——我们会在后面解释原因。然而，如果我们正在处理归一化的数据，特别是在利用余弦度量距离时，无需担心，因为Weaviate会为您无缝处理数据归一化。现在，让我们讨论增加维数的影响。&lt;/p&gt;
&lt;h4&gt;一维向量的BQ&lt;/h4&gt;
&lt;p&gt;在下面的图片中，我们绘制了一个归一化后，在一维空间中唯一可能的两个点(0, 1)，用红色表示。量化器会分配给正向向量 &lt;code&gt;1&lt;/code&gt;，分配给负向向量 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/bp-vecs7.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;让我们扩大我们的视角，涵盖两个维度。因为我们考虑的是归一化后的向量，我们预期所有向量都位于以(0,0)为中心，半径为1的圆内。我们的重点是理解量化器如何将数据分成四个不同的区域，利用两个可能的&lt;code&gt;bit&lt;/code&gt;值和两个维度来实现二次幂的效果。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;display: flex; justify-content: space-between;&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/bp-vecs8.png&quot; alt=&quot;原始图片&quot; style=&quot;max-width: 45%;&quot;/&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/bp-vecs9.png&quot; alt=&quot;二值化后的图片&quot; style=&quot;max-width: 45%;&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;在这种情况下，绿色区域（编码11）包含了两个维度都为正的点，而蓝色区域（编码为00）则包含了两个维度都为负的点。红色区域（编码为10）表示第一个维度为正，第二个维度为负的情况，而黄色区域（编码为01）表示第一个维度为负，第二个维度为正的情况。&lt;/p&gt;
&lt;p&gt;重要的是，在每个区域内，任何点与同一区域内的任何其他点之间的距离都是零。而相邻区域中的点之间的距离为1。和完全相反的区域中的点，距离延伸到2。&lt;/p&gt;
&lt;p&gt;这种区分强调了数据分布的关键作用。虽然我们使用的是归一化数据，但是归一化并不是强制性的，但归一化后的数据与所描述的情景非常一致。那么让我们分析另一种情况。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;display: flex; justify-content: space-between;&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/bp-vecs10.png&quot; alt=&quot;原始图片&quot; style=&quot;max-width: 45%;&quot;/&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/bp-vecs11.png&quot; alt=&quot;二值化后的图片&quot; style=&quot;max-width: 45%;&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;我们所有的数据都落在第一象限中。因此，所有向量都被编码为&lt;code&gt;11&lt;/code&gt;，这使得所有向量之间难以区分。这种情况说明了一个不好的数据分布可以使&lt;code&gt;Binary quantization&lt;/code&gt;无法使用。正如先前所述，虽然归一化不是强制性的，但选择归一化的数据在数据分布方面提供了一定程度的保证，有助于使用&lt;code&gt;Binary quantization&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;然而，如果你的数据没有归一化，确保区域的平衡和逻辑划分就变得至关重要。考虑以下例子。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/bp-vecs12.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;在这种情况下，使用&lt;code&gt;Binary quantization&lt;/code&gt;会表明黄色点距离红色点更远，而与蓝色和绿色点更接近。虽然在基于角度的度量（如余弦）中这是成立的，但它与在L2度量下的距离相矛盾，我们可以看到黄色和红色点实际上更接近。&lt;/p&gt;
&lt;h4&gt;N维向量的BQ&lt;/h4&gt;
&lt;p&gt;让我们考虑数据量以及在应用&lt;code&gt;Binary quantization&lt;/code&gt;后能够唯一表示向量的能力。在得知了维数和数据量后，我们对碰撞的程度可以有一个预期，我们将两个向量之间的碰撞定义为&lt;code&gt;两个不同的向量进行Binary quantization后具有相同的表示&lt;/code&gt;。如前面的例子所示，在对二维向量进行二&lt;code&gt;Binary quantization&lt;/code&gt;时，我们只能构建四个区域。因此，当向量数超过四个时，就会发生碰撞，使得两个不同的向量无法区分。&lt;/p&gt;
&lt;p&gt;然而，好在随着维度的增加，可划分的区域数量呈指数增长。对于每一个维度的增长，区域数量翻倍$2^d$，提供了更强大的向量表示能力。例如，当维度数为$756$时，你已经有令人惊讶的$2^{756}$个区域可供使用——即使你有数十亿或数万亿个向量，向量之间的碰撞也几乎不可能发生。而当维度数来到了$1500$，区域的数量可以轻松容纳任何数量的向量，而不会发生任何碰撞。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/bp-vecs13.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;BQ的性能提升&lt;/h2&gt;
&lt;p&gt;让我们重新审视一下&lt;code&gt;Binary quantization&lt;/code&gt;的优势。通常，我们使用量化的方法来节省内存，将每个数字编码为&lt;code&gt;1-bit&lt;/code&gt;。在Weaviate中，浮点向量被表示为&lt;code&gt;float32&lt;/code&gt;数组，从而产生了1:32的压缩比，这已经值得令人称赞了。&lt;/p&gt;
&lt;p&gt;然而，&lt;code&gt;Binary quantization&lt;/code&gt;还有一个显著的次要好处：现在，位操作可以被用来计算量化后的向量之间的距离计算。仅需要对两个二进制数组之间进行简单的异或（XOR）操作，统计结果中的1的数量。而Go语言提供了针对这些二进制函数进行SIMD优化的操作，从而计算速度比使用原始向量快得多。但确切地说快多少呢？&lt;/p&gt;
&lt;p&gt;为了回答这个问题，我们展示了使用我们的&lt;code&gt;Binary quantization&lt;/code&gt;和原始向量进行的暴力搜索结果。我们对维度范围从&lt;code&gt;768&lt;/code&gt;、&lt;code&gt;1536&lt;/code&gt;到&lt;code&gt;4608&lt;/code&gt;的10,000个向量进行100次查询搜索&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维数&lt;/th&gt;
&lt;th&gt;原始向量延迟 (microseconds)&lt;/th&gt;
&lt;th&gt;压缩后向量延迟 (microseconds)&lt;/th&gt;
&lt;th&gt;Recall&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;768d&lt;/td&gt;
&lt;td&gt;1771.85&lt;/td&gt;
&lt;td&gt;230.72 (13%)&lt;/td&gt;
&lt;td&gt;0.745&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1536d&lt;/td&gt;
&lt;td&gt;3703.68&lt;/td&gt;
&lt;td&gt;353.3 (9%)&lt;/td&gt;
&lt;td&gt;0.744&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4608d&lt;/td&gt;
&lt;td&gt;16724.41&lt;/td&gt;
&lt;td&gt;896.37 (5%)&lt;/td&gt;
&lt;td&gt;0.757&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;虽然召回率并不是很高，但我们可以通过超额获取候选邻居并重新评分来解决这个问题。值得注意的是，随着向量维度的增加，我们可以观察到更显著的加速。例如，当暴力搜索&lt;code&gt;768&lt;/code&gt;维的压缩向量时，与使用未压缩向量相比，我们只需花费13%的时间。同样地，对于1536维的压缩向量，我们只需花费9%的时间，而对于4608维的压缩向量，只需花费未压缩向量时间的5%。&lt;/p&gt;
&lt;p&gt;一般而言，我们依靠构建图来进行ANN搜索，因为一个个搜索数百万个向量是不切实际的。然而，由于时间显著减少，暴力搜索数据现在成为了一种可行的选择。例如，在768维的情况下，暴力搜索100万个向量只需花费23毫秒。即使在最坏的情况下（4608维），现在也是切实可行的，大约需要90毫秒。&lt;/p&gt;
&lt;p&gt;那么，最终结论是什么呢？Weaviate能够为您的数据提供闪电般快速的暴力搜索吗？答案取决于您的数据大小和搜索速度的期望。&lt;/p&gt;
&lt;p&gt;暴力搜索有几个优点。首先，你可以不需要索引，节省构建索引所需的时间。虽然在Weaviate中建立索引并不是特别缓慢，但暴力搜索允许您完全跳过此步骤。其次，你不再需要存储邻接点，从而进一步节省内存。事实上，如果你选择直接从磁盘上暴力搜索数据，内存使用量将会变得微不足道——仅仅100MB就足以托管您的应用程序。&lt;/p&gt;
&lt;p&gt;最近，Weaviate引入了&lt;code&gt;Flat Index&lt;/code&gt;，提供了从磁盘暴力搜索数据的选项（默认行为），或者只保留内存中的压缩数据，并从磁盘获取一小部分完整向量进行最终排序。与传统的HNSW索引相比，这两种方法都加快了数据加载速度，同时减少了内存消耗。然而，如果您的需求要求高性能，HNSW仍然是首选。尽管如此，&lt;code&gt;Flat Index&lt;/code&gt;提供了一种经济高效、高性能的替代方案。此外，Weaviate现在支持二进制量化（BQ），可用于和HNSW索引。&lt;/p&gt;
&lt;h3&gt;索引时间的提升&lt;/h3&gt;
&lt;p&gt;现在，让我们讨论一些性能指标。所有实验均使用Go benchmark进行。在博客的最后，我们将提供有关如何使用自己的数据复现这些实验。首先，我们将使用来自DBPedia的一个中等规模的数据集，使用ADA002（1536维）和Cohere v2（4096维）的embedding向量，首先从索引时间开始。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimensions&lt;/th&gt;
&lt;th&gt;1536&lt;/th&gt;
&lt;th&gt;4096&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Flat index&lt;/td&gt;
&lt;td&gt;5s&lt;/td&gt;
&lt;td&gt;7s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hnsw index&lt;/td&gt;
&lt;td&gt;47s&lt;/td&gt;
&lt;td&gt;1m36s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hnsw index+BQ&lt;/td&gt;
&lt;td&gt;21s&lt;/td&gt;
&lt;td&gt;25s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;正如前所述，&lt;code&gt;Flat Index&lt;/code&gt;没有数据索引的需要。因此，我们只需将数据发送给服务器并将其存储起来即可。相反，&lt;code&gt;HNSW&lt;/code&gt;需要构建索引。值得注意的是，就索引时间而言，HNSW索引也可以从这种压缩中获得明显的的性能提升。&lt;/p&gt;
&lt;h3&gt;内存占用的提升&lt;/h3&gt;
&lt;p&gt;现在，让我们讨论内存占用。我们将区分&lt;code&gt;Flat Index&lt;/code&gt;的不同配置项，因为它们具有不同的内存占用。当使用&lt;code&gt;Flat Index&lt;/code&gt;时，无论数据大小如何，无论是否使用&lt;code&gt;BQ&lt;/code&gt;, 所有数据都从磁盘中检索。如果我们选择缓存压缩数据，它将存储在内存中。由于不需要索引，&lt;code&gt;Flat Index&lt;/code&gt;的内存占用低于&lt;code&gt;HNSW+BQ&lt;/code&gt;的内存占用。此外，我们将展示HNSW不同情况下的内存占用。在这两种情况下，您可以预期内存占用量随维数和向量数量的增加而呈更多或更少线性增长。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimensions&lt;/th&gt;
&lt;th&gt;1536&lt;/th&gt;
&lt;th&gt;4096&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Flat index&lt;/td&gt;
&lt;td&gt;77MB&lt;/td&gt;
&lt;td&gt;77MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flat index + BQ + Cache&lt;/td&gt;
&lt;td&gt;141MB&lt;/td&gt;
&lt;td&gt;183MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hnsw index&lt;/td&gt;
&lt;td&gt;1.02GB&lt;/td&gt;
&lt;td&gt;1.79GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hnsw index+BQ&lt;/td&gt;
&lt;td&gt;214MB&lt;/td&gt;
&lt;td&gt;297MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;延迟分析&lt;/h3&gt;
&lt;p&gt;最后，让我们来看一下QPS与召回率曲线，以了解不同方案之间的性能。为了生成这样的曲线，我们修改了HNSW下的ef参数，以及&lt;code&gt;Flat Index&lt;/code&gt;下的&lt;code&gt;rescoringLimit&lt;/code&gt;参数。我们还使用了10个并发核心来测量QPS。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/bp-vecs14.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;请注意，纯&lt;code&gt;Flat Index&lt;/code&gt;场景中的QPS较低（显示为右下角的绿点）。是因为这种情境下，我们需要在磁盘中检索所有完整的向量，并在未压缩的向量上执行暴力搜索。虽然这种方法性能较差，但它不需要内存分配。&lt;/p&gt;
&lt;p&gt;接下来，我们采用相同的方式，但集成了&lt;code&gt;Binary quantization&lt;/code&gt;（BQ）。在这个情景中，我们需要从磁盘中读取的数据较少，因为我们仅需要访问压缩后的向量（比未压缩相比小32倍）。此外，由于我们仅需要用位操作计算距离，因此暴力搜索也变得更快。暴力搜索后，我们会生成一个候选列表，然后对它们进行重新评分。在重新评分过程中，我们只需要检索少量完整向量来构建最终结果。这个方式仍然保持了磁盘操作，同时提供了更好的性能。需要注意的是，这种方法取决于BQ的兼容性；否则，可能无法实现最佳的召回率。此外，确保足够高的&lt;code&gt;rescoringLimit&lt;/code&gt;对于保证良好的召回率至关重要。&lt;/p&gt;
&lt;p&gt;最后，我们测试了具有缓存的压缩向量的平坦索引（用蓝色曲线表示）。这种方式QPS在600到1000之间。当然在这种情况下，内存占用量略微增加，因为压缩后的向量被保留在内存中，只有一小部分向量从磁盘中获取。&lt;/p&gt;
&lt;p&gt;接下来，我们将考虑较大维度的情况。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/bq-vecs16.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/bq_vecs15.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;鉴于这些结果，值得考虑以下几点：对于一个相对较小的向量数据集(100,000)，如果你的目标是非常高的召回率，那么 &lt;code&gt;flat-compressed-cached&lt;/code&gt;曲线与&lt;code&gt;HNSW&lt;/code&gt;之间的性能差异并不十分明显。有人可能会认为&lt;code&gt;100,000&lt;/code&gt;个向量并不是一个很大的数量，这是一个正确的观点。然而，让我们考虑将此功能与多租户结合。&lt;/p&gt;
&lt;p&gt;Weaviate确保了每个租户的信息完全隔离。如果我们有1000个租户，每个租户都有100,000个向量。令人惊讶的是，预期的性能保持了差不多的一致性。而这1亿个向量则构成了大量的数据。此外，Weaviate支持租户快速停用/惰性重新激活，这可以创建出一个性能异常出色、内存占用极低的性能方案，前提是您已经设计了一个健壮的架构。&lt;/p&gt;
&lt;p&gt;现在，让我们将数字进一步放大。对于更大的数据集，暴力搜索与数据大小呈线性关系。如果我们将数据集增加到&lt;code&gt;1,000,000&lt;/code&gt;个向量，那么QPS将比这里展示的要慢大约10倍。然而，即使有了这种增加的延迟，对于某些应用程序来说，暴力搜索仍然是一个可行的选项。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/bq-vecs17.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/bq-vecs18.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;PQ与BQ的对比&lt;/h2&gt;
&lt;p&gt;现在你在&lt;code&gt;Weaviate&lt;/code&gt;中有多种量化技术可供选择，那么问题就来了，PQ和BQ哪个更好，应该在哪里使用&lt;code&gt;PQ vs. BQ&lt;/code&gt;。这个决定将取决于你具体的数据，并且需要你运行自己的基准测试。我们在下一节提供了代码和说明，以便您进行这样的测试。下面的内存和性能实验旨在让你更容易地做出PQ vs. BQ的选择。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/bq-vecs19.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;请注意，&lt;code&gt;BQ&lt;/code&gt;的主要优势不仅仅是压缩向量。更高效的位计算也起着重要作用。这就是为什么我们上面讨论的&lt;code&gt;flat+bq&lt;/code&gt;选项是一个如此好的选择。我们不仅需要从磁盘读取的数据更少，而且更快的距离计算使得在Weaviate中的暴力搜索更快。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Index&lt;/th&gt;
&lt;th&gt;Indexing Time&lt;/th&gt;
&lt;th&gt;Memory Usage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HNSW&lt;/td&gt;
&lt;td&gt;8m42s&lt;/td&gt;
&lt;td&gt;6437.05MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HNSW+PQ&lt;/td&gt;
&lt;td&gt;21m25s&lt;/td&gt;
&lt;td&gt;930.36MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HNSW+BQ&lt;/td&gt;
&lt;td&gt;3m43s&lt;/td&gt;
&lt;td&gt;711.38MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FLAT+BQ&lt;/td&gt;
&lt;td&gt;54s&lt;/td&gt;
&lt;td&gt;260.16MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FLAT+BQ+CACHE&lt;/td&gt;
&lt;td&gt;53s&lt;/td&gt;
&lt;td&gt;352.99MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注意&lt;code&gt;BQ&lt;/code&gt;是如何极大地缩短了与&lt;code&gt;HNSW&lt;/code&gt;的索引时间。&lt;/p&gt;
&lt;h2&gt;用你自己的数据来测试BQ&lt;/h2&gt;
&lt;p&gt;在这里，我们提供了代码和说明，这将帮助你在你自己的数据上自行复现上述实验来找到召回率、延迟和内存占用量，最佳的平衡。&lt;/p&gt;
&lt;p&gt;我们在这个&lt;a href=&quot;https://github.com/weaviate&quot;&gt;仓库&lt;/a&gt;中包含了一些非常有用的工具。要轻松运行这些测试（或者使用你的数据运行任何测试），你需要将数据以&lt;code&gt;hdf5&lt;/code&gt;格式存储，并且具有与&lt;a href=&quot;https://ann-benchmarks.com/&quot;&gt;ANN基准测试&lt;/a&gt;中描述的相同格式。您可以使用&lt;a href=&quot;https://github.com/weaviate/weaviate-benchmarking&quot;&gt;Go基准测试工具&lt;/a&gt;对数据进行索引。这个基准测试工具可以给您一个更好的QPS概念，同时使用并发查询。它接受几个参数，您可以探索，但在我们的运行中，我们使用以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go run main.go ann-benchmark -v ~/Documents/datasets/dbpedia-100k-openai-ada002.hdf5 -d cosine --indexType flat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意参数-d用于距离和--indexType用于在hnsw和flat之间切换。&lt;/p&gt;
&lt;p&gt;要运行压缩（启用BQ）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go run main.go ann-benchmark -v ~/Documents/datasets/dbpedia-100k-openai-ada002.hdf5 -d cosine --indexType flat --bq enabled
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意参数&lt;code&gt;-bq&lt;/code&gt;用于激活压缩。&lt;/p&gt;
&lt;p&gt;一旦您运行脚本，您将在运行结束时在终端上看到不同的指标。特别注意QPS和召回率。结果将以JSON格式保存在与脚本相同路径下的名为results的存储库中。接下来，您还可以运行visualize.py脚本，生成我们在本文中显示的相同图形。您的图形将在与脚本相同路径下的output.png中可用。&lt;/p&gt;
&lt;p&gt;祝愉快压缩！🚀&lt;/p&gt;
</content:encoded><category>vector search</category><category>translate</category><category>binary quantization</category><author>tang-hi</author></item><item><title>DuckDB -- 浮点数的压缩</title><link>https://tangdh.life/posts/database/duckdb-alp/</link><guid isPermaLink="true">https://tangdh.life/posts/database/duckdb-alp/</guid><description>DuckDB 是一款开源 OLAP 数据库。与 SQLite 类似，本文将介绍DuckDB是如何对它的浮点数数组进行压缩</description><pubDate>Sun, 03 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;浮点数的压缩一直是一个难以解决的问题。因为其在计算机中存储格式的特殊，导致浮点数的压缩率和解压速度都不是那么令人满意。
DuckDB采用了论文 &lt;a href=&quot;https://dl.acm.org/doi/pdf/10.1145/3626717&quot;&gt;ALP&lt;/a&gt; 中所提出的方法来对浮点数进行压缩，各方面都取得了不错的进展，这篇博客将介绍ALP中的压缩方法。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/alp-compare.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;前置知识&lt;/h2&gt;
&lt;h3&gt;IEEE 754 Double 的表示方法&lt;/h3&gt;
&lt;p&gt;首先我们来回忆一下浮点数在计算机内部的表示方式。浮点数由三部分组成&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;符号位 (sign)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;指数位 (exponent)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分数位 (fraction)&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/double-represent.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
我们通过这三部分可以得到浮点数的值为
$$
double = (-1)^{\sign} \times 2^{(exp-1023)} \times \left(1 + \sum_{i=1}^{52}\left({b_{52-i}} {2^{-i}}\right)\right)
$$&lt;/p&gt;
&lt;h2&gt;压缩&lt;/h2&gt;
&lt;p&gt;我们之所以难以对浮点数进行压缩，原因在于浮点数的整个二进制表示是分成三部分的，我们没有办法将其像整数一样作为一个整体进行压缩。&lt;/p&gt;
&lt;p&gt;因此大体上对浮点数的压缩方式有&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将浮点数转换为整数后进行压缩&lt;/li&gt;
&lt;li&gt;分部份对浮点数进行进行压缩.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1. 将浮点数转化为为整数&lt;/h3&gt;
&lt;p&gt;将浮点数转化为整数的想法，看上去很简单，我们只需要乘上一个系数后，将其右边的小数部分消除即可&lt;/p&gt;
&lt;p&gt;我们以8.0605为例, 我们仅需乘上$10^4$, 便可将其转化为整数, 同时我们只需要记录下这个系数, 我们便可以在解压的过程中，还原这个浮点数。
$$
80605 = 8.0605 * 10^4 \newline
8.0605 = 80605 * 10^{(-4)}
$$&lt;/p&gt;
&lt;p&gt;但是这样的转换方式，因为计算机对浮点数表示的精度原因。我们没有办法在解压的时候获取于原来一样的数值，如下图所示。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/loss.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
我们可以看到解压后的数据与压缩前的数据存在细微的差别，这种有损压缩对于金融相关的业务而言是不可接受的。因此我们需要找到一个方法
能够对其进行无损压缩，目前最常用的方式就是增加系数。&lt;/p&gt;
&lt;p&gt;当我们将系数增加到$10^7$时，我们会发现该压缩方式变成了无损压缩。但问题也随之产生, 压缩率也下降了,在一些极端的例子中, 可能还不如不压缩。&lt;/p&gt;
&lt;h3&gt;2. 分部分进行压缩&lt;/h3&gt;
&lt;p&gt;当第一种方式无法进行有效压缩时, 我们会采取分部分进行压缩。这是因为通过观察我们发现，在一组浮点数中，指数位的方差较小，也就是指数位的值较为相似。 因此我们可以对
浮点数的数据集进行采样，决定一个分割点，左半部分是相似的指数位，我们对其使用&lt;code&gt;dictionary encoding&lt;/code&gt;， 而对于右半部分，我们使用&lt;code&gt;bit packing&lt;/code&gt;进行压缩。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/alprd.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
通过这种方式我们也能进行有效压缩&lt;/p&gt;
&lt;h3&gt;ALP&lt;/h3&gt;
&lt;p&gt;ALP使用的是第一种压缩方法，它首先会对待压缩的浮点数数组进行采样，确定系数$10^e$。该系数确保大部分的数字可以做到无损压缩，同时它还会确定一个系数$10^{-f}$。这是因为如果我们在第一步
为了保证精度，选择的系数过大，那么整数后面有大量的0，同样浪费空间，因此我们选择一个合适的系数$10^{-f}$,消除后置0。这里也许会有人担心再乘以一个系数可能导致引入新的误差。
但是，根据论文的说法，其实并不会导致新的误差。因为论文中使用的&lt;code&gt;round&lt;/code&gt;是自己实现的一个高效round， 十分契合SIMD加速。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static const long long SWEET = (1ll &amp;lt;&amp;lt; 51) + (1ll &amp;lt;&amp;lt; 52);
long long fast_round(double d) {
  return static_cast&amp;lt;long&amp;gt;(d + SWEET - SWEET);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们仍旧以 8.0605 为例, 假设系数分别为$10^{14}$, $10^{-10}$.我们用以下的代码测试&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  #include &amp;lt;limits&amp;gt;
  #include &amp;lt;iomanip&amp;gt;
  #include &amp;lt;iostream&amp;gt;
  using namespace std;
  static const long long SWEET = (1ll &amp;lt;&amp;lt; 51) + (1ll &amp;lt;&amp;lt; 52);
  long long fast_round(double d) {
  return static_cast&amp;lt;long&amp;gt;(d + SWEET - SWEET);
  }

  int main (int argc, char *argv[])
  {
      double number = 8.0605;
      std:cout &amp;lt;&amp;lt; &quot;before compressd: &quot;;
      std::cout &amp;lt;&amp;lt; std::fixed &amp;lt;&amp;lt; std::setprecision(std::numeric_limits&amp;lt;double&amp;gt;::digits10) &amp;lt;&amp;lt; number &amp;lt;&amp;lt; std::endl;
      long compressd = fast_round(number * 1e14 * 1e-10);
      double decompressed = (double(compressd * 1e10) * (double)1e-14);
      std::cout &amp;lt;&amp;lt; &quot;after compressd: &quot;;
      std::cout &amp;lt;&amp;lt; std::fixed &amp;lt;&amp;lt; std::setprecision(std::numeric_limits&amp;lt;double&amp;gt;::digits10) &amp;lt;&amp;lt; decompressed &amp;lt;&amp;lt; std::endl;
      return 0;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终的测试结果为&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;before compressd: 8.060499999999999&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;after compressd: 8.060499999999999&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此ALP的算法流程为&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;采样确定系数$10^e$ , $10^{-f}$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对数组中的每一个数乘以第一步的两个系数，确定是否会损失精度&lt;/p&gt;
&lt;p&gt;2.1.  如果不会损失精度，直接保存为整数&lt;/p&gt;
&lt;p&gt;2.2.  如果会损失精度，作为异常值单独进行存储&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于第二步产生的整数数组使用算法&lt;code&gt;FOR&lt;/code&gt;进行压缩&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;ALPRD&lt;/h3&gt;
&lt;p&gt;对于无法使用&lt;code&gt;ALP&lt;/code&gt;的情况下(大部分数字无法无损压缩/压缩率不高)， 我们会使用之前介绍的分部分压缩法。
算法的流程为&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对数据进行采样，确定从哪一位(P)开始分割。&lt;/li&gt;
&lt;li&gt;P 位左边的二进制使用&lt;code&gt;dictionary encode&lt;/code&gt;进行压缩&lt;/li&gt;
&lt;li&gt;P 位右边的二进制使用&lt;code&gt;bit packing&lt;/code&gt;进行压缩
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/alprd.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ALP&lt;/code&gt;使用非常简洁高效的算法对浮点数数组进行压缩，它不仅具有良好的压缩率，同时该算法是&lt;code&gt;SIMD friendly&lt;/code&gt;, 可以充分利用硬件对该算法进行加速，提高解压和压缩的速度。
这篇博客只是对&lt;code&gt;ALP&lt;/code&gt;进行粗略地介绍，想要充分了解的读者还是推荐阅读论文原文&lt;a href=&quot;https://dl.acm.org/doi/pdf/10.1145/3626717&quot;&gt;ALP&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>DataBase</category><category>DuckDB</category><category>Compression</category><category>Alp</category><author>tang-hi</author></item><item><title>DuckDB -- table&apos;s file format</title><link>https://tangdh.life/posts/database/duckdb-file-en/</link><guid isPermaLink="true">https://tangdh.life/posts/database/duckdb-file-en/</guid><description>DuckDB is an open-source OLAP database. Similar to SQLite, this article will introduce how DuckDB stores its table structure.</description><pubDate>Tue, 12 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;/&amp;gt;&lt;/p&gt;
&lt;p&gt;In this article, we will explore how DuckDB organizes its table structures. We&apos;ll focus solely on the table structures while disregarding any irrelevant details.&lt;/p&gt;
&lt;h2&gt;Background Information&lt;/h2&gt;
&lt;h3&gt;Block Types&lt;/h3&gt;
&lt;p&gt;DuckDB stands out from other databases by storing all of its data within a single file. To manage this data effectively, DuckDB utilizes two types of blocks: &lt;code&gt;MetaBlocks&lt;/code&gt; and &lt;code&gt;DataBlocks&lt;/code&gt;.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;A &lt;code&gt;DataBlock&lt;/code&gt; represents an individual unit of stored information.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;On the other hand, a &lt;code&gt;MetaBlock&lt;/code&gt; functions as a collection of blocks with its first 8 bytes indicating the value of &apos;next_block_id&apos;. By employing such block lists when necessary due to excessive content volume.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/meta-block.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h3&gt;Field Reader&lt;/h3&gt;
&lt;p&gt;In certain cases, when retrieving data from a Block, we employ a technique called &quot;Field Reader&quot; for reading purposes. This &quot;Field Reader&quot; is independent of the table&apos;s fields and serves as an initial step before accessing specific data by first extracting information about two key parameters: &lt;code&gt;max_field_count&lt;/code&gt; and &lt;code&gt;total_size&lt;/code&gt;.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;max_field_count&lt;/code&gt;: Indicates the number of fields that will be retrieved afterwards.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;total_size&lt;/code&gt;: Represents the overall size (in bytes) of the subsequent data retrieval process.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/field-reader.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h3&gt;Segment Tree&lt;/h3&gt;
&lt;p&gt;The concept of &lt;code&gt;Segment&lt;/code&gt; refers to a block of data, and we use a data structure called &lt;code&gt;segment tree&lt;/code&gt; (or &quot;SegementTree&quot;) for managing these segments. Despite its name, a segment tree internally utilizes vectors for storing segments rather than trees. Moreover, binary search is employed for locating specific segments within this structure; thus, it assumes that segments are stored in an ordered manner. One notable characteristic of segment trees is their support for lazy loading: instead of reading all segments into memory at once, they retrieve individual segments from disk on-demand.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/segmenttree.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;文件结构&lt;/h2&gt;
&lt;p&gt;In this section, we will begin by introducing the file structure of DuckDB.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/header.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;Looking at the diagram, we notice that DuckDB has three headers - but don&apos;t worry! These headers won&apos;t confuse us when it comes to understanding how tables are stored; they&apos;re just some extra information we&apos;ll briefly cover.&lt;/p&gt;
&lt;p&gt;Let&apos;s start with the MainHeader:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Checksum&lt;/strong&gt;: A handy way to verify data integrity.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Magic bytes&lt;/strong&gt;: These special bytes confirm that this file belongs to duckDB.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version numbers&lt;/strong&gt;: Keep track of software versions for compatibility purposes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flags&lt;/strong&gt;: Indicate if you have permission to read or write on this database.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now let&apos;s move on to DataBaseHeader:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Iteration&lt;/strong&gt;: How many times things have been iterated over (processed repeatedly).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Meta block&lt;/strong&gt;: The unique identifier for the first data block in storage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Free list&lt;/strong&gt;: Blocks ready for reuse, saving space and resources.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Block count&lt;/strong&gt;: The total number of blocks in the database.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now, let&apos;s take a look at how the actual data is stored. It consists of a &lt;code&gt;schema count&lt;/code&gt; and &lt;code&gt;${schema_count}&lt;/code&gt; individual &lt;code&gt;schemas&lt;/code&gt;. In DuckDB, think of a schema as a database.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20241026165754.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;Let&apos;s take a closer look at how schemas are stored in DuckDB databases. The first piece of information is the schema&apos;s name, followed by a count of each type it contains. Now let&apos;s briefly go over these different types together! For more detailed definitions, feel free to visit their official website.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://duckdb.org/docs/sql/data_types/enum&quot;&gt;enum&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://duckdb.org/docs/sql/statements/create_sequence&quot;&gt;sequence&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://duckdb.org/docs/sql/statements/create_view&quot;&gt;view&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://duckdb.org/docs/sql/statements/create_macro&quot;&gt;macro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://duckdb.org/docs/sql/indexes&quot;&gt;indexes&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now, let&apos;s shift our attention specifically to tables and explore their structures further!&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/table_data.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;From the diagram, we can observe that the first three item in the table are labeled as &lt;code&gt;catalog name&lt;/code&gt;,&lt;code&gt;schema name&lt;/code&gt; and &lt;code&gt;table name&lt;/code&gt;. By using these three items, we can identify to which file (catalog), database (schema), and specific table this particular one belongs. The field named &quot;constraints&quot; provides information about certain restrictions imposed on this table, such as &lt;code&gt;Not Null&lt;/code&gt; or &lt;code&gt;Unique&lt;/code&gt; properties. However, for now, let&apos;s not dive too deep into this aspect; instead, let&apos;s shift our attention towards exploring what lies within both the &apos;Columns&apos; section and &apos;table data&apos; field.&lt;/p&gt;
&lt;h3&gt;Columns&lt;/h3&gt;
&lt;p&gt;Within this section resides a collection of definitions for each individual column found within a given table.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/column-define.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;We can observe that in our dataset, the first attribute called &lt;code&gt;column count&lt;/code&gt; which represents how many columns exist in total within our dataset file; following this attribute are individual definitions for each specific column present within our dataset file.&lt;/p&gt;
&lt;p&gt;Now let&apos;s delve into what each attribute signifies:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;column name&lt;/strong&gt; - Field name&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;column type&lt;/strong&gt; - Field type&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;expression&lt;/strong&gt; - Expression, some fields are generated through expressions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;table Column type&lt;/strong&gt; - This is different from the &lt;strong&gt;column type&lt;/strong&gt;, it does not represent the field type, but only has two values: &lt;strong&gt;STANDARD&lt;/strong&gt; and &lt;strong&gt;GENERATED&lt;/strong&gt;. (Actually, I&apos;m not quite sure about the meaning of this field, it probably indicates whether this field is generated or not)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;compression type&lt;/strong&gt; - Indicates the compression method used for this field.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once we have obtained information regarding the types and characteristics of each column, we gain comprehensive knowledge about the overall structure and organization of our dataset. The remaining components consist of actual data entries and index-related information, which can be accessed through the &lt;code&gt;table data&lt;/code&gt; field.&lt;/p&gt;
&lt;h3&gt;table data&lt;/h3&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/table_data.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;Since indexes and table data are usually large in size, we don&apos;t store them directly here. Instead, we store pointers (block-id, offset) that point to their actual locations.&lt;/p&gt;
&lt;p&gt;Let&apos;s explain each field using this diagram:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;table data block&lt;/strong&gt;: Pointer to the actual table data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;total rows&lt;/strong&gt;: The number of rows in this table.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;index num&lt;/strong&gt;: The total number of indexes in this table.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;index&lt;/strong&gt;: Pointer to the index.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now let&apos;s examine how &lt;code&gt;table data block&lt;/code&gt; stores its actual structured information.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/row-group.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;The initial storage contains metadata about a series of column data (the structure of the column data block will be explained later). The last two fields are easy to understand. The first one stores statistical information about the table, and the other one stores the number of &apos;row groups&apos;.
Now, let&apos;s address two questions: what is a &apos;row group&apos;, and why is the storage format different from before, which was &lt;code&gt;&amp;lt;data-count, data, data,...data&amp;gt;&lt;/code&gt;, but now only stores a &apos;row group pointer&apos;? What happens if there are more than 1 &apos;row group&apos;?&lt;/p&gt;
&lt;h3&gt;row group&lt;/h3&gt;
&lt;p&gt;We all know that OLAP generally uses columnar storage while OLTP uses row-based storage. Although columnar storage is superior in terms of reading and computation, when it comes to frequent insertions, deletions or updates, row-based storage outperforms columnar storage. Therefore, DuckDB has come up with a compromise here by grouping tuples and storing them in columns within each group. Currently, every &lt;code&gt;122880&lt;/code&gt; tuples form one group.&lt;/p&gt;
&lt;h3&gt;Why only one row group pointer?&lt;/h3&gt;
&lt;p&gt;Because row groups are always stored sequentially according to their line numbers and they store blocks as meta blocks. Hence they can be managed through a SegmentTree for lazy loading subsequent &lt;code&gt;row groups&lt;/code&gt;. When needed, they can be read directly from behind. That&apos;s why we only need to store the block-id of the first block here.&lt;/p&gt;
&lt;p&gt;Now let&apos;s take a look at the storage structure of &lt;code&gt;row groups&lt;/code&gt;.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Row start&lt;/strong&gt;: The starting line number of this row group.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tuple count&lt;/strong&gt;: The number of rows in this row group.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Column pointers&lt;/strong&gt;: Since columns are stored within each row group vertically (column-wise), this pointer points to the actual storage address for each column.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Versions&lt;/strong&gt;: I haven&apos;t looked into this field too closely; it should be related to MVCC.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let&apos;s continue with the storage structure of &lt;code&gt;column data blocks&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/column-data-block.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;To our surprise, we discovered that this isn&apos;t actually where the data itself is stored; it still holds pointers instead! But why? Well, it turns out that the real column data resides in what&apos;s called &quot;pure blocks.&quot; Unlike their counterparts known as &quot;meta blocks,&quot; these pure blocks don&apos;t have a convenient way to keep track of all their contents using a simple list structure like before.&lt;/p&gt;
&lt;p&gt;As usual, let&apos;s explain the meanings of each field:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;row start&lt;/strong&gt;: The starting row number of this data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;tuple count&lt;/strong&gt;: The total number of rows stored.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;block id&lt;/strong&gt;: The block ID where the actual data resides.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;offset&lt;/strong&gt;: The offset within the block ID where the actual data resides.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;compress&lt;/strong&gt;: The compression method used for the data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;stat&lt;/strong&gt;: Statistical information about this portion of data.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now, we&apos;ve finally arrived at the very heart of it all—the block housing our precious columnar data! However, please note that its storage format can vary depending on which compression technique was employed during processing. I&apos;ll briefly introduce a few common types here, but if you&apos;re curious about others, feel free to explore them further on your own!&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/compress1.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h3&gt;Const  Column&lt;/h3&gt;
&lt;p&gt;In a const column, every single value is identical. This means we don&apos;t need to store any actual data at all. Instead, we can simply retrieve the minimum value from the statistical information.&lt;/p&gt;
&lt;h3&gt;uncompress column&lt;/h3&gt;
&lt;p&gt;When it comes to uncompressing columns in a dataset, it means that there is no compression applied to these specific columns. For data types such as &lt;code&gt;uint32&lt;/code&gt; or &lt;code&gt;uint8&lt;/code&gt;, which have fixed sizes, we can easily read each value individually without any additional steps required. However, when dealing with variable-length data types like strings that do not have predetermined lengths, a different approach called Dictionary Compression is used&lt;/p&gt;
&lt;p&gt;For strings, the first two fields give us the position of the dictionary:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;dict_start = dict_end - dict_size&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dict_end = dict_end&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dict_size = dict_size&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here, we can consider the dictionary as a string pool and offsets as corresponding starting positions, where &lt;code&gt;offsets[i] - offsets[i-1]&lt;/code&gt; represents the length. This might sound abstract, so let&apos;s take an example.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/string-compress.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;In this example, we have three strings: &lt;code&gt;foo&lt;/code&gt;, &lt;code&gt;bar&lt;/code&gt;, and &lt;code&gt;duckdb&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We store these three strings in reverse order in a dictionary. The offset is relative to the &quot;dict end&quot;. This allows us to locate the starting address of the corresponding string.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;foo&lt;/strong&gt;&lt;br /&gt;
head = dict - offset = dict - 3&lt;/p&gt;
&lt;p&gt;length = 3 - 0 = 3&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;bar&lt;/strong&gt;&lt;br /&gt;
head = dict - offset = dict - 6&lt;/p&gt;
&lt;p&gt;length = 6 - 3 = 3&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;foo&lt;/strong&gt;&lt;br /&gt;
head = dict - offset = dict - 12&lt;/p&gt;
&lt;p&gt;length = 12 - 6=6&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now, let&apos;s address a potential issue: what if a string exceeds the maximum allowed length? In such cases, we utilize negative offsets to indicate that these strings are longer than usual. To access them, we store their (block id, offset) pairs in our dictionary and retrieve them from another block where they are stored.&lt;/p&gt;
&lt;h3&gt;RLE column and bitpacking&lt;/h3&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/compress2.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;RLE column is relatively simple, with the values stored in the front and the number of occurrences of each value stored in the back. They are separated by &lt;code&gt;RLE count offset&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;As for bitpack columns, I&apos;ll let curious readers delve into that topic themselves.&lt;/p&gt;
&lt;h3&gt;Dictionary column&lt;/h3&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/compress3.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;If you grasp how strings are stored in &quot;uncompress column,&quot; understanding &quot;Dictionary column&quot; becomes much easier as well. In this case, the term &apos;dict&apos; retains its original meaning while &apos;index Buffer&apos; refers to what was previously mentioned as &apos;offsets&apos; and &apos;bitpacking.&apos; It represents which position within the index Buffer corresponds to each row&apos;s value. By using &lt;code&gt;dict.get(indexBuffer[bitpacking[i]])&lt;/code&gt;, we can retrieve the stored value.&lt;/p&gt;
&lt;p&gt;One important optimization technique employed here involves decompressing the dict during actual scanning. However, if it turns out that all data needs to be scanned, only decompressing bitpacking would suffice.&lt;/p&gt;
&lt;h3&gt;Last&lt;/h3&gt;
&lt;p&gt;In this article, we explored how tables are stored in DuckDB. Unlike other databases, DuckDB takes a unique approach by utilizing just one file to store all of its data (although I&apos;m unsure whether this is advantageous or not). Designed as a single-machine database without distributed capabilities in mind, it aims to optimize performance through techniques such as lazy loading with row groups. Additionally, column data can be compressed using various formats for efficient storage.&lt;/p&gt;
</content:encoded><category>DataBase</category><category>DuckDB</category><author>tang-hi</author></item><item><title>有趣的知识 -- CPU利用率，延迟，吞吐量之间的关系</title><link>https://tangdh.life/posts/interesting/latency-and-throughput/</link><guid isPermaLink="true">https://tangdh.life/posts/interesting/latency-and-throughput/</guid><description>通过数学的方式确定CPU利用率，延迟，吞吐量之间的关系</description><pubDate>Mon, 13 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;
本文从排队论的角度出发分析CPU利用率，延迟和吞吐量之间的关系，这篇文章大量参考&lt;a href=&quot;https://blog.betacat.io/post/2023/05/explain-latency-and-utilization-using-queueing-theory/&quot;&gt;喵叔的文章&lt;/a&gt;, &lt;a href=&quot;https://www.youtube.com/watch?v=Hda5tMrLJqc&quot;&gt;油管视频&lt;/a&gt;,
本质上只是简单的复述，作为我自己学习的整理。&lt;/p&gt;
&lt;h2&gt;CPU利用率和延迟之间的关系&lt;/h2&gt;
&lt;p&gt;做在线服务的时候，我们经常会给CPU利用率设置一个阈值，如果超过了这个阈值，我们会对服务扩容。这个阈值到底应该如何选择，为什么当CPU利用率到达了50%(&lt;strong&gt;意味着它有一半时间是空闲的&lt;/strong&gt;),我们就需要对其进行扩容？ 要回答这些问题就意味着，我们不能仅仅对CPU和延迟有一个感性的认识，而应该有一个定量的描述。&lt;/p&gt;
&lt;h3&gt;1. &lt;a href=&quot;https://en.wikipedia.org/wiki/Little%27s_law&quot;&gt;Little&apos;s Law&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Little&apos;s Law 是排队论中的一个重要定律，由 John D. C. Little 在 1961 年提出。该定律指出，在一个系统中，平均的顾客数等于平均的到达率乘以平均的服务时间。
换算到计算机中即系统中的平均请求数等于单位时间内的请求数量乘以平均的处理时间，其公式可以表示为&lt;/p&gt;
&lt;p&gt;$$
L = \lambda W
$$&lt;/p&gt;
&lt;p&gt;其中:&lt;/p&gt;
&lt;p&gt;$L$ : 系统中的平均请求数,即当前正在处理以及待处理的请求&lt;/p&gt;
&lt;p&gt;$\lambda$ : 单位时间内到达的请求数,即&lt;strong&gt;QPS&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$W$ : 单个请求的平均处理时间, 即延迟&lt;/p&gt;
&lt;h3&gt;2. 模型构建&lt;/h3&gt;
&lt;p&gt;在得知上述定理后，我们可以对我们的服务进行一个简单的模型构建&lt;a href=&quot;https://zh.wikipedia.org/zh-cn/M/M/1&quot;&gt;M/M/1模型&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;我们只有一个服务器&lt;/li&gt;
&lt;li&gt;服务器一次只处理一个请求&lt;/li&gt;
&lt;li&gt;请求的到来服从泊松分布&lt;/li&gt;
&lt;li&gt;请求的处理时间服从泊松分布&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因为整个服务是动态的，每时每刻待处理的请求数量都是变化的，但是它们都服从马尔科夫链，其中每个状态的变化都是一个泊松过程。&lt;/p&gt;
&lt;p&gt;因此如果当前状态为 $i$ , 那么它的上一个状态为 $ j $ ( $ i \pm 1 $ ), 由于整个系统处于平稳的状态，所以离开状态的速率等于进入状态的速率, 即&lt;/p&gt;
&lt;p&gt;$$
\lambda * P(X=i) = \mu * P(X=j)
$$&lt;/p&gt;
&lt;p&gt;其中:&lt;/p&gt;
&lt;p&gt;$P(X=i)$ 代表状态$i$的概率，即请求数量为$i$的概率&lt;/p&gt;
&lt;p&gt;$P(X=j)$ 代表状态$j$的概率，即请求数量为$j$的概率&lt;/p&gt;
&lt;p&gt;$\lambda$ 代表QPS&lt;/p&gt;
&lt;p&gt;$\mu$ 代表单位时间内处理请求的数量&lt;/p&gt;
&lt;p&gt;根据上面的式子，我们可以得到如下的递推式&lt;/p&gt;
&lt;p&gt;$$
\lambda * P(X=0) = \mu * P(X=1)\newline
\lambda * P(X=1) = \mu * P(X=2)\newline
\lambda * P(X=2) = \mu * P(X=3)\newline
.\newline
.\newline
$$&lt;/p&gt;
&lt;p&gt;我们可以从上述公式推导出&lt;/p&gt;
&lt;p&gt;$$
P(X=i) = (\frac{\lambda}{\mu})^n * P(X=0)
$$&lt;/p&gt;
&lt;p&gt;又由于所有的概率之和为1&lt;/p&gt;
&lt;p&gt;$$
\sum_{i=0}^{\infty}P(X=i) = 1\newline
(\frac{\lambda}{\mu})^0 * P(X=0) + (\frac{\lambda}{\mu})^1 * P(X=0) ... (\frac{\lambda}{\mu})^n * P(X=0) = 1\newline
P(X=0) = \frac{(\mu - \lambda)}{\mu}
$$&lt;/p&gt;
&lt;p&gt;同样的我们如果按照这个模型来计算平均待处理的请求数，可以得到&lt;/p&gt;
&lt;p&gt;$$
\sum_{i=0}^{\infty}P(X=i) * i\newline
= P(X=0) * \frac{\lambda}{\mu} * (\frac{1}{(1-\frac{\lambda}{\mu})^2})
$$&lt;/p&gt;
&lt;p&gt;将P(X=0)代入可以得到&lt;/p&gt;
&lt;p&gt;$$
L = \frac{\lambda^2}{\mu * (\mu - \lambda)}
$$&lt;/p&gt;
&lt;p&gt;再通过&lt;strong&gt;Little&apos;s Law&lt;/strong&gt;的公式我们可以得到&lt;/p&gt;
&lt;p&gt;$$
W = \frac{\lambda}{\mu * (\mu - \lambda)}
$$&lt;/p&gt;
&lt;p&gt;整个推导过程较长，你可以只记住最后一个公式，最后我们得到&lt;strong&gt;延迟&lt;/strong&gt;和&lt;strong&gt;QPS&lt;/strong&gt;以及&lt;strong&gt;单位时间处理请求量&lt;/strong&gt;之间的关系。&lt;/p&gt;
&lt;h3&gt;3.CPU利用率的表示&lt;/h3&gt;
&lt;p&gt;我们可以将CPU的利用率看作一段时间内的请求数量除以同一段时间内CPU最大可处理的请求数量，那么我们有以下公式&lt;/p&gt;
&lt;p&gt;$$
\rho = \frac{\lambda * T}{\mu * T} = \frac{\lambda}{\mu}
$$&lt;/p&gt;
&lt;p&gt;将其代入第二节我们得到的公式可以得到&lt;strong&gt;延迟&lt;/strong&gt;与&lt;strong&gt;CPU利用率&lt;/strong&gt;之间的关系&lt;/p&gt;
&lt;p&gt;$$
W = \frac{\rho}{\mu*(1-\rho)}
$$&lt;/p&gt;
&lt;p&gt;那么这个图具体长什么样呢？我们可以通过Walfram查看&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/relation.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;从这幅图中我们可以发现随着CPU利用率的提升，延迟会快速的增长，我们可以通过下面的数据更具体的感受这一关系&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CPU usage&lt;/th&gt;
&lt;th&gt;latency&lt;/th&gt;
&lt;th&gt;latency increase&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.2&lt;/td&gt;
&lt;td&gt;0.0025&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.3&lt;/td&gt;
&lt;td&gt;0.0042&lt;/td&gt;
&lt;td&gt;68%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.4&lt;/td&gt;
&lt;td&gt;0.006&lt;/td&gt;
&lt;td&gt;42%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;td&gt;0.01&lt;/td&gt;
&lt;td&gt;66%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.6&lt;/td&gt;
&lt;td&gt;0.015&lt;/td&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.7&lt;/td&gt;
&lt;td&gt;0.023&lt;/td&gt;
&lt;td&gt;53%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.8&lt;/td&gt;
&lt;td&gt;0.040&lt;/td&gt;
&lt;td&gt;73%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.9&lt;/td&gt;
&lt;td&gt;0.090&lt;/td&gt;
&lt;td&gt;125%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.99&lt;/td&gt;
&lt;td&gt;0.99&lt;/td&gt;
&lt;td&gt;1000%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;可以看到每次CPU利用率提升10%，延迟就会增加50%以上。如果CPU利用率到达80%，延迟的增长率甚至能到125%.
所以，我们可以得出以下结论&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;为什么有时候CPU利用率到达了50%就需要扩容？ 因为延迟的增长和CPU的利用率并不是&lt;strong&gt;线性增长&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;如果CPU利用率到达了&lt;strong&gt;80%&lt;/strong&gt;，就需要高度重视服务的负载了.&lt;/li&gt;
&lt;li&gt;如果你想要低延迟，你需要保持低CPU利用率。在代码不变的情况下，&lt;strong&gt;高CPU利用率&lt;/strong&gt;意味着&lt;strong&gt;高延迟&lt;/strong&gt;。(想到了之前公司领导提出的高利用率，低延迟的降本增效项目)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;吞吐量和延迟之间的关系&lt;/h2&gt;
&lt;p&gt;吞吐量和延迟一直是系统调优中永恒不变的话题，那么他们的到关系到底是怎么样的，我们能否像之前一样通过某种公式来描述它？&lt;/p&gt;
&lt;h3&gt;1. 模型构建&lt;/h3&gt;
&lt;p&gt;首先我们假定以下条件&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;我们只有一个服务器&lt;/li&gt;
&lt;li&gt;服务器一次只处理一个请求&lt;/li&gt;
&lt;li&gt;请求的到来服从泊松分布&lt;/li&gt;
&lt;li&gt;请求的处理时间服从泊松分布&lt;/li&gt;
&lt;li&gt;每个请求的处理时间保持一样&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;根据以上的假设，我们可以用以下的图片来表明该模型，横轴表明时间，纵轴表明待处理的工作量。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/request.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;我们可以用下面的图来表明更一般的情况，即有可能一个请求还没处理完，下一个请求就来了。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/normal-request.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;现在我们可以尝试计算这个图形的面积，我们有两种方式计算这个图像的面积&lt;/p&gt;
&lt;p&gt;第一种方式
$$
Area = T * (average ~~ height) \newline
= T * (average ~~ wait ~~ time) \newline
= T * W\newline
$$&lt;/p&gt;
&lt;p&gt;其中:&lt;/p&gt;
&lt;p&gt;$T$ 为时间的长度&lt;/p&gt;
&lt;p&gt;$W$ 为请求的平均等待时间&lt;/p&gt;
&lt;p&gt;第二种方式
$$
Area = (area ~ of ~ triangles) + (area ~ of ~ parallelograms)\newline
= (number ~ of ~ request) * (area ~ of ~ single ~ triangle  + area ~ of ~ single ~ parallelogram) \newline
= T*\lambda * ( (\frac{S^2}{2}) + S * W)
$$&lt;/p&gt;
&lt;p&gt;其中:&lt;/p&gt;
&lt;p&gt;$T$ 为时间的长度&lt;/p&gt;
&lt;p&gt;$\lambda$ 为单位时间内可以处理的请求数，即吞吐量&lt;/p&gt;
&lt;p&gt;$S$ 为每个请求的处理时间&lt;/p&gt;
&lt;p&gt;$W$ 为请求的平均等待时间&lt;/p&gt;
&lt;p&gt;我们将上面两个公式进行求解&lt;/p&gt;
&lt;p&gt;$$
T * W = T*\lambda * ( (\frac{S^2}{2}) + S * W)
$$&lt;/p&gt;
&lt;p&gt;可以得到&lt;/p&gt;
&lt;p&gt;$$
W = \frac{\lambda*S^2}{2 * (1 - S\lambda)}
$$&lt;/p&gt;
&lt;p&gt;如果我们固定S(每个请求的处理时间), 我们可以获得以下W与 $\lambda$ 的函数图像，即延迟和吞吐量的关系。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/relation2.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;从该公式我们可以得到以下结论&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;延迟与高吞吐成正比，也就是说低延迟和高吞吐是不可能同时存在的。&lt;/li&gt;
&lt;li&gt;我们也获得了一个符合直觉的情况，&lt;strong&gt;如果要高吞吐你需要高CPU利用率，如果你需要低延迟你需要低CPU利用率&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;如果你可以将处理请求的时间缩短一半，你可以在吞吐量增长一倍的情况下依旧保持之前一般的延迟。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;结论&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;为什么有时候CPU利用率到达了50%就需要扩容？ 因为延迟的增长和CPU的利用率并不是&lt;strong&gt;线性增长&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;如果CPU利用率到达了&lt;strong&gt;80%&lt;/strong&gt;，就需要高度重视服务的负载了.&lt;/li&gt;
&lt;li&gt;如果你想要低延迟，你需要保持低CPU利用率。在代码不变的情况下，&lt;strong&gt;高CPU利用率&lt;/strong&gt;意味着&lt;strong&gt;高延迟&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;延迟与高吞吐成正比，也就是说低延迟和高吞吐是不可能同时存在的。&lt;/li&gt;
&lt;li&gt;我们也获得了一个符合直觉的情况，&lt;strong&gt;如果要高吞吐你需要高CPU利用率，如果你需要低延迟你需要低CPU利用率&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;如果你可以将处理请求的时间缩短一半，你可以在吞吐量增长一倍的情况下依旧保持之前一般的延迟。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><category>Interesting</category><author>tang-hi</author></item><item><title>DuckDB --  MVCC和增删改查</title><link>https://tangdh.life/posts/database/duckdb-mvcc/</link><guid isPermaLink="true">https://tangdh.life/posts/database/duckdb-mvcc/</guid><description>DuckDB 是一款开源 OLAP 数据库。与 SQLite 类似，本文将介绍DuckDB内部所使用的MVCC机制以及增删改查的实现</description><pubDate>Fri, 04 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;DuckDB的MVCC实现来自于&lt;a href=&quot;https://db.in.tum.de/~muehlbau/papers/mvcc.pdf&quot;&gt;论文&lt;/a&gt;，但是DuckDB做了一定的简化。即它的隔离级别并不是可串行化，而是保证Snapshot的隔离，从而它的实现复杂度大幅降低。这篇文章会详细描述DuckDB的MVCC机制，以及增删改查是如何实现的。&lt;/p&gt;
&lt;h2&gt;注意:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;DuckDB是我看的第一个数据库的实现。因此这篇文章并不会比较它与其他数据库在MVCC上的优劣。&lt;/li&gt;
&lt;li&gt;这篇文章并不会事无巨细的把所有实现细节解析出来，只是为了让你可以完整了解是怎么实现的，后续实际看源码时可以更方便的理解。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;前置知识&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DuckDB的状态跟踪&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DuckDB无论增删改查都会有一个状态一直跟踪整个过程，比如查询表的话，它会有一个&lt;code&gt;TableScanGlobalSourceState&lt;/code&gt;和一个&lt;code&gt;TableScanLocalSourceState&lt;/code&gt;对整个查询流程进行跟踪，这个state主要追踪的是当前进行到哪一行了，还剩多少行，等等。&lt;/p&gt;
&lt;p&gt;对于每个算子，这个global和local代表的具体含义都会一些不同，后面具体讲增删改查的时候会进行描述。因为DuckDB的table格式可以划分为rowGroups -&amp;gt; rowGroup -&amp;gt; column -&amp;gt; segment。所以实际上每一个单元都有一个相应的state进行追踪。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/mvcc-state.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DuckDB的local storage&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DuckDB的存储可以分为两块。一块是&lt;code&gt;table&lt;/code&gt;，代表这个表在磁盘中的状态，另一块是&lt;code&gt;local storage&lt;/code&gt;, 代表这个事务中对该表做的操作，比如增删改查等等.... 而&lt;code&gt;local storage&lt;/code&gt;只有在commit的时候才会去和table进行合并。这有两点好处。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;增加事务的并发度。&lt;/li&gt;
&lt;li&gt;rollback时几乎无成本。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/mvcc-local-storage.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DuckDB的MVCC粒度&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DuckDB的的MVCC粒度是对&lt;code&gt;Segment&lt;/code&gt;而言的，即每一个column中的部分数据，会有一个&lt;code&gt;version info&lt;/code&gt;记录着它是被哪个事务加入的，又是被哪个事务删除的。同时还会有一个&lt;code&gt;Update Segment&lt;/code&gt;记录着它的Update Version.
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/mvcc-singualrity.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;MVCC&lt;/h2&gt;
&lt;p&gt;DuckDB会为每一个新创建的transaction赋两个值。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;transaction id（从2^62开始递增）&lt;/li&gt;
&lt;li&gt;start time （从2开始递增)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样赋值的原因在于，在一个&lt;code&gt;transaction&lt;/code&gt;还未提交时，我们会使用&lt;code&gt;transaction id&lt;/code&gt;作为它的&lt;code&gt;commit id&lt;/code&gt;,只有当它提交以后，我们才会将&lt;code&gt;commit id&lt;/code&gt;设置为提交的这一时间。这样就可以确保当事务仍未提交时，它所作出的更改不会被看到。&lt;/p&gt;
&lt;p&gt;我先用文字描述MVCC的实现。然后通过一个例子更直观的理解该实现。&lt;/p&gt;
&lt;h3&gt;文字描述&lt;/h3&gt;
&lt;p&gt;我们会对每一个&lt;code&gt;Segment&lt;/code&gt;维护一个链表，链表中存储版本信息。版本信息中的&lt;code&gt;version&lt;/code&gt;初始化为&lt;code&gt;transaction id&lt;/code&gt;，当commit时，再更新为&lt;code&gt;commit id&lt;/code&gt; &lt;strong&gt;(版本信息中保存的是，这个事务变更前的数据)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当我们对数据进行扫描时，我们会不断比较&lt;code&gt;version&lt;/code&gt;与当前事务的&lt;code&gt;start time&lt;/code&gt;。当满足以下两个条件，我们就会应用其保存的版本。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;version_number &amp;gt; start_time&lt;/p&gt;
&lt;p&gt;说明这个版本还未commit，或者这个版本在事务开始之后才commit.那么我们应当还原成这个事务之前的版本，即应用该版本。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;version_number != transaction_id&lt;/p&gt;
&lt;p&gt;我们不会将数据还原为这次事务之前的版本。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当我们对数据进行更改时，我们会直接在原地进行修改，然后将更改之前的数据保存进&lt;code&gt;Undo Buffer&lt;/code&gt;，插入链表的头部。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;DuckDB为了可以对列进行压缩，并没有直接进行原地更改，相反它是在链表头部保存了一个哑节点。它的原地修改就是直接修改哑节点，这个并不妨碍理解MVCC，所以可以直接认为Duck也在原地修改。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;例子&lt;/h3&gt;
&lt;p&gt;下面我们考虑以下例子。&lt;/p&gt;
&lt;p&gt;我们有一张银行存款表，里面每一个储户的余额都为10，同时我们有4个事务同时执行。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Txn1&lt;/strong&gt; Thomas 向 Larry 转1元&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Txn2&lt;/strong&gt; Thomas 向 Tom 转1元&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Txn3&lt;/strong&gt; 求和&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Txn4&lt;/strong&gt; Thomas 向 Andy 转1元&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/mvcc-init.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;我们假设&lt;strong&gt;Txn1&lt;/strong&gt;, &lt;strong&gt;Txn4&lt;/strong&gt;已经commit， 而 &lt;strong&gt;Txn2&lt;/strong&gt;, &lt;strong&gt;Txn3&lt;/strong&gt;仍在执行，并且&lt;strong&gt;Txn1&lt;/strong&gt;, &lt;strong&gt;Txn4&lt;/strong&gt;在&lt;code&gt;T1&lt;/code&gt;, &lt;code&gt;T2&lt;/code&gt;commit，而&lt;strong&gt;Txn2&lt;/strong&gt;, &lt;strong&gt;Txn3&lt;/strong&gt;在&lt;code&gt;T3&lt;/code&gt;,&lt;code&gt;T4&lt;/code&gt;开启了事务。他们的&lt;code&gt;transaction id&lt;/code&gt;为一个十分大的数。那么此时整体的version info 如下图所示。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/mvcc-version-info.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;我们可以看到每一个事务都有一个对应的&lt;code&gt;Undo Buffer&lt;/code&gt;,同时每一版本的信息都有一个链表来进行维护。我们下面来考虑事务&lt;strong&gt;Txn3&lt;/strong&gt;的执行情况。&lt;/p&gt;
&lt;p&gt;当读取Thomas的Balance时，table中的数据为10,但是因为Thomas的版本信息不为null，所以我们需要遍历链表查看是否有更合适的版本。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;哑节点直接应用， banlance 变为7&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UndoBuffer Ty&lt;/code&gt; , 因为Ty &amp;gt; T3， balance变为8。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UndoBuffer T2&lt;/code&gt; , 因为T2  &amp;lt; T3， 不应用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UndoBuffer T1&lt;/code&gt; , 因为T1  &amp;lt; T3， 不应用。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最终得到的结果为8.符合快照隔离的要求。&lt;/p&gt;
&lt;p&gt;后续几个读取的流程留给读者自己练习, 我们下面介绍DuckDB的增删改查。&lt;/p&gt;
&lt;h2&gt;Insert&lt;/h2&gt;
&lt;p&gt;Insert 的入口函数为&lt;code&gt;PhysicalInsert::Sink&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (!parallel) {
		// init global state if not initialized
		if (!gstate.initialized) {
			storage.InitializeLocalAppend(gstate.append_state, context.client);
			gstate.initialized = true;
		}

		// check if has some conflict with the rules such as UNIQUE, FOREIGN KEY, etc.
		idx_t updated_tuples = OnConflictHandling(table, context, lstate);
		gstate.insert_count += lstate.insert_chunk.size();
		gstate.insert_count += updated_tuples;
		storage.LocalAppend(gstate.append_state, table, context.client, lstate.insert_chunk, true);

		if (return_chunk) {
			gstate.return_collection.Append(lstate.insert_chunk);
		}
	} else {
		// add into local state&apos;s insert chunk
		D_ASSERT(!return_chunk);
		// parallel append
		if (!lstate.local_collection) {
			lock_guard&amp;lt;mutex&amp;gt; l(gstate.lock);
			auto &amp;amp;table_info = storage.info;
			auto &amp;amp;block_manager = TableIOManager::Get(storage).GetBlockManagerForRowData();
			lstate.local_collection =
			    make_uniq&amp;lt;RowGroupCollection&amp;gt;(table_info, block_manager, insert_types, MAX_ROW_ID);
			lstate.local_collection-&amp;gt;InitializeEmpty();
			lstate.local_collection-&amp;gt;InitializeAppend(lstate.local_append_state);
			lstate.writer = &amp;amp;gstate.table.GetStorage().CreateOptimisticWriter(context.client);
		}
		OnConflictHandling(table, context, lstate);

		auto new_row_group = lstate.local_collection-&amp;gt;Append(lstate.insert_chunk, lstate.local_append_state);
		if (new_row_group) {
			lstate.writer-&amp;gt;WriteNewRowGroup(*lstate.local_collection);
		}
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从代码中，我们可以看到DuckDB的Insert有两种模式&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;并行化，每一个算子有自己独立的存储空间，并行插入，Combine的时候合入全局的存储空间 (合入的成本相较于插入成本低很多，因为只需要把指针指向新的位置即可)。&lt;/li&gt;
&lt;li&gt;非并行化，每一个算子直接往全局的存储空间进行插入。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/mvcc-insert-1.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;这里我只介绍非并行化，因为插入的流程是一样的，只是处理的方式不同，因此如果你理解了非并行化，那么你也理解了并行化的方式。&lt;/p&gt;
&lt;p&gt;还记得前置知识中，我们说过，DuckDB中每一个table除了它在磁盘中的表示形式，他还有一个&lt;code&gt;Local Storage&lt;/code&gt;专门用来存储未提交的事务对&lt;code&gt;table&lt;/code&gt;进行的增量操作.而这个&lt;code&gt;Local Storae&lt;/code&gt;的格式与table是完全一致的.即我们的添加流程为。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;找到&lt;code&gt;table&lt;/code&gt;中要添加的&lt;code&gt;RowGroup&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;找到&lt;code&gt;RowGroup&lt;/code&gt;中要添加的&lt;code&gt;Column&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;找到&lt;code&gt;Column&lt;/code&gt;要添加的&lt;code&gt;Segment&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;根据&lt;code&gt;Segement&lt;/code&gt;使用的压缩方法不同，调用不同的压缩算法，把数据添加进&lt;code&gt;Segment&lt;/code&gt;。
对应的代码片段参考如下&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// add into rowGroups
bool RowGroupCollection::Append(DataChunk &amp;amp;chunk, TableAppendState &amp;amp;state) {
	
	idx_t append_count = chunk.size();
	idx_t remaining = chunk.size();
	auto current_row_group = state.row_group_append_state.row_group;
		// check how much we can fit into the current row_group
	idx_t append_count =
		    MinValue&amp;lt;idx_t&amp;gt;(remaining, RowGroup::ROW_GROUP_SIZE - state.row_group_append_state.offset_in_row_group);
		if (append_count &amp;gt; 0) {
			// !! insert into row group
			current_row_group-&amp;gt;Append(state.row_group_append_state, chunk, append_count);
		// skip....
}

// add into rowGroup
void RowGroup::Append(RowGroupAppendState &amp;amp;state, DataChunk &amp;amp;chunk, idx_t append_count) {
	// append to the current row_group
	// append into all column
	for (idx_t i = 0; i &amp;lt; GetColumnCount(); i++) {
		auto &amp;amp;col_data = GetColumn(i);
		col_data.Append(state.states[i], chunk.data[i], append_count);
	}
	// update row group append state
	state.offset_in_row_group += append_count;
}

// add into column
void ColumnData::AppendData(BaseStatistics &amp;amp;stats, ColumnAppendState &amp;amp;state, UnifiedVectorFormat &amp;amp;vdata, idx_t count) {
	
	while (true) {
		// append the data from the vector
		idx_t copied_elements = state.current-&amp;gt;Append(state, vdata, offset, count);

		// we couldn&apos;t fit everything we wanted in the current column segment, create a new one
		{
			auto l = data.Lock();
			AppendTransientSegment(l, state.current-&amp;gt;start + state.current-&amp;gt;count);
			state.current = data.GetLastSegment(l);
			state.current-&amp;gt;InitializeAppend(state);
		}
		// skip...
	}
}

// use compress function to add data into column
idx_t ColumnSegment::Append(ColumnAppendState &amp;amp;state, UnifiedVectorFormat &amp;amp;append_data, idx_t offset, idx_t count) {
	D_ASSERT(segment_type == ColumnSegmentType::TRANSIENT);
	if (!function.get().append) {
		throw InternalException(&quot;Attempting to append to a segment without append method&quot;);
	}
	return function.get().append(*state.append_state, *this, stats, append_data, offset, count);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码中有几点需要注意&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;如果&lt;code&gt;Segment&lt;/code&gt;空间不够，我们会创建新的&lt;code&gt;Segment&lt;/code&gt;,但是这个&lt;code&gt;Segement&lt;/code&gt;的类型为transientSegment。意味着这是一个临时Segment，当内存不足时，会把它写到临时文件中，然后释放这块内存。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当我们写满一块&lt;code&gt;RowGroup&lt;/code&gt;时，我们会将其刷入磁盘,仿佛这个&lt;code&gt;RowGroup&lt;/code&gt;已经被添加到了table中。这是因为如果不这么做，当我们要插入的数据非常大时，我们需要频繁的把数据写到临时文件，这可能造成较大的性能问题。而提前刷入磁盘，我们只需要在rollback时，标记该区域为未使用区域，唯一的问题就是可能造成数据库磁盘文件膨胀。有兴趣的可以查看这个&lt;a href=&quot;https://github.com/duckdb/duckdb/pull/4996&quot;&gt;PR&lt;/a&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在我们将数据添加到&lt;code&gt;Local Storage&lt;/code&gt;后,我们需要对该Insert进行Commit。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;string DuckTransaction::Commit(AttachedDatabase &amp;amp;db, transaction_t commit_id, bool checkpoint) noexcept {
    // skip...
	try {
		
		storage-&amp;gt;Commit(commit_state, *this);
		undo_buffer.Commit(iterator_state, log, commit_id);
		if (log) {
			// commit any sequences that were used to the WAL
			for (auto &amp;amp;entry : sequence_usage) {
				log-&amp;gt;WriteSequenceValue(*entry.first, entry.second);
			}
		}
		if (storage_commit_state) {
            // WAL Flush to DISK
			storage_commit_state-&amp;gt;FlushCommit();
		}
		return string();
	} catch (std::exception &amp;amp;ex) {
		return ex.what();
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码中我们可以看到事务的提交就是三个流程&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;storage commit&lt;/li&gt;
&lt;li&gt;UndoBuffer commit&lt;/li&gt;
&lt;li&gt;WAL 刷到磁盘中&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Storage Commit&lt;/h3&gt;
&lt;p&gt;这个相对简单就是遍历&lt;code&gt;LocalStorage&lt;/code&gt;中的chunk，然后将其添加到&lt;code&gt;table&lt;/code&gt;中。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意DuckDB每一个column都有insert_id, delete_id来描述，它是由哪个&lt;code&gt;transaction&lt;/code&gt;添加的，由哪个&lt;code&gt;transaction&lt;/code&gt;删除的。代码中将其称为&lt;code&gt;Version Info&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在将数据添加到&lt;code&gt;table&lt;/code&gt;后，我们将添加的信息加入到&lt;code&gt;UndoBuffer&lt;/code&gt;中。格式为&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/mvcc-undo-insert.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h3&gt;UndoBuffer Commit&lt;/h3&gt;
&lt;p&gt;逆序遍历&lt;code&gt;UndoBuffer&lt;/code&gt;，根据不同的&lt;code&gt;Undo Flag&lt;/code&gt;对每一个&lt;code&gt;Entry&lt;/code&gt;进行不同的操作。
对于Insert而言&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将新增的数据写到LOG中&lt;/li&gt;
&lt;li&gt;将table中的对应的version info 由&lt;code&gt;transaction id&lt;/code&gt; 更改为&lt;code&gt;commit id&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;WAL 刷到磁盘中&lt;/h3&gt;
&lt;p&gt;在WAL中写WAL_FLUSH后，全部刷新到磁盘。后续Replay时，只有遇到WAL_FLUSH才会进行commit。因此如果在WAL刷到磁盘前断电，哪怕Storage/UndoBuffer Commit了，重启后也是不可见的。&lt;/p&gt;
&lt;h2&gt;Delete&lt;/h2&gt;
&lt;p&gt;Delete 的入口函数为&lt;code&gt;PhysicalDelete::Sink&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SinkResultType PhysicalDelete::Sink(ExecutionContext &amp;amp;context, DataChunk &amp;amp;chunk, OperatorSinkInput &amp;amp;input) const {
	auto &amp;amp;gstate = input.global_state.Cast&amp;lt;DeleteGlobalState&amp;gt;();
	auto &amp;amp;ustate = input.local_state.Cast&amp;lt;DeleteLocalState&amp;gt;();

	// get rows and
	auto &amp;amp;transaction = DuckTransaction::Get(context.client, table.db);
	auto &amp;amp;row_identifiers = chunk.data[row_id_index];

	// skip...
	gstate.deleted_count += table.Delete(tableref, context.client, row_identifiers, chunk.size());

	return SinkResultType::NEED_MORE_INPUT;
}

idx_t DataTable::Delete(TableCatalogEntry &amp;amp;table, ClientContext &amp;amp;context, Vector &amp;amp;row_identifiers, idx_t count) {
	while (pos &amp;lt; count) {
		idx_t start = pos;
		// transaction inserted tuples have row identifiers &amp;gt;= MAX_ROW_ID
		bool is_transaction_delete = ids[pos] &amp;gt;= MAX_ROW_ID;
		// figure out which batch of rows to delete now
		for (pos++; pos &amp;lt; count; pos++) {
			bool row_is_transaction_delete = ids[pos] &amp;gt;= MAX_ROW_ID;
			if (row_is_transaction_delete != is_transaction_delete) {
				break;
			}
		}
		idx_t current_offset = start;
		idx_t current_count = pos - start;

		Vector offset_ids(row_identifiers, current_offset, pos);
		if (is_transaction_delete) {
			// transaction-local delete
			// transaction add and transaction delete
			delete_count += local_storage.Delete(*this, offset_ids, current_count);
		} else {
			// regular table delete
			delete_count += row_groups-&amp;gt;Delete(transaction, *this, ids + current_offset, current_count);
		}
	}
	return delete_count;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从代码中可以看到，&lt;code&gt;delete&lt;/code&gt;不同于&lt;code&gt;insert&lt;/code&gt;，它是直接对table进行删除。但是delete会区分要删除的数据是transaction local的，还是table的。即是&lt;code&gt;local storage&lt;/code&gt;还是&lt;code&gt;table&lt;/code&gt;的,区分逻辑为transaction local的行号都是大于&lt;code&gt;MAX_ROW_ID&lt;/code&gt;的。（删除逻辑是一样的，因此我们只需要看一个就行了）&lt;/p&gt;
&lt;p&gt;首先我们需要找到要删除的&lt;code&gt;Row Group&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;idx_t RowGroupCollection::Delete(TransactionData transaction, DataTable &amp;amp;table, row_t *ids, idx_t count) {
	idx_t delete_count = 0;
	// delete is in the row groups
	// we need to figure out for each id to which row group it belongs
	// usually all (or many) ids belong to the same row group
	// we iterate over the ids and check for every id if it belongs to the same row group as their predecessor
	idx_t pos = 0;
	do {
		idx_t start = pos;
		auto row_group = row_groups-&amp;gt;GetSegment(ids[start]);
		for (pos++; pos &amp;lt; count; pos++) {
			D_ASSERT(ids[pos] &amp;gt;= 0);
			// check if this id still belongs to this row group
			if (idx_t(ids[pos]) &amp;lt; row_group-&amp;gt;start) {
				// id is before row_group start -&amp;gt; it does not
				break;
			}
			if (idx_t(ids[pos]) &amp;gt;= row_group-&amp;gt;start + row_group-&amp;gt;count) {
				// id is after row group end -&amp;gt; it does not
				break;
			}
		}
		delete_count += row_group-&amp;gt;Delete(transaction, table, ids + start, pos - start);
	} while (pos &amp;lt; count);
	return delete_count;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是我们并不需要实际删除该数据，我们所要做的仅仅是标记删除，即将对应数据的&lt;code&gt;delete id&lt;/code&gt;标记为当前的&lt;code&gt;transaction id&lt;/code&gt;, 表明被当前&lt;code&gt;transaction&lt;/code&gt;删除。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void VersionDeleteState::Flush() {
	// no need to flush if there is nothing to flush
	if (count == 0) {
		return;
	}

	// it is possible for delete statements to delete the same tuple multiple times when combined with a USING clause
	// in the current_info-&amp;gt;Delete, we check which tuples are actually deleted (excluding duplicate deletions)
	// this is returned in the actual_delete_count
	auto actual_delete_count = current_info-&amp;gt;Delete(transaction.transaction_id, rows, count);
	delete_count += actual_delete_count;
	// we actually delete some tuples: push the delete into the undo buffer
	if (transaction.transaction &amp;amp;&amp;amp; actual_delete_count &amp;gt; 0) {
		// now push the delete into the undo buffer, but only if any deletes were actually performed
		transaction.transaction-&amp;gt;PushDelete(table, current_info, rows, actual_delete_count, base_row + chunk_row);
	}
	count = 0;
}

// delete according row
idx_t ChunkVectorInfo::Delete(transaction_t transaction_id, row_t rows[], idx_t count) {
	any_deleted = true;

	idx_t deleted_tuples = 0;
	for (idx_t i = 0; i &amp;lt; count; i++) {

		// already deleted
		if (deleted[rows[i]] == transaction_id) {
			continue;
		}

		// first check the chunk for conflicts
		if (deleted[rows[i]] != NOT_DELETED_ID) {
			// tuple was already deleted by another transaction
			throw TransactionException(&quot;Conflict on tuple deletion!&quot;);
		}
		// delete
		deleted[rows[i]] = transaction_id;
		rows[deleted_tuples] = rows[i];
		deleted_tuples++;
	}
	return deleted_tuples;
}

// add undo buffer
void DuckTransaction::PushDelete(DataTable &amp;amp;table, ChunkVectorInfo *vinfo, row_t rows[], idx_t count, idx_t base_row) {
	auto delete_info = reinterpret_cast&amp;lt;DeleteInfo *&amp;gt;(
	    undo_buffer.CreateEntry(UndoFlags::DELETE_TUPLE, sizeof(DeleteInfo) + sizeof(row_t) * count));
	delete_info-&amp;gt;vinfo = vinfo;
	delete_info-&amp;gt;table = &amp;amp;table;
	delete_info-&amp;gt;count = count;
	delete_info-&amp;gt;base_row = base_row;
	memcpy(delete_info-&amp;gt;rows, rows, sizeof(row_t) * count);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面的代码中我们可以看到我们会将当前的&lt;code&gt;transaction id&lt;/code&gt;赋值给&lt;code&gt;deleted&lt;/code&gt;数组中对应的元素，同时往&lt;code&gt;UndoBuffer&lt;/code&gt;中添加对应的&lt;code&gt;Entry&lt;/code&gt;, 即将删除的行号写到&lt;code&gt;UndoBuffer&lt;/code&gt;中。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/mvcc-delete-undo.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;同样的事务的提交为三个流程&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;storage commit&lt;/p&gt;
&lt;p&gt;&lt;code&gt;storage commit&lt;/code&gt; 在Insert中已经讲过了，值得注意的是，当我们扫描&lt;code&gt;Local Storage&lt;/code&gt;时，我们会忽略被删除的数据, 因此被删除的数据不会被合并进table中.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;UndoBuffer Commit&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;WAL 刷到磁盘中&lt;/p&gt;
&lt;p&gt;与Insert完全一致。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面我们来分析一下&lt;code&gt;UndoBuffer Commit&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case UndoFlags::DELETE_TUPLE: {
		// deletion:
		auto info = reinterpret_cast&amp;lt;DeleteInfo *&amp;gt;(data);

		// write delete info into wal log
		if (HAS_LOG &amp;amp;&amp;amp; !info-&amp;gt;table-&amp;gt;info-&amp;gt;IsTemporary()) {
			WriteDelete(*info);
		}

		// mark the tuples as committed
		info-&amp;gt;vinfo-&amp;gt;CommitDelete(commit_id, info-&amp;gt;rows, info-&amp;gt;count);
		break;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到和Insert几乎一样&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将删除的行号写进LOG.&lt;/li&gt;
&lt;li&gt;将table中的对应的version info 由&lt;code&gt;transaction id&lt;/code&gt; 更改为&lt;code&gt;commit id&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Update&lt;/h2&gt;
&lt;p&gt;Update 的入口函数为&lt;code&gt;PhysicalUpdate::Sink&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SinkResultType PhysicalUpdate::Sink(ExecutionContext &amp;amp;context, DataChunk &amp;amp;chunk, OperatorSinkInput &amp;amp;input) const {
	//skip....
		table.Update(tableref, context.client, row_ids, columns, update_chunk);
	// skip...

}

void DataTable::Update(TableCatalogEntry &amp;amp;table, ClientContext &amp;amp;context, Vector &amp;amp;row_ids,
                       const vector&amp;lt;PhysicalIndex&amp;gt; &amp;amp;column_ids, DataChunk &amp;amp;updates) {
	// skip...
	auto ids = FlatVector::GetData&amp;lt;row_t&amp;gt;(row_ids);
	auto first_id = FlatVector::GetValue&amp;lt;row_t&amp;gt;(row_ids, 0);
	if (first_id &amp;gt;= MAX_ROW_ID) {
		// update is in transaction-local storage: push update into local storage
		auto &amp;amp;local_storage = LocalStorage::Get(context, db);
		local_storage.Update(*this, row_ids, column_ids, updates);
		return;
	}

	// update is in the row groups
	// we need to figure out for each id to which row group it belongs
	// usually all (or many) ids belong to the same row group
	// we iterate over the ids and check for every id if it belongs to the same row group as their predecessor
	row_groups-&amp;gt;Update(transaction, ids, column_ids, updates);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和delete一样，我们也会通过&lt;code&gt;row-id&lt;/code&gt;区分更改的是&lt;code&gt;local storage&lt;/code&gt;还是&lt;code&gt;table&lt;/code&gt;,我们来看&lt;code&gt;Update&lt;/code&gt;的具体逻辑。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// RowGroup Update
void RowGroup::Update(TransactionData transaction, DataChunk &amp;amp;update_chunk, row_t *ids, idx_t offset, idx_t count,
                      const vector&amp;lt;PhysicalIndex&amp;gt; &amp;amp;column_ids) {
	for (idx_t i = 0; i &amp;lt; column_ids.size(); i++) {
		auto column = column_ids[i];
		auto &amp;amp;col_data = GetColumn(column.index);

		if (offset &amp;gt; 0) {
			Vector sliced_vector(update_chunk.data[i], offset, offset + count);
			sliced_vector.Flatten(count);
			col_data.Update(transaction, column.index, sliced_vector, ids + offset, count);
		} else {
			col_data.Update(transaction, column.index, update_chunk.data[i], ids, count);
		}
	}
}
// Column Update
void ColumnData::Update(TransactionData transaction, idx_t column_index, Vector &amp;amp;update_vector, row_t *row_ids,
                        idx_t update_count) {
	lock_guard&amp;lt;mutex&amp;gt; update_guard(update_lock);
	if (!updates) {
		updates = make_uniq&amp;lt;UpdateSegment&amp;gt;(*this);
	}
	Vector base_vector(type);
	ColumnScanState state;
	auto fetch_count = Fetch(state, row_ids[0], base_vector);

	base_vector.Flatten(fetch_count);
	updates-&amp;gt;Update(transaction, column_index, update_vector, row_ids, update_count, base_vector);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面的代码我们可以得知，我们仍旧是先找需要Update的&lt;code&gt;RowGroup&lt;/code&gt;, 再找需要Update的&lt;code&gt;ColumnData&lt;/code&gt;,每一个ColumnData都有一个&lt;code&gt;UpdateSegment&lt;/code&gt;，这里面存放着数据的历史版本。而其修改的流程与我们前面介绍的MVCC一致。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/ @brief Update the segment with the given transaction data
// @param transaction The transaction data
// @param column_index The index of the column to update
// @param update The vector containing the update data
// @param ids The row ids to update
// @param count The amount of ids to update
// @param base_data The original data of the column
void UpdateSegment::Update(TransactionData transaction, idx_t column_index, Vector &amp;amp;update, row_t *ids, idx_t count,
                           Vector &amp;amp;base_data) {
	// obtain an exclusive lock
	auto write_lock = lock.GetExclusiveLock();

	update.Flatten(count);
	// skip....
	if (root-&amp;gt;info[vector_index]) {
		// there is already a version here, check if there are any conflicts and search for the node that belongs to
		// this transaction in the version chain
		auto base_info = root-&amp;gt;info[vector_index]-&amp;gt;info.get();
		
		auto node = base_info-&amp;gt;next;
		while (node) {
			if (node-&amp;gt;version_number == transaction.transaction_id) {
				// it has! use this node
				break;
			}
			node = node-&amp;gt;next;
		}
			node-&amp;gt;segment = this;
			node-&amp;gt;vector_index = vector_index;
			node-&amp;gt;N = 0;
			node-&amp;gt;column_index = column_index;

			// insert the new node into the chain
			node-&amp;gt;next = base_info-&amp;gt;next;
			if (node-&amp;gt;next) {
				node-&amp;gt;next-&amp;gt;prev = node;
			}
			node-&amp;gt;prev = base_info;
			base_info-&amp;gt;next = transaction.transaction ? node : nullptr;
		}
		// now we are going to perform the merge
		// because we found this txn has done update before
		// so we just merge the update into the node
		merge_update_function(base_info, base_data, node, update, ids, count, sel);
	} else {
		// there is no version info yet: create the top level update info and fill it with the updates
		auto result = make_uniq&amp;lt;UpdateNodeData&amp;gt;();

		result-&amp;gt;info = make_uniq&amp;lt;UpdateInfo&amp;gt;();
		result-&amp;gt;tuples = make_unsafe_uniq_array&amp;lt;sel_t&amp;gt;(STANDARD_VECTOR_SIZE);
		result-&amp;gt;tuple_data = make_unsafe_uniq_array&amp;lt;data_t&amp;gt;(STANDARD_VECTOR_SIZE * type_size);
		result-&amp;gt;info-&amp;gt;tuples = result-&amp;gt;tuples.get();
		result-&amp;gt;info-&amp;gt;tuple_data = result-&amp;gt;tuple_data.get();
		result-&amp;gt;info-&amp;gt;version_number = TRANSACTION_ID_START - 1;
		result-&amp;gt;info-&amp;gt;column_index = column_index;
		InitializeUpdateInfo(*result-&amp;gt;info, ids, sel, count, vector_index, vector_offset);
		// skip...
		InitializeUpdateInfo(*transaction_node, ids, sel, count, vector_index, vector_offset);

		// we write the updates in the update node data, and write the updates in the info
		initialize_update_function(transaction_node, base_data, result-&amp;gt;info.get(), update, sel);

		result-&amp;gt;info-&amp;gt;next = transaction.transaction ? transaction_node : nullptr;
		result-&amp;gt;info-&amp;gt;prev = nullptr;
		transaction_node-&amp;gt;next = nullptr;
		transaction_node-&amp;gt;prev = result-&amp;gt;info.get();
		transaction_node-&amp;gt;column_index = column_index;

		transaction_node-&amp;gt;Verify();
		result-&amp;gt;info-&amp;gt;Verify();

		root-&amp;gt;info[vector_index] = std::move(result);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码很长，但是实际干的事情就是一件事，将修改前的数据保存下来做成一个&lt;code&gt;UndoBuffer&lt;/code&gt;的Entry写入&lt;code&gt;UndoBuffer&lt;/code&gt;，然后直接本地修改，即&lt;code&gt;base_info&lt;/code&gt;更新数据，然后将Entry插入到&lt;code&gt;base_info&lt;/code&gt;的next。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/mvcc-undo-update.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;最后相同的流程
同样的事务的提交为三个流程&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;storage commit&lt;/li&gt;
&lt;li&gt;UndoBuffer Commit&lt;/li&gt;
&lt;li&gt;WAL 刷到磁盘中&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;不同的只有&lt;code&gt;UndoBuffer Commit&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case UndoFlags::UPDATE_TUPLE: {
		// update:
		auto info = reinterpret_cast&amp;lt;UpdateInfo *&amp;gt;(data);
		if (HAS_LOG &amp;amp;&amp;amp; !info-&amp;gt;segment-&amp;gt;column_data.GetTableInfo().IsTemporary()) {
			WriteUpdate(*info);
		}
		info-&amp;gt;version_number = commit_id;
		break;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样将哪些column变了，写入到WAL中。然后将&lt;code&gt;Update Info&lt;/code&gt;的&lt;code&gt;version number&lt;/code&gt;从&lt;code&gt;transaction id&lt;/code&gt;变为&lt;code&gt;commit id&lt;/code&gt;,表明提交成功。&lt;/p&gt;
&lt;h2&gt;Scan&lt;/h2&gt;
&lt;p&gt;最后我们来讲一下Scan，有了前面的铺垫，Scan就相对容易一些了。
Scan的入口函数为&lt;code&gt;PhysicalTableScan::GetData&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SourceResultType PhysicalTableScan::GetData(ExecutionContext &amp;amp;context, DataChunk &amp;amp;chunk,
                                            OperatorSourceInput &amp;amp;input) const {
	D_ASSERT(!column_ids.empty());
	auto &amp;amp;gstate = input.global_state.Cast&amp;lt;TableScanGlobalSourceState&amp;gt;();
	auto &amp;amp;state = input.local_state.Cast&amp;lt;TableScanLocalSourceState&amp;gt;();

	TableFunctionInput data(bind_data.get(), state.local_state.get(), gstate.global_state.get());
	function.function(context.client, data, chunk);

	return chunk.size() == 0 ? SourceResultType::FINISHED : SourceResultType::HAVE_MORE_OUTPUT;
}

static void TableScanFunc(ClientContext &amp;amp;context, TableFunctionInput &amp;amp;data_p, DataChunk &amp;amp;output) {
	// skip...
	do {
		if(/*skip....*/) {
		} else {
			// scan!!
			storage.Scan(transaction, output, state.scan_state);
		}
		// skip...
	} while (true);
}

void DataTable::Scan(DuckTransaction &amp;amp;transaction, DataChunk &amp;amp;result, TableScanState &amp;amp;state) {
	// scan the persistent segments
	// table state is the the presistent data
	if (state.table_state.Scan(transaction, result)) {
		D_ASSERT(result.size() &amp;gt; 0);
		return;
	}

	// scan the transaction-local segments

	// this was added to the local storage
	auto &amp;amp;local_storage = LocalStorage::Get(transaction);
	local_storage.Scan(state.local_state, state.GetColumnIds(), result);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从代码中我们可以看到Scan的流程为先扫描&lt;code&gt;Table&lt;/code&gt;再扫描&lt;code&gt;Local Storage&lt;/code&gt;。 对于Table的扫描，同样也是一个rowGroup，一个rowGroup来扫描的。我们主要看一下对&lt;code&gt;RowGroup&lt;/code&gt;的扫描。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;TableScanType TYPE&amp;gt;
void RowGroup::TemplatedScan(TransactionData transaction, CollectionScanState &amp;amp;state, DataChunk &amp;amp;result) {

	auto table_filters = state.GetFilters();
	const auto &amp;amp;column_ids = state.GetColumnIds();
	auto adaptive_filter = state.GetAdaptiveFilter();
	while (true) {
		
		idx_t current_row = state.vector_index * STANDARD_VECTOR_SIZE;
		// each time scan entire vector, unless remaining less than STANDARD_VECTOR_SIZE
		auto max_count = MinValue&amp;lt;idx_t&amp;gt;(STANDARD_VECTOR_SIZE, state.max_row_group_row - current_row);

		// second, scan the version chunk manager to figure out which tuples to load for this transaction
		idx_t count;
		SelectionVector valid_sel(STANDARD_VECTOR_SIZE);
		if (TYPE == TableScanType::TABLE_SCAN_REGULAR) {
			// get what is needed to scan in this vector
			// may be it&apos;s deleted by this transaction or inserted by other transaction
			count = state.row_group-&amp;gt;GetSelVector(transaction, state.vector_index, valid_sel, max_count);
			if (count == 0) {
				// nothing to scan for this vector, skip the entire vector
				// increase state.vector_idx, and make every column skip ${count} vector data
				NextVector(state);
				continue;
			}
		}
			if (count == 0) {
				// nothing to scan for this vector, skip the entire vector
				NextVector(state);
				continue;
			}
		} else {
			count = max_count;
		}
		// skip...
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为代码很长，我们分段来看，首先上面的代码中最重要的就是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;state.row_group-&amp;gt;GetSelVector(transaction, state.vector_index, valid_sel, max_count);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这句的含义是，确定这个&lt;code&gt;rowGroup&lt;/code&gt;有哪些是我们这个&lt;code&gt;transaction&lt;/code&gt;可见的，因为有些数据可能是被其他&lt;code&gt;transaction&lt;/code&gt;添加的，对于我们来说应该是不可见的。我们可以通过&lt;code&gt;insert-id&lt;/code&gt;和&lt;code&gt;delete-id&lt;/code&gt;来进行判断&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
	static bool UseInsertedVersion(transaction_t start_time, transaction_t transaction_id, transaction_t id) {
		return id &amp;lt; start_time || id == transaction_id;
	}

	static bool UseDeletedVersion(transaction_t start_time, transaction_t transaction_id, transaction_t id) {
		return !UseInsertedVersion(start_time, transaction_id, id);
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于Insert,如果它的Commit时间小于start time 或者 它就是这个事务添加的。那么应该是可见的。
对于Delete,如果它的Commit时间大于start time 或者 它不是这个事务删除的。那么它不应该被删除，即应该是可见的。&lt;/p&gt;
&lt;p&gt;在确定了哪些tuple是可见后，我们就应该尝试去读取数据了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (count == max_count &amp;amp;&amp;amp; !table_filters) {
	// scan all vectors completely: full scan without deletions or table filters
	for (idx_t i = 0; i &amp;lt; column_ids.size(); i++) {
		const auto &amp;amp;column = column_ids[i];
		if (column == COLUMN_IDENTIFIER_ROW_ID) {
			// scan row id
			D_ASSERT(result.data[i].GetType().InternalType() == ROW_TYPE);
			result.data[i].Sequence(this-&amp;gt;start + current_row, 1, count);
		} else {
			auto &amp;amp;col_data = GetColumn(column);
			if (TYPE != TableScanType::TABLE_SCAN_REGULAR) {
				col_data.ScanCommitted(state.vector_index, state.column_scans[i], result.data[i], ALLOW_UPDATES);
			} else {
				col_data.Scan(transaction, state.vector_index, state.column_scans[i], result.data[i]);
			}
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果全部可见，且没有filter，那么我们直接对每一个column进行读取。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;bool SCAN_COMMITTED, bool ALLOW_UPDATES&amp;gt;
idx_t ColumnData::ScanVector(TransactionData transaction, idx_t vector_index, ColumnScanState &amp;amp;state, Vector &amp;amp;result) {
	// we have got data in the table into the result
	// the total count in this result is scan count
	auto scan_count = ScanVector(state, result, STANDARD_VECTOR_SIZE);

	lock_guard&amp;lt;mutex&amp;gt; update_guard(update_lock);
	if (updates) {
		if (!ALLOW_UPDATES &amp;amp;&amp;amp; updates-&amp;gt;HasUncommittedUpdates(vector_index)) {
			throw TransactionException(&quot;Cannot create index with outstanding updates&quot;);
		}
		result.Flatten(scan_count);
		if (SCAN_COMMITTED) {
			updates-&amp;gt;FetchCommitted(vector_index, result);
		} else {
			updates-&amp;gt;FetchUpdates(transaction, vector_index, result);
		}
	}
	return scan_count;
}

// MVCC read
template &amp;lt;class T&amp;gt;
	static void UpdatesForTransaction(UpdateInfo *current, transaction_t start_time, transaction_t transaction_id,
	                                  T &amp;amp;&amp;amp;callback) {
		while (current) {
			if (current-&amp;gt;version_number &amp;gt; start_time &amp;amp;&amp;amp; current-&amp;gt;version_number != transaction_id) {
				// these tuples were either committed AFTER this transaction started or are not committed yet, use
				// tuples stored in this version
				// update the coressponding data
				callback(current);
			}
			current = current-&amp;gt;next;
		}
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码先读取这个column的原始数据，然后看它有没有Update，如果有的话，就根据我们之前描述的MVCC的方式进行更新。&lt;/p&gt;
&lt;p&gt;如果有Filter的话，会根据Filter条件先进行过滤，再根据过滤后的数据去获得相应的ColumnData，方式与上面描述的一样。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (table_filters) {
	D_ASSERT(adaptive_filter);
	D_ASSERT(ALLOW_UPDATES);
	for (idx_t i = 0; i &amp;lt; table_filters-&amp;gt;filters.size(); i++) {
		auto tf_idx = adaptive_filter-&amp;gt;permutation[i];
		auto col_idx = column_ids[tf_idx];
		auto &amp;amp;col_data = GetColumn(col_idx);
		col_data.Select(transaction, state.vector_index, state.column_scans[tf_idx], result.data[tf_idx],sel, approved_tuple_count, *table_filters-&amp;gt;filters[tf_idx]);
	}
	for (auto &amp;amp;table_filter : table_filters-&amp;gt;filters) {
		result.data[table_filter.first].Slice(sel, approved_tuple_count);
	}
}

//! Now we use the selection vector to fetch data for the other columns.
for (idx_t i = 0; i &amp;lt; column_ids.size(); i++) {
	// we fetch column data for all columns that were not used for filtered
	// skip...
	col_data.FilterScanCommitted(state.vector_index, state.column_scans[i], result.data[i], sel,approved_tuple_count, ALLOW_UPDATES);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后将读取的数据全部返回。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;MVCC与增删改查的东西确实太多了，很难面面俱到，因此这篇文章也只能说把大体的轮廓介绍了一下。如果想知道全部的细节，还是需要去阅读源码。如果有任何不理解，或者觉得描述的不太清晰的，请随时留言提出。&lt;/p&gt;
</content:encoded><category>DataBase</category><category>DuckDB</category><author>tang-hi</author></item><item><title>DuckDB --  ART索引</title><link>https://tangdh.life/posts/database/duckdb-index/</link><guid isPermaLink="true">https://tangdh.life/posts/database/duckdb-index/</guid><description>DuckDB 是一款开源 OLAP 数据库。与 SQLite 类似，本文将介绍DuckDB内部所使用的索引结构</description><pubDate>Fri, 21 Jul 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;DuckDB不同于其他的数据库，并没有使用B+树作为主要索引结构，而是使用了ART(Adaptive Radix Tree)作为它内部的主要索引结构。本文将介绍这一索引&lt;/p&gt;
&lt;h2&gt;ART(Adaptive Radix Tree)&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://db.in.tum.de/~leis/papers/ART.pdf&quot;&gt;ART&lt;/a&gt; 索引是由Viktor Leis, Alfons Kemper, Thomas Neumann等人提出，它相比于B+数的主要区别在于B+树是面向磁盘的，而ART则是面向内存的。即ART索引是需要全部加载到内存中的。DuckDB之所以选择这个索引有以下几方面的考虑&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;随着内存越来越大，并且价格也越来越便宜，我们可以使用纯内存的索引，从而避免磁盘IO，提升性能。&lt;/li&gt;
&lt;li&gt;ART索引可以很大程度上的节省空间。&lt;/li&gt;
&lt;li&gt;ART索引支持范围查询。&lt;/li&gt;
&lt;li&gt;ART索引有着较高的性能。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;后续本文会先介绍ART这一数据结构，然后配合着DuckDB的代码描述ART是如何实现的。&lt;/p&gt;
&lt;h2&gt;数据结构&lt;/h2&gt;
&lt;p&gt;在讲ART索引之前，我们先看一下Trie树。(如果你不知道Trie树，可以参考&lt;a href=&quot;https://en.wikipedia.org/wiki/Trie#:~:text=A%20prefix%20trie%20is%20an,of%20words%20with%20common%20prefixes.&quot;&gt;Trie&lt;/a&gt; )&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/trie.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;我们可以看到Trie树在检索时的优点是，它的检索时间仅与最长的字符串长度有关，而与存储的字符数量无关，这一特性在数据量极大的情况下十分优秀。但是它的缺点是浪费空间，即每个内部节点都需要保存&lt;strong&gt;固定数量&lt;/strong&gt;的指针，即使它仅有极少的子结点。&lt;/p&gt;
&lt;p&gt;比如图中的root节点，尽管他只有三个子结点，但是它仍然需要保存指向&lt;code&gt;a,c,e...&lt;/code&gt;的空指针。这十分浪费空间。其次Trie树仅支持保存字符串。&lt;/p&gt;
&lt;p&gt;ART则是在Trie树的基础上，解决了它缺点的同时，保留了它的优点。下面我们来介绍ART索引。&lt;/p&gt;
&lt;p&gt;对于一个索引而言，我们希望它有以下两个特点&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;查询速度快&lt;/li&gt;
&lt;li&gt;空间占用小&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但是如果我们使用Trie树做索引(ART是Trie的一个变种)，我们就要面临取舍，如果内部节点可以拥有的最大子结点越多(空间占据越多)，那么它的高度也越低(速度越快)。如果内部节点拥有的最大子结点越少(空间占据越少)，那么他的高度也越高(速度越慢)。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/trade-off.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;ART树选择每个内部节点的大小为8bit(子结点的数量为256),刚好是一个&lt;code&gt;byte&lt;/code&gt;。这样的好处是免去了内存对齐的问题，同时在空间与速度上取得了一个较好的平衡。我们称内部节点所占据的位宽为&lt;code&gt;span&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;尽管如此，面对稀疏的数据时，每个节点有256个子结点仍旧会浪费空间，为了解决这个问题。ART将内部节点进一步细分为以下四类, 我们分别来对其进行介绍。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Node4&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Node16&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Node48&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Node256&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Node4&lt;/h3&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/node4.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
从图中可以看出，Node4分为两个部分，一个是key数组，一个是child数组。&lt;code&gt;key数组&lt;/code&gt;存放key的部分内容(也就是key的一个byte)，&lt;code&gt;child数组&lt;/code&gt;则是保存对应的子结点的指针。注意，我们为了可以范围查询，key数组要求顺序存储。&lt;/p&gt;
&lt;h3&gt;Node16&lt;/h3&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/node16.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
Node16和Node4几乎一样，区别只是从4个&lt;code&gt;slot&lt;/code&gt;变为16个&lt;code&gt;slot&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Node48&lt;/h3&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/node48.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
Node48和之前介绍的Node一样也是分为&lt;code&gt;key数组&lt;/code&gt;和&lt;code&gt;child数组&lt;/code&gt;,区别在于Node48的&lt;code&gt;key数组&lt;/code&gt;长度为256,这样子我们就无须通过遍历找到对应的数组，而是可以直接通过key的二进制值作为下标直接定位到对应的&lt;code&gt;key slot&lt;/code&gt;。&lt;code&gt;key slot&lt;/code&gt;中存放的是指针，指向对应的子结点在child数组中的位置。因此child数组的长度仅需要48就可以了。&lt;/p&gt;
&lt;p&gt;实际查询仅需要&lt;code&gt;child_array[key_array[key]]&lt;/code&gt;即可。&lt;/p&gt;
&lt;h3&gt;Node256&lt;/h3&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/node256.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
Node256就是Trie树原始的内部节点表示形式，仅需要一个数组，数组的下标即为key，数组中存放的就是子结点的指针。&lt;/p&gt;
&lt;p&gt;各种不同类型的Node可以相互转换，如果子结点数量超过限制容量就向上转换，如果节点数量相较于限制容量太小就向下转换。&lt;/p&gt;
&lt;h3&gt;Leaf&lt;/h3&gt;
&lt;p&gt;ART中的叶节点存放的就是Key对应的Value值
ART的叶节点可以采用三种形式&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;单独有一个叶节点类型专门保存Value&lt;/li&gt;
&lt;li&gt;和中间节点保持一致的类型，唯一区别则是child数组不保存指针而是值&lt;/li&gt;
&lt;li&gt;如果值足够小可以通过位操作和指针一起保存，那么我们可以将值直接存放在内部节点中。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;DuckDB采用的是第一种方式。&lt;/p&gt;
&lt;h3&gt;优化&lt;/h3&gt;
&lt;p&gt;在解决了ART的空间问题，我们希望可i进一步优化查询速度，即减少树的高度。论文中有两种方式，但实际上我们可以通过一种简单的做法同时获得这两种优化，每个节点加上Prefix标识。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;lazy expansion
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/lazy-expansion.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
其实这个优化相当简单，我们只需&lt;code&gt;Leaf&lt;/code&gt;可以保存多个byte即可，这样子对于多个只有一个子结点的路径来说，我们可以将其都保存在&lt;code&gt;Leaf&lt;/code&gt;中，从而减少树的高度。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;path compression
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/path-compress.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
这个优化和lazy expansion类似，我们只需让&lt;code&gt;内部节点&lt;/code&gt;可以保存多个byte即可。即如果内部节点有相同的前缀，我们可以将其保存在Prefix中，&lt;code&gt;key数组&lt;/code&gt;仅仅只对&lt;strong&gt;key不同&lt;/strong&gt;的部分作区分。这样子也可以有效的减少树的高度。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果这里没看懂也没关系，后续我们会分析DuckDB的代码，那样会更加清晰。&lt;/p&gt;
&lt;h3&gt;数据转换&lt;/h3&gt;
&lt;p&gt;对于ART来说，我们前面介绍的都是对于字符串类型，如果作为一个被广泛使用的索引，那我们也需要支持不同类型的数据。而ART索引实际上是把&lt;code&gt;Key&lt;/code&gt;作为数据流进行处理的，也就是说如果想要通过ART进行范围搜索，我们需要让&lt;code&gt;Key&lt;/code&gt;保持一个性质，即二进制的大小与该类型的语义大小相同。即
$$
\text{memcmp}(binary(x), binary(y)) &amp;lt; 0 \iff \text{x} &amp;lt; \text{y}
$$&lt;/p&gt;
&lt;p&gt;$$
\text{memcmp}(binary(x), binary(y)) = 0 \iff \text{x} = \text{y}
$$&lt;/p&gt;
&lt;p&gt;$$
\text{memcmp}(binary(x), binary(y)) &amp;gt; 0 \iff \text{x} &amp;gt; \text{y}
$$&lt;/p&gt;
&lt;p&gt;因此我们需要对某些数字进行转换&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;unsigned integers&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;无需转化，已经满足需求。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;signed integers&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;将符号位flip即可&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Floating Point Numbers&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static inline uint32_t EncodeFloat(float x) {
	uint64_t buff;

	//! zero
	if (x == 0) {
		buff = 0;
		buff |= (1u &amp;lt;&amp;lt; 31);
		return buff;
	}
	// nan
	if (Value::IsNan(x)) {
		return UINT_MAX;
	}
	//! infinity
	if (x &amp;gt; FLT_MAX) {
		return UINT_MAX - 1;
	}
	//! -infinity
	if (x &amp;lt; -FLT_MAX) {
		return 0;
	}
	buff = Load&amp;lt;uint32_t&amp;gt;(const_data_ptr_cast(&amp;amp;x));
	if ((buff &amp;amp; (1u &amp;lt;&amp;lt; 31)) == 0) { //! +0 and positive numbers
		buff |= (1u &amp;lt;&amp;lt; 31);
	} else {          //! negative numbers
		buff = ~buff; //! complement 1
	}

	return buff;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Character Strings&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;UCA算法已经做出了定义&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Null&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们可以将该值设置为比最大位数仍多1位。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Compound Keys&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;按照其包含的基本类型进行拼接即可&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;源码解析&lt;/h2&gt;
&lt;p&gt;这一章节我们通过阅读DuckDB的源码，来看一下ART索引的实现。
ART索引的相关实现都在&lt;code&gt;art.cpp&lt;/code&gt;和&lt;code&gt;art.hpp&lt;/code&gt;，我们主要关注&lt;code&gt;Insert&lt;/code&gt;和&lt;code&gt;Find&lt;/code&gt;, 其他的函数留给读者自行了解。&lt;/p&gt;
&lt;h3&gt;Insert&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;bool ART::Insert(Node &amp;amp;node, const ARTKey &amp;amp;key, idx_t depth, const row_t &amp;amp;row_id) {

	if (!node.IsSet()) {
		// node is currently empty, create a leaf here with the key
		Leaf::New(*this, node, key, depth, row_id);
		return true;
	}

	if (node.DecodeARTNodeType() == NType::LEAF) {

		// add a row ID to a leaf, if they have the same key
		auto &amp;amp;leaf = Leaf::Get(*this, node);
		auto mismatch_position = leaf.prefix.KeyMismatchPosition(*this, key, depth);

		// identical equal
		if (mismatch_position == leaf.prefix.count &amp;amp;&amp;amp; depth + leaf.prefix.count == key.len) {
			return InsertToLeaf(node, row_id);
		}

		// example:
		// prefix : hello
		// key[depth] : heel;
		// mismatch_position = 2
		// replace leaf with Node4 and store both leaves in it
		auto old_node = node;
		auto &amp;amp;new_n4 = Node4::New(*this, node);

		// new prefix
		// new_n4&apos;s prefix is he
		new_n4.prefix.Initialize(*this, key, depth, mismatch_position);

		// old_node&apos;s prefix change to llo
		auto key_byte = old_node.GetPrefix(*this).Reduce(*this, mismatch_position);

		// add child
		Node4::InsertChild(*this, node, key_byte, old_node);

		Node leaf_node;
		Leaf::New(*this, leaf_node, key, depth + mismatch_position + 1, row_id);
		// add child
		Node4::InsertChild(*this, node, key[depth + mismatch_position], leaf_node);

		return true;
	}

	// handle prefix of inner node
	auto &amp;amp;old_node_prefix = node.GetPrefix(*this);
	if (old_node_prefix.count) {

		auto mismatch_position = old_node_prefix.KeyMismatchPosition(*this, key, depth);
		if (mismatch_position != old_node_prefix.count) {

			// prefix differs, create new node
			auto old_node = node;
			auto &amp;amp;new_n4 = Node4::New(*this, node);
			new_n4.prefix.Initialize(*this, key, depth, mismatch_position);

			auto key_byte = old_node_prefix.Reduce(*this, mismatch_position);
			Node4::InsertChild(*this, node, key_byte, old_node);

			Node leaf_node;
			Leaf::New(*this, leaf_node, key, depth + mismatch_position + 1, row_id);
			Node4::InsertChild(*this, node, key[depth + mismatch_position], leaf_node);

			return true;
		}
		depth += node.GetPrefix(*this).count;
	}

	// recurse
	D_ASSERT(depth &amp;lt; key.len);
	auto child = node.GetChild(*this, key[depth]);
	if (child) {
		bool success = Insert(*child, key, depth + 1, row_id);
		node.ReplaceChild(*this, key[depth], *child);
		return success;
	}

	// insert at position
	Node leaf_node;
	Leaf::New(*this, leaf_node, key, depth + 1, row_id);
	Node::InsertChild(*this, node, key[depth], leaf_node);
	return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码还是比较多的，我们先介绍一下参数的意义&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;node&lt;/strong&gt; 即为当前要进行插入的节点.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;key&lt;/strong&gt; 即为要插入的key&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;depth&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;即当前已经处理到key的第几个byte,举个例子，key为&lt;code&gt;hello&lt;/code&gt;, depth为3，那么说明&lt;code&gt;he&lt;/code&gt;已经保存在了node的祖先节点中，我们当前要处理的是&lt;code&gt;l&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;row_id&lt;/strong&gt; 即为key对应的value值.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;bool ART::Insert(Node &amp;amp;node, const ARTKey &amp;amp;key, idx_t depth, const row_t &amp;amp;row_id) {

	if (!node.IsSet()) {
		// node is currently empty, create a leaf here with the key
		Leaf::New(*this, node, key, depth, row_id);
		return true;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果当前节点为空，那么直接设置该节点为叶节点，并且将&lt;code&gt;row_id&lt;/code&gt;进行保存，注意这里我们会使用&lt;code&gt;lazy-expansion&lt;/code&gt;, 即将key剩余未处理的字符全部保存在叶节点中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool ART::Insert(Node &amp;amp;node, const ARTKey &amp;amp;key, idx_t depth, const row_t &amp;amp;row_id) {

	// .... skip
	if (node.DecodeARTNodeType() == NType::LEAF) {

		// add a row ID to a leaf, if they have the same key
		auto &amp;amp;leaf = Leaf::Get(*this, node);
		auto mismatch_position = leaf.prefix.KeyMismatchPosition(*this, key, depth);

		// identical equal
		if (mismatch_position == leaf.prefix.count &amp;amp;&amp;amp; depth + leaf.prefix.count == key.len) {
			return InsertToLeaf(node, row_id);
		}

		// example:
		// prefix : hello
		// key[depth] : heel;
		// mismatch_position = 2
		// replace leaf with Node4 and store both leaves in it
		auto old_node = node;
		auto &amp;amp;new_n4 = Node4::New(*this, node);

		// new prefix
		// new_n4&apos;s prefix is he
		new_n4.prefix.Initialize(*this, key, depth, mismatch_position);

		// old_node&apos;s prefix change to llo
		auto key_byte = old_node.GetPrefix(*this).Reduce(*this, mismatch_position);

		// add child
		Node4::InsertChild(*this, node, key_byte, old_node);

		Node leaf_node;
		Leaf::New(*this, leaf_node, key, depth + mismatch_position + 1, row_id);
		// add child
		Node4::InsertChild(*this, node, key[depth + mismatch_position], leaf_node);

		return true;
	}
	
	//skip....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果当前遇到的是叶节点，同时key完全相同，那么我们可以直接将&lt;code&gt;row_id&lt;/code&gt;插入叶节点中。不然的话，我们需要将叶节点变为内部节点，同时将不同的部分作为该内部节点的叶节点。如下图所示。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/leaf-insert.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool ART::Insert(Node &amp;amp;node, const ARTKey &amp;amp;key, idx_t depth, const row_t &amp;amp;row_id) {

	// skip ....
	// handle prefix of inner node
	auto &amp;amp;old_node_prefix = node.GetPrefix(*this);
	if (old_node_prefix.count) {

		auto mismatch_position = old_node_prefix.KeyMismatchPosition(*this, key, depth);
		if (mismatch_position != old_node_prefix.count) {

			// prefix differs, create new node
			auto old_node = node;
			auto &amp;amp;new_n4 = Node4::New(*this, node);
			new_n4.prefix.Initialize(*this, key, depth, mismatch_position);

			auto key_byte = old_node_prefix.Reduce(*this, mismatch_position);
			Node4::InsertChild(*this, node, key_byte, old_node);

			Node leaf_node;
			Leaf::New(*this, leaf_node, key, depth + mismatch_position + 1, row_id);
			Node4::InsertChild(*this, node, key[depth + mismatch_position], leaf_node);

			return true;
		}
		depth += node.GetPrefix(*this).count;
	}

	// recurse
	D_ASSERT(depth &amp;lt; key.len);
	auto child = node.GetChild(*this, key[depth]);
	if (child) {
		bool success = Insert(*child, key, depth + 1, row_id);
		node.ReplaceChild(*this, key[depth], *child);
		return success;
	}

	// insert at position
	Node leaf_node;
	Leaf::New(*this, leaf_node, key, depth + 1, row_id);
	Node::InsertChild(*this, node, key[depth], leaf_node);
	return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是内部节点，那我们需要讨论&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果前缀完全相同，即“hello&quot;和”hellopxxx“。那么我们仅需要找出子结点进行插入即可。如下图所示。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/node-insert1.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果前缀有不同指出，那么我们需要创建一个新的节点。并将两个节点作为子结点进行插入。如下图所示。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/node-insert2.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;可以看到，我们只需要在内部节点，和叶节点中支持存储多个字符后，便天然支持上述的有化方案。&lt;/p&gt;
&lt;h3&gt;Find&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Node ART::Lookup(Node node, const ARTKey &amp;amp;key, idx_t depth) {

	while (node.IsSet()) {
		if (node.DecodeARTNodeType() == NType::LEAF) {
			auto &amp;amp;leaf = Leaf::Get(*this, node);

			// check if leaf contains key
			for (idx_t i = 0; i &amp;lt; leaf.prefix.count; i++) {
				if (leaf.prefix.GetByte(*this, i) != key[i + depth]) {
					return Node();
				}
			}
			return node;
		}
		auto &amp;amp;node_prefix = node.GetPrefix(*this);
		if (node_prefix.count) {
			for (idx_t pos = 0; pos &amp;lt; node_prefix.count; pos++) {
				if (key[depth + pos] != node_prefix.GetByte(*this, pos)) {
					// prefix mismatch, subtree of node does not contain key
					return Node();
				}
			}
			depth += node_prefix.count;
		}

		// prefix matches key, but no child at byte, does not contain key
		auto child = node.GetChild(*this, key[depth]);
		if (!child) {
			return Node();
		}

		// recurse into child
		node = *child;
		D_ASSERT(node.IsSet());
		depth++;
	}

	return Node();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查找的代码相对来说比较简单&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;查找到了 &lt;code&gt;Leaf&lt;/code&gt; 节点,检查Prefix是否匹配。如果不匹配说明Key不存在，若匹配直接返回该叶节点即可。&lt;/li&gt;
&lt;li&gt;查找到了 &lt;code&gt;内部节点&lt;/code&gt;,检查Prefix是否匹配。如果不匹配说明Key不存在，若匹配继续搜索对应的子节点。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Last&lt;/h2&gt;
&lt;p&gt;本文介绍了DuckDB的ART索引，可以看到尽管ART索引的树会比B+树更高，因此如果是面向磁盘的情况下，B+树会比ART索引优势更大，但是如果是内存索引的情况下，ART索引更加紧凑，同时他的渐进时间复杂度仅与key的长度有关，可能也更加cache friendly？它的节点相较于B+树更加的小，可以更多的保存在cache中。从论文中的实验来看，它的性能会比B+树更好。相较于&lt;code&gt;Hash table&lt;/code&gt;,它支持范围查询。基于此DuckDB将ART索引作为其的主要索引。&lt;/p&gt;
</content:encoded><category>DataBase</category><category>DuckDB</category><author>tang-hi</author></item><item><title>DuckDB -- table的存储格式</title><link>https://tangdh.life/posts/database/duckdb-file/</link><guid isPermaLink="true">https://tangdh.life/posts/database/duckdb-file/</guid><description>DuckDB 是一款开源 OLAP 数据库。与 SQLite 类似，本文将介绍DuckDB是如何存储它的表结构</description><pubDate>Wed, 19 Jul 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;本文将介绍DuckDB是如何存储它的表结构，本文仅涉及表结构，其他对于理解表结构无关的内容会进行忽略或者一笔带过。&lt;/p&gt;
&lt;h2&gt;前置知识&lt;/h2&gt;
&lt;h3&gt;Block Type&lt;/h3&gt;
&lt;p&gt;DuckDB与其他数据库不同，它将所有的信息都存储在了同一个文件中。文件之中使用Block进行管理,Block分为&lt;code&gt;MetaBlock&lt;/code&gt;以及&lt;code&gt;DataBlock&lt;/code&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;DataBlock&lt;/code&gt; 即为单纯的一个Block&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MetaBlock&lt;/code&gt; 是一个Block List，它的头8个字节表示 &lt;code&gt;next_block_id&lt;/code&gt;。因此如果内容过多，我们可以使用这样一个Block list来存储。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/meta-block.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h3&gt;Field Reader&lt;/h3&gt;
&lt;p&gt;我们有时在一个Block中读取数据时，会采用&lt;code&gt;Field Reader&lt;/code&gt;的方式来进行读取。该&lt;code&gt;Field Reader&lt;/code&gt;与表的字段无关，仅仅是在你读取一些数据前，会先读取&lt;code&gt;max_field_count&lt;/code&gt;和&lt;code&gt;total_size&lt;/code&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;max_field_count&lt;/code&gt;后续要读取的字段个数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;total_size&lt;/code&gt; 后续要读取的总字节数。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/field-reader.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h3&gt;Segment Tree&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Segment&lt;/code&gt;可以认为是一块数据，我们使用&lt;code&gt;SegmentTree&lt;/code&gt;来对&lt;code&gt;Segment&lt;/code&gt;进行管理，虽然它的名字叫做&lt;code&gt;SegmentTree&lt;/code&gt;，但实际上它内部是使用&lt;code&gt;vector&lt;/code&gt;来保存&lt;code&gt;Segment&lt;/code&gt;的。并且会使用二分查找来寻找指定的&lt;code&gt;Segment&lt;/code&gt;，因此这要求Segment是按序存储的。&lt;code&gt;SegmentTree&lt;/code&gt;的另一个特点就是支持懒加载。它并不会一次性将要管理的&lt;code&gt;Segment&lt;/code&gt;全部读取进来，相反，它会在需要时，才从磁盘中读取&lt;code&gt;Segment&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/segmenttree.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;文件结构&lt;/h2&gt;
&lt;p&gt;这一节我们开始介绍&lt;code&gt;DuckDB&lt;/code&gt;的文件结构。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/header.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;我们从图中可以看到&lt;code&gt;DuckDB&lt;/code&gt;有三个&lt;code&gt;Header&lt;/code&gt;，因为这三个Header并不影响我们理解表的存储，因此这里只是简单的介绍一下。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;MainHeader&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;checksum&lt;/strong&gt; 校验和&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Magic bytes&lt;/strong&gt; 确定这是duckDB的文件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;version numbers&lt;/strong&gt; 版本号&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;flags&lt;/strong&gt; 表明该数据库是否可读，可写&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DataBaseHeader&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;iteration&lt;/strong&gt; 迭代次数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;meta block&lt;/strong&gt; 存放data的第一个block的block-id&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;free list&lt;/strong&gt; 可被复用的block&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;block count&lt;/strong&gt; 总block数&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面我们可以看到&lt;code&gt;Data&lt;/code&gt;的存储,它由一个&lt;code&gt;schema count&lt;/code&gt;和 &lt;code&gt;${schema_count}&lt;/code&gt; 个&lt;code&gt;schema&lt;/code&gt;组成，我们的表就存储在&lt;code&gt;schema&lt;/code&gt;中。(schema在DuckDB中可以认为就是一个database)
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/20241026165754.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
我们继续看&lt;code&gt;schema&lt;/code&gt;的存储结构，第一个字段就是&lt;code&gt;schema&lt;/code&gt;的名称,随后跟着的就是该&lt;code&gt;schema&lt;/code&gt;所拥有的各种类型的个数。下面我们简单介绍各种类型。有兴趣的可以自己看一下官网的定义。这里我们只关注&lt;code&gt;table&lt;/code&gt;的数据.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://duckdb.org/docs/sql/data_types/enum&quot;&gt;enum&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://duckdb.org/docs/sql/statements/create_sequence&quot;&gt;sequence&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://duckdb.org/docs/sql/statements/create_view&quot;&gt;view&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://duckdb.org/docs/sql/statements/create_macro&quot;&gt;macro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://duckdb.org/docs/sql/indexes&quot;&gt;indexes&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们继续查看&lt;code&gt;table&lt;/code&gt;的结构&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/table_data.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;我们从图中可以看到&lt;code&gt;table&lt;/code&gt;中前三个字段分别是&lt;code&gt;catalog name&lt;/code&gt;, &lt;code&gt;schema name&lt;/code&gt;, &lt;code&gt;table name&lt;/code&gt;. 我们通过这三个字段可以确定这个表属于哪一个文件(catalog)的哪一个数据库(schema)的哪一个表.
&lt;code&gt;costraints&lt;/code&gt;这一字段来表明该表的一些约束,比如Not Null, Unique等.我们这篇文章不会深究这部分,我们主要研究&lt;code&gt;Columns&lt;/code&gt;以及&lt;code&gt;table data&lt;/code&gt;字段.&lt;/p&gt;
&lt;h3&gt;Columns&lt;/h3&gt;
&lt;p&gt;该字段保存的是&lt;code&gt;table&lt;/code&gt;各个列的定义.
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/column-define.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
我们可以看到,第一个字段保存的是column的数量,该字段后面紧跟着每个column的定义.我们下面来看一下各个字段的意义&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;column name&lt;/strong&gt; 字段名&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;column type&lt;/strong&gt; 字段类型&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;expression&lt;/strong&gt; 表达式, 有些字段是通过表达式生成的.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;table Column type&lt;/strong&gt; 这个不同于&lt;strong&gt;column type&lt;/strong&gt;, 并不表示字段类型,他只有两个取值,一个是&lt;strong&gt;STANDARD&lt;/strong&gt;, 另一个则是&lt;strong&gt;GENERATED&lt;/strong&gt; . (其实我也不是太明白这个字段的意思,大概是用来判断这个字段是不是生成的)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;compression type&lt;/strong&gt; 表明这个字段所采用的压缩方法.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在获得了column的类型以后,其实我们已经完全知晓了table的整个结构,剩下的就是实际数据以及索引的数据了.而这些数据则需要通过&lt;code&gt;table data&lt;/code&gt;这个字段获得.&lt;/p&gt;
&lt;h3&gt;table data&lt;/h3&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/table_data.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;因为索引以及表的实际数据一般都比较大,因此我们并没有在这里直接存储,而是存储了指向实际数据的指针(block-id, offset).&lt;/p&gt;
&lt;p&gt;我们配合这张图来对各个字段进行解释&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;table data block&lt;/strong&gt; 指向实际表数据的指针.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;total rows&lt;/strong&gt; 该表的行数.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;index num&lt;/strong&gt; 该表的所有索引数量.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;index&lt;/strong&gt; 指向索引的指针.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面我们看一下&lt;code&gt;table data block&lt;/code&gt;的实际数据存储的结构
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/row-group.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;最开始存储的是一系列column数据的元信息（后面会介绍column data block的结构)，后两个个字段十分好理解，第一个存储着表的统计信息，另一个存储着 &lt;code&gt;row group&lt;/code&gt; 数量。
这里引出两个问题，什么是&lt;code&gt;row group&lt;/code&gt; ，为什么存储格式不和前面一样即&lt;code&gt;&amp;lt;data-count, data, data, ... data&amp;gt;&lt;/code&gt;.而是只存储了一个&lt;code&gt;row group pointer&lt;/code&gt;,如果&lt;code&gt;row group count&lt;/code&gt; 大于1怎么办？&lt;/p&gt;
&lt;h3&gt;row group&lt;/h3&gt;
&lt;p&gt;我们都知道OLAP一般采取列式存储,而OLTP则采取行式存储。尽管在读取，计算方面列式存储优于行式存储，但如果是频繁的增删改查，行式存储则优于列式存储。因此DuckDB在这里做了一个折衷方案，即将tuple进行分组，组内进行列式存储。目前是每&lt;code&gt;122880&lt;/code&gt;分为一组。&lt;/p&gt;
&lt;h3&gt;为什么只有一个row group pointer&lt;/h3&gt;
&lt;p&gt;因为row group一定是按照行号按序存储的，同时它存储的block为&lt;code&gt;meta block&lt;/code&gt;，所以它可以通过SegmentTree进行管理，从而可以对后续的&lt;code&gt;row group&lt;/code&gt;进行&lt;strong&gt;懒加载&lt;/strong&gt;, 当需要时再直接向后读取即可，因此在这里只需要存储第一块的block-id了。&lt;/p&gt;
&lt;p&gt;下面我们再看一下&lt;code&gt;row group&lt;/code&gt;的存储结构&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;row start&lt;/strong&gt; 该row group的起始行号&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;tuple count&lt;/strong&gt; 该row group的行数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;column pointers&lt;/strong&gt; 因为row group中是按列存储，因此该pointer指向column的实际存储地址&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;versions&lt;/strong&gt; 这个字段没太细看，应该是mvcc相关的内容。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们继续看&lt;code&gt;column data block&lt;/code&gt;的存储结构
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/column-data-block.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;我们惊讶的发现这里仍旧不是实际存储数据的地方，存放的还是指针，这是为什么？原因在于实际的column数据是存放在&lt;code&gt;pure block&lt;/code&gt;中的，即它没法像&lt;code&gt;meta block&lt;/code&gt;那样有一个 &lt;code&gt;block list&lt;/code&gt;，而每个block的大小是定死的，因此我们需要一个个block存储，这里的&lt;code&gt;data pointer&lt;/code&gt;就充当了&lt;code&gt;block list&lt;/code&gt;的链接作用。&lt;/p&gt;
&lt;p&gt;按照惯例，依旧解释一下各字段的含义&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;row start&lt;/strong&gt; 这片数据的开始行号&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;tuple count&lt;/strong&gt; 存储的总行数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;block id&lt;/strong&gt; 实际数据所在的block id&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;offset&lt;/strong&gt; 实际数据所在的block id 的offset&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;compress&lt;/strong&gt; 数据所采用的压缩方式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;stat&lt;/strong&gt; 该部分数据的统计信息&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;现在我们来到了column数据所在的block。存储的格式会因为压缩方式不同而不同，我这里简单介绍几种，有兴趣的可以自己看一下其他几种。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/compress1.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h3&gt;Const  Column&lt;/h3&gt;
&lt;p&gt;const column，即所有的值都一样，所以我们可以完全不存储任何数据。只需从统计信息中得到min value即可&lt;/p&gt;
&lt;h3&gt;uncompress column&lt;/h3&gt;
&lt;p&gt;uncompress column，即不压缩。对于像&lt;code&gt;uint32&lt;/code&gt;, &lt;code&gt;uint8&lt;/code&gt;这样的数据类型，因为是固定大小，因此我们只要一个个读取即可。但是对于像&lt;code&gt;string&lt;/code&gt; 这样非定长的数据类型,我们就会采用另一种方式来存储,即 Dictionary Compress(说好的不压缩呢！)&lt;/p&gt;
&lt;p&gt;对于string首先前两个字段就可以得到&lt;code&gt;dict&lt;/code&gt;的位置&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;dict_start = dict_end - dict_size&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dict_end = dict_end&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dict_size = dict_size&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们在这里将&lt;code&gt;dict&lt;/code&gt;可以看作string pool，而offset则是对应的起始位置，而&lt;code&gt;offsets[i] - offsets[i-1]&lt;/code&gt;即为长度。这么说有点抽象，我们举一个例子。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/string-compress.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;这个例子里面我们一共有三个字符串 &lt;code&gt;foo&lt;/code&gt; ,&lt;code&gt;bar&lt;/code&gt; , &lt;code&gt;duckdb&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;我们将这三个字符串逆序存放在dict中。offset则是相对于&lt;code&gt;dict end&lt;/code&gt;的offset.通过这种方式我们可以定位到相应的string的首地址。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;foo&lt;/strong&gt;&lt;br /&gt;
head = dict - offset = dict - 3&lt;/p&gt;
&lt;p&gt;length = 3 - 0 = 3&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;bar&lt;/strong&gt;&lt;br /&gt;
head = dict - offset = dict - 6&lt;/p&gt;
&lt;p&gt;length = 6 - 3 = 3&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;foo&lt;/strong&gt;&lt;br /&gt;
head = dict - offset = dict - 12&lt;/p&gt;
&lt;p&gt;length = 12 - 6 = 6&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;还记得我们说过column data所在的block都是&lt;code&gt;pure block&lt;/code&gt;， 如果string的长度超长怎么办？
我们会通过将offset取负数，表明该string较长，同时在dict中对应位置存储(block id, offset)，然后去另一个block中读取该string。&lt;/p&gt;
&lt;h3&gt;RLE column and bitpacking&lt;/h3&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/compress2.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;RLE column相对简单，前面存储的是值，后面存储该值出现的次数。通过 &lt;code&gt;RLE count offset&lt;/code&gt;将两者进行分隔。&lt;/p&gt;
&lt;p&gt;bitpack column留给有兴趣的读者自己研究。&lt;/p&gt;
&lt;h3&gt;Dictionary column&lt;/h3&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/compress3.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;如果你理解了&lt;code&gt;uncompress column&lt;/code&gt;中string的存储方式，那你也会较为容易的理解&lt;code&gt;Dictionary column&lt;/code&gt;， 其中dict含义保持不变，index Buffer则是之前提到的&lt;strong&gt;offsets&lt;/strong&gt;,&lt;strong&gt;bitpacking&lt;/strong&gt; 存储的则是该行对应的值是index Buffer中的第几个。通过 &lt;code&gt;dict.get(indexBuffer[bitpacking[i]])&lt;/code&gt; 获得存储的值。&lt;/p&gt;
&lt;p&gt;值得注意的是，这里还有一个优化时，在实际扫描时，会先对dict进行解压，而后如果发现要扫描所有数据时，只需要解压bitpacking即可。&lt;/p&gt;
&lt;h3&gt;Last&lt;/h3&gt;
&lt;p&gt;本文介绍了DuckDB中table的存储结构，duckDB相比于其他的数据库，它仅使用一个文件存储整个数据库(其实我也不知道这是好是坏，但是它的定位是单机数据库，不寻求分布式能力，也许还可以？) 同时它使用了row group的方案，并对其进行懒加载的方式提升性能。column也支持多种压缩格式。&lt;/p&gt;
</content:encoded><category>DataBase</category><category>DuckDB</category><author>tang-hi</author></item><item><title>LevelDB(3) -- 压实与版本</title><link>https://tangdh.life/posts/database/leveldb-compact/</link><guid isPermaLink="true">https://tangdh.life/posts/database/leveldb-compact/</guid><description>LevelDB 是一个高效的KV数据库，本文将介绍LevelDB的压实与版本</description><pubDate>Wed, 21 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;本文将介绍LevelDB的压实操作，以及相应的版本管理。&lt;/p&gt;
&lt;h2&gt;Compact&lt;/h2&gt;
&lt;p&gt;通过前面的文章，我们知道&lt;code&gt;LevelDB&lt;/code&gt;会先将添加的&lt;code&gt;KV&lt;/code&gt;写入到内存中的&lt;code&gt;MemTable&lt;/code&gt;中，等到&lt;code&gt;MemTable&lt;/code&gt;到达一定阈值后，再将&lt;code&gt;MemTable&lt;/code&gt;dump到磁盘中，而且&lt;code&gt;LevelDB&lt;/code&gt;为了写的性能，并不会做&lt;code&gt;update-in-place&lt;/code&gt;,而是标记删除。这就会导致，随着数据的增多，无用的数据也增多(被标记删除的旧记录)，文件数也会越来越多。因此我们需要将多个小文件合并为一个大文件，从而删除无用的数据，并且减少文件数从而提升查询性能。&lt;/p&gt;
&lt;p&gt;合并可以减少空间的占用也许比较好理解，但是为什么减少文件数可以提升性能呢？首先，如果文件数多，那么做一个查询时，需要查询的文件数也相应会变多。其次通过压实合并文件，同一&lt;code&gt;level&lt;/code&gt;中的文件可以保证&lt;code&gt;key&lt;/code&gt;之间没有重叠，从而每一层只需要查找一个文件即可，不同&lt;code&gt;level&lt;/code&gt;之间的文件中的&lt;code&gt;key&lt;/code&gt;也尽可能没有重叠。&lt;/p&gt;
&lt;p&gt;下面我们来看一下&lt;code&gt;LevelDB&lt;/code&gt;的Compact实现&lt;/p&gt;
&lt;h3&gt;Compact的时机&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;LevelDB&lt;/code&gt;在三种情况下会尝试触发Compact&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;DB刚被打开时，此时会尝试触发一次Compact&lt;/li&gt;
&lt;li&gt;有数据写入时，此时也会尝试触发一次Compact&lt;/li&gt;
&lt;li&gt;查询数据时，也会尝试触发一次Compact&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第一种情况没什么好说的，就是打开的时候看看能不能让整个&lt;code&gt;DB&lt;/code&gt;更整洁。&lt;/p&gt;
&lt;p&gt;第二种情况则是如果数据写入前，发现&lt;code&gt;MemTable&lt;/code&gt;已经到达阈值了，那么此时需要将当前的&lt;code&gt;MemTable&lt;/code&gt;dump到磁盘中（这也是一种压实）dump的具体细节在 &lt;a href=&quot;../leveldb-write/&quot;&gt;LevelDB(1) -- 写::ldb文件的格式与生成&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;第三种情况则是查询数据时，如果我们一次查询，查询了多个文件，这就说明level与level之间有&lt;code&gt;key&lt;/code&gt;重叠（同level中key不重叠,除了level-0，因此如果查询了多个文件说明一定涉及多个level), 对于这种情况我们会记录这个文件被查询的次数,当到达阈值后，我们就要尝试进行&lt;code&gt;Compact&lt;/code&gt;，这样子后续再查时，我们可能只需要查找一个文件就可以了。&lt;/p&gt;
&lt;p&gt;一个文件可以被查询的阈值是如何设置的，我们直接看代码与注释，相信就可以很好的理解了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 	  // We arrange to automatically compact this file after
      // a certain number of seeks.  Let&apos;s assume:
      //   (1) One seek costs 10ms
      //   (2) Writing or reading 1MB costs 10ms (100MB/s)
      //   (3) A compaction of 1MB does 25MB of IO:
      //         1MB read from this level
      //         10-12MB read from next level (boundaries may be misaligned)
      //         10-12MB written to next level
      // This implies that 25 seeks cost the same as the compaction
      // of 1MB of data.  I.e., one seek costs approximately the
      // same as the compaction of 40KB of data.  We are a little
      // conservative and allow approximately one seek for every 16KB
      // of data before triggering a compaction.
      f-&amp;gt;allowed_seeks = static_cast&amp;lt;int&amp;gt;((f-&amp;gt;file_size / 16384U));
      if (f-&amp;gt;allowed_seeks &amp;lt; 100) f-&amp;gt;allowed_seeks = 100;

      levels_[level].deleted_files.erase(f-&amp;gt;number);
      levels_[level].added_files-&amp;gt;insert(f);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Compact的实现&lt;/h3&gt;
&lt;p&gt;我们先看代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void DBImpl::BackgroundCall() {
  MutexLock l(&amp;amp;mutex_);
  // 该标识已经在 DBImpl::MaybeScheduleCompaction 进行设置
  assert(background_compaction_scheduled_);
  if (shutting_down_.Acquire_Load()) {
    // No more background work when shutting down.
  } else if (!bg_error_.ok()) {
    // No more background work after a background error.
  } else {
    // 执行具体的压实任务
    BackgroundCompaction();
  }

  background_compaction_scheduled_ = false;

  // 前一次压实可能在某个 level 产生了过多文件, 所以再调度
  // 一次压实, 如果判断真得需要的话.
  MaybeScheduleCompaction();
  background_work_finished_signal_.SignalAll();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里我们可以看到，我们再&lt;code&gt;Compact&lt;/code&gt;后，仍然会尝试再次&lt;code&gt;Compact&lt;/code&gt;这是因为再上一次的&lt;code&gt;Compact&lt;/code&gt;后，可能我们产生了过多的文件，从而需要再次&lt;code&gt;Compact&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;下面的代码是&lt;code&gt;Compact&lt;/code&gt;的具体实现.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 该方法仅在 DBImpl::BackgroundCall 调用
void DBImpl::BackgroundCompaction() {
  // 压实过程需要全程持有锁, 这也暗示压实不能耗费太多时间.
  mutex_.AssertHeld();

  // 先压实已满的 memtable
  if (imm_ != nullptr) {
    CompactMemTable();
    return;
  }
    
  // 如果手动触发了一个压实
  if (is_manual) {
    // ...
  } else {
    // 否则根据统计信息确定待压实 level
    c = versions_-&amp;gt;PickCompaction();
  }


  Status status;
  if (c == nullptr) {
    // 无需压实
  } else if (!is_manual &amp;amp;&amp;amp; c-&amp;gt;IsTrivialMove()) {
    // 不做压实, 直接把文件从 level 移动到 level+1
    assert(c-&amp;gt;num_input_files(0) == 1);
    FileMetaData* f = c-&amp;gt;input(0, 0);
    // 将该文件从 level 层删除
    c-&amp;gt;edit()-&amp;gt;DeleteFile(c-&amp;gt;level(), f-&amp;gt;number);
    // 将该文件增加到 level+1
    c-&amp;gt;edit()-&amp;gt;AddFile(c-&amp;gt;level() + 1, f-&amp;gt;number, f-&amp;gt;file_size,
                       f-&amp;gt;smallest, f-&amp;gt;largest);
    // 应用本次移动操作
    status = versions_-&amp;gt;LogAndApply(c-&amp;gt;edit(), &amp;amp;mutex_);
    if (!status.ok()) {
      RecordBackgroundError(status);
    }
    VersionSet::LevelSummaryStorage tmp;
    Log(options_.info_log, &quot;Moved #%lld to level-%d %lld bytes %s: %s\n&quot;,
        static_cast&amp;lt;unsigned long long&amp;gt;(f-&amp;gt;number),
        c-&amp;gt;level() + 1,
        static_cast&amp;lt;unsigned long long&amp;gt;(f-&amp;gt;file_size),
        status.ToString().c_str(),
        versions_-&amp;gt;LevelSummary(&amp;amp;tmp));
  } else {
    CompactionState* compact = new CompactionState(c);
    // 做压实
    status = DoCompactionWork(compact);
    if (!status.ok()) {
      RecordBackgroundError(status);
    }
    // 清理压实现场
    CleanupCompaction(compact);
    // 释放压实用到的输入文件
    c-&amp;gt;ReleaseInputs();
    // 删除过期文件
    DeleteObsoleteFiles();
  }
  delete c;

  //....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从代码中可以看到，整个&lt;code&gt;Compact&lt;/code&gt;的实现分为两部分，如果有需要被dump到磁盘的&lt;code&gt;MemTable&lt;/code&gt;,那么就直接进行压实。具体流程我在&lt;a href=&quot;../leveldb-write/&quot;&gt;LevelDB(1) -- 写::ldb文件的格式与生成&lt;/a&gt;有详细的描述，这里不赘述。在本篇文章中，我们主要关注第二部分，即&lt;code&gt;自动Compact&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;首先我们会尝试挑选出需要被&lt;code&gt;Compact&lt;/code&gt;的level。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	Compaction* c;
  int level;

  const bool size_compaction = (current_-&amp;gt;compaction_score_ &amp;gt;= 1);
  const bool seek_compaction = (current_-&amp;gt;file_to_compact_ != nullptr);

  // 我们倾向于因为某层数据太多而触发的压实,
  // 而非因为查询次数超过上限(即 FileMetaData-&amp;gt;allowed_seeks)触发的压实.
  // 实现办法就是先检查大小后检查查询次数.

  // 先看有无 level 存储比值已经超过上限
  if (size_compaction) {
    level = current_-&amp;gt;compaction_level_;
    assert(level &amp;gt;= 0);
    assert(level+1 &amp;lt; config::kNumLevels);
    c = new Compaction(options_, level);

    // 找到待压实 level 第一个可能包含 compact_pointer_[level] 的文件
    for (size_t i = 0; i &amp;lt; current_-&amp;gt;files_[level].size(); i++) {
      FileMetaData* f = current_-&amp;gt;files_[level][i];
      if (compact_pointer_[level].empty() ||
          icmp_.Compare(f-&amp;gt;largest.Encode(), compact_pointer_[level]) &amp;gt; 0) {
        // 把这个文件追加到 level 对应的待压实文件集合中
        c-&amp;gt;inputs_[0].push_back(f);
        break;
      }
    }
    // 如果 level 对应的待压实文件集合为空(说明 compact_pointer_[level]
    // 位于 level 最后一个文件之后), 则回绕到开头, 将其第一个
    // 文件加入到待压实集合.
    if (c-&amp;gt;inputs_[0].empty()) {
      // Wrap-around to the beginning of the key space
      c-&amp;gt;inputs_[0].push_back(current_-&amp;gt;files_[level][0]);
    }
  } else if (seek_compaction) { // 再看是否有文件因为查询次数过多
    // (Version::Get() 时候疑似包含但实际不包含目标 key 的最底层
    // level 的第一个文件会被记录到统计信息中, 然后会被 Version::UpdateStats() 处理)
    // 而可以触发压实
    level = current_-&amp;gt;file_to_compact_level_;
    c = new Compaction(options_, level);
    c-&amp;gt;inputs_[0].push_back(current_-&amp;gt;file_to_compact_);
  } else {
    return nullptr;
  }

  c-&amp;gt;input_version_ = current_;
  c-&amp;gt;input_version_-&amp;gt;Ref();

  // level-0 文件可能彼此重叠, 所以要把全部重叠文件都加入到待压实文件集合中
  if (level == 0) {
    InternalKey smallest, largest;
    GetRange(c-&amp;gt;inputs_[0], &amp;amp;smallest, &amp;amp;largest);
    // Note that the next call will discard the file we placed in
    // c-&amp;gt;inputs_[0] earlier and replace it with an overlapping set
    // which will include the picked file.
    // 注意下面这个方法会清除 inputs[0] 内容, 不过不用担心, 由于已经提前提取到了
    // inputs[0] 键范围所以下面这个方法会把那个被清除的文件重新捞回来.
    current_-&amp;gt;GetOverlappingInputs(0, &amp;amp;smallest, &amp;amp;largest, &amp;amp;c-&amp;gt;inputs_[0]);
    assert(!c-&amp;gt;inputs_[0].empty());
  }

  // 将 level+1 中与 level 对应待压实集合重叠的文件拿出来做压实
  SetupOtherInputs(c);

  return c;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据&lt;code&gt;Compact&lt;/code&gt;的触发原因不同，我们采用不同的策略&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;由于某一层的数据超过阈值导致的&lt;code&gt;Compact&lt;/code&gt;,对于这种情况我们采用&lt;code&gt;round-robin&lt;/code&gt;的方式来进行&lt;code&gt;Compact&lt;/code&gt;，即如果上一次&lt;code&gt;Compact&lt;/code&gt;的最大的key为“A1”，那么我们这一次就挑选出比&quot;A1&quot;大的文件来做&lt;code&gt;Compact&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;如果是由于查询次数过大导致的&lt;code&gt;Compact&lt;/code&gt;,那么我们就直接选择该文件来做&lt;code&gt;Compact&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意，我们会对&lt;code&gt;Level-0&lt;/code&gt;做特殊的处理，因为&lt;code&gt;Level-0&lt;/code&gt;中文件的Key会重叠，因此我们会将所有Key重叠的文件都作为准备&lt;code&gt;Compact&lt;/code&gt;的候选项。&lt;/p&gt;
&lt;p&gt;在获得需要&lt;code&gt;Compact&lt;/code&gt;的文件后，我们需要在上一层寻找与当前层重叠的文件作为一个整体一起compact。如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/pick-level.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;在得到上一层准备被&lt;code&gt;Compact&lt;/code&gt;的文件后，我们会获得key的范围，如上图所示，一开始&lt;code&gt;Level-x&lt;/code&gt;的范围为（50-700），在得到&lt;code&gt;Level-x+1&lt;/code&gt;准备被Compact的文件后,范围来到了(50-720)。在某些情况下，我们在&lt;strong&gt;不改变&lt;code&gt;Level-x+1&lt;/code&gt;准备被Compact的文件数&lt;/strong&gt;的前提下，从&lt;code&gt;Level-x&lt;/code&gt;中选择更多的文件来进行&lt;code&gt;Compact&lt;/code&gt;，从而使得&lt;code&gt;Compact&lt;/code&gt;的效率更高。之所以不改变&lt;code&gt;Level-x+1&lt;/code&gt;准备被Compact的文件数，是为了防止无限制的循环下去，从而导致&lt;code&gt;Level-x&lt;/code&gt;和&lt;code&gt;Level-x+1&lt;/code&gt;的所有文件全部都需要进行&lt;code&gt;Compact&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在决定了需要&lt;code&gt;Compact&lt;/code&gt;的文件后，我们有两种方式进行&lt;code&gt;Compact&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;TrivialMove&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;return (num_input_files(0) == 1 &amp;amp;&amp;amp; num_input_files(1) == 0 &amp;amp;&amp;amp;
          TotalFileSize(grandparents_) &amp;lt;=
              MaxGrandParentOverlapBytes(vset-&amp;gt;options_));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 &lt;code&gt;level&lt;/code&gt; 层只有 1 个待压实文件 ， &lt;code&gt;level+1&lt;/code&gt; 层没有与 &lt;code&gt;level&lt;/code&gt; 待压实文件发生重叠的文件 且 &lt;code&gt;level+2 层与 level&lt;/code&gt; 待压实文件重叠的字节数不大于上限,则可以用移动替代压实.这里之所以要判断**&lt;code&gt;level+2 层与 level&lt;/code&gt; 待压实文件重叠的字节数不大于上限** 是因为如果 &lt;code&gt;level&lt;/code&gt; 与祖父(即 &lt;code&gt;level+2&lt;/code&gt;) 有大量重叠数据, 合并后会创建一个父文件(即 &lt;code&gt;level+1&lt;/code&gt;), 很显然这个文件和自己父亲 &lt;code&gt;level&lt;/code&gt;(即上面说的 &lt;code&gt;level+2&lt;/code&gt;)存在大量重叠数据, 这个情况会导致后续非常昂贵的合并.&lt;/p&gt;
&lt;h4&gt;DoCompactionWork&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 具体压实就做一件事情:
// 遍历待压实文件, 如果某个 key (位于 level-L 或者 level-(L+1))的类型属性取值为&quot;删除&quot;,
// 则确认其在 level-(L+2) 或之上是否存在, 若不存在则丢弃之, 否则写入合并后的文件.
Status DBImpl::DoCompactionWork(CompactionState* compact) {
  const uint64_t start_micros = env_-&amp;gt;NowMicros();
  // 用于 imm_ 压实耗时统计
  int64_t imm_micros = 0;

  Log(options_.info_log,  &quot;Compacting %d@%d + %d@%d files&quot;,
      compact-&amp;gt;compaction-&amp;gt;num_input_files(0),
      compact-&amp;gt;compaction-&amp;gt;level(),
      compact-&amp;gt;compaction-&amp;gt;num_input_files(1),
      compact-&amp;gt;compaction-&amp;gt;level() + 1);

  assert(versions_-&amp;gt;NumLevelFiles(compact-&amp;gt;compaction-&amp;gt;level()) &amp;gt; 0);
  assert(compact-&amp;gt;builder == nullptr);
  assert(compact-&amp;gt;outfile == nullptr);
  // 如果快照列表为空, 则将最新的操作序列号作为最小的快照
  if (snapshots_.empty()) {
    compact-&amp;gt;smallest_snapshot = versions_-&amp;gt;LastSequence();
  } else {
    // 否则从快照列表获取最老的快照对应的序列号作为最小快照.
    // 虽然最老, 但是没有 release 就是要保障可见性的.
    compact-&amp;gt;smallest_snapshot = snapshots_.oldest()-&amp;gt;sequence_number();
  }

  // 真正做压实工作的之前要释放锁
  mutex_.Unlock();

  // 针对待压实的全部文件创建一个大迭代器
  Iterator* input = versions_-&amp;gt;MakeInputIterator(compact-&amp;gt;compaction);
  // 迭代器指针拨到开头
  input-&amp;gt;SeekToFirst();
  Status status;
  ParsedInternalKey ikey;
  // 下面三个临时变量用来处理多个文件(如果压实涉及了 level-0)
  // 或多个 level 存在同名 user key 的问题, 典型地有如下两种:
  // 1. level-0 文件可能存在重叠, 同名 user key 后出现的更新,
  // 序列号也更大.
  // 2. 低 level  和高 level 之间可能重叠(这个可能其实是肯定,
  // 因为不重叠就不用压实了), 同名 user key 先出现的更新, 序列号也更大.
  std::string current_user_key;
  bool has_current_user_key = false;
  // 如果 user key 出现多次, 下面这个用于记录上次出现时对应的
  // internal key 的序列号.
  SequenceNumber last_sequence_for_key = kMaxSequenceNumber;
  for (; input-&amp;gt;Valid() &amp;amp;&amp;amp; !shutting_down_.Acquire_Load(); ) {
    // 优先处理已经写满待压实的 memtable
    if (has_imm_.NoBarrier_Load() != nullptr) {
      const uint64_t imm_start = env_-&amp;gt;NowMicros();
      mutex_.Lock();
      if (imm_ != nullptr) {
        // immutable memtable 落盘
        CompactMemTable();
        // 如有必要唤醒 MakeRoomForWrite()
        background_work_finished_signal_.SignalAll();
      }
      mutex_.Unlock();
      imm_micros += (env_-&amp;gt;NowMicros() - imm_start);
    }

    // 即将被处理的 key
    Slice key = input-&amp;gt;key();
    // 当发现截止到 key, level 和 level+2 重叠数据量已经达到上限, 则
    // 开始进行压实; key 也是压实的最右区间.
    //　一进来循环看到这个判断代码可能比较懵, 肯定看不太懂, 其实下面这个判断一般
    // 要经过若干循环才能成立, 先看后面代码再回来看这个判断.
    if (compact-&amp;gt;compaction-&amp;gt;ShouldStopBefore(key) &amp;amp;&amp;amp;
        compact-&amp;gt;builder != nullptr) {
      // 将压实生成的文件落盘
      status = FinishCompactionOutputFile(compact, input);
      if (!status.ok()) {
        break;
      }
    }

    // Handle key/value, add to state, etc.
    bool drop = false;
    // 反序列化 internal key
    if (!ParseInternalKey(key, &amp;amp;ikey)) {
      // Do not hide error keys
      current_user_key.clear();
      has_current_user_key = false;
      last_sequence_for_key = kMaxSequenceNumber;
    } else {
      // 如果这个 user key 之前迭代未出现过, 记下来
      if (!has_current_user_key ||
          user_comparator()-&amp;gt;Compare(ikey.user_key,
                                     Slice(current_user_key)) != 0) {
        current_user_key.assign(ikey.user_key.data(), ikey.user_key.size());
        has_current_user_key = true;
        // 标记这个 user key 截止目前轮次迭代对应的序列号;
        // 因为是首次出现所以这里直接置为序列号最大可能取值.
        // 确保最新的数据一定不会被drop
        last_sequence_for_key = kMaxSequenceNumber;
      }

      // 序列号过小, 丢弃这个 key 本次迭代对应的数据; 后面还有这个 key
      // 对应的更新的数据.
      // 上一个seq &amp;lt;= smallest_snapshot, 那么这个必然 &amp;lt; smallest_snapshot
      // 因此可以直接丢弃。
      if (last_sequence_for_key &amp;lt;= compact-&amp;gt;smallest_snapshot) {
        // Hidden by an newer entry for same user key
        drop = true;    // 规则 (A)
      } else if (ikey.type == kTypeDeletion &amp;amp;&amp;amp;
                 ikey.sequence &amp;lt;= compact-&amp;gt;smallest_snapshot &amp;amp;&amp;amp;
                 compact-&amp;gt;compaction-&amp;gt;IsBaseLevelForKey(ikey.user_key)) {
        // 对于这个 user key:
        // (1) 更高的 levels(指的是祖父 level 及之上)没有对应数据了
        // (2) 更低的 levels 对应的数据的序列号会更大(这个是显然地)
        // (3) 目前正在被压实的各个 levels(即 level 和 level+1) 中序列号
        // 更小的数据在循环的未来几次迭代中会被丢弃(根据上面的规则(A)).
        //
        // 综上, 这个删除标记已经过期了并且可以被丢弃.
        drop = true;
      }
	  // 如果没有snapshot，相同的user key只保存最新数据。
      last_sequence_for_key = ikey.sequence;
    }
#if 0
    Log(options_.info_log,
        &quot;  Compact: %s, seq %d, type: %d %d, drop: %d, is_base: %d, &quot;
        &quot;%d smallest_snapshot: %d&quot;,
        ikey.user_key.ToString().c_str(),
        (int)ikey.sequence, ikey.type, kTypeValue, drop,
        compact-&amp;gt;compaction-&amp;gt;IsBaseLevelForKey(ikey.user_key),
        (int)last_sequence_for_key, (int)compact-&amp;gt;smallest_snapshot);
#endif

    // 如果当前数据项不丢弃, 则进行压实落盘
    if (!drop) {
      // 如有必要则创建新的 output file
      if (compact-&amp;gt;builder == nullptr) {
        status = OpenCompactionOutputFile(compact);
        if (!status.ok()) {
          break;
        }
      }
      if (compact-&amp;gt;builder-&amp;gt;NumEntries() == 0) {
        // 如果一个都没写过, input 迭代器又是从小到大遍历,
        // 所以当前 user key 肯定是最小的
        compact-&amp;gt;current_output()-&amp;gt;smallest.DecodeFrom(key);
      }
      // 否则当前 user key 目前就是最大的
      compact-&amp;gt;current_output()-&amp;gt;largest.DecodeFrom(key);
      // 将该 user key 对应的数据项写入 sstable.
      // TODO 这里有个地方没看明白:
      // 如果当前 user key 首次出现, 则
      // 上面 last_sequence_for_key 被置为 kMaxSequenceNumber,
      // 且类型不是 kTypeDeletion, 那当前数据项就不会被 drop, 即使
      // 这个数据项实际 sequence number 小于 smallest_snapshot,
      // 有点矛盾了.
      compact-&amp;gt;builder-&amp;gt;Add(key, input-&amp;gt;value());

      // 如果 sstable 文件足够大, 则落盘并关闭
      if (compact-&amp;gt;builder-&amp;gt;FileSize() &amp;gt;=
          compact-&amp;gt;compaction-&amp;gt;MaxOutputFileSize()) {
        status = FinishCompactionOutputFile(compact, input);
        if (!status.ok()) {
          break;
        }
      }
    }

    // 处理下个 key
    input-&amp;gt;Next();
  }

  if (status.ok() &amp;amp;&amp;amp; shutting_down_.Acquire_Load()) {
    status = Status::IOError(&quot;Deleting DB during compaction&quot;);
  }
  if (status.ok() &amp;amp;&amp;amp; compact-&amp;gt;builder != nullptr) {
    status = FinishCompactionOutputFile(compact, input);
  }
  if (status.ok()) {
    status = input-&amp;gt;status();
  }
  delete input;
  input = nullptr;

  CompactionStats stats;
  stats.micros = env_-&amp;gt;NowMicros() - start_micros - imm_micros;
  for (int which = 0; which &amp;lt; 2; which++) {
    for (int i = 0; i &amp;lt; compact-&amp;gt;compaction-&amp;gt;num_input_files(which); i++) {
      stats.bytes_read += compact-&amp;gt;compaction-&amp;gt;input(which, i)-&amp;gt;file_size;
    }
  }
  for (size_t i = 0; i &amp;lt; compact-&amp;gt;outputs.size(); i++) {
    stats.bytes_written += compact-&amp;gt;outputs[i].file_size;
  }

  mutex_.Lock();
  stats_[compact-&amp;gt;compaction-&amp;gt;level() + 1].Add(stats);

  if (status.ok()) {
    status = InstallCompactionResults(compact);
  }
  if (!status.ok()) {
    RecordBackgroundError(status);
  }
  VersionSet::LevelSummaryStorage tmp;
  Log(options_.info_log,
      &quot;compacted to: %s&quot;, versions_-&amp;gt;LevelSummary(&amp;amp;tmp));
  return status;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际的压实工作其实很简单，对于待压实的文件构建一个统一的迭代器，从小到大顺序访问（还记得InternalKey的构造吗，该构造可以保证我们总是最先读到最新的数据),不断的将数据写入到新的文件中。注意，如果遇到遇到&lt;strong&gt;标记删除&lt;/strong&gt;的数据,不应该马上&lt;code&gt;drop&lt;/code&gt;，而是应该确定上层没有该key的数据了，再&lt;code&gt;drop&lt;/code&gt;(因为如果&lt;code&gt;drop&lt;/code&gt;了，会导致以后读取该数据，可能读到上层的数据，从而导致一个本该被删除的数据又被读到了)&lt;/p&gt;
&lt;p&gt;后面每次把最新的数据写到新的文件中。下面两种情况需要将新生成的文件落盘。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;文件到达阈值&lt;/li&gt;
&lt;li&gt;文件与grandparent重叠的byte数到达阈值（减少后续的&lt;code&gt;Compact&lt;/code&gt;压力）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最终完成压实的操作。&lt;/p&gt;
&lt;h2&gt;Version&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;LevelDB&lt;/code&gt;的最后，我们介绍一下版本，版本可以认为是&lt;code&gt;LevelDB&lt;/code&gt;管理文件的一个接口，如果你想要获取文件，获取某一Level的文件，你都需要通过Version。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;LevelDB&lt;/code&gt;的版本由三部分组成&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;VersionSet 负责维护所有的Version&lt;/li&gt;
&lt;li&gt;Version 一个确定的版本，可以认为是数据库的Snapshot。&lt;/li&gt;
&lt;li&gt;VersionEdit 增量更新的版本，当完成增量更新后，VersionEdit就会变为Version。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们先看一下Version&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Version {
 
  VersionSet* vset_;           
  // 接下来两个指针使得 Version 可以构成双向循环链表
  // 指向链表中下个 version 的指针
  Version* next_;              
  // 指向链表中前个 version 的指针
  Version* prev_;              
  // 该 version 的活跃引用计数
  int refs_;                    

  // 核心成员, 该成员保存了当前最新的 level 架构信息,
  // 即 db 每个 level 的文件元数据链表
  std::vector&amp;lt;FileMetaData*&amp;gt; files_[config::kNumLevels];

  // 基于查询统计而得出的下个待压实的文件及其所在的 level
  FileMetaData* file_to_compact_;
  int file_to_compact_level_;

  // 基于存储比值计算的压实分数,
  // 小于 1 意味着未到上限, 压实不是很需要.
  // 由 Finalize() 计算.
  double compaction_score_;
  // 基于存储比值而得出的下个待压实的 level.
  // 由 Finalize() 计算.
  int compaction_level_;
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到&lt;code&gt;Version&lt;/code&gt;最重要的是保存着每一层的文件元信息，通过这些信息，我们可以获得每一个&lt;code&gt;Level&lt;/code&gt;所拥有的文件。这也就是Version最重要的作用，确定Level的格式。&lt;/p&gt;
&lt;p&gt;我们再看一下&lt;code&gt;VersionSet&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class VersionSet {
  Env* const env_;
  const std::string dbname_;
  const Options* const options_;
  // 每次用户进行查询操作的时候(DBImpl::Get())可能需要去查询
  // 磁盘上的文件, 这就要求有个缓存功能来加速.
  // 下面这个成员会缓存 sstable 文件对应的 Table 实例, 
  // 用于加速用户的查询, 否则每次读文件解析
  // 就很慢了. 目前在用的缓存策略是 LRU.
  // 该变量实际值来自 DBImpl 实例, 具体见 VersionSet 构造方法.
  TableCache* const table_cache_;
  const InternalKeyComparator icmp_;
  uint64_t next_file_number_;
  uint64_t manifest_file_number_;
  // 记录最近一次更新操作对应的序列号(逐一递增, WriteBatch 包含一批更新操作, 每个更新操作都会有一个序列号).
  // 具体修改建 DbImpl::Write 方法
  uint64_t last_sequence_;
  uint64_t log_number_;
  uint64_t prev_log_number_;  // 0 or backing store for memtable being compacted

  // Opened lazily
  // 当前 MANIFEST 文件
  WritableFile* descriptor_file_;
  // MANIFEST 文件格式同 log 文件, 所以写入方法就复用了.
  // 其每条日志就是一个序列化后的 VersionEdit.
  log::Writer* descriptor_log_; 
  // 属于该 VersionSet 的 Version 都会被维护到一个双向循环链表中,
  // 而且新加入的 Version 都会插入到 dummy_versions_ 前面. 
  // dummy_versions_.next_ 默认指向自己(具体见 Version 构造函数)后续指向最老的 version.
  Version dummy_versions_; 
  // 指向当前 Version == dummy_versions_.prev_
  Version* current_;       

  // Per-level key at which the next compaction at that level should start.
  // Either an empty string, or a valid InternalKey.
  // 记录了每个 level 各自对应的下次压实的起始 key
  std::string compact_pointer_[config::kNumLevels];

  // No copying allowed
  VersionSet(const VersionSet&amp;amp;);
  void operator=(const VersionSet&amp;amp;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到VersionSet保存着整个数据库的元信息，例如下一个文件的&lt;code&gt;number&lt;/code&gt;，最新的&lt;code&gt;sequence&lt;/code&gt;...,同时也维护者最新的&lt;code&gt;Version&lt;/code&gt;，我们可以将其视作数据库的元信息。&lt;/p&gt;
&lt;p&gt;我们再看一下&lt;code&gt;VersionEdit&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class VersionEdit {

 private:
  friend class VersionSet;

  typedef std::set&amp;lt; std::pair&amp;lt;int, uint64_t&amp;gt; &amp;gt; DeletedFileSet;

  // 比较器名称
  std::string comparator_; // comparator name
  uint64_t log_number_;
  uint64_t prev_log_number_;
  // 下个 MANIFEST 文件编号, 从 1 开始
  uint64_t next_file_number_;
  // 下个写操作的序列号
  SequenceNumber last_sequence_;
  bool has_comparator_;
  bool has_log_number_;
  bool has_prev_log_number_;
  bool has_next_file_number_;
  bool has_last_sequence_;

  // 记录每个 level 下次压实的起始 key
  std::vector&amp;lt; std::pair&amp;lt;int, InternalKey&amp;gt; &amp;gt; compact_pointers_;
  // 保存从当前 level 架构要删除的一个文件
  DeletedFileSet deleted_files_;
  // 保存要新增到当前 level 架构中的文件(注意第二个参数不是指针类型)
  std::vector&amp;lt; std::pair&amp;lt;int, FileMetaData&amp;gt; &amp;gt; new_files_;
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;VersionEdit&lt;/code&gt;存储着目前数据库的增量信息，可以认为是实时的&lt;code&gt;Version&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;我们从数据库的&lt;code&gt;Open&lt;/code&gt;过程，来将这三个类串联起来。&lt;code&gt;Open&lt;/code&gt;的过程主要还是&lt;code&gt;Recover&lt;/code&gt;完成的，我们来看&lt;code&gt;Recover&lt;/code&gt;的代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Status DBImpl::Recover(VersionEdit* edit, bool *save_manifest) {
  mutex_.AssertHeld();

  // Ignore error from CreateDir since the creation of the DB is
  // committed only when the descriptor is created, and this directory
  // may already exist from a previous failed creation attempt.
  // 创建数据库目录(一个目录代表一个数据库)
  env_-&amp;gt;CreateDir(dbname_);
  assert(db_lock_ == nullptr);
  // 锁定该目录
  Status s = env_-&amp;gt;LockFile(LockFileName(dbname_), &amp;amp;db_lock_);
  if (!s.ok()) {
    return s;
  }

  // 如果 CURRENT 文件(记录当前 MENIFEST 文件名称)不存在则创建之
  if (!env_-&amp;gt;FileExists(CurrentFileName(dbname_))) {
    if (options_.create_if_missing) {
      // 创建之
      s = NewDB();
      if (!s.ok()) {
        return s;
      }
    } else {
      // 报错
      return Status::InvalidArgument(
          dbname_, &quot;does not exist (create_if_missing is false)&quot;);
    }
  } else {
    if (options_.error_if_exists) {
      return Status::InvalidArgument(
          dbname_, &quot;exists (error_if_exists is true)&quot;);
    }
  }
  //....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先&lt;code&gt;Recover&lt;/code&gt;会将目录锁定起来，如果目录下没有CURRENT文件，那么该数据库为新建立的，新建一个数据库即可。&lt;/p&gt;
&lt;p&gt;在这里&lt;code&gt;CURRENT&lt;/code&gt;文件记录着最新的&lt;code&gt;MANIFEST&lt;/code&gt;文件名。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/current-file.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;在得到了最新的&lt;code&gt;MANIFEST&lt;/code&gt;文件名后，我们就可以调用&lt;code&gt;VersionSet::Recover&lt;/code&gt;读取&lt;code&gt;MANIFEST&lt;/code&gt;文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  Builder builder(this, current_);

  {
    LogReporter reporter;
    reporter.status = &amp;amp;s;
    log::Reader reader(file, &amp;amp;reporter, true/*checksum*/, 0/*initial_offset*/);
    Slice record;
    std::string scratch;
    // 循环读取 MANIFEST 文件日志, 每一行日志就是一个 VersionEdit
    while (reader.ReadRecord(&amp;amp;record, &amp;amp;scratch) &amp;amp;&amp;amp; s.ok()) {
      VersionEdit edit;
      // 将 record 反序列化为 version_edit
      s = edit.DecodeFrom(record);
      if (s.ok()) {
        if (edit.has_comparator_ &amp;amp;&amp;amp;
            edit.comparator_ != icmp_.user_comparator()-&amp;gt;Name()) {
          s = Status::InvalidArgument(
              edit.comparator_ + &quot; does not match existing comparator &quot;,
              icmp_.user_comparator()-&amp;gt;Name());
        }
      }

      // 将 VersionEdit 保存到 VersionSet 的 builder 中, 
      // 后者可以一次性将这些文件变更与当前 Version 合并构成新 version.
      if (s.ok()) {
        builder.Apply(&amp;amp;edit);
      }

      if (edit.has_log_number_) {
        // 保存最新的日志文件名, 越后面的日志(record)记录的日志文件名越新
        log_number = edit.log_number_;
        have_log_number = true;
      }

      if (edit.has_prev_log_number_) {
        prev_log_number = edit.prev_log_number_;
        have_prev_log_number = true;
      }

      if (edit.has_next_file_number_) {
        next_file = edit.next_file_number_;
        have_next_file = true;
      }

      if (edit.has_last_sequence_) {
        last_sequence = edit.last_sequence_;
        have_last_sequence = true;
      }
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Manifest&lt;/code&gt;的文件格式与LOG文件保持一致，LOG文件的具体格式如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/log-format.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;而里面的data则是&lt;code&gt;VersionEdit&lt;/code&gt;的序列化形式（&lt;strong&gt;里面的tag可能出现多次&lt;/strong&gt;）。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/version-edit.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;在反序列化&lt;code&gt;VersionEdit&lt;/code&gt;后，我们可以将所有的&lt;code&gt;VersionEdit&lt;/code&gt;合并给一个&lt;code&gt;Version&lt;/code&gt;,本质上就是把&lt;code&gt;VersionEdit&lt;/code&gt;的新增文件以及删除文件，compact_pointer,和现有的&lt;code&gt;Version&lt;/code&gt;进行合并。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void Apply(VersionEdit* edit) {
    // 更新压实指针信息
    // 将 edit 中保存的每一层下次压实起始 key 复制到 VersionSet 中
    for (size_t i = 0; i &amp;lt; edit-&amp;gt;compact_pointers_.size(); i++) {
      const int level = edit-&amp;gt;compact_pointers_[i].first;
      // 与下面新增和删除不同, 这里直接修改 vset
      vset_-&amp;gt;compact_pointer_[level] =
          edit-&amp;gt;compact_pointers_[i].second.Encode().ToString();
    }

    // 删除文件
    // 将 edit 中保存的待删除文件集合导入到 levels_[].deleted_files 中
    const VersionEdit::DeletedFileSet&amp;amp; del = edit-&amp;gt;deleted_files_;
    for (VersionEdit::DeletedFileSet::const_iterator iter = del.begin();
         iter != del.end();
         ++iter) {
      const int level = iter-&amp;gt;first;
      const uint64_t number = iter-&amp;gt;second;
      levels_[level].deleted_files.insert(number);
    }

    // 添加新文件
    // 将 edit 中保存的新增文件集合导入到 levels_[].added_files 中
    for (size_t i = 0; i &amp;lt; edit-&amp;gt;new_files_.size(); i++) {
      // pair 第一个参数为 level
      const int level = edit-&amp;gt;new_files_[i].first;
      // pair 第二个参数为 FileMetaData
      FileMetaData* f = new FileMetaData(edit-&amp;gt;new_files_[i].second);
      f-&amp;gt;refs = 1;

      // leveldb 针对经过一定查询次数的文件进行自动压实. 我们假设:
      //    (1)一次查询消耗 10ms
      //    (2)写或者读 1MB 数据消耗 10ms(即 100MB/s, 这是一般磁盘 IO 速度)
      //    (3)1MB 数据的压实做了 25MB 数据的 IO 工作: 
      //        从 level-L 读取了 1MB
      //        从 level-(L+1) 读取了 10-12MB(边界可能没有对齐)重叠数据
      //        将压实后的 10-12MB 数据写入到 level-(L+1)
      // 基于上述假设, 我们可以得出, 执行 25 次查询消耗的时间与压实 1MB 数据
      // 的时间相同, 都是 250ms. 也就是说, 一次查询大约相当于压实 40KB (=1MB/25)数据.
      // 现实可能没这么理想, 我们保守一些, 假设每次查询大约相当于压实 16KB 数据, 这样
      // 我们就可以得出压实之前一个文件被允许查询的次数 == [文件字节数/16KB],
      // 一个文件最大 2MB, 则在压实前最多允许查询 128 次, 超过次数会触发压实操作.
      f-&amp;gt;allowed_seeks = (f-&amp;gt;file_size / 16384);
      // 如果允许查询次数小于 100, 则按 100 次处理. 
      if (f-&amp;gt;allowed_seeks &amp;lt; 100) f-&amp;gt;allowed_seeks = 100;

      // todo 一个文件会同时出现在删除列表和新增列表? 
      levels_[level].deleted_files.erase(f-&amp;gt;number);
      levels_[level].added_files-&amp;gt;insert(f);
    }
  }

  // 将当前 version 与 builder 保存的新增文件按序合并
  // 追加到新 Version v 中.
  void SaveTo(Version* v) {
    BySmallestKey cmp;
    cmp.internal_comparator = &amp;amp;vset_-&amp;gt;icmp_;
    // 从低到高将当前 Version base_ 每个 level 文件列表和 Builder::levels_ 每个对应 level
    // 新增文件列表合并, 并保存到 Version v 对应 level 中.
    for (int level = 0; level &amp;lt; config::kNumLevels; level++) {
      // 把新加的文件和已有文件进行合并, 丢弃已被删除的文件, 最终结果保存到 *v.

      // Version base_ 中 level-L 对应的文件列表
      const std::vector&amp;lt;FileMetaData*&amp;gt;&amp;amp; base_files = base_-&amp;gt;files_[level];
      std::vector&amp;lt;FileMetaData*&amp;gt;::const_iterator base_iter = base_files.begin();
      std::vector&amp;lt;FileMetaData*&amp;gt;::const_iterator base_end = base_files.end();
      // builder 保存的 level-L 对应的新增文件集合
      const FileSet* added = levels_[level].added_files;
      v-&amp;gt;files_[level].reserve(base_files.size() + added-&amp;gt;size());
      // 下面两个循环按照文件包含的 key 从小到大顺序合并前述两个文件列表.
      // (具体逻辑就是将两个有序列表合并的过程.)
      for (FileSet::const_iterator added_iter = added-&amp;gt;begin();
           added_iter != added-&amp;gt;end();
           ++added_iter) {
        // 针对 builder 中每个新增文件 *added_iter,
        // 从 base_ 对应 level 寻找第一个大于它的文件,
        // 然后将这个文件之前的文件(builder 里文件列表从小到大有序)
        // 都追加到 v 中.
        // 寻找过程采用 BySmallestKey 比较器(这个抽象极好).
        for (std::vector&amp;lt;FileMetaData*&amp;gt;::const_iterator bpos
                 = std::upper_bound(base_iter, base_end, *added_iter, cmp);
             base_iter != bpos; // 如果相等说明 builder 全部文件都比 added_iter 大
             ++base_iter) {
          // bpos 位置处文件小于 added_iter,
          // 将其追加到 Version v 对应 level 的文件列表中
          MaybeAddFile(v, level, *base_iter);
        }

        // builder 中小于 added_iter 的文件都追加过了,
        // 将 *added_iter 追加到 Version v 的对应 level 的文件列表中.
        MaybeAddFile(v, level, *added_iter);
      }

      // Add remaining base files
      // 将 Version base_ 中 level-L 对应的文件列表剩余的文件追加到 Version v 的对应 level-L 的文件列表中
      for (; base_iter != base_end; ++base_iter) {
        MaybeAddFile(v, level, *base_iter);
      }
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后将新生成的&lt;code&gt;Version&lt;/code&gt;加入到&lt;code&gt;VersionSet&lt;/code&gt;中，并设置为&lt;code&gt;current&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void VersionSet::AppendVersion(Version* v) {
  // Make &quot;v&quot; current
  assert(v-&amp;gt;refs_ == 0);
  assert(v != current_);
  if (current_ != nullptr) {
    current_-&amp;gt;Unref();
  }
  current_ = v;
  // current_ 引用了 v, 将 v 引用计数加一
  v-&amp;gt;Ref();

  // Append to linked list
  // 将 v 加入到双向循环链表中, 新插入的永远是 dummy_versions_ 的前驱.
  v-&amp;gt;prev_ = dummy_versions_.prev_;
  v-&amp;gt;next_ = &amp;amp;dummy_versions_;
  v-&amp;gt;prev_-&amp;gt;next_ = v;
  v-&amp;gt;next_-&amp;gt;prev_ = v;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你想知道在&lt;code&gt;Compact&lt;/code&gt;时，是如何生成新的&lt;code&gt;Version&lt;/code&gt;与新的&lt;code&gt;MANIFEST&lt;/code&gt;,那么你可以看一下&lt;code&gt;VersionSet::LogAndApply&lt;/code&gt;的实现。&lt;/p&gt;
&lt;h3&gt;Overview&lt;/h3&gt;
&lt;p&gt;本文介绍了LevelDB的Compact以及版本。这篇文章并没有将所有的细节都写出来，如果你想要详细了解，我推荐你还是需要去读相关代码。这篇文章更侧重描写出LevelDB的大概轮廓，以及一些比较重要的细节。希望对你理解LevelDB相关代码有所帮助。&lt;/p&gt;
</content:encoded><category>DataBase</category><category>LevelDB</category><author>tang-hi</author></item><item><title>LevelDB(2) -- 读</title><link>https://tangdh.life/posts/database/leveldb-read/</link><guid isPermaLink="true">https://tangdh.life/posts/database/leveldb-read/</guid><description>LevelDB 是一个高效的KV数据库，本文将介绍LevelDB的读操作，以及相应的迭代器.</description><pubDate>Mon, 19 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;本文将介绍LevelDB的读操作，以及相应的迭代器&lt;/p&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;我们先看一下levelDB读操作的整体流程。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/read-overview.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;我们首先会尝试从&lt;code&gt;MemTable&lt;/code&gt;中读取对应的KV，如果没获取到，我们会从&lt;code&gt;ImmutableMemTable&lt;/code&gt;中读取，如果仍旧没读到，我们就会尝试去&lt;code&gt;${version}.ldb&lt;/code&gt;中获取对应的KV。&lt;/p&gt;
&lt;p&gt;因为&lt;code&gt;MemTable&lt;/code&gt;与&lt;code&gt;ImmutableMemTable&lt;/code&gt;的结构完全一致，他们的区别仅仅是一个是目前正在使用的&lt;code&gt;MemTable&lt;/code&gt;,一个是已经达到&lt;code&gt;Flush&lt;/code&gt;的阈值，准备往磁盘中写了。因此这篇文章会分为两部分来介绍&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从&lt;code&gt;Memtable&lt;/code&gt;中读取KV。&lt;/li&gt;
&lt;li&gt;从&lt;code&gt;${version}.ldb&lt;/code&gt;中读取KV。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Read From Memtable&lt;/h2&gt;
&lt;p&gt;还记得我们在 &lt;a href=&quot;../leveldb-write/&quot;&gt;LevelDB(1) -- 写&lt;/a&gt;中对于&lt;code&gt;Memtable的&lt;/code&gt;描述,它会将用户输入的Key转化为&lt;strong&gt;InternalKey&lt;/strong&gt;再插入，因此为了查询的时候，Key保持一致。我们也需要先将Key转化为InternalKey。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/internal-key.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;在这里&lt;code&gt;sequence number&lt;/code&gt;为最大值（这样我们才能获取最新的数据），如果用户有指定&lt;code&gt;snapshot&lt;/code&gt;。那么这个&lt;code&gt;sequence number&lt;/code&gt;则为该&lt;code&gt;snapshot&lt;/code&gt;的值。tag则为&lt;code&gt;kValueTypeForSeek&lt;/code&gt;* ,因为我们排序数据项时会考虑序列号, 而且会在 user_key 部分相等时按照 tag (由七个字节序列号后跟一个字节 ValueType 构成)降序排列(&lt;strong&gt;tag 越大 internal_key 越小&lt;/strong&gt;), 所以我们应该使用&lt;strong&gt;最大&lt;/strong&gt;的 ValueType,这样调用 MemTable.Seek(k) 确保找到的第一个大于等于 k 的数据项(&lt;code&gt;MemTable&lt;/code&gt; 中数据项从小到大排序)就是我们要找的数据项.&lt;/p&gt;
&lt;p&gt;在完成InternalKey的构造后，我们开始在&lt;code&gt;Memtable&lt;/code&gt;中查询数据。&lt;code&gt;Memtable&lt;/code&gt;的整个查询接口都是由迭代器暴露出来的，因此我们先看一下迭代器的接口。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Iterator {
   public:
    // Initialize an iterator over the specified list.
    // The returned iterator is not valid.
    //
    // 构造方法返回的迭代器是无效的
    explicit Iterator(const SkipList* list);

    // Returns true iff the iterator is positioned at a valid node.
    //
    // 当且仅当迭代器指向有效的 node 时才返回 true. 
    bool Valid() const;

    // Returns the key at the current position.
    // REQUIRES: Valid()
    //
    // 返回迭代器当前位置的 key. 
    // 要求: 当前迭代器有效. 
    const Key&amp;amp; key() const;

    // Advances to the next position.
    // REQUIRES: Valid()
    //
    // 将迭代器移动到下个位置. 
    // 要求: 当前迭代器有效. 
    void Next();

    // Advances to the previous position.
    // REQUIRES: Valid()
    //
    // 将迭代器倒退一个位置. 
    // 要求: 当前迭代器有效. 
    void Prev();

    // Advance to the first entry with a key &amp;gt;= target
    //
    // 将迭代器移动到第一个 key &amp;gt;= target 的数据项所在位置. 
    void Seek(const Key&amp;amp; target);

    // Position at the first entry in list.
    // Final state of iterator is Valid() iff list is not empty.
    //
    // 将迭代器移动到 skiplist 第一个数据项所在位置. 
    // 迭代器的最终状态是有效的, 当且仅当 skiplist 不为空. 
    void SeekToFirst();

    // Position at the last entry in list.
    // Final state of iterator is Valid() iff list is not empty.
    //
    // 将迭代器移动到 skiplist 最后一个数据项所在位置. 
    // 迭代器的最终状态是有效的, 当且仅当 skiplist 不为空. 
    void SeekToLast();

   private:
    const SkipList* list_;
    Node* node_;
    // Intentionally copyable
  };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里面我们主要关注&lt;code&gt;Seek&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename Key, class Comparator&amp;gt;
inline void SkipList&amp;lt;Key,Comparator&amp;gt;::Iterator::Seek(const Key&amp;amp; target) {
  node_ = list_-&amp;gt;FindGreaterOrEqual(target, nullptr); 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到里面实际使用的还是&lt;code&gt;SkipList::FindFreaterOrEqual&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename Key, class Comparator&amp;gt;
typename SkipList&amp;lt;Key,Comparator&amp;gt;::Node* 
SkipList&amp;lt;Key,Comparator&amp;gt;::FindGreaterOrEqual(const Key&amp;amp; key, Node** prev)
    const {
  // head_ 为 SkipList 原始数据链表的起始节点,
  // 该节点不存储用户数据, 仅用作哨兵.
  Node* x = head_;
  // 每次查找都是从最高索引层开始查找, 只要确认可能存在
  // 才会降到下一级更细致索引层继续查找.
  // 索引层计数从 0 开始, 所以这里减一才是最高层.
  int level = GetMaxHeight() - 1; 
  while (true) {
    // 下面用的 Next 方法是带同步设施的, 其实由于 SkipList 对外开放的操作
    // 需要调用者自己提供同步, 所以这里可以直接用 NoBarrier_Next.
    Node* next = x-&amp;gt;Next(level);
    if (KeyIsAfterNode(key, next)) {
      // key 大于 next, 在该索引层继续向后找
      x = next; 
    } else {
      // key 可能存在.
      //
      // 如果 key 比 SkipList 中每个 node 的 key 都小, 
      // 那么最后返回的 node 为 head_-&amp;gt;Next(0), 
      // 同时 pre 里面存的都是 dummy head; 
      // 调用者需要使用返回的 node 与自己持有 key进一步进行对比,
      // 以确定是否找到目标节点. 
      if (prev != nullptr) prev[level] = x;
      if (level == 0) {
        // 就是它！如果 key 比 SkipList 里每个 node 的都大, 则 next 最终为 nullptr.
        return next;  
      } else {
        // 确定目标范围, 但是粒度太粗, 下沉一层继续找
        level--;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码写的十分简单易懂，从最高层开始寻找，如果&lt;code&gt;Key&lt;/code&gt;大于当前节点，那么往后继续找，否则下沉一层继续找，如果已经是最后一层了，那么返回该节点。&lt;/p&gt;
&lt;p&gt;在找到节点后，我们根据&lt;strong&gt;InternalKey&lt;/strong&gt;的格式将用户输入的key解析出来进行比较，如果不相等，那么返回未找到，如果相等说明我们查到了这个key的最新值，我们查看该节点的&lt;code&gt;tag&lt;/code&gt;是&lt;code&gt;kTypeValue&lt;/code&gt; 还是&lt;code&gt;kTypeDeletion&lt;/code&gt;？如果是&lt;code&gt;kTypeValue&lt;/code&gt;，那么我们找到了那个值，但如果是&lt;code&gt;kTypeDeletion&lt;/code&gt;则说明该值已经被删除。&lt;/p&gt;
&lt;p&gt;可以看到从&lt;code&gt;Memtable&lt;/code&gt;中查询数据还相对较为简单，只要明白&lt;strong&gt;InternalKey&lt;/strong&gt;的排列顺序即可。&lt;/p&gt;
&lt;h2&gt;Read From LDB FILE&lt;/h2&gt;
&lt;p&gt;如果从&lt;code&gt;Memtable&lt;/code&gt;中无法找到对应的KV,那么我们就需要从文件中进行查找了。
这里我们分为两种情况，Level-0的的文件查找以及Level-1以上的文件查找。之所以要这么区分是因为Level-0各个文件的key可能重叠,例如&lt;strong&gt;file1&lt;/strong&gt;的range为[0,100],&lt;strong&gt;file2&lt;/strong&gt;的range为[50,200]。而level-1及以上的文件的key则不重叠，也就是不相交，因此我们会采用不同的方式来进行读取。查找的过程先从Level-0开始，而后逐级向上。这是因为Level越低，数据越新，如果我们得到了最新的数据，就不用再往下面找了。&lt;/p&gt;
&lt;h3&gt;1. 确定需要读取的文件&lt;/h3&gt;
&lt;p&gt;首先要做的就是要确定哪些文件。从下图中我们可以看到，对于Level-0以及其他Level，确定要读取的文件是不一样的。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/determin-file.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;因为Level-0中的文件key之间可能有重叠，因此我们需要一个个进行检查，而对于Level-x(x&amp;gt;1)来说，每个文件不相交，因此我们可以对他们按key排序后，进行二分查找，从而加快查找的速度。&lt;/p&gt;
&lt;h3&gt;2. 读取ldb文件，查找KV&lt;/h3&gt;
&lt;p&gt;在确定了读取的文件后，我们需要读取相应的文件，并检查需要查询的&lt;code&gt;KEY&lt;/code&gt;是否在&lt;code&gt;ldb&lt;/code&gt;文件中。&lt;/p&gt;
&lt;p&gt;为了读取文件，我们会先尝试从&lt;code&gt;TableCache&lt;/code&gt;中找到需要的文件Cache，如果没找到，我们再去磁盘中读取并反序列化为&lt;code&gt;Table&lt;/code&gt;,将反序列化的&lt;code&gt;Table&lt;/code&gt;插入到&lt;code&gt;TableCache&lt;/code&gt;中。&lt;/p&gt;
&lt;h4&gt;2.1 TableCache的实现&lt;/h4&gt;
&lt;p&gt;目前&lt;code&gt;LevelDB&lt;/code&gt;使用的缓存策略为LRU，我们先看一下Cache的接口。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class LEVELDB_EXPORT Cache {
 public:
  Cache() = default;

  Cache(const Cache&amp;amp;) = delete;
  Cache&amp;amp; operator=(const Cache&amp;amp;) = delete;

  // 析构时调用构造时传入的 deleter 函数销毁每一个数据项. 
  virtual ~Cache();

  // Cache 中存储的数据项的抽象类型, 具体实现参见 LRUHandle
  struct Handle { };

  /**
   * 插入一对 &amp;lt;key, value&amp;gt; 到 cache 中, 同时为这个映射设置
   * 一个对 cache 容量的消耗, 具体使用时候用的是要插入的数据
   * 字节数. 
   *
   * 该方法返回一个 handle, 对应本次插入的映射. 
   * 当调用者不再需要这个映射的时候, 需要调用 this-&amp;gt;Release(handle). 
   *
   * 当被插入的数据项不再被需要时, key 和 value 将会被传递给这里指定的 deleter. 
   * @param key 要插入的映射的 key
   * @param value 要插入的映射的 value
   * @param charge 要插入的映射对应的花费
   * @param deleter 要插入的映射对应的 deleter
   * @return 要插入的映射对应的 handle
   */
  virtual Handle* Insert(const Slice&amp;amp; key, void* value, size_t charge,
                         void (*deleter)(const Slice&amp;amp; key, void* value)) = 0;

  /**
   * 如果 cache 中没有针对 key 的映射, 返回 nullptr. 
   * 其它情况返回对应该映射的 handle. 
   * 当不再需要这个映射的时候, 调用者必须调用 this-&amp;gt;Release(handle). 
   * @param key 要查询映射的 key
   * @return 要查询的映射对应的 handle
   */
  virtual Handle* Lookup(const Slice&amp;amp; key) = 0;


  /**
   * 先通过 Lookup 查询映射对应的 handle, 然后调用该函数来释放该映射. 
   *
   * 前提一: handle 之前未被释放过.
   * 前提二: handle 必须是通过在 *this 上调用某个方法返回的.
   * @param handle 通过 Lookup 查询到的映射对应的 handle
   */
  virtual void Release(Handle* handle) = 0;

  /**
   * 成功调用 Lookup 后返回的 handle 中封装的 value 可以通过该方法解析. 
   *
   * 前提一: handle 之前未被释放过
   * 前提二: handle 必须是通过在 *this 上调用某个方法返回的
   * @param handle
   * @return
   */
  virtual void* Value(Handle* handle) = 0;

  /**
   * 如果 cache 包含了 key 对应的映射, 删除之. 
   * 注意, 底层的数据项将会继续存在直到现有的指向该数据项的全部 handles 已被释放掉. 
   * @param key 要删除的映射对应的 key
   */
  virtual void Erase(const Slice&amp;amp; key) = 0;

  /**
   * 返回一个新生成的数字 id. 
   * 可能会被共享同一个 cache 的多个客户端用来对键空间进行分区.
   *
   * 典型地用法是, 某个客户端在启动时调用该方法生成一个新 id, 
   * 然后将该 id 作为它的 keys 的前缀. 
   * @return
   */
  virtual uint64_t NewId() = 0;

  /**
   * 移除 cache 中全部不再活跃的数据项. 
   * 内存受限的应用可以调用该方法来减少缓存造成的内存消耗. 
   *
   * 该方法的默认实现什么也不做, 强烈建议在派生类实现中重写该方法. 
   * leveldb 未来版本可能会将该方法修改为一个纯抽象方法. 
   */
  virtual void Prune() {}

  /**
   * 返回 cache 为了存储当前全部元素的总花费的估计值
   * @return
   */
  virtual size_t TotalCharge() const = 0;

 private:
  void LRU_Remove(Handle* e);
  void LRU_Append(Handle* e);
  void Unref(Handle* e);

  struct Rep;
  Rep* rep_;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于Cache接口的实现则为&lt;code&gt;ShardedLRUCache&lt;/code&gt;，它维护了多个cache shard，从而在并发访问时，无须使用一把大锁，而是可以更加细粒度的加锁，从而提升并发时的性能。&lt;/p&gt;
&lt;p&gt;我们看一下&lt;code&gt;ShardedLRUCache::Insert&lt;/code&gt;的实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;virtual Handle* Insert(const Slice&amp;amp; key, void* value, size_t charge,
                         void (*deleter)(const Slice&amp;amp; key, void* value)) {
    // 计算 hash
    const uint32_t hash = HashSlice(key);
    // 基于 hash 做 sharding
    return shard_[Shard(hash)].Insert(key, hash, value, charge, deleter);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从代码中，我们可以看到&lt;code&gt;ShardedLRUCache&lt;/code&gt;只计算key所属的shard，然后具体的逻辑由&lt;code&gt;LRUCache&lt;/code&gt;执行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 该方法类似 Cache::Insert() 不过多了一个 hash 参数.
 * 该方法线程安全, 允许多个线程并发向同一个 shard 中插入.
 *
 * @param key 要插入的数据项的 key
 * @param hash 要插入的数据项的 hash
 * @param value 要插入的数据项的 value
 * @param charge 要插入的数据项的 charge
 * @param deleter 要插入的数据项的 deleter
 * @return 返回插入的数据项的句柄
 */
Cache::Handle* LRUCache::Insert(
    const Slice&amp;amp; key, uint32_t hash, void* value, size_t charge,
    void (*deleter)(const Slice&amp;amp; key, void* value)) {
  MutexLock l(&amp;amp;mutex_);

  // 基于 LRUHandle 本身大小和 key 的实际长度来分配空间. 
  // 减掉的 1 指的是 LRUHandle 初始化时为 key_data 预占的空间, 
  // 不减掉的话后面加上 key.size() 就多了一个字节. 
  LRUHandle* e = reinterpret_cast&amp;lt;LRUHandle*&amp;gt;(
      malloc(sizeof(LRUHandle)-1 + key.size()));
  e-&amp;gt;value = value;
  e-&amp;gt;deleter = deleter;
  e-&amp;gt;charge = charge;
  e-&amp;gt;key_length = key.size();
  e-&amp;gt;hash = hash;
  e-&amp;gt;in_cache = false;
  // 能存在于 cache 中的最小 ref 值, 
  // 表示当前除了 cache 对象还没有任何外部引用.
  e-&amp;gt;refs = 1;  
  memcpy(e-&amp;gt;key_data, key.data(), key.size());

  if (capacity_ &amp;gt; 0) {
    // 放入 in_use_ 列表就要增加引用.
    e-&amp;gt;refs++;
    // 该数据项被放到了 shard 中
    e-&amp;gt;in_cache = true;
    // 将该数据项追加到 shard 的 in_use 链表
    LRU_Append(&amp;amp;in_use_, e);
    usage_ += charge;
    // 将数据项插入到 hashtable, 这可以看做一个二级缓存.
    // 如果 shard 中存在与 e &quot;相同的 key 相同的 hash&quot; 的项, 
    // 则将 e 插入同时将老的数据项从 shard 彻底删除.
    FinishErase(table_.Insert(e));
  } else {
    // 如果 capacity_&amp;lt;= 0 意味着关闭了缓存功能. 
    // 此处的赋值是防止 key() 方法的 assert 失败. 
    e-&amp;gt;next = nullptr;
  }
	// 下面这个循环解释了 LRUCache 的 LRU 效果.
  // 如果本 shard 的使用量大于容量并且 lru 链表不为空, 
  // 则从 lru 链表里面淘汰数据项, lru 链表数据当前肯定未被使用, 
  // 直至使用量小于容量或者 lru 清空. 
  while (usage_ &amp;gt; capacity_ &amp;amp;&amp;amp; lru_.next != &amp;amp;lru_) {
		// 这很重要, lru_.next 是 least recently used 的元素
    LRUHandle* old = lru_.next;
    // lru 链表里面的数据项除了被该 shard 引用不会被任何客户端引用
    assert(old-&amp;gt;refs == 1);
    // 从 shard 将 old 彻底删除
    bool erased = FinishErase(table_.Remove(old-&amp;gt;key(), old-&amp;gt;hash));
    if (!erased) {  
      // to avoid unused variable when compiled NDEBUG
      assert(erased);
    }
  }

  // 将 LRUHandle 重新解释为 Cache::Handle
  return reinterpret_cast&amp;lt;Cache::Handle*&amp;gt;(e);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们首先会malloc一个新的&lt;code&gt;LRUHandle&lt;/code&gt;,然后对该&lt;code&gt;LRUHandle&lt;/code&gt;进行赋值。随后我们直接使用头插法将这个&lt;code&gt;LRUHandle&lt;/code&gt;插入链表中，并且如果这个key之前缓存过，那么我们将旧缓存删除，最后如果发现使用量超过限额，就尝试去除过期的数据。&lt;/p&gt;
&lt;p&gt;我们再看看查找的代码，可以发现代码相当简单，通过key去&lt;code&gt;hashtable&lt;/code&gt;中找到对应的&lt;code&gt;LRUHandle&lt;/code&gt;并返回。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Cache::Handle* LRUCache::Lookup(const Slice&amp;amp; key, uint32_t hash) {
  MutexLock l(&amp;amp;mutex_);
  // table_ 是个哈希表, 存储了该 shard 全部数据项的指针, 
  // O(1) 复杂度. 
  LRUHandle* e = table_.Lookup(key, hash); 
  if (e != nullptr) {
    // 如果查到, 则将该数据项引用数加 1, 
    // 查询命中后续就要. 
    Ref(e); 
  }
  return reinterpret_cast&amp;lt;Cache::Handle*&amp;gt;(e);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2 LDB的反序列化与查找&lt;/h4&gt;
&lt;p&gt;反序列化LDB文件的入口函数为&lt;code&gt;Table::Open&lt;/code&gt;, 我们配合着LDB的&lt;code&gt;Layout&lt;/code&gt;来理解LDB的反序列化。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/ldb-overview.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;我们首先读取&lt;code&gt;Footer&lt;/code&gt;获得&lt;code&gt;index Block&lt;/code&gt;,&lt;code&gt;filter index block&lt;/code&gt;的位置和大小，随后我们通过&lt;code&gt;index block&lt;/code&gt;的位置和大小解析出&lt;code&gt;index Block&lt;/code&gt;的内容（这里我们仅仅只是通过CRC检查文件完整性，及其根据compress type来解压文件，并不做更进一步的解析),然后我们再根据&lt;code&gt;filter index block&lt;/code&gt;解析出&lt;code&gt;filter block&lt;/code&gt;(这里我们会将&lt;code&gt;filter block&lt;/code&gt;的&lt;code&gt;base&lt;/code&gt;, &lt;code&gt;offset&lt;/code&gt;, &lt;code&gt;bloom value&lt;/code&gt;全部解析出来)。注意，我们并不会解析&lt;code&gt;data block&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;到了这里，我们就算将&lt;code&gt;ldb&lt;/code&gt;文件反序列化为&lt;code&gt;Table&lt;/code&gt;了。下面我们看一下查找的过程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 在 table 中查找 k 对应的数据项. 
// 如果 table 具有 filter, 则用 filter 找; 
// 如果没有 filter 则去 data block 里面查找, 
// 并且在找到后通过 saver 保存 key/value. 
// 注意, 针对 data block 的读取和解析发生在这个方法里.
Status Table::InternalGet(const ReadOptions&amp;amp; options, const Slice&amp;amp; k,
                          void* arg,
                          void (*saver)(void*, const Slice&amp;amp;, const Slice&amp;amp;)) {
  Status s;
  // 针对 data index block 构造 iterator
  Iterator* iiter = rep_-&amp;gt;index_block-&amp;gt;NewIterator(rep_-&amp;gt;options.comparator);
  // 在 data index block 中寻找第一个大于等于 k 的数据项, 这个数据项
  // 就是目标 data block 的 handle.
  iiter-&amp;gt;Seek(k);
  if (iiter-&amp;gt;Valid()) {
    // 取出对应的 data block 的 BlockHandle
    Slice handle_value = iiter-&amp;gt;value(); 
    FilterBlockReader* filter = rep_-&amp;gt;filter;
    BlockHandle handle;
    // 如果有 filter 找起来就快了, 如果确定
    // 不存在就可以直接反悔了.
    if (filter != nullptr &amp;amp;&amp;amp;
        handle.DecodeFrom(&amp;amp;handle_value).ok() &amp;amp;&amp;amp;
        !filter-&amp;gt;KeyMayMatch(handle.offset(), k)) {
      // 没在该 data block 对应的过滤器找到这个 key, 肯定不存在
    } else { 
      // 如果没有 filter, 或者在 filter 中查询时无法笃定
      // key 不存在, 就需要在 block 中进行查找.
      // 看到了没? Open() 方法没有解析任何 data block, 解析
      // 是在这里进行的, 因为这里要查询数据了.
      Iterator* block_iter = BlockReader(this, options, iiter-&amp;gt;value());
      block_iter-&amp;gt;Seek(k);
      if (block_iter-&amp;gt;Valid()) {
        // 将找到的 key/value 保存到输出型参数 arg 中, 
        // 因为后面会将迭代器释放掉.
        (*saver)(arg, block_iter-&amp;gt;key(), block_iter-&amp;gt;value()); 
      }
      s = block_iter-&amp;gt;status();
      delete block_iter;
    }
  }
  if (s.ok()) {
    s = iiter-&amp;gt;status();
  }
  delete iiter;
  return s;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们首先会在&lt;code&gt;index block&lt;/code&gt;中寻找刚好大于等于&lt;code&gt;k&lt;/code&gt;的数据项，从而可以快速定位到&lt;code&gt;data block&lt;/code&gt;,当然在往&lt;code&gt;data block&lt;/code&gt;中查找前，我们可以先使用布隆过滤器来确定该值是否在这个&lt;code&gt;data block&lt;/code&gt; 中,如果说在的话，我们就可以直接在&lt;code&gt;data block&lt;/code&gt;中进行查找，并返回查找结果。&lt;/p&gt;
&lt;h3&gt;Overview&lt;/h3&gt;
&lt;p&gt;本文介绍了LevelDB的读操作的流程。这篇文章并没有将所有的细节都写出来，如果你想要详细了解，我推荐你还是需要去读相关代码。这篇文章更侧重描写出LevelDB的大概轮廓，以及一些比较重要的细节。希望对你理解LevelDB相关代码有所帮助。&lt;/p&gt;
</content:encoded><category>DataBase</category><category>LevelDB</category><author>tang-hi</author></item><item><title>LevelDB(1) -- 写</title><link>https://tangdh.life/posts/database/leveldb-write/</link><guid isPermaLink="true">https://tangdh.life/posts/database/leveldb-write/</guid><description>LevelDB 是一个高效的KV数据库，本文将介绍LevelDB的写操作，以及相应的文件格式.</description><pubDate>Thu, 15 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;本文将介绍LevelDB是如何存储写入数据, 以及数据在磁盘中的存储格式.&lt;/p&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;我们先看一下LevelDB整体的写流程是什么样子的.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/write-overview.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;从图中可以看出levelDB采用经典的WAL方式来进行写入,即先将写入的操作写到log文件中,再将实际的数据写到内存中的&lt;code&gt;Memtable&lt;/code&gt;中(&lt;code&gt;Memtable&lt;/code&gt;采用skipList实现),当&lt;code&gt;Memtable&lt;/code&gt;达到阈值后再将其转化为&lt;code&gt;ImmutableMemTable&lt;/code&gt;,最终将其落盘持久化保存.&lt;/p&gt;
&lt;p&gt;因此本文后面将主要介绍&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;log文件的格式与生成&lt;/li&gt;
&lt;li&gt;memtable的实现&lt;/li&gt;
&lt;li&gt;ldb文件的格式与生成&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;写操作的生成&lt;/h2&gt;
&lt;p&gt;在讲述文件的格式与生成之前,我们需要先描述写操作是如何生成.&lt;/p&gt;
&lt;p&gt;当用户调用&lt;code&gt;DB::Put(const WriteOptions&amp;amp; opt, const Slice&amp;amp; key, const Slice&amp;amp; value) &lt;/code&gt;时,我们会将key,value包装成一个&lt;code&gt;WriteBatch&lt;/code&gt;,顾名思义,&lt;code&gt;WriteBatch&lt;/code&gt;中会有许多的写操作.其定义如下所示.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class LEVELDB_EXPORT WriteBatch {
 public:
  WriteBatch();
  // skip ....
  // Intentionally copyable.
  WriteBatch(const WriteBatch&amp;amp;) = default; // 默认拷贝构造
  WriteBatch&amp;amp; operator =(const WriteBatch&amp;amp;) = default; // 默认赋值构造
  // skip ....
  // Store the mapping &quot;key-&amp;gt;value&quot; in the database.
  void Put(const Slice&amp;amp; key, const Slice&amp;amp; value);
  ~WriteBatch();
  // skip ....
 private:

  std::string rep_;  // See comment in write_batch.cc for the format of rep_
};

}  // namespace leveldb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到其本质就是一个&lt;strong&gt;string&lt;/strong&gt;,我们会将写操作通过&lt;code&gt;Put&lt;/code&gt;接口将其写入到rep_中,&lt;code&gt;WriteBatch&lt;/code&gt;在内存中的格式如下图所示.
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/write-batch.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;从图中可以看到，WriteBatch由&lt;code&gt;SequenceNumber&lt;/code&gt;,&lt;code&gt;count&lt;/code&gt;以及&lt;code&gt;count&lt;/code&gt;个KV对组成, 其中前8个byte为&lt;code&gt;SequenceNumber&lt;/code&gt;(LevelDB中写操作的唯一自增编号)，紧跟着的4个byte为存储的KV对个数。KV对则由&lt;code&gt;tag&lt;/code&gt;,&lt;code&gt;key-size And key&apos;s content&lt;/code&gt;, &lt;code&gt;value-size And value&apos;s content&lt;/code&gt;构成。&lt;/p&gt;
&lt;p&gt;其中&lt;code&gt;tag&lt;/code&gt;为枚举类型,&lt;code&gt;kTypeDeletion&lt;/code&gt;表明删除,&lt;code&gt;kTypeDeletion&lt;/code&gt;表明增加.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum ValueType {
  kTypeDeletion = 0x0,
  kTypeValue = 0x1
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在生成WriteBatch后(此时WriteBatch中仅有用户输入的KV对)，我们生成Writer, 并将WriteBatch存入Writer中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct DBImpl::Writer {
  Status status;
  WriteBatch* batch;
  bool sync;
  bool done;
  port::CondVar cv;

  explicit Writer(port::Mutex* mu) : cv(mu) { }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后，我们会将Writer放到队列中（一个经典的生产者，消费者模型）。当该Writer为队首时才会被拿出来执行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;writers_.push_back(&amp;amp;w);
while (!w.done &amp;amp;&amp;amp; &amp;amp;w != writers_.front()) {
	w.cv.Wait();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当该Writer被拿出来执行时，我们首先会确保&lt;code&gt;Memtable&lt;/code&gt;仍然有较为充足的空间给它进行写入，不然的话我们可能要进行compact(目前可以不关注，后续在讲compact的时候会详细阐述，这里先假定空间一定充足)。&lt;/p&gt;
&lt;p&gt;此时，我们会尝试将多个WriterBatch合并为一个后一起执行。具体逻辑可以参考代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WriteBatch* DBImpl::BuildBatchGroup(Writer** last_writer) {
  mutex_.AssertHeld();
  assert(!writers_.empty());
  // 取出队首 writer
  Writer* first = writers_.front();
  // 取出队首 writer 的待写数据集
  WriteBatch* result = first-&amp;gt;batch;
  assert(result != nullptr);

  // 计算队首 writer 数据集大小
  size_t size = WriteBatchInternal::ByteSize(first-&amp;gt;batch);

	// 虽然支持合并, 但是有两个限制条件:
	// 1. 不合并同步写入操作(设置了 writer.sync), 发现同步写操作立马停止后续合并操作并返回已合并内容.
	// 2. 为了避免小数据量写入操作被延迟太久, 针对合并上限做了限制, 最大 1MB.
  size_t max_size = 1 &amp;lt;&amp;lt; 20;
	// 如果队首 writer 要写内容大小不超过 128KB
  if (size &amp;lt;= (128&amp;lt;&amp;lt;10)) {
	// 则 max_size 改为不超过 256KB
    max_size = size + (128&amp;lt;&amp;lt;10);
  }

  *last_writer = first;
  std::deque&amp;lt;Writer*&amp;gt;::iterator iter = writers_.begin();
  ++iter;  // Advance past &quot;first&quot;
  // iter 从 first 之后 writer 开始遍历
  for (; iter != writers_.end(); ++iter) {
    Writer* w = *iter;
		// 同步写操作不做合并
    if (w-&amp;gt;sync &amp;amp;&amp;amp; !first-&amp;gt;sync) {
      // Do not include a sync write into a batch handled by a non-sync write.
      break;
    }

    if (w-&amp;gt;batch != nullptr) {
      size += WriteBatchInternal::ByteSize(w-&amp;gt;batch);
      if (size &amp;gt; max_size) {
        // 避免 batch group 过大
        break;
      }

      // Append to *result
      if (result == first-&amp;gt;batch) {
        // 不篡改 first writer 的 batch, 而是把若干 batch 合并到临时的 tmp_batch_ 中
        result = tmp_batch_;
        assert(WriteBatchInternal::Count(result) == 0);
        WriteBatchInternal::Append(result, first-&amp;gt;batch);
      }
      WriteBatchInternal::Append(result, w-&amp;gt;batch);
    }
    // last_writer 指向被合并的最后一个 writer
    *last_writer = w;
  }
  return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单来说，遍历待执行的WriteBatch，只要它&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;不要求同步&lt;/li&gt;
&lt;li&gt;合并后不会导致WriteBatch大小超过&lt;code&gt;max_size&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;都会被合并，但只要违反上述任意一条，合并流程就会终止。&lt;/p&gt;
&lt;p&gt;经过上述流程，我们完成了对写操作的所有预处理，可以进行真正的写操作了。&lt;/p&gt;
&lt;h2&gt;log文件的格式与生成&lt;/h2&gt;
&lt;p&gt;在生成了待写入的&lt;code&gt;WriteBatch&lt;/code&gt;后,我们首先将其写入到&lt;code&gt;log文件&lt;/code&gt;中。&lt;code&gt;log文件&lt;/code&gt;的内部格式是通过block进行组织的，具体结构如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/log-format.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;我们可以看到&lt;code&gt;log文件&lt;/code&gt;是由一个个Block组成的，而每一个Block的大小都是固定的&lt;code&gt;32KB&lt;/code&gt;，Block中存储着多个WriteBatch:头四个byte为校验和，后两个byte为data的长度，后续的一个byte为type(前七个byte被统称为&lt;code&gt;Header&lt;/code&gt;)，最后剩下的就是data的数据，也就是WriteBatch中的&lt;code&gt;rep_&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果一个block剩余空间不足以存储&lt;code&gt;Header&lt;/code&gt;，也就是他剩下的存储空间小于7byte,那么我们会对这个Block末尾填充0,然后将数据写到新的Block中。&lt;/p&gt;
&lt;p&gt;现在我们来讲一下Header中的&lt;code&gt;type&lt;/code&gt;代表着什么。考虑这么一种情况，如果block中的剩余空间太小，以致于我们的&lt;code&gt;WriteBatch&lt;/code&gt;无法全部存储在该&lt;code&gt;Block&lt;/code&gt;中，那么我们可能要将数据分为不同的块存储到不同的Block中，为了后续读的时候可以知道，这是不是一个完整的块，以及何时读完了完整的块。我们需要&lt;code&gt;type&lt;/code&gt;来进行标识。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;type&lt;/code&gt;依旧为枚举类型.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum RecordType {
  // Zero is reserved for preallocated files
  kZeroType = 0,

  kFullType = 1,

  // For fragments
  kFirstType = 2,
  kMiddleType = 3,
  kLastType = 4
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;kFullType 表明后续的data数据为&lt;strong&gt;完整&lt;/strong&gt;的数据&lt;/li&gt;
&lt;li&gt;kFirstType 表明这是分块后的&lt;strong&gt;第一块&lt;/strong&gt;数据，仍需要继续读取&lt;/li&gt;
&lt;li&gt;kMiddleType表明这是分块后的&lt;strong&gt;中间&lt;/strong&gt;数据，仍需要继续读取&lt;/li&gt;
&lt;li&gt;kLastType 表明这是分块后的&lt;strong&gt;最后一块&lt;/strong&gt;数据，无需读取。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在将WriteBatch的数据写入到log文件后，我们就完成了写入的第一步，写日志。&lt;/p&gt;
&lt;h3&gt;MemTable的实现&lt;/h3&gt;
&lt;p&gt;在将&lt;code&gt;writeBatch&lt;/code&gt;写入到&lt;code&gt;log文件&lt;/code&gt;后，我们便可以将数据写入&lt;code&gt;MemTable&lt;/code&gt;中。&lt;/p&gt;
&lt;p&gt;我们会对&lt;code&gt;writeBatch&lt;/code&gt;中的&lt;code&gt;(tag,Key,value)&lt;/code&gt;进行遍历，根据&lt;code&gt;tag&lt;/code&gt;的不同，决定是向&lt;code&gt;MemTable&lt;/code&gt;添加还是删除。&lt;/p&gt;
&lt;p&gt;因为&lt;code&gt;MemTable&lt;/code&gt;的内部实现是&lt;strong&gt;skiplist&lt;/strong&gt;，而&lt;strong&gt;skiplist&lt;/strong&gt;只能回答&lt;strong&gt;key&lt;/strong&gt;在不在，而不能回答&lt;strong&gt;key&lt;/strong&gt;关联的&lt;strong&gt;value&lt;/strong&gt;是什么，因此我们需要将用户输入的&lt;code&gt;(key,value)&lt;/code&gt;转化为&lt;strong&gt;skiplist&lt;/strong&gt;中内部使用的&lt;strong&gt;key&lt;/strong&gt;。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/internal-key.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;从图中可以发现，&lt;code&gt;key&lt;/code&gt;和&lt;code&gt;value&lt;/code&gt;中间被&lt;code&gt;(sequence number , tag)&lt;/code&gt;隔开，这样的目的是为了后续在排序时，我们可以先按照&lt;code&gt;key&lt;/code&gt;从小到大排序, 当&lt;code&gt;key&lt;/code&gt;相同时，&lt;code&gt;(sequence number, tag)&lt;/code&gt;按照由大到小排序，通过这种方式，永远是版本最新的在最前面（&lt;code&gt;(sequence number, tag)&lt;/code&gt;越大，版本越新）。&lt;/p&gt;
&lt;p&gt;最后就是向&lt;code&gt;MemTable&lt;/code&gt;中插入该&lt;code&gt;internal key&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename Key, class Comparator&amp;gt;
void SkipList&amp;lt;Key,Comparator&amp;gt;::Insert(const Key&amp;amp; key) {
  // pre 将用于存储 key 对应的各个索引层的前驱节点
  Node* prev[kMaxHeight];
  // 找到第一个大与于目标 key 的节点, 一会会把 key
  // 插到这个节点前面.
  // 如果为 nullptr 表示当前 SkipList 节点都比 key 小.
  Node* x = FindGreaterOrEqual(key, prev); 

  // 虽然 x 是我们找到的第一个大于等于目标 key 的节点, 
  // 但是 leveldb 不允许重复插入 key 相等的数据项.
  assert(x == nullptr || !Equal(key, x-&amp;gt;key));

  // 确定待插入节点的最大索引层数
  int height = RandomHeight();
  // 更新 SkipList 实例维护的最大索引层数
  if (height &amp;gt; GetMaxHeight()) {
    // 如果最大索引层数有变, 则当前节点将是索引层数最多的节点,
    // 需要将前面求得的待插入节点的前驱节点高度补齐.
    for (int i = GetMaxHeight(); i &amp;lt; height; i++) {
      // 新生成了几个 level, key 对应的前驱节点肯定都是 dummy head
      prev[i] = head_; 
    }
    //fprintf(stderr, &quot;Change height from %d to %d\n&quot;, max_height_, height);

    // 这里在修改 max_height_ 无需同步, 哪怕同时有多个并发读线程. 
    // 其它并发读线程如果观察到新的 max_height_ 值, 
    // 那它们将会要么看到 dummy head 新的索引层(注意 SkipList 
    // 初始化时会把 dummy head 的索引高度直接初始化为最大, 默认是 12, 
    // 所以不存在越界问题)的值都为 nullptr, 要么看到的是
    // 下面循环将要赋值的新节点 x. 
    max_height_.NoBarrier_Store(reinterpret_cast&amp;lt;void*&amp;gt;(height));
  }

  // 为待插入数据创建一个新节点
  x = NewNode(key, height);
  // 将 x 插入到每一层前后节点之间, 注意是每一层, 
  // 插入的时候都是先采用 no barrier 方式为 x 后继赋值, 此时 x 还不会被其它线程看到; 
  // 然后插入一个 barrier, 则上面 no barrier 的修改针对全部线程都可见了(其中也包括
  // 了 NewNode 时可能发生的通过 NoBarrier_Store 方式修改的 arena_.memory_usage_), 
  // 最后修改 x 前驱的后继为自己. 
  for (int i = 0; i &amp;lt; height; i++) {
    // 注意该循环就下面两步, 而且只有第二步采用了同步设施, 尽管如此,
    // 第一步的写操作对其它线程也是可见的. 
    // 这是 Release-Acquire ordering 语义所保证的. 
    x-&amp;gt;NoBarrier_SetNext(i, prev[i]-&amp;gt;NoBarrier_Next(i));
    prev[i]-&amp;gt;SetNext(i, x);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了可以读懂上面的代码，我们先来阐述一下&lt;strong&gt;LevelDB&lt;/strong&gt;是如何实现&lt;strong&gt;skiplist&lt;/strong&gt;的。&lt;/p&gt;
&lt;p&gt;首先我们先看一下&lt;code&gt;SkipList::Node&lt;/code&gt;的定义&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename Key, class Comparator&amp;gt;
struct SkipList&amp;lt;Key,Comparator&amp;gt;::Node {
  explicit Node(const Key&amp;amp; k) : key(k) { }

  Key const key;

  Node* Next(int n) {
    assert(n &amp;gt;= 0);
    return reinterpret_cast&amp;lt;Node*&amp;gt;(next_[n].Acquire_Load());
  }
  void SetNext(int n, Node* x) {
    assert(n &amp;gt;= 0);
    next_[n].Release_Store(x);
  }

  Node* NoBarrier_Next(int n) {
    assert(n &amp;gt;= 0);
    return reinterpret_cast&amp;lt;Node*&amp;gt;(next_[n].NoBarrier_Load());
  }

  void NoBarrier_SetNext(int n, Node* x) {
    assert(n &amp;gt;= 0);
    next_[n].NoBarrier_Store(x);
  }

 private:
  // Array of length equal to the node height. 
  // next_[0] is lowest level link.
  port::AtomicPointer next_[1];
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从代码中，我们看到&lt;code&gt;Node&lt;/code&gt;有两个成员变量，一个是&lt;code&gt;key&lt;/code&gt;，一个是&lt;code&gt;next_&lt;/code&gt;，&lt;code&gt;key&lt;/code&gt;没有什么好说的，主要需要理解的是&lt;code&gt;next_&lt;/code&gt;,简单点来说，&lt;code&gt;next_&lt;/code&gt;的长度等于Node的高度，&lt;code&gt;next_[i]&lt;/code&gt;为&lt;code&gt;Node&lt;/code&gt;在&lt;code&gt;level-i&lt;/code&gt;的后继节点。通过下图，相信你可以更好理解。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/node.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;了解完&lt;code&gt;Node&lt;/code&gt;后，我们还需要知道&lt;code&gt;SkipList::head_&lt;/code&gt;,这个成员变量是一个dummy node,它的类型也是&lt;code&gt;Node&lt;/code&gt;,它的&lt;code&gt;next_&lt;/code&gt;点保存着每一个level的首节点。
因此插入的过程可以描述为&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;找到第一个大与于目标 key 的节点, 一会会把 key插到这个节点前面,如果为 nullptr 表示当前 skipList 节点都比 key 小.同时会记录每一层刚好比key小的节点。&lt;/li&gt;
&lt;li&gt;为该节点随机生成一个层数，作为该节点的最大层数。&lt;/li&gt;
&lt;li&gt;通过之前找到的刚好比他大的节点，以及刚好比他小的节点，将该节点插入进skiplist.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当将WriteBatch所有的&lt;code&gt;(key, value,tag)&lt;/code&gt;，全部插入&lt;code&gt;MemTable&lt;/code&gt;后，我们可以认为插入的过程已经全部完成了，剩下的就是将之前合并到新的WriteBatch的那些&lt;code&gt;Writer&lt;/code&gt;移出队列，并唤醒队列的头部&lt;code&gt;Writer&lt;/code&gt;,相信通过代码，可以很容易理解。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while (true) {
    // [&amp;amp;w, last_writer] 的 batch 被合并写入 log 了, 所以将其出队.
    Writer* ready = writers_.front();
    writers_.pop_front();
    // &amp;amp;w 并没有 wait, 它是本次负责合并写入的 writer,
		// 所以它 &amp;amp;w 的 status 和 done 可以不用改, 反正也用不到.
    if (ready != &amp;amp;w) {
			// 传递合并写执行结果给 group 中各个 writer
      ready-&amp;gt;status = status;
      ready-&amp;gt;done = true;
      // 唤醒当前方法入口的 w.cv.Wait(), 通过此处被唤醒的
			// writers 都是被合并到队首 writer 统一写入 log 文件的.
      // 它们被唤醒后, 只需检查下 done 状态就可以返回了.
      ready-&amp;gt;cv.Signal();
    }
    // last_writer 指向被合并处理的最后一个 writer
    if (ready == last_writer) break;
  }

  // 如果当前 writers_ 队列不为空, 唤醒当前的队首节点.
  if (!writers_.empty()) {
    // 叫醒新的待写入 writer
    writers_.front()-&amp;gt;cv.Signal();
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ldb文件的格式与生成&lt;/h3&gt;
&lt;p&gt;当把WriteBatch所有的&lt;code&gt;(key, value,tag)&lt;/code&gt;，全部插入&lt;code&gt;MemTable&lt;/code&gt;后，&lt;code&gt;Put&lt;/code&gt;流程就算结束了。 ldb文件的格式与生成，应当属于compact中的内容。但是趁现在对&lt;code&gt;MemTable&lt;/code&gt;的记性还比较新，可以顺便将&lt;code&gt;ldb文件&lt;/code&gt;一起讲了。而且&lt;code&gt;ldb文件&lt;/code&gt;本质上就是将&lt;code&gt;MemTable&lt;/code&gt;落盘，内容上也不算突兀。&lt;/p&gt;
&lt;p&gt;ldb文件由五部分组成&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;data blocks&lt;/li&gt;
&lt;li&gt;filter blcoks&lt;/li&gt;
&lt;li&gt;filterindex block&lt;/li&gt;
&lt;li&gt;index block&lt;/li&gt;
&lt;li&gt;footer&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其中&lt;code&gt;data blocks&lt;/code&gt;中保存着KV数据，&lt;code&gt;filter blocks&lt;/code&gt;中存储着布隆过滤器，&lt;code&gt;filterindex block&lt;/code&gt;存储着指向&lt;code&gt;filter blocks&lt;/code&gt;的索引，&lt;code&gt;index block&lt;/code&gt;存储着指向&lt;code&gt;data blocks&lt;/code&gt;的索引，&lt;code&gt;footer&lt;/code&gt;存储着指向&lt;code&gt;filterindex block&lt;/code&gt;和&lt;code&gt;index block&lt;/code&gt;的索引。&lt;/p&gt;
&lt;p&gt;我们先看一下组成&lt;code&gt;data block&lt;/code&gt;以及&lt;code&gt;index block&lt;/code&gt;的基础组成部分。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/block-layout.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;因为&lt;code&gt;data block&lt;/code&gt;和&lt;code&gt;index block&lt;/code&gt;都是由KV组成，&lt;code&gt;data block&lt;/code&gt;的Key是skipList的&lt;code&gt;internal key&lt;/code&gt;, Value是用户输入的&lt;code&gt;Value&lt;/code&gt;.&lt;code&gt;index block&lt;/code&gt;的Key是每个&lt;code&gt;data block&lt;/code&gt;的最后一个Key, Value是&lt;code&gt;data block&lt;/code&gt;的&lt;code&gt;handle&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;我们可以详细看一下这个Block的结构组成,因为我们的Key是按照顺序的,因此我们可以使用仅存储两个Key不同的部分,从而减少空间占用,因此我们先存储两个Key相同的长度大小,需要存储的Key的大小,Value的长度大小,存储的Key的内容,存储的Value内容.我们可以通过下面的例子更好的阐述一下这个概念&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/block-demo.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;如图所示,因为&lt;code&gt;hello&lt;/code&gt;以及&lt;code&gt;hellz&lt;/code&gt;共享&lt;code&gt;hell&lt;/code&gt;,因此对于&lt;code&gt;hellz&lt;/code&gt;我们仅需要存储z即可.&lt;/p&gt;
&lt;p&gt;每个 block 的前缀压缩不是从第一个数据项开始就一直下去, 而是每隔一段(间隔可配置)设置一个新的前缀压缩起点(作为新起点的数据项的 key 保存原值而非做前缀压缩), &lt;code&gt;restart&lt;/code&gt;指的就是新起点, 从这个地方开始继续做前缀压缩.在写入文件前,我们还需要对KV对以及restart数据一起进行压缩,压缩的方式由&lt;code&gt;compress type&lt;/code&gt;表示,&lt;/p&gt;
&lt;p&gt;因此单个&lt;code&gt;Block&lt;/code&gt;由&lt;code&gt;KV对&lt;/code&gt;, &lt;code&gt;restart数组&lt;/code&gt;,&lt;code&gt;restart数组长度&lt;/code&gt;,&lt;code&gt;压缩类型&lt;/code&gt;,&lt;code&gt;CRC校验和&lt;/code&gt;组成.而&lt;code&gt;data block&lt;/code&gt;以及&lt;code&gt;index block&lt;/code&gt;则是由一个个&lt;code&gt;Block&lt;/code&gt;组成.&lt;/p&gt;
&lt;p&gt;而&lt;code&gt;filter block&lt;/code&gt;则是根据&lt;code&gt;data block&lt;/code&gt;的大小生成布隆过滤器,默认每2K个大小生成一个布隆过滤器.它的存储结构为
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/filter-block.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;开始存着一系列的布隆过滤器,然后是各个布隆过滤器的offset数组,紧跟着offset的offset(通过该值找到offset,因为offset是数组是一个变值),最后跟的是这个&lt;code&gt;filter block&lt;/code&gt;的元信息(&lt;code&gt;data block&lt;/code&gt;数据多大后产生一个布隆过滤器)&lt;/p&gt;
&lt;p&gt;而&lt;code&gt;filter index block&lt;/code&gt;则使用&lt;code&gt;Block&lt;/code&gt;的存储格式存储着&lt;strong&gt;Key: &quot;filter.$(filter.name)&quot;, Value: offset and size of filter block&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最后的&lt;code&gt;Footer&lt;/code&gt;的格式则为下图所示.
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/footer.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Footer&lt;/code&gt;中存储着&lt;code&gt;filter index block&lt;/code&gt;的指针以及&lt;code&gt;index block&lt;/code&gt;的指针以及&lt;code&gt;magic number&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在介绍完各个模块的磁盘结构后,我们可以看一下ldb文件的全貌,以及各部分之间的关系,如下图所示.
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/ldb-overview.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;在了解了整个全貌后,我们可以看一下整个生成的流程是怎么样的.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先构建出一个新的&lt;code&gt;TableBuilder&lt;/code&gt;, 然后按序将&lt;code&gt;Memtable&lt;/code&gt;中的数据写入&lt;code&gt;TableBuilder&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TableBuilder&lt;/code&gt;将数据全部写入&lt;code&gt;data block&lt;/code&gt;中 &lt;strong&gt;(按照Block的格式写)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;当Block的大小超过4K时,将生成的&lt;code&gt;data block&lt;/code&gt;落盘,&lt;strong&gt;尝试&lt;/strong&gt;生成一个&lt;code&gt;filter block&lt;/code&gt;,并生成一个&lt;code&gt;index handle&lt;/code&gt;,将其插入到&lt;code&gt;index block&lt;/code&gt;中.(注意,这里插入index block前,会尝试缩短key的大小,详情请参考代码&lt;code&gt;Comparator::FindShortestSeparator&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最后等所有数据添加完后,依次写入&lt;code&gt;data block&lt;/code&gt;, &lt;code&gt;filter block&lt;/code&gt;, &lt;code&gt;filter index block&lt;/code&gt;, &lt;code&gt;index block&lt;/code&gt;,以及&lt;code&gt;footer&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Overview&lt;/h3&gt;
&lt;p&gt;本文介绍了LevelDB的写操作的流程，以及相关文件的生成与格式。这篇文章并没有将所有的细节都写出来，如果你想要详细了解，我推荐你还是需要去读相关代码。这篇文章更侧重描写出LevelDB的大概轮廓，以及一些比较重要的细节。希望对你理解LevelDB相关代码有所帮助。&lt;/p&gt;
</content:encoded><category>DataBase</category><category>LevelDB</category><author>tang-hi</author></item><item><title>How Lucene Stores Its Forward Index</title><link>https://tangdh.life/posts/lucene/how-lucene-store-storedfields/</link><guid isPermaLink="true">https://tangdh.life/posts/lucene/how-lucene-store-storedfields/</guid><description>This article will introduce how Lucene 9.6 stores its forward index, to help readers better understand its internal workings.</description><pubDate>Tue, 23 May 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;This article will introduce how Lucene 9.6 stores its forward index, to help readers better understand its internal workings.&lt;/p&gt;
&lt;p&gt;A forward index, also known as a direct index, is a basic data structure in information retrieval systems. It stores the content and attributes of each document in the order of the documents, allowing the system to quickly access detailed information of any specified document. In Lucene, the storage mechanism of forward data is one of the key factors enabling it to efficiently perform full-text searches.&lt;/p&gt;
&lt;p&gt;Since the main focus of this article is the storage format of the forward index on the disk, the preprocessing of the document and how the &lt;code&gt;docID&lt;/code&gt; is obtained will be ignored.&lt;/p&gt;
&lt;h2&gt;What is a Forward Index&lt;/h2&gt;
&lt;p&gt;Simply put, a forward index is a structure that allows querying the corresponding document through &lt;code&gt;docID&lt;/code&gt;. We can compare it to a key-value pair, where &lt;code&gt;docID&lt;/code&gt; is the key and the document content is the value.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/doc-first.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;Therefore, the layout of Lucene&apos;s forward index on the disk must allow quick location of the document content through &lt;code&gt;docID&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Building the Forward Index&lt;/h2&gt;
&lt;p&gt;The entry function for building the forward index is &lt;code&gt;IndexingChain#processDocument&lt;/code&gt; (Lucene refers to the forward index as StoredFields).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; void processDocument(int docID, Iterable&amp;lt;? extends IndexableField&amp;gt; document) throws IOException {
   	
    startStoredFields(docID);
    try {
	  // skip .....
      docFieldIdx = 0;
      for (IndexableField field : document) {
        if (processField(docID, field, docFields[docFieldIdx])) {
          fields[indexedFieldCount] = docFields[docFieldIdx];
          indexedFieldCount++;
        }
        docFieldIdx++;
      }
    } finally {
      if (hasHitAbortingException == false) {
      	// skip ...
        // finish forward index
        finishStoredFields();
        
        // skip ...
      }
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we only focus on the processing of the forward index, we will find that Lucene does three things for the forward index:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Initialization based on &lt;code&gt;docID&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Processing each field in the document.&lt;/li&gt;
&lt;li&gt;Finalizing the forward index for this document.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If we are only interested in how the index is stored on the disk, we only need to pay attention to the last two points.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private boolean processField(int docID, IndexableField field, PerField pf) throws IOException {
    // skip....
    
    // Add stored fields
    if (fieldType.stored()) {
      StoredValue storedValue = field.storedValue();
      if (storedValue == null) {
        throw new IllegalArgumentException(&quot;Cannot store a null value&quot;);
      } else if (storedValue.getType() == StoredValue.Type.STRING
          &amp;amp;&amp;amp; storedValue.getStringValue().length() &amp;gt; IndexWriter.MAX_STORED_STRING_LENGTH) {
        throw new IllegalArgumentException(
            &quot;stored field \&quot;&quot;
                + field.name()
                + &quot;\&quot; is too large (&quot;
                + storedValue.getStringValue().length()
                + &quot; characters) to store&quot;);
      }
      try {
        storedFieldsConsumer.writeField(pf.fieldInfo, storedValue);
      } catch (Throwable th) {
        onAbortingException(th);
        throw th;
      }
    }

    // skip...
  }

void writeField(FieldInfo info, StoredValue value) throws IOException {
    switch (value.getType()) {
      case INTEGER -&amp;gt; writer.writeField(info, value.getIntValue());
      case LONG -&amp;gt; writer.writeField(info, value.getLongValue());
      case FLOAT -&amp;gt; writer.writeField(info, value.getFloatValue());
      case DOUBLE -&amp;gt; writer.writeField(info, value.getDoubleValue());
      case BINARY -&amp;gt; writer.writeField(info, value.getBinaryValue());
      case STRING -&amp;gt; writer.writeField(info, value.getStringValue());
      default -&amp;gt; throw new AssertionError();
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can see that when processing the forward index, we use &lt;code&gt;writeField&lt;/code&gt; to process each field in the document.&lt;/p&gt;
&lt;p&gt;Let&apos;s see how Lucene handles fixed-length and variable-length fields.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  @Override
  public void writeField(FieldInfo info, double value) throws IOException {
    ++numStoredFieldsInDoc;
    final long infoAndBits = (((long) info.number) &amp;lt;&amp;lt; TYPE_BITS) | NUMERIC_DOUBLE;
    bufferedDocs.writeVLong(infoAndBits);
    writeZDouble(bufferedDocs, value);
  }

  @Override
  public void writeField(FieldInfo info, BytesRef value) throws IOException {
    ++numStoredFieldsInDoc;
    final long infoAndBits = (((long) info.number) &amp;lt;&amp;lt; TYPE_BITS) | BYTE_ARR;
    bufferedDocs.writeVLong(infoAndBits);
    bufferedDocs.writeVInt(value.length);
    bufferedDocs.writeBytes(value.bytes, value.offset, value.length);
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The common point is that every time a field is written, &lt;code&gt;numStoredFieldsInDoc++&lt;/code&gt;. This variable is easy to understand, recording how many fields are stored in this document. Then it adds the relevant information of this field to &lt;code&gt;bufferedDocs&lt;/code&gt; (which can be considered as a memory array).&lt;/p&gt;
&lt;p&gt;The relevant information of the field can be considered to have three types:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Field number (each field has a unique number)&lt;/li&gt;
&lt;li&gt;Field data type&lt;/li&gt;
&lt;li&gt;Field data, that is, the value of the field.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Because the data type of the field is only a few limited types, Lucene will store it with the field number as a long type&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final long infoAndBits = (((long) info.number) &amp;lt;&amp;lt; TYPE_BITS) | NUMERIC_DOUBLE;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the field is of fixed length, we will directly write it into &lt;code&gt;bufferedDocs&lt;/code&gt;. But when the field is variable length, we will first write the number of bytes occupied by this value into &lt;code&gt;bufferedDocs&lt;/code&gt;, and then write this value into &lt;code&gt;bufferedDocs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/bufferedDocs.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;After processing all the fields in each document, we can consider that we have buffered this document in memory, and then we need to finalize the forward index, that is, flush it to the disk. The function for finalizing the forward index of the document is &lt;code&gt;finishDocument&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
public void finishDocument() throws IOException {
    if (numBufferedDocs == this.numStoredFields.length) {
      final int newLength = ArrayUtil.oversize(numBufferedDocs + 1, 4);
      this.numStoredFields = ArrayUtil.growExact(this.numStoredFields, newLength);
      endOffsets = ArrayUtil.growExact(endOffsets, newLength);
    }
    this.numStoredFields[numBufferedDocs] = numStoredFieldsInDoc;
    numStoredFieldsInDoc = 0;
    endOffsets[numBufferedDocs] = Math.toIntExact(bufferedDocs.size());
    ++numBufferedDocs;
    if (triggerFlush()) {
      flush(false);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this function, we will find that it does four things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Record the number of fields that need to be stored in each document and save it in the array &lt;code&gt;numStoredFields&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Record the write-in position of the last byte of this document and save it in the array &lt;code&gt;endOffsets&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Record the number of documents currently stored in memory, saved in the variable &lt;code&gt;numBufferedDocs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Determine whether it is necessary to flush the documents in memory to the disk. If a flush is needed, it is performed.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/finishDocument1.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;By the above diagram and code, we should have understood the first three points. Next, we will focus on the fourth point.&lt;/p&gt;
&lt;h3&gt;When to Flush to Disk&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;private boolean triggerFlush() {
    return bufferedDocs.size() &amp;gt;= chunkSize
        || // chunks of at least chunkSize bytes
        numBufferedDocs &amp;gt;= maxDocsPerChunk;
  }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From the code, we can see that when the number of Docs cached in memory &lt;strong&gt;reaches a threshold&lt;/strong&gt; or the Docs &lt;strong&gt;memory usage reaches a threshold&lt;/strong&gt;, both will trigger the operation of flushing to disk.&lt;/p&gt;
&lt;h3&gt;Flushing to Disk&lt;/h3&gt;
&lt;p&gt;From here, we start to really understand how Lucene saves its forward data on the disk. Let&apos;s assume that we have cached three documents in memory.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/flush-overview.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void flush(boolean force) throws IOException {
    // skip...
    numChunks++;
   
    // skip...

    // transform end offsets into lengths
    final int[] lengths = endOffsets;
    for (int i = numBufferedDocs - 1; i &amp;gt; 0; --i) {
      lengths[i] = endOffsets[i] - endOffsets[i - 1];
      assert lengths[i] &amp;gt;= 0;
    }
    final boolean sliced = bufferedDocs.size() &amp;gt;= 2L * chunkSize;
    final boolean dirtyChunk = force;
    // skip...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From the code, we can see that before actually writing to the disk, we still need to do some calculations in memory:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Increment the number of chunks written to the disk.&lt;/li&gt;
&lt;li&gt;Convert the previously saved position of the last byte of each document (endOffsets) into the length of each document.&lt;/li&gt;
&lt;li&gt;Determine whether to slice, sliced.&lt;/li&gt;
&lt;li&gt;Determine whether it is a dirtyChunk.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The last two points can be ignored for now, just understand the first two.&lt;/p&gt;
&lt;p&gt;The files we need to write to the disk are five in total:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;fdt&lt;/li&gt;
&lt;li&gt;fdm&lt;/li&gt;
&lt;li&gt;fdx&lt;/li&gt;
&lt;li&gt;seg-xx-doc_ids&lt;/li&gt;
&lt;li&gt;seg-xx-file_pointers&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Among them, 4 and 5 are temporary files and will not appear in the final index file. They only serve the task of temporarily storing data. The specific values of each variable in memory and the disk files that need to be written can be seen in the following figure.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/wait-to-flush.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;First, we will write the number of documents saved in this chunk and the starting position of this chunk in the fdt file to the files seg-xx-doc_ids and seg-xx-file_pointers.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
private void flush(boolean force) throws IOException {
    // skip...
    indexWriter.writeIndex(numBufferedDocs, fieldsStream.getFilePointer());
    //skip...
}

void writeIndex(int numDocs, long startPointer) throws IOException {
    assert startPointer &amp;gt;= previousFP;
    docsOut.writeVInt(numDocs);
    filePointersOut.writeVLong(startPointer - previousFP);
    previousFP = startPointer;
    totalDocs += numDocs;
    totalChunks++;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We notice that when writing filePointers, we store not the actual value but the difference. This is because filePointers is definitely a continuously increasing array. In this case, storing the difference can make the elements actually stored smaller than the original value, which is conducive to compression. Imagine that &lt;strong&gt;the number of bits required to store 100000 is much greater than the number of bits required to store 3&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The state after writing the files seg-xx-doc_ids and seg-xx-file_pointers is as shown in the following figure.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/index-writer.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;After writing the files seg-xx-doc_ids and seg-xx-file_pointers, we need to write the cached document content into the fdt file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
private void flush(boolean force) throws IOException {
    // skip...
    writeHeader(docBase, numBufferedDocs, numStoredFields, lengths, sliced, dirtyChunk);
    //skip...
    if (sliced) {
      // big chunk, slice it, using ByteBuffersDataInput ignore memory copy
      final int capacity = (int) bytebuffers.size();
      for (int compressed = 0; compressed &amp;lt; capacity; compressed += chunkSize) {
        int l = Math.min(chunkSize, capacity - compressed);
        ByteBuffersDataInput bbdi = bytebuffers.slice(compressed, l);
        compressor.compress(bbdi, fieldsStream);
      }
    } else {
      compressor.compress(bytebuffers, fieldsStream);
    }
}

private void writeHeader(
      int docBase,
      int numBufferedDocs,
      int[] numStoredFields,
      int[] lengths,
      boolean sliced,
      boolean dirtyChunk)
      throws IOException {
    final int slicedBit = sliced ? 1 : 0;
    final int dirtyBit = dirtyChunk ? 2 : 0;
    // save docBase and numBufferedDocs
    fieldsStream.writeVInt(docBase);
    fieldsStream.writeVInt((numBufferedDocs &amp;lt;&amp;lt; 2) | dirtyBit | slicedBit);

    // save numStoredFields
    saveInts(numStoredFields, numBufferedDocs, fieldsStream);

    // save lengths
    saveInts(lengths, numBufferedDocs, fieldsStream);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can see that we will write &lt;code&gt;docBase&lt;/code&gt;, &lt;code&gt;numBufferedDocs&lt;/code&gt;, &lt;code&gt;dirtyBit&lt;/code&gt;, &lt;code&gt;slicedBit&lt;/code&gt;, &lt;code&gt;numStoredFields&lt;/code&gt;, &lt;code&gt;lengths&lt;/code&gt;, and &lt;code&gt;bufferedDocs&lt;/code&gt; into fdt.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;docBase&lt;/code&gt; is the first &lt;code&gt;DocID&lt;/code&gt; of this chunk.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;numBufferedDocs&lt;/code&gt; is the total number of Docs cached in this chunk.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dirtyBit&lt;/code&gt;, &lt;code&gt;slicedBit&lt;/code&gt; can be ignored for now.&lt;/li&gt;
&lt;li&gt;The array &lt;code&gt;numStoredFields&lt;/code&gt; is the number of fields to be stored for each Doc.&lt;/li&gt;
&lt;li&gt;The array &lt;code&gt;lengths&lt;/code&gt; is the length of each Doc.&lt;/li&gt;
&lt;li&gt;The array &lt;code&gt;bufferedDocs&lt;/code&gt; is the actual stored data of all Docs.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The state after writing fdt is as shown in the following figure.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/fdt.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;The function &lt;code&gt;flush&lt;/code&gt; has been fully introduced. This is how Lucene processes one Doc after another, first caching them in memory, and then flushing them to the disk when a certain number is cached.&lt;/p&gt;
&lt;h3&gt;Generating the Final Index File&lt;/h3&gt;
&lt;p&gt;When Lucene has processed all the documents, it will call &lt;code&gt;finish&lt;/code&gt; to generate the final index file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
public void finish(int numDocs) throws IOException {
    if (numBufferedDocs &amp;gt; 0) {
      flush(true);
    } else {
      assert bufferedDocs.size() == 0;
    }
    if (docBase != numDocs) {
      throw new RuntimeException(
          &quot;Wrote &quot; + docBase + &quot; docs, finish called with numDocs=&quot; + numDocs);
    }
    indexWriter.finish(numDocs, fieldsStream.getFilePointer(), metaStream);
    metaStream.writeVLong(numChunks);
    metaStream.writeVLong(numDirtyChunks);
    metaStream.writeVLong(numDirtyDocs);
    CodecUtil.writeFooter(metaStream);
    CodecUtil.writeFooter(fieldsStream);
    assert bufferedDocs.size() == 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can now understand what &lt;code&gt;dirty&lt;/code&gt; means in &lt;code&gt;flush&lt;/code&gt;. When
the Docs cached in memory have not reached the flush condition, but the documents have been fully processed, we need to forcibly flush them to the disk. In this case, we will set &lt;code&gt;dirty&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;. As for &lt;code&gt;sliced&lt;/code&gt;, it is because if the length of &lt;code&gt;bufferedDocs&lt;/code&gt; is very large, in order to ensure the effect of compression, we will slice it, compress the slices, and write them to the fdt file.&lt;/p&gt;
&lt;p&gt;After flushing all the cached Docs to the disk, we start generating the fdx and fdm files.
We first focus on &lt;code&gt;indexWriter.finish(numDocs, fieldsStream.getFilePointer(), metaStream);&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void finish(int numDocs, long maxPointer, IndexOutput metaOut) throws IOException {
    if (numDocs != totalDocs) {
      throw new IllegalStateException(&quot;Expected &quot; + numDocs + &quot; docs, but got &quot; + totalDocs);
    }
    CodecUtil.writeFooter(docsOut);
    CodecUtil.writeFooter(filePointersOut);
    IOUtils.close(docsOut, filePointersOut);

    // skip...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lucene will first write a &lt;code&gt;Footer&lt;/code&gt; to the files seg-xx-doc_ids and seg-xx-file_pointers to mark the completion of writing. Also, the &lt;code&gt;Footer&lt;/code&gt; can protect the integrity of the file.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/temp-footer.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;Then we will write into fdx and fdm.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void finish(int numDocs, long maxPointer, IndexOutput metaOut) throws IOException {
    //skip...

    try (IndexOutput dataOut =
        dir.createOutput(IndexFileNames.segmentFileName(name, suffix, extension), ioContext)) {
      CodecUtil.writeIndexHeader(dataOut, codecName + &quot;Idx&quot;, VERSION_CURRENT, id, suffix);

      metaOut.writeInt(numDocs);
      metaOut.writeInt(blockShift);
      metaOut.writeInt(totalChunks + 1);
      metaOut.writeLong(dataOut.getFilePointer());

      try (ChecksumIndexInput docsIn = dir.openChecksumInput(docsOut.getName())) {
        CodecUtil.checkHeader(docsIn, codecName + &quot;Docs&quot;, VERSION_CURRENT, VERSION_CURRENT);
        Throwable priorE = null;
        try {
          final DirectMonotonicWriter docs =
              DirectMonotonicWriter.getInstance(metaOut, dataOut, totalChunks + 1, blockShift);
          long doc = 0;
          docs.add(doc);
          for (int i = 0; i &amp;lt; totalChunks; ++i) {
            doc += docsIn.readVInt();
            docs.add(doc);
          }
          docs.finish();
          if (doc != totalDocs) {
            throw new CorruptIndexException(&quot;Docs don&apos;t add up&quot;, docsIn);
          }
        } catch (Throwable e) {
          priorE = e;
        } finally {
          CodecUtil.checkFooter(docsIn, priorE);
        }
      }
      dir.deleteFile(docsOut.getName());
      docsOut = null;

      metaOut.writeLong(dataOut.getFilePointer());
      try (ChecksumIndexInput filePointersIn = dir.openChecksumInput(filePointersOut.getName())) {
        CodecUtil.checkHeader(
            filePointersIn, codecName + &quot;FilePointers&quot;, VERSION_CURRENT, VERSION_CURRENT);
        Throwable priorE = null;
        try {
          final DirectMonotonicWriter filePointers =
              DirectMonotonicWriter.getInstance(metaOut, dataOut, totalChunks + 1, blockShift);
          long fp = 0;

          for (int i = 0; i &amp;lt; totalChunks; ++i) {
            fp += filePointersIn.readVLong();
            filePointers.add(fp);
          }
          if (maxPointer &amp;lt; fp) {
            throw new CorruptIndexException(&quot;File pointers don&apos;t add up&quot;, filePointersIn);
          }
          filePointers.add(maxPointer);
          filePointers.finish();
        } catch (Throwable e) {
          priorE = e;
        } finally {
          CodecUtil.checkFooter(filePointersIn, priorE);
        }
      }
      dir.deleteFile(filePointersOut.getName());
      filePointersOut = null;

      metaOut.writeLong(dataOut.getFilePointer());
      metaOut.writeLong(maxPointer);

      CodecUtil.writeFooter(dataOut);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We will first write &lt;code&gt;numDocs&lt;/code&gt;, &lt;code&gt;blockShift&lt;/code&gt;, &lt;code&gt;totalChunks+1&lt;/code&gt;, &lt;code&gt;dataOut.getFilePointer()&lt;/code&gt; into fdm:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;numDocs&lt;/code&gt; is the total number of docs.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;blockShift&lt;/code&gt; is the meta information used for decompression and compression.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;totalChunks+1&lt;/code&gt; is the total number of chunks plus one.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dataOut.getFilePointer()&lt;/code&gt; is the next position to be written in the fdx file.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Then we compress the content saved in the file seg-xx-doc_ids and write it into the fdx file, and write the meta information needed for decompression into fdm, and finally write the next position to be written in fdm into fdm.
The same way, compress the content saved in the file seg-xx-file_pointers and write it into the fdx file, and write the meta information needed for decompression into fdm, and finally write the next position to be written in fdm and fdt into fdm.
The final state is as shown in the following figure.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/fdx-finish.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
From the diagram, we notice that the &lt;code&gt;chunkSize&lt;/code&gt; after the &lt;code&gt;Header&lt;/code&gt; of fdm is not reflected in the above code. This is because this variable is written when fdm is created.&lt;/p&gt;
&lt;p&gt;After completing the above steps, we only need to write &lt;code&gt;numChunks&lt;/code&gt;, &lt;code&gt;numDirtyChunks&lt;/code&gt;, &lt;code&gt;numDirtyDocs&lt;/code&gt; into fdm.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
public void finish(int numDocs) throws IOException {
    //skip...
    metaStream.writeVLong(numChunks);
    metaStream.writeVLong(numDirtyChunks);
    metaStream.writeVLong(numDirtyDocs);
    CodecUtil.writeFooter(metaStream);
    CodecUtil.writeFooter(fieldsStream);
    assert bufferedDocs.size() == 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, the complete index file is as shown in the following figure.
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/finish-write.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h3&gt;Overview&lt;/h3&gt;
&lt;p&gt;Finally, here is a schematic diagram of the index file and its relationships.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/overview.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
</content:encoded><category>Lucene</category><category>Full Text Search</category><author>tang-hi</author></item><item><title>Lucene如何存储正排索引</title><link>https://tangdh.life/posts/lucene/lucene-storefields/</link><guid isPermaLink="true">https://tangdh.life/posts/lucene/lucene-storefields/</guid><description>本文将介绍Lucene9.6如何存储它的正排索引，以帮助读者更好地理解其内部工作原理。</description><pubDate>Tue, 23 May 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;本文将介绍Lucene9.6如何存储它的正排索引，以帮助读者更好地理解其内部工作原理。&lt;/p&gt;
&lt;p&gt;正排索引，也被称为前向索引，是信息检索系统中的一种基本数据结构。它按照文档的顺序存储每个文档的内容和属性，使得系统能够快速地获取到任何指定文档的详细信息。在Lucene中，正排数据的存储机制是其能够高效执行全文搜索的关键因素之一。&lt;/p&gt;
&lt;p&gt;因为本文的主要关注点是正排索引在磁盘中的存储格式，因此对于文档的预处理以及&lt;code&gt;docID&lt;/code&gt;是如何获得会进行忽略。&lt;/p&gt;
&lt;h2&gt;什么是正排索引&lt;/h2&gt;
&lt;p&gt;简单来说，正排索引就是可以通过docID 查询到对应的文档。我们可以将其类比为键值对（Key-Value），其中docID为Key，文档内容为Value。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/doc-first.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;因此，Lucene的正排索引在磁盘中的布局必须能够通过docID快速定位到文档的内容。&lt;/p&gt;
&lt;h2&gt;正排索引的构建&lt;/h2&gt;
&lt;p&gt;正排索引构建的入口函数&lt;code&gt;IndexingChain#processDocument&lt;/code&gt; （Lucene中将正排索引称为StoredFields）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; void processDocument(int docID, Iterable&amp;lt;? extends IndexableField&amp;gt; document) throws IOException {
   	
    startStoredFields(docID);
    try {
	  // skip .....
      docFieldIdx = 0;
      for (IndexableField field : document) {
        if (processField(docID, field, docFields[docFieldIdx])) {
          fields[indexedFieldCount] = docFields[docFieldIdx];
          indexedFieldCount++;
        }
        docFieldIdx++;
      }
    } finally {
      if (hasHitAbortingException == false) {
      	// skip ...
        // finish forward index
        finishStoredFields();
        
        // skip ...
      }
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们如果只关注正排索引的处理，会发现Lucene对于正排索引一共会做三件事&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;根据&lt;code&gt;docID&lt;/code&gt;进行初始化。&lt;/li&gt;
&lt;li&gt;对文档中的每一个字段进行处理。&lt;/li&gt;
&lt;li&gt;对这篇doc中的正排索引进行收尾操作。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果只是关注索引是如何存储在磁盘中的话，我们只需要关注后两件事。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private boolean processField(int docID, IndexableField field, PerField pf) throws IOException {
    // skip....
    
    // Add stored fields
    if (fieldType.stored()) {
      StoredValue storedValue = field.storedValue();
      if (storedValue == null) {
        throw new IllegalArgumentException(&quot;Cannot store a null value&quot;);
      } else if (storedValue.getType() == StoredValue.Type.STRING
          &amp;amp;&amp;amp; storedValue.getStringValue().length() &amp;gt; IndexWriter.MAX_STORED_STRING_LENGTH) {
        throw new IllegalArgumentException(
            &quot;stored field \&quot;&quot;
                + field.name()
                + &quot;\&quot; is too large (&quot;
                + storedValue.getStringValue().length()
                + &quot; characters) to store&quot;);
      }
      try {
        storedFieldsConsumer.writeField(pf.fieldInfo, storedValue);
      } catch (Throwable th) {
        onAbortingException(th);
        throw th;
      }
    }

    // skip...
  }

void writeField(FieldInfo info, StoredValue value) throws IOException {
    switch (value.getType()) {
      case INTEGER -&amp;gt; writer.writeField(info, value.getIntValue());
      case LONG -&amp;gt; writer.writeField(info, value.getLongValue());
      case FLOAT -&amp;gt; writer.writeField(info, value.getFloatValue());
      case DOUBLE -&amp;gt; writer.writeField(info, value.getDoubleValue());
      case BINARY -&amp;gt; writer.writeField(info, value.getBinaryValue());
      case STRING -&amp;gt; writer.writeField(info, value.getStringValue());
      default -&amp;gt; throw new AssertionError();
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以发现在处理正排索引时，我们使用writeField对文档中的每一个字段进行处理。&lt;/p&gt;
&lt;p&gt;我们看一下对于定长以及变长的字段，Lucene分别是如何处理的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  @Override
  public void writeField(FieldInfo info, double value) throws IOException {
    ++numStoredFieldsInDoc;
    final long infoAndBits = (((long) info.number) &amp;lt;&amp;lt; TYPE_BITS) | NUMERIC_DOUBLE;
    bufferedDocs.writeVLong(infoAndBits);
    writeZDouble(bufferedDocs, value);
  }

  @Override
  public void writeField(FieldInfo info, BytesRef value) throws IOException {
    ++numStoredFieldsInDoc;
    final long infoAndBits = (((long) info.number) &amp;lt;&amp;lt; TYPE_BITS) | BYTE_ARR;
    bufferedDocs.writeVLong(infoAndBits);
    bufferedDocs.writeVInt(value.length);
    bufferedDocs.writeBytes(value.bytes, value.offset, value.length);
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以发现，无论字段是定长还是变长，每写入一个字段，都会使&lt;code&gt;numStoredFieldsInDoc&lt;/code&gt;增加1。这个变量很好理解，它记录了这篇文档中存储了多少个字段。随后会向&lt;code&gt;bufferedDocs&lt;/code&gt;（可以认为是一个内存数组）添加这个字段的相关信息。&lt;/p&gt;
&lt;p&gt;字段的相关信息我们可以认为有三种&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;字段的编号(每个字段都有一个独一无二的编号)&lt;/li&gt;
&lt;li&gt;字段的数据类型&lt;/li&gt;
&lt;li&gt;字段的数据，即该字段的值。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因为字段的数据类型只有有限的几种，因此Lucene会将其与字段的编号一起存储为一个long类型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final long infoAndBits = (((long) info.number) &amp;lt;&amp;lt; TYPE_BITS) | NUMERIC_DOUBLE;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而当字段为定长时，我们会直接将其写入&lt;code&gt;bufferedDocs&lt;/code&gt;。但是当字段为变长时，我们会先将该值所占的bytes数写入&lt;code&gt;bufferedDocs&lt;/code&gt;后，再将该值写入&lt;code&gt;bufferedDocs&lt;/code&gt;。因此我们可以认为&lt;code&gt;bufferedDocs&lt;/code&gt;的数据格式为&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/bufferedDocs.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;当处理完每篇文档的字段后，我们可以认为我们已经将这篇文档缓存在了内存中，而后我们需要做的就是对正排索引进行收尾工作，即将其flush到磁盘中。
对文档的正排索引进行收尾工作的函数为&lt;code&gt;finishDocument&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
public void finishDocument() throws IOException {
    if (numBufferedDocs == this.numStoredFields.length) {
      final int newLength = ArrayUtil.oversize(numBufferedDocs + 1, 4);
      this.numStoredFields = ArrayUtil.growExact(this.numStoredFields, newLength);
      endOffsets = ArrayUtil.growExact(endOffsets, newLength);
    }
    this.numStoredFields[numBufferedDocs] = numStoredFieldsInDoc;
    numStoredFieldsInDoc = 0;
    endOffsets[numBufferedDocs] = Math.toIntExact(bufferedDocs.size());
    ++numBufferedDocs;
    if (triggerFlush()) {
      flush(false);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个函数中我们会发现它一共做了四件事&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将每一篇文档中需要进行存储的字段数量记录下来，保存在数组&lt;code&gt;numStoredFields&lt;/code&gt;中&lt;/li&gt;
&lt;li&gt;记录下这篇文档最后一个byte的写入位置，保存在数组&lt;code&gt;endOffsets&lt;/code&gt;中&lt;/li&gt;
&lt;li&gt;记录下目前已经在内存中存储的文档数，保存在变量&lt;code&gt;numBufferedDocs&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;判断是否需要将内存中的文档刷到磁盘中， 如果需要进行flush。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/finishDocument1.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;通过上面的图和代码，我们应该已经明白了前三件事，后续我们重点关注第四件事。&lt;/p&gt;
&lt;h3&gt;刷到磁盘的时机&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;private boolean triggerFlush() {
    return bufferedDocs.size() &amp;gt;= chunkSize
        || // chunks of at least chunkSize bytes
        numBufferedDocs &amp;gt;= maxDocsPerChunk;
  }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从代码中我们可以看到，当内存中缓存的Doc&lt;strong&gt;数量达到阈值&lt;/strong&gt;或者缓存的Doc&lt;strong&gt;所占用的内存达到阈值&lt;/strong&gt;时，都会触发落盘这一操作。&lt;/p&gt;
&lt;h3&gt;刷新到磁盘&lt;/h3&gt;
&lt;p&gt;从这里开始，我们开始真正了解，Lucene是如何将他的正排数据保存在磁盘中。我们假设我们内存中一共缓存了三篇文档。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/flush-overview.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void flush(boolean force) throws IOException {
    // skip...
    numChunks++;
   
    // skip...

    // transform end offsets into lengths
    final int[] lengths = endOffsets;
    for (int i = numBufferedDocs - 1; i &amp;gt; 0; --i) {
      lengths[i] = endOffsets[i] - endOffsets[i - 1];
      assert lengths[i] &amp;gt;= 0;
    }
    final boolean sliced = bufferedDocs.size() &amp;gt;= 2L * chunkSize;
    final boolean dirtyChunk = force;
    // skip...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从代码中我们可以看到在实际写到磁盘前，我们仍然需要在内存中做一些计算&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;递增写到磁盘的chunk数&lt;/li&gt;
&lt;li&gt;将之前保存的每篇文档最后一byte所处的位置(endOffsets)转化为每篇文档的长度。&lt;/li&gt;
&lt;li&gt;判断是否需要分片, sliced&lt;/li&gt;
&lt;li&gt;判断是否为dirtyChunk&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;后两个目前不需要了解，只需要理解前两个即可。
我们需要向磁盘中写入的文件一共有5个&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;fdt&lt;/li&gt;
&lt;li&gt;fdm&lt;/li&gt;
&lt;li&gt;fdx&lt;/li&gt;
&lt;li&gt;seg-xx-doc_ids&lt;/li&gt;
&lt;li&gt;seg-xx-file_pointers&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其中4,5为临时文件，并不会出现在最后的索引文件中，仅仅起到暂时存储数据的任务。具体内存中各变量的值，以及需要写的磁盘文件可见下图。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/wait-to-flush.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;首先我们会将该chunk所保存的文档数以及该chunk在fdt文件中的起始位置写到文件seg-xx-doc_ids,seg-xx-file_pointers中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
private void flush(boolean force) throws IOException {
    // skip...
    indexWriter.writeIndex(numBufferedDocs, fieldsStream.getFilePointer());
    //skip...
}

void writeIndex(int numDocs, long startPointer) throws IOException {
    assert startPointer &amp;gt;= previousFP;
    docsOut.writeVInt(numDocs);
    filePointersOut.writeVLong(startPointer - previousFP);
    previousFP = startPointer;
    totalDocs += numDocs;
    totalChunks++;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们注意到当写filePointers时，我们存的并不是实际的值而是差值，这是因为filePointers一定是连续递增的数组，对于这种情况
存储差值可以使得实际存储的元素相较于原值更小，从而有利于压缩。想象一下，&lt;strong&gt;存储100000所需要的bit数是远大于3所需要的bit数&lt;/strong&gt;。
写完文件seg-xx-doc_ids,seg-xx-file_pointers后的状态可参考下图。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/index-writer.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;在写完文件seg-xx-doc_ids,seg-xx-file_pointers后，我们需要将缓存的文档内容写入文件fdt中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
private void flush(boolean force) throws IOException {
    // skip...
    writeHeader(docBase, numBufferedDocs, numStoredFields, lengths, sliced, dirtyChunk);
    //skip...
    if (sliced) {
      // big chunk, slice it, using ByteBuffersDataInput ignore memory copy
      final int capacity = (int) bytebuffers.size();
      for (int compressed = 0; compressed &amp;lt; capacity; compressed += chunkSize) {
        int l = Math.min(chunkSize, capacity - compressed);
        ByteBuffersDataInput bbdi = bytebuffers.slice(compressed, l);
        compressor.compress(bbdi, fieldsStream);
      }
    } else {
      compressor.compress(bytebuffers, fieldsStream);
    }
}

private void writeHeader(
      int docBase,
      int numBufferedDocs,
      int[] numStoredFields,
      int[] lengths,
      boolean sliced,
      boolean dirtyChunk)
      throws IOException {
    final int slicedBit = sliced ? 1 : 0;
    final int dirtyBit = dirtyChunk ? 2 : 0;
    // save docBase and numBufferedDocs
    fieldsStream.writeVInt(docBase);
    fieldsStream.writeVInt((numBufferedDocs &amp;lt;&amp;lt; 2) | dirtyBit | slicedBit);

    // save numStoredFields
    saveInts(numStoredFields, numBufferedDocs, fieldsStream);

    // save lengths
    saveInts(lengths, numBufferedDocs, fieldsStream);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到我们会向fdt中写入&lt;code&gt;docBase&lt;/code&gt;,&lt;code&gt;numBufferedDocs&lt;/code&gt;,&lt;code&gt;dirtyBit&lt;/code&gt;,&lt;code&gt;slicedBit&lt;/code&gt;,&lt;code&gt;numStoredFields&lt;/code&gt;,&lt;code&gt;lengths&lt;/code&gt;以及&lt;code&gt;bufferedDocs&lt;/code&gt;.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;docBase&lt;/code&gt;为这个chunk的第一个&lt;code&gt;DocID&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;numBufferedDocs&lt;/code&gt;为这个chunk总共缓存的Doc数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dirtyBit&lt;/code&gt;,&lt;code&gt;slicedBit&lt;/code&gt;目前可以忽略&lt;/li&gt;
&lt;li&gt;数组&lt;code&gt;numStoredFields&lt;/code&gt; 为每篇Doc需要存储的字段数量&lt;/li&gt;
&lt;li&gt;数组&lt;code&gt;lengths&lt;/code&gt;为每篇Doc的长度&lt;/li&gt;
&lt;li&gt;数组&lt;code&gt;numBufferedDocs&lt;/code&gt;为全部Doc实际存储的数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;写完fdt后的状态如下图所示&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/fdt.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;函数&lt;code&gt;flush&lt;/code&gt;目前全部介绍完毕，Lucene就是这样处理一篇一篇的Doc,先缓存在内存中，当缓存一定数量后再flush到磁盘中。&lt;/p&gt;
&lt;h3&gt;生成最后的索引文件&lt;/h3&gt;
&lt;p&gt;当Lucene处理完全部的文档后，会调用&lt;code&gt;finish&lt;/code&gt;生成最后的索引文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
public void finish(int numDocs) throws IOException {
    if (numBufferedDocs &amp;gt; 0) {
      flush(true);
    } else {
      assert bufferedDocs.size() == 0;
    }
    if (docBase != numDocs) {
      throw new RuntimeException(
          &quot;Wrote &quot; + docBase + &quot; docs, finish called with numDocs=&quot; + numDocs);
    }
    indexWriter.finish(numDocs, fieldsStream.getFilePointer(), metaStream);
    metaStream.writeVLong(numChunks);
    metaStream.writeVLong(numDirtyChunks);
    metaStream.writeVLong(numDirtyDocs);
    CodecUtil.writeFooter(metaStream);
    CodecUtil.writeFooter(fieldsStream);
    assert bufferedDocs.size() == 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这个函数我们现在可以知道&lt;code&gt;flush&lt;/code&gt;中的&lt;code&gt;dirty&lt;/code&gt;是什么意思,当我们内存缓存的Doc并未达到flush的条件，但是文档已经处理完了，我们需要将其强制
flush到磁盘中，对于这种情况，我们会将dirty设置为&lt;code&gt;true&lt;/code&gt;。至于&lt;code&gt;sliced&lt;/code&gt;则是因为如果&lt;code&gt;bufferedDocs&lt;/code&gt;的长度很大，为了保证压缩的效果，我们会对其
进行分片，分片压缩并写入到文件fdt中。&lt;/p&gt;
&lt;p&gt;在将缓存的Doc全部flush到磁盘后，我们开始生成文件fdx，fdm。
我们先关注&lt;code&gt;indexWriter.finish(numDocs, fieldsStream.getFilePointer(), metaStream);&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void finish(int numDocs, long maxPointer, IndexOutput metaOut) throws IOException {
    if (numDocs != totalDocs) {
      throw new IllegalStateException(&quot;Expected &quot; + numDocs + &quot; docs, but got &quot; + totalDocs);
    }
    CodecUtil.writeFooter(docsOut);
    CodecUtil.writeFooter(filePointersOut);
    IOUtils.close(docsOut, filePointersOut);

    // skip...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lucene首先会给文件seg-xx-doc_ids,seg-xx-file_pointers写上&lt;code&gt;Footer&lt;/code&gt;标记写入完成。同时&lt;code&gt;Footer&lt;/code&gt;也可以保护文件的完整性。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/temp-footer.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;随后我们会像fdx以及fdm中写入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void finish(int numDocs, long maxPointer, IndexOutput metaOut) throws IOException {
    //skip...

    try (IndexOutput dataOut =
        dir.createOutput(IndexFileNames.segmentFileName(name, suffix, extension), ioContext)) {
      CodecUtil.writeIndexHeader(dataOut, codecName + &quot;Idx&quot;, VERSION_CURRENT, id, suffix);

      metaOut.writeInt(numDocs);
      metaOut.writeInt(blockShift);
      metaOut.writeInt(totalChunks + 1);
      metaOut.writeLong(dataOut.getFilePointer());

      try (ChecksumIndexInput docsIn = dir.openChecksumInput(docsOut.getName())) {
        CodecUtil.checkHeader(docsIn, codecName + &quot;Docs&quot;, VERSION_CURRENT, VERSION_CURRENT);
        Throwable priorE = null;
        try {
          final DirectMonotonicWriter docs =
              DirectMonotonicWriter.getInstance(metaOut, dataOut, totalChunks + 1, blockShift);
          long doc = 0;
          docs.add(doc);
          for (int i = 0; i &amp;lt; totalChunks; ++i) {
            doc += docsIn.readVInt();
            docs.add(doc);
          }
          docs.finish();
          if (doc != totalDocs) {
            throw new CorruptIndexException(&quot;Docs don&apos;t add up&quot;, docsIn);
          }
        } catch (Throwable e) {
          priorE = e;
        } finally {
          CodecUtil.checkFooter(docsIn, priorE);
        }
      }
      dir.deleteFile(docsOut.getName());
      docsOut = null;

      metaOut.writeLong(dataOut.getFilePointer());
      try (ChecksumIndexInput filePointersIn = dir.openChecksumInput(filePointersOut.getName())) {
        CodecUtil.checkHeader(
            filePointersIn, codecName + &quot;FilePointers&quot;, VERSION_CURRENT, VERSION_CURRENT);
        Throwable priorE = null;
        try {
          final DirectMonotonicWriter filePointers =
              DirectMonotonicWriter.getInstance(metaOut, dataOut, totalChunks + 1, blockShift);
          long fp = 0;

          for (int i = 0; i &amp;lt; totalChunks; ++i) {
            fp += filePointersIn.readVLong();
            filePointers.add(fp);
          }
          if (maxPointer &amp;lt; fp) {
            throw new CorruptIndexException(&quot;File pointers don&apos;t add up&quot;, filePointersIn);
          }
          filePointers.add(maxPointer);
          filePointers.finish();
        } catch (Throwable e) {
          priorE = e;
        } finally {
          CodecUtil.checkFooter(filePointersIn, priorE);
        }
      }
      dir.deleteFile(filePointersOut.getName());
      filePointersOut = null;

      metaOut.writeLong(dataOut.getFilePointer());
      metaOut.writeLong(maxPointer);

      CodecUtil.writeFooter(dataOut);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们首先会向fdm中写入&lt;code&gt;numDocs&lt;/code&gt;,&lt;code&gt;blockShift&lt;/code&gt;,&lt;code&gt;totalChunks+1&lt;/code&gt;,&lt;code&gt;dataOut.getFilePointer()&lt;/code&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;numDocs&lt;/code&gt; 全量的doc数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;blockShift&lt;/code&gt; 用于解压以及压缩的元信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;totalChunks+1&lt;/code&gt; 全部的chunk数+1&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dataOut.getFilePointer()&lt;/code&gt; 文件fdx下一个待写入的位置。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;随后将文件seg-xx-doc_ids中保存的内容压缩后写入fdx中， 并将解压所需要的元信息写入fdm，最后将fdx下一个待写入的位置写入fdm。
同样的方式将seg-xx-file_pointers中保存的内容压缩后写入fdx中， 并将解压所需要的元信息写入fdm，并将fdx以及fdt下一个待写入的位置写入fdm。
最终的状态如下图所示
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/fdx-finish.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
从图中，我们注意到fdm的&lt;code&gt;Header&lt;/code&gt;后的&lt;code&gt;chunkSize&lt;/code&gt;并没有在上述代码中体现，这是因为这个变量是在创建fdm时就写入的。&lt;/p&gt;
&lt;p&gt;完成上述步骤后,我们只需要往fdm中写入&lt;code&gt;numChunks&lt;/code&gt;,&lt;code&gt;numDirtyChunks&lt;/code&gt;,&lt;code&gt;numDirtyDocs&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
public void finish(int numDocs) throws IOException {
    //skip...
    metaStream.writeVLong(numChunks);
    metaStream.writeVLong(numDirtyChunks);
    metaStream.writeVLong(numDirtyDocs);
    CodecUtil.writeFooter(metaStream);
    CodecUtil.writeFooter(fieldsStream);
    assert bufferedDocs.size() == 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后完整的的索引文件如下图所示
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/finish-write.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h3&gt;Overview&lt;/h3&gt;
&lt;p&gt;最后给出一张索引文件的概略以及相互的关系图
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/overview.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
</content:encoded><category>Lucene</category><category>Full Text Search</category><author>tang-hi</author></item><item><title>C++ Memory Model</title><link>https://tangdh.life/posts/c/memory-order/</link><guid isPermaLink="true">https://tangdh.life/posts/c/memory-order/</guid><description>这篇文章是因为对C++的内存模型和内存顺序感兴趣，在探索后对所学的知识进行一个总结，希望能以一个便于理解的方式让读者轻松了解C++的Memory Model</description><pubDate>Mon, 15 May 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;这篇文章是因为对C++的内存模型和内存顺序感兴趣，在探索后对所学的知识进行一个总结，希望能以一个便于理解的方式让读者轻松了解C++的Memory Model.我们不会直接讨论&lt;code&gt;memory model&lt;/code&gt;做了什么,而是它&lt;strong&gt;要做什么&lt;/strong&gt;, 在它出来之前我们是怎么做的，它是怎么集成了之前的做法，从而形成它独有的模型。&lt;/p&gt;
&lt;h2&gt;Data Race的原因&lt;/h2&gt;
&lt;p&gt;首先我们知道，如果出现了&lt;code&gt;Data Race&lt;/code&gt;的情况，最简单的方式就是一把大锁保平安，但是为什么锁就可以保证不出现&lt;code&gt;Data Race&lt;/code&gt;? 锁究竟对我们的代码做了什么，从而导致&lt;code&gt;Data Race&lt;/code&gt;的情况消失了? 这就是我们这篇文章想探索的问题。&lt;/p&gt;
&lt;h3&gt;Compiler优化&lt;/h3&gt;
&lt;p&gt;我们通过两个示例代码来阐述为什么编译器的优化会造成Data Race。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int Value = 0;
int IsPublished = 0;

void sendValue(int x)
{
    Value = 1 + x;
    IsPublished = 1 ;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先看第一个例子&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;设置&lt;code&gt;Value&lt;/code&gt;的值&lt;/li&gt;
&lt;li&gt;设置&lt;code&gt;IsPublished = 1&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果我们有另一个线程，它会不断读取&lt;code&gt;IsPublished&lt;/code&gt;,当&lt;code&gt;IsPublished == 1&lt;/code&gt;时再去读取&lt;code&gt;Value&lt;/code&gt;的值。在这种情况下，我们另一个线程可能读到&lt;code&gt;Value&lt;/code&gt;的值为0！为什么？我们看一下这份代码所产生的汇编代码 &lt;strong&gt;(如果没有特特殊说明, 我们使用的编译器为gcc9.5, 采用-O2 -std=c++11编译选项)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sendValue(int):
        mov     DWORD PTR IsPublished[rip], 1             ## first set isPublished = 1
        add     edi, 2
        mov     DWORD PTR Value[rip], edi
        ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从汇编代码中可以发现汇编与代码的顺序相反，我们首先设置&lt;code&gt;IsPublished = 1&lt;/code&gt;，再设置&lt;code&gt;Value&lt;/code&gt;的值。这就导致另一个线程看到&lt;code&gt;IsPublished = 1&lt;/code&gt;时，&lt;code&gt;Value&lt;/code&gt;的值可能还未设置。从而发生了&lt;code&gt;Data Race&lt;/code&gt;。这是因为编译器在编译时，为了性能考虑，它可以任意交换代码的顺序。只要&lt;strong&gt;单线程执行，交换顺序后的结果与不交换的结果保持一致&lt;/strong&gt;，我们将其称为&lt;code&gt;as-if法则&lt;/code&gt;。因为&lt;code&gt;IsPublished&lt;/code&gt;，&lt;code&gt;Value&lt;/code&gt;是两个不相关的变量，交换它们的执行顺序并不会导致单线程的执行结果被改变，所以编译器可以进行这种优化，尽管对于多线程的程序来说，这会导致&lt;code&gt;Data Race&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;我们再来看另一个例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int value = 90;
void foo() {

    value = 100;               // A                
    while(value == 100)
    {
        // do something
    }
    // exit loop
}

void end() {
    value = 99;				  // B
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设我们有两个线程，一个执行&lt;code&gt;foo&lt;/code&gt;,一个执行&lt;code&gt;end&lt;/code&gt;. 我们的预期是如果&lt;code&gt;A&lt;/code&gt;比&lt;code&gt;B&lt;/code&gt;先执行(假定都是原子操作)，线程&lt;code&gt;foo&lt;/code&gt;会跳出循环。但是实际上&lt;code&gt;foo&lt;/code&gt;可能永远都不会结束。还是老样子，我们看看汇编代码说了什么。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;foo():
        mov     DWORD PTR value[rip], 100
.L21:
        jmp     .L21                 # loop forever
end():
        mov     DWORD PTR value[rip], 99
        ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以发现汇编后的&lt;code&gt;foo&lt;/code&gt;，本质上就是一个死循环,它在将&lt;code&gt;value&lt;/code&gt;设置为100后，就不停的循环下去了，根本不会再校验&lt;code&gt;value&lt;/code&gt;的值。其原因也是&lt;code&gt;as-if法则&lt;/code&gt;,因为上一条语句已经将&lt;code&gt;value&lt;/code&gt;设置为100了，因此对于单线程来说，直接将语句转化为死循环就行了，不需要浪费时间再去检查&lt;code&gt;value&lt;/code&gt;值了。&lt;/p&gt;
&lt;p&gt;从上面的两个例子中，我们可以看到编译器的优化尽管会增加单线程的效率，但是会破坏多线程的正确性。因此C++的&lt;code&gt;memory order&lt;/code&gt;一定需要解决这个问题。在这一小节，我不会过多阐述C++的&lt;code&gt;memory order&lt;/code&gt;是怎么做的，而是阐述&lt;code&gt;我们之前是怎么做的&lt;/code&gt;。在下一节，我们才会正式介绍C++的&lt;code&gt;memory order&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;如何解决&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;我们有两个方式可以禁止Compiler的优化&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用Compiler barrier&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;volatile&lt;/code&gt;关键字&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;strong&gt;Compiler barrier&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;在代码中直接插入&lt;code&gt;asm volatile (&quot;&quot; ::: &quot;memory&quot;)&lt;/code&gt;,可以保证&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在&lt;code&gt;compiler barrier&lt;/code&gt;后的代码不会被编译器优化到&lt;code&gt;compiler barrier&lt;/code&gt;前&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;compiler barrier&lt;/code&gt;前的代码不会被编译器优化到&lt;code&gt;compiler barrier&lt;/code&gt;后&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们直接看第一个例子添加&lt;code&gt;asm volatile (&quot;&quot; ::: &quot;memory&quot;)&lt;/code&gt;后的汇编代码，可以发现&lt;code&gt;IsPublished = 1;&lt;/code&gt;的汇编语句出现在了&lt;code&gt;Value = x + 2;&lt;/code&gt;后，从而禁止了编译器不恰当的优化！( &lt;strong&gt;compiler barrier 仅在编译阶段生效&lt;/strong&gt; )&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# void sendValue(int x)
# {
#     Value = x + 2;
#     asm volatile(&quot;&quot; ::: &quot;memory&quot;);
#     IsPublished = 1 ;
# } 

sendValue(int):
        add     edi, 2
        mov     DWORD PTR Value[rip], edi
        mov     DWORD PTR IsPublished[rip], 1
        ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;volatile&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通过将变量声明为&lt;code&gt;volatile&lt;/code&gt;,我们告诉编译器，这个变量可能在程序之外被修改（在嵌入式中使用的较多）。因此编译器会对该变量作出如下保证。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;保证该变量一定会从内存中读取，而不是寄存器。&lt;/li&gt;
&lt;li&gt;编译器不会将含有该变量的语句优化掉。&lt;/li&gt;
&lt;li&gt;编译器不会将标记为&lt;code&gt;volatile&lt;/code&gt;的变量进行重排序（不仅是该变量，而是所有被标记为&lt;code&gt;volatile&lt;/code&gt;的变量）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们看看上述两个例子使用&lt;code&gt;volatile&lt;/code&gt;后的改变。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Example 1
# volatile int Value;
# volatile int IsPublished = 0;
 
# void sendValue(int x)
# {
#     Value = x + 2;
#     IsPublished = 1 ;
# } 
endValue(int):
        add     edi, 2
        mov     DWORD PTR Value[rip], edi
        mov     DWORD PTR IsPublished[rip], 1
        
# Example 2
# volatile int value = 90;
# void foo() {

#     value = 100;
 
#     while(value == 100)
#     {
#         // do something
#     }
#     // exit loop
# }

# void end() {
#     value = 99;
# }

foo():
        mov     DWORD PTR value[rip], 100
.L21:
        mov     eax, DWORD PTR value[rip]
        cmp     eax, 100
        je      .L21
        ret
end():
        mov     DWORD PTR value[rip], 99
        ret

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一个例子可以看到，因为声明为&lt;code&gt;volatile&lt;/code&gt;，编译器不会再将其重排序了，这里之所以要将两个变量都声明为&lt;code&gt;volatile&lt;/code&gt;是因为，&lt;strong&gt;volatile&lt;/strong&gt; 和 &lt;strong&gt;非volatile&lt;/strong&gt;变量是可以重排序的，只有都为&lt;code&gt;volatile&lt;/code&gt;才不会重排序。而第二个例子中，可以看到编译器保证对&lt;code&gt;value&lt;/code&gt;的读取都会从内存读取，并且不会对含有&lt;code&gt;volatile&lt;/code&gt;的变量进行优化。&lt;/p&gt;
&lt;h3&gt;CPU乱序执行&lt;/h3&gt;
&lt;p&gt;这里我们仍旧使用第一个例子，不同的是我们加上&lt;code&gt;compiler barrier&lt;/code&gt;的约束&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int Value = 0;
int IsPublished = 0;

void sendValue(int x)
{
    Value = 1 + x;
    asm volatile(&quot;&quot; ::: &quot;memory&quot;);
    IsPublished = 1 ;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尽管我们在这里添加了&lt;code&gt;compiler barrier&lt;/code&gt;，但是实际运行时，仍然可能出现在读到 &lt;code&gt;IsPublished = 1&lt;/code&gt;后，&lt;code&gt;Value&lt;/code&gt;的值为0的情况。这是因为CPU也可以乱序执行你所写的指令，只要符合&lt;code&gt;as-if法则&lt;/code&gt;.因此可能在实际执行时，CPU先执行&lt;code&gt;IsPublished = 1 &lt;/code&gt;后执行&lt;code&gt;Value = 1 + x&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;如何解决&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;如果想要强制要求CPU按某种顺序执行指令，我们需要在代码中插入&lt;code&gt;memory barrier&lt;/code&gt;, 这里仅介绍x86架构下的&lt;code&gt;memory barrier&lt;/code&gt;,一共有三种&lt;code&gt;memory barrier&lt;/code&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;mfence&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;Performs a serializing operation on all load-from-memory and store-to-memory instructions that were issued prior the MFENCE instruction. This serializing operation guarantees that every load and store instruction that precedes in program order the MFENCE instruction is globally visible before any load or store instruction that follows the MFENCE instruction is globally visible&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;lfence&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;Performs a serializing operation on all load-from-memory instructions that were issued prior the LFENCE instruction. This serializing operation guarantees that every load instruction that precedes in program order the LFENCE instruction is globally visible before any load instruction that follows the LFENCE instruction is globally visible&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;sfence&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;Performs a serializing operation on all store-to-memory instructions that were issued prior the SFENCE instruction. This serializing operation guarantees that every store instruction that precedes in program order the SFENCE instruction is globally visible before any store instruction that follows the SFENCE instruction is globally visible.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;mfence&lt;/strong&gt;，所有在mfence前的&lt;strong&gt;读写&lt;/strong&gt;指令，都会被&lt;strong&gt;mfence&lt;/strong&gt;后的&lt;strong&gt;读写&lt;/strong&gt;指令感知到(可见),这么说可能比较抽象,我们看下面的图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/pic/C++/mfence.png&quot; alt=&quot;image-20230622222630329&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了满足该正式定义其实很简单。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在mfence之前的读写代码，&lt;strong&gt;可以&lt;/strong&gt;乱序执行。&lt;/li&gt;
&lt;li&gt;在mfence之后的读写代码，&lt;strong&gt;可以&lt;/strong&gt;乱序执行。&lt;/li&gt;
&lt;li&gt;乱序执行的读写代码&lt;strong&gt;不可以&lt;/strong&gt;跨过mfence指令。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;只要满足上述四点就可以满足&lt;code&gt;mfence&lt;/code&gt;定义，这里不做证明，有兴趣的可以自己尝试着证明一下。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;lfence&lt;/strong&gt;，所有在lfence前的&lt;strong&gt;读&lt;/strong&gt;指令，都会被&lt;strong&gt;lfence&lt;/strong&gt;后的&lt;strong&gt;读&lt;/strong&gt;指令感知到(可见),这么说可能比较抽象,我们看下面的图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/pic/C++/lfence.png&quot; alt=&quot;image-20230622222630329&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了满足该正式定义。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在lfence之前的读写代码，&lt;strong&gt;可以&lt;/strong&gt;乱序执行。&lt;/li&gt;
&lt;li&gt;在lfence之后的读写代码，&lt;strong&gt;可以&lt;/strong&gt;乱序执行。&lt;/li&gt;
&lt;li&gt;乱序执行的&lt;strong&gt;读读&lt;/strong&gt;代码&lt;strong&gt;不可以&lt;/strong&gt;跨过lfence指令，乱序执行的读写，写读，写写代码可以跨过lfence指令。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;sfence&lt;/strong&gt;，所有在sfence前的&lt;strong&gt;写&lt;/strong&gt;指令，都会被&lt;strong&gt;sfence&lt;/strong&gt;后的&lt;strong&gt;写&lt;/strong&gt;指令感知到(可见),这么说可能比较抽象,我们看下面的图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/pic/C++/sfence.png&quot; alt=&quot;image-20230622222630329&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了满足该正式定义。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在sfence之前的读写代码，&lt;strong&gt;可以&lt;/strong&gt;乱序执行。&lt;/li&gt;
&lt;li&gt;在sfence之后的读写代码，&lt;strong&gt;可以&lt;/strong&gt;乱序执行。&lt;/li&gt;
&lt;li&gt;乱序执行的&lt;strong&gt;写写&lt;/strong&gt;代码&lt;strong&gt;不可以&lt;/strong&gt;跨过sfence指令，乱序执行的读写，写读，读读代码可以跨过sfence指令。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;C++的内存模型&lt;/h2&gt;
&lt;p&gt;我们按照难易度开始介绍C++的内存模型&lt;/p&gt;
&lt;h3&gt;RELAXED&lt;/h3&gt;
&lt;p&gt;这个内存模型是最宽松的内存模型（性能最好），也就是约束最小的，简单来说，它只保证原子操作，不会对CPU的乱序执行进行任何保证（编译乱序，乱序执行，只要有一个不保证，就是全不保证）。因此它适合计数功能，比如&lt;code&gt;shared_ptr&lt;/code&gt;的引用计数(在析构时不适用).&lt;/p&gt;
&lt;p&gt;一般使用的方式为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;atomic&amp;lt;int&amp;gt; ref;
ref.store(1, memory_order_relaxed);

atomic_thread_fence(memory_order_relaxed); // 这个语句没有任何用处
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;SEQUENTIALLY-CONSISTENT&lt;/h3&gt;
&lt;p&gt;这个内存模型是最严格的内存模型（性能最差）&lt;/p&gt;
&lt;h4&gt;Atomic operation&lt;/h4&gt;
&lt;p&gt;作为原子操作时，该模型除了保证原子操作外，它同时保证我们可以将各个线程的原子操作拿出来，然后确定一个执行顺序(即先执行线程A，再线程B，再线程A)。而这个顺序是全局确定的，即每一个线程看到的都是这个顺序。简单来说就是&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;代码实际执行时按照你写的顺序执行&lt;/li&gt;
&lt;li&gt;对内存修改读取的顺序，所有线程唯一。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这也是最符合直觉的内存模型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::atomic&amp;lt;bool&amp;gt; x,y;
std::atomic&amp;lt;int&amp;gt; z;
void write_x()
{
   x.store(true,std::memory_order_seq_cst);     // A
}
void write_y()
{
    y.store(true,std::memory_order_seq_cst);   // B
}
void read_x_then_y()
{
    while(!x.load(std::memory_order_seq_cst));   // C
    if(y.load(std::memory_order_seq_cst))		// E
        ++z;
}
void read_y_then_x()
{
    while(!y.load(std::memory_order_seq_cst));  // D
    if(x.load(std::memory_order_seq_cst))       // F
        ++z;
}
int main()
{
    x=false;
    y=false;
    z=0;
    std::thread a(write_x);
    std::thread b(write_y);
    std::thread c(read_x_then_y);
    std::thread d(read_y_then_x);
    a.join();
    b.join();
    c.join();
    d.join();
    assert(z.load()!=0); // always true!
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从这个代码中我们可以得知&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当C发生时,A一定已经发生&lt;/li&gt;
&lt;li&gt;当D发生时,B一定已经发生&lt;/li&gt;
&lt;li&gt;当E发生时,C, A一定已经发生&lt;/li&gt;
&lt;li&gt;当F发生时,D, B一定已经发生&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此若E,F都为false,说明A,B都未发生但C,D都已经发生.这与1,2矛盾.因此&lt;code&gt;assert&lt;/code&gt;一定为true.&lt;/p&gt;
&lt;h4&gt;Fence&lt;/h4&gt;
&lt;p&gt;当&lt;code&gt;std::memory_order_seq_cst&lt;/code&gt;用于&lt;code&gt;std::atomic_thread_fence&lt;/code&gt;等价于代码中插入&lt;code&gt;mfence&lt;/code&gt;,对于&lt;code&gt;mfence&lt;/code&gt;的定义可以看前面的描述.&lt;/p&gt;
&lt;h3&gt;ACQUIRE_RELEASE&lt;/h3&gt;
&lt;p&gt;这个内存模型的严格性居中（性能居中）&lt;/p&gt;
&lt;h4&gt;Atomic operation&lt;/h4&gt;
&lt;p&gt;这个模型没法再取得全局唯一的执行顺序了(即有可能线程A看到线程C的执行顺序是ABCD,但是线程B看到线程C的执行顺序为BDAC).
它只能通过&lt;code&gt;acquire&lt;/code&gt;和&lt;code&gt;release&lt;/code&gt;来进行同步线程.&lt;/p&gt;
&lt;p&gt;那么&lt;code&gt;acqurie&lt;/code&gt;与&lt;code&gt;release&lt;/code&gt;分别代表了什么样的语义呢?
&lt;img src=&quot;/pic/C++/acquire-release.png&quot; alt=&quot;image-20230622222630329&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从图中可以看到如果&lt;code&gt;acquire&lt;/code&gt;不允许后面的指令重排序越过该语句,而&lt;code&gt;release&lt;/code&gt;不允许前面的指令重排序越过该语句,因此他们两个一对很好的构成了临界区.是的,我们可以把&lt;code&gt;acquire&lt;/code&gt;看作&lt;code&gt;lock&lt;/code&gt;,把&lt;code&gt;release&lt;/code&gt;看作&lt;code&gt;unlock&lt;/code&gt;.
&lt;img src=&quot;/pic/C++/section.png&quot; alt=&quot;image-20230622222630329&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那么这两个怎么对线程取得同步呢?很简单,如果&lt;code&gt;acquire&lt;/code&gt;获得了&lt;code&gt;release&lt;/code&gt;存储的值,那么这两个线程就取得了同步.&lt;code&gt;release&lt;/code&gt;之前的所有内存操作都会被&lt;code&gt;acquire&lt;/code&gt;感知到.&lt;/p&gt;
&lt;p&gt;现在我们看下面的例子.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::atomic&amp;lt;bool&amp;gt; x,y;
std::atomic&amp;lt;int&amp;gt; z;
void write_x()
{
   x.store(true,std::memory_order_release);     // A
}
void write_y()
{
    y.store(true,std::memory_order_release);   // B
}
void read_x_then_y()
{
    while(!x.load(std::memory_order_acquire));   // C
    if(y.load(std::memory_order_acquire))		// E
        ++z;
}
void read_y_then_x()
{
    while(!y.load(std::memory_order_acquire));  // D
    if(x.load(std::memory_order_acquire))       // F
        ++z;
}
int main()
{
    x=false;
    y=false;
    z=0;
    std::thread a(write_x);
    std::thread b(write_y);
    std::thread c(read_x_then_y);
    std::thread d(read_y_then_x);
    a.join();
    b.join();
    c.join();
    d.join();
    assert(z.load()!=0); // maybe failed!
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从这个代码中我们可以知道在&lt;code&gt;SEQUENTIALLY-CONSISTENT&lt;/code&gt;的情况下,assert一定为true,但是在&lt;code&gt;acquire-release&lt;/code&gt;中,这个assert可能失败.&lt;/p&gt;
&lt;p&gt;尽管A和C同步,B和D同步. 但是E和B, F和A并不同步,他们仍然可能读到的是false.从而导致assert失败.(因为读到了线程本地的cache)&lt;/p&gt;
&lt;h4&gt;Fence&lt;/h4&gt;
&lt;p&gt;当&lt;code&gt;std::memory_order_acquire&lt;/code&gt;用于&lt;code&gt;std::atomic_thread_fence&lt;/code&gt;等价于禁止LOAD，STORE重排序到这条语句前，同时禁止LOAD语句重排序到这条语句后。&lt;/p&gt;
&lt;p&gt;当&lt;code&gt;std::memory_order_acquire&lt;/code&gt;用于&lt;code&gt;std::atomic_thread_fence&lt;/code&gt;等价于禁止LOAD，STORE重排序到这条语句后，同时禁止STORE语句重排序到这条语句前。&lt;/p&gt;
&lt;h3&gt;Overview&lt;/h3&gt;
&lt;p&gt;因为对于CPU的MESI协议等的理解还不够,因此这篇文章写的还比较浅显,等后面完全弄懂了CPU的缓存一致性协议再继续完成.&lt;/p&gt;
</content:encoded><category>C++</category><category>concurrency</category><author>tang-hi</author></item><item><title>Effective cpp</title><link>https://tangdh.life/posts/books/effective-cpp/</link><guid isPermaLink="true">https://tangdh.life/posts/books/effective-cpp/</guid><description>这篇文章是我对Effective Cpp的读书总结</description><pubDate>Mon, 15 May 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;h2&gt;1. 视C++为一个语言联邦&lt;/h2&gt;
&lt;p&gt;C++ 可以认为由&lt;code&gt;C&lt;/code&gt;, &lt;code&gt;Object-Oriented C++&lt;/code&gt;, &lt;code&gt;Template C++&lt;/code&gt;, &lt;code&gt;STL&lt;/code&gt;组成, 将他们分开看，这样子当写代码时，写到特定的领域，使用特定的写法。&lt;/p&gt;
&lt;h2&gt;2.尽量以const，enum，inline替代#define&lt;/h2&gt;
&lt;p&gt;使用#define定义的变量可能会宏展开，被编译器移走，从而从未进入符号表，这种情况下难以debug，而且也可能导致目标码变大，因为可能有多份数据。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对于常量我们使用&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;const double PI = 3.14;
const char* const NAME = &quot;Tang donghai&quot;;
const std::string NAME(&quot;Tang donghai&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;类专属的常量&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;class Const {
    static const int FOUR = 3; // 整数类型
   	constexpr static const char* NAME const = &quot;NAME&quot;; // non 整数类型, 或在实现文件中定义
}

// 如果需要取地址，需要在实现文件中加上
// const int Const::FOUR;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;一些简单的函数&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;#define CALL_WITH_MAX(a, b) f ((a) &amp;gt; (b) ? (a) : (b))

template &amp;lt;typename T&amp;gt;
inline void callWithMax(const T&amp;amp; a, const T&amp;amp; b) {
	f(a &amp;gt; b ? a : b);
} 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 尽可能使用const&lt;/h2&gt;
&lt;p&gt;如果一个变量，参数，函数不该产生变化，那么就使用const.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;const在星号左边表示所指的内容不可变，在星号右边表示指针不变。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;const int* a; // *a 不变
int* const a; // a 不变

const std::vector&amp;lt;int&amp;gt;::iterator iter;  =====&amp;gt; T* const // 配合typedef时尤其要注意。
std::vector&amp;lt;int&amp;gt;::const_iterator citer; =====&amp;gt; const T*
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;如果返回值是value, 最好加上const&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;Rational operator+ (Rational&amp;amp; a, Rational&amp;amp; b); // bad
(a + b) = c; // ok

const Rational operator+ (Rational&amp;amp; a, Rational&amp;amp; b); // good
(a + b) = c; // wrong!
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;如果成员函数不会被修改，那就应该声明为&lt;code&gt;const&lt;/code&gt;,const的函数可以被重载。&lt;/li&gt;
&lt;li&gt;如果想要取得逻辑不变性，可以对成员变量声明为&lt;code&gt;mutable&lt;/code&gt;，这样即使在&lt;code&gt;const&lt;/code&gt;函数中依旧可以修改。&lt;/li&gt;
&lt;li&gt;当既要实现const函数，又要实现非const函数版本&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;class TextBook {
  public:
    const char&amp;amp; operator[](std::size_t position) const {
        //...
        //...
        //...
        return text[position];
    }
    
    char&amp;amp; operator[](std::size_t position) {
        return const_cast&amp;lt;char&amp;amp;&amp;gt;(static_cast&amp;lt;const TextBook&amp;amp;&amp;gt;(*this)[position]);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 确保对象被使用前已被初始化&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;内置类型最好手动初始化&lt;/li&gt;
&lt;li&gt;成员变量初始化顺序为它的申明顺序，可以在申明的时候初始化。&lt;/li&gt;
&lt;li&gt;不同编译单元的non-local static 不保证初始化顺序。可以将其变为local-static放到函数里面，通过调用函数保证初始化&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;static Global global;
||
||
||
\/
Global&amp;amp; getGlobal() {
    static Global global;
    return global;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 了解C++默默编写并调用哪些函数&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;默认构造函数&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果用户没有提供&lt;/li&gt;
&lt;li&gt;成员变量都有默认构造函数/基类有默认构造函数&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拷贝构造函数&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果用户没有提供&lt;/li&gt;
&lt;li&gt;用户的基类，成员可被拷贝&lt;/li&gt;
&lt;li&gt;用户的基类，成员有析构函数&lt;/li&gt;
&lt;li&gt;用户并未定义提供移动构造函数，移动赋值函数。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拷贝赋值函数&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果用户没有提供&lt;/li&gt;
&lt;li&gt;类的成员都可被拷贝赋值即没有引用类型或者const修饰的非class类型。&lt;/li&gt;
&lt;li&gt;用户并未定义移动构造函数，移动赋值函数。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;移动构造函数&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户没有提供&lt;/li&gt;
&lt;li&gt;用户未定义，拷贝构造函数，移动赋值函数，拷贝赋值函数，析构函数&lt;/li&gt;
&lt;li&gt;非静态成员可被移动，基类可被移动，基类含有析构函数&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;移动赋值函数&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户没有提供&lt;/li&gt;
&lt;li&gt;用户未定义，拷贝构造函数，移动构造函数，拷贝赋值函数，析构函数&lt;/li&gt;
&lt;li&gt;非静态成员可被移动，基类可被移动，基类含有析构函数&lt;/li&gt;
&lt;li&gt;非静态成员没有引用类型，const类型&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;析构函数&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户没有提供&lt;/li&gt;
&lt;li&gt;非静态成员不可被析构。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;6. 若不想使用编译器自动生成的函数，就该明确拒绝&lt;/h2&gt;
&lt;p&gt;明确使用&lt;code&gt;= delete;&lt;/code&gt;将编译器生成的函数明确拒绝。&lt;/p&gt;
&lt;h2&gt;7. 为多态基类声明virtual析构函数&lt;/h2&gt;
&lt;p&gt;如果一个类有&lt;code&gt;virtual&lt;/code&gt;函数，那么你需要将析构函数声明为&lt;code&gt;virtual&lt;/code&gt;。否则的话，你可能造成内存泄漏，因为如果你delete &lt;code&gt;derived class&lt;/code&gt;,可能不会调用子类的析构函数。&lt;/p&gt;
&lt;h2&gt;8.别让异常函数逃离析构函数&lt;/h2&gt;
&lt;p&gt;如果析构函数中会抛出异常，很有可能在抛出一个异常后，再析构的时候又抛出异常，这样子程序会直接结束。&lt;/p&gt;
&lt;p&gt;如果可能抛出异常，应该将可能抛出异常的代码包装在一个函数中，由析构函数去调用它。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DBConn::~DBConn() {
    if (!closed) {
    	try {
        	db.close()
    	} catch(...) {
            
        }
    }
}

class DBConn {
    void close() {
        db.close();
        closed = true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;交给用户权利去调用&lt;code&gt;close&lt;/code&gt;,如果他们不去，依赖析构函数，那么析构函数吞下异常也应该是意料之中的行为。&lt;/p&gt;
&lt;h2&gt;9. 绝不在构造和析构过程中调用virtual函数&lt;/h2&gt;
&lt;p&gt;当你的类执行构造函数时，首先执行的是base的构造函数，而在这期间因为derived还未构造完成，因此你调用的virtual函数将会是base类的.析构函数同理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Base {
public:
	Base() {
		hello();    // error!!
	}
	virtual void hello();
};

class Derived {
public:
	Derived() {
		
	}
	void hello() override {
		///....
	}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;10. 令operator= 返回一个reference to *thiss&lt;/h2&gt;
&lt;p&gt;C++世界的默认规矩&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Widget&amp;amp; operator=(const Widget&amp;amp; rhs) {
	//....
	return *this;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;11. 在operator= 中处理“自我赋值”&lt;/h2&gt;
&lt;p&gt;需要考虑是否为同一个变量
思考以下代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Widget&amp;amp;
Widget::operator=(const Widget&amp;amp; rhs) {
	delete rhs.xxx; // bad!!!!
	pb = new XXX(*rhs.xxx);
	return *this;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要考虑是否为同一个，可以使用以下方式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Widget&amp;amp;
Widget::operator=(const Widget&amp;amp; rhs) {
	if (this == &amp;amp;rhs) return *this;
	delete rhss.xxx;           // ok
	pb = new XXX(*rhs.xxx);
	return *this;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者采用copy-swap&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Widget&amp;amp;
Widget::operator=(const Widget&amp;amp; rhs) {
	Widget temp(rhs);
	swap(temp);
	return *this;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;12. 复制对象时勿忘记其每一个成分&lt;/h2&gt;
&lt;p&gt;没什么好说的，复制时不要忘记就好！子类不要忘记父类！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Derived{
public:
	Derive(const Derived&amp;amp; derived) : Base(derived), xxx(xxx) {}
	Derived&amp;amp; operator=(const Derived&amp;amp; derived) {
		//..........
		Base::operator=(derived);
		//..........
	}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;13.  以对象管理资源&lt;/h2&gt;
&lt;p&gt;使用RAII的方式进行管理，同时注意条款8,在管理资源时别让异常逃出异构函数&lt;/p&gt;
&lt;h2&gt;14. 在资源管理类中小心copying行为&lt;/h2&gt;
&lt;p&gt;复制RAII对象时，必须一并复制它所管理的资源，所以资源的copying行为决定RAII对象的行为&lt;/p&gt;
&lt;p&gt;一般而言，我们对RAII对象会采取如下方式&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;禁止copy  mutex&lt;/li&gt;
&lt;li&gt;采用引用计数，当计数变为0时，释放资源 shared_ptr&lt;/li&gt;
&lt;li&gt;转移资源 unique_ptr&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;15. 在资源管理类中提供对原始资源的访问&lt;/h2&gt;
&lt;p&gt;一般而言，我们有两种做法&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;显示提供get接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A {
    data_ptr* get() const;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;提供隐式转换接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A {
    operator B() const;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;隐式转换接口，增加了误用的概率，尽管相比于显式更加自然。我更倾向于显示的接口。&lt;/p&gt;
&lt;h2&gt;16. 成对使用new 和 delete时要采取相同形式&lt;/h2&gt;
&lt;p&gt;被&lt;code&gt;new&lt;/code&gt;出来的对象,要使用&lt;code&gt;delete&lt;/code&gt;删除，被&lt;code&gt;new []&lt;/code&gt;出来的对象，要使用&lt;code&gt;delete []&lt;/code&gt;删除。&lt;/p&gt;
&lt;h2&gt;17. 以独立语句将newed对象置入智能指针&lt;/h2&gt;
&lt;p&gt;考虑以下的函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;process(std::shared_ptr(new Widget), processor());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于这样的语句，编译器可以任意决定执行顺序，只要new  Widget在shared_ptr的构造函数前执行就行。&lt;/p&gt;
&lt;p&gt;因此，我们可以以下顺序&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;new Widget&lt;/li&gt;
&lt;li&gt;processor()&lt;/li&gt;
&lt;li&gt;shared_ptr&apos;s ctor&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果2抛了异常，我们就面临内存泄漏的问题。&lt;/p&gt;
&lt;p&gt;因此为了保证异常安全，我们应该以独立的语句将new对象放入智能指针。&lt;/p&gt;
&lt;p&gt;即&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auto p = std::shared_ptr(new Widget);
process(p, processor());
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;18. 让接口容易被正确使用，不易被误用&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;不易被误用，这需要加许多限制(最好是编译器的限制)。&lt;/li&gt;
&lt;li&gt;接口最好与内置类型保持一致性。&lt;/li&gt;
&lt;li&gt;使用条款13, 以对象管理资源。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;19. 设计class犹如设计type&lt;/h2&gt;
&lt;p&gt;假设你将为系统中引入一个新的type来设计class，应该如何被创建和销毁，对象的初始化和赋值有什么差别....&lt;/p&gt;
&lt;h2&gt;20.宁以pass-by-reference-to-const 替换 pass-by-value&lt;/h2&gt;
&lt;p&gt;这条本义是减少拷贝，但是考虑到rvo机制，也许不一定需要如此，对于内置类型，可能pass-by-value性能更好。&lt;/p&gt;
&lt;h2&gt;21. 必须返回对象时，别忘想返回reference&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const A operator*(const A* lhs, const A* rhs) { // fine copy it
    // 
    return a;
}

//------------------------------------------
const A&amp;amp; operator*(const A* lhs, const A* rhs) {
    A = lhs * rhs;
    return A;           // error! dangling reference!
}
//------------------------------------------
const A&amp;amp; operator*(const A* lhs, const A* rhs) {
    static A a;
    a = ///...
    return a; /// error!!!!
}

auto a = a1 * a2;
auto b = a * a2;
a == b // true!
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;22. 将成员变量声明为private&lt;/h2&gt;
&lt;p&gt;将成员声明为private，从而保证了封装以及日后随时修改的权利&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;封装性是当你删去该代码时，所影响的代码量。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;以这个评判角度来看，public(所有使用的代码)和protected（所有继承的代码）有着一样的封装性&lt;/p&gt;
&lt;p&gt;因此尽可能将成员变量声明为private&lt;/p&gt;
&lt;h2&gt;23. 宁以non-member non-friend 替换member函数&lt;/h2&gt;
&lt;p&gt;和条款22一样，当我们采用member函数/friend函数，意味着我们增加了一个函数可以访问private的成员变量，这就意味着我们的代码封装性下降了（更多的代码可以访问private成员了）。&lt;/p&gt;
&lt;p&gt;因此如果可以的话，使用non-member non-friend替换member函数，同时将同一个类的non-member函数分类存放在不同的头文件中。减少编译依赖。&lt;/p&gt;
&lt;p&gt;如果想将一个member函数转化为非member函数，不要先考虑变为friend函数，因为这两个封装性一致。要考虑转化为non-member函数。&lt;/p&gt;
&lt;h2&gt;24. 若所有参数皆需类型转换，请为此采用non-member函数。&lt;/h2&gt;
&lt;p&gt;考虑一个乘法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const Rational operator*(const Rational&amp;amp; lhs, const Rational&amp;amp; rhs); // 1


const Rational operator*(const Rational&amp;amp; rhs); // 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1 比 2好，因为两种参数都可以进行隐式转换。&lt;/p&gt;
&lt;h2&gt;25. 考虑出写出一个不抛异常的swap函数。&lt;/h2&gt;
&lt;p&gt;首先swap函数不应当抛出异常，因为如果你想要写出异常安全的代码，很大程度上你要依赖swap函数，因此不要写出会抛出异常的代码&lt;/p&gt;
&lt;p&gt;怎么自定义高效的swap函数？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class Efficient {
public:
    void swap(Efficient&amp;amp; a) noexcept {
        // efficient
    }
    
    
};

template &amp;lt;typename T&amp;gt;
void swap(Efficient&amp;lt;T&amp;gt;&amp;amp; lhs, Efficient&amp;lt;T&amp;gt;&amp;amp; rhs) {
    lhs.swap(rhs);
}

namespace std {
    template&amp;lt;&amp;gt;
    void swap&amp;lt;Widget&amp;gt;(Widget&amp;amp; lhs, Widget&amp;amp; rhs) {
        
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自定义高效的swap函数&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;定义public的成员函数，实现具体逻辑&lt;/li&gt;
&lt;li&gt;定义non-member的模板函数，调用成员函数。&lt;/li&gt;
&lt;li&gt;如果你定义的不是class template，而是class，可以全特化std中的swap。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;26. 尽可能延后变量定义式的出现时间&lt;/h2&gt;
&lt;p&gt;尽可能仅在必要时定义你所需要的变量，尤其是class具有constructor的成本，防止无意义的构造成本。&lt;/p&gt;
&lt;h2&gt;27. 尽量少做转型动作&lt;/h2&gt;
&lt;p&gt;尽量少做转型动作，这并不是没有代价的，很有可能会产生对应的汇编代码。&lt;/p&gt;
&lt;p&gt;如果转型也尽量使用新式的转型&lt;code&gt;static_cast&lt;/code&gt; &lt;code&gt;dynamic_cast&lt;/code&gt;....&lt;/p&gt;
&lt;h2&gt;28. 避免返回handles指向对象内部成分&lt;/h2&gt;
&lt;p&gt;避免将内部private的函数通过引用，指针等方式泄露出去，有时我们必须这么干，如果不想用户可以更改它，将返回值加上const的限制。并且保证handle的生命周期一直有效。&lt;/p&gt;
&lt;h2&gt;29. 为&quot;异常安全&quot;而努力是值得的&lt;/h2&gt;
&lt;p&gt;时刻保证即使抛出异常，各成员，class也处于有效的合法的状态（基本保证）&lt;/p&gt;
&lt;p&gt;强烈保证（要么调用前，要么成功）&lt;/p&gt;
&lt;p&gt;使用智能指针控制new的内存，copy and swap机制来保证。&lt;/p&gt;
&lt;h2&gt;30. 透彻了解inlining的里里外外&lt;/h2&gt;
&lt;p&gt;仅将inline加在短小的函数中，被频繁调用的函数。&lt;/p&gt;
&lt;h2&gt;31. 将文件间的编译依存关系降至最低&lt;/h2&gt;
&lt;p&gt;现在还没什么体会。&lt;/p&gt;
&lt;h2&gt;32. 确定你的public继承塑模出is-a关系&lt;/h2&gt;
&lt;p&gt;适用于base class身上的每一件事情一定也适用于derived class身上，因为每一个derived class对象也都是一个base class对象。这个可能需要后面的体会。&lt;/p&gt;
&lt;h2&gt;33. 避免遮掩继承而来的名称&lt;/h2&gt;
&lt;p&gt;如果你有一个base class&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Base {
 public:
    void mf1();
    void mf1(int x);
    void mf1(int x, int y);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你想写一个&lt;code&gt;derived&lt;/code&gt;class，并且重新override一部分函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Derived : public Base{
  public:
    void mf1();
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这样就掩盖了Base class的其他mf1的函数了，如果你仍然想要使用Base class的mf1函数，那么使用&lt;code&gt;using&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Derived : public Base {
  public:
    using Base::mf1;         // use this!!
    void mf1();
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是如果你只想继承部分的基类函数(例如private 继承)，那么你需要使用forward function&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Derived : private Base {
    void mf1() {           // 名称掩盖
        Base::mf1();      // 内部使用Base
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;34. 区分接口继承和实现继承&lt;/h2&gt;
&lt;p&gt;继承分为继承&lt;code&gt;成员函数的接口&lt;/code&gt;以及&lt;code&gt;成员函数的实现&lt;/code&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;当你声明一个函数为&lt;code&gt;pure virtual&lt;/code&gt;,说明你只希望他们继承接口，而不是实现。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当你声明一个函数为&lt;code&gt;virtual&lt;/code&gt;，说明你希望他们继承接口，同时提供一份默认实现。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当你将一个函数声明为&lt;code&gt;non virtual&lt;/code&gt;时，说明你希望他们继承接口，但是接受一个强制的实现。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但是有时候，我们会担心后续开发者，忘记修改默认的&lt;code&gt;virtual&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Airplane {
public:
    virtual void fly() = 0;
protected:
    void defaultFly();
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;缺省实现放在defaultFly函数中，同时将fly设置为pure virtual,这样就可以防止后续开发者忘记实现&lt;code&gt;fly&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;35.考虑virtual函数以外的其他选择&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;virtual&lt;/code&gt;函数的一些替换方案是&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用函数指针，由调用者决定不同的表现形式&lt;/li&gt;
&lt;li&gt;使用NVI，即public的&lt;code&gt;non-virtual&lt;/code&gt;函数，调用private 的 &lt;code&gt;virtual&lt;/code&gt;函数。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;36.绝不重新定义继承而来的non-virtual函数&lt;/h2&gt;
&lt;p&gt;不要定义继承而来的non-virtual函数，第一这违反oop原则，其次调用者可能会错误使用，例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Base {
    void mf1();
};


class Derived : public Base{
    
};

D x;
B* b = &amp;amp;x;
D* d = &amp;amp;d;
b-&amp;gt;mf1(); // diff if you derived
d-&amp;gt;mf1();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;37. 绝不重新定义继承而来的缺省参数值&lt;/h2&gt;
&lt;p&gt;因为参数缺省定义是静态绑定的，这个和&lt;code&gt;virtual&lt;/code&gt;函数相反，&lt;code&gt;virtual&lt;/code&gt;函数是动态的绑定的。因此如果你重新定义继承而来的缺省参数，从而导致一个错误的情况。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Base {
    virtual void hello(int a = 1);
};

class Derived {
    virtual void hello(int a = 2);  // ooooops! 
}；

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;38. 通过复合塑造出has-a 或&quot;根据某物实现出&quot;&lt;/h2&gt;
&lt;p&gt;继承是is-a关系，而复合是has-a，你并不一定需要继承它的接口，那么你可以使用复合的方式在内部将该对象设置为成员变量，通过该对象的调用完成。&lt;/p&gt;
&lt;h2&gt;39. 明智而审慎的使用private继承&lt;/h2&gt;
&lt;p&gt;private继承意味着并不会在引用时自动转换，同时所有继承而来的成员变量以及函数都是private类型的。&lt;/p&gt;
&lt;p&gt;这意味着你并不想继承函数定义，你只是想要它的部分实现，这很类似于复合的方式。&lt;/p&gt;
&lt;p&gt;但是选取private而不是复合的原因是因为涉及到&lt;code&gt;virtual&lt;/code&gt;函数以及部分protected的成员变量。&lt;/p&gt;
&lt;p&gt;当没有更好的办法时，private是个好方法。&lt;/p&gt;
&lt;h2&gt;40.明智而审慎的使用多重继承&lt;/h2&gt;
&lt;p&gt;使用多重继承，会非常复杂，而且更可能增加名称冲突的概率，而如果是菱形继承那么，你可能需要virtual继承消除多个成员变量的重复值。&lt;/p&gt;
&lt;p&gt;而你最应该的使用的使用public继承接口，然后用private继承继承实现部分。&lt;/p&gt;
&lt;h2&gt;41. 了解隐式接口和编译器多态&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;class&lt;/code&gt;和&lt;code&gt;template&lt;/code&gt;都支持接口和多态。对&lt;code&gt;class&lt;/code&gt;而言接口是显式的，而且多态要通过virtual来保证。&lt;/p&gt;
&lt;p&gt;而&lt;code&gt;template&lt;/code&gt;则是隐式的，而且编译期就可以实现多态&lt;/p&gt;
&lt;h2&gt;42. 了解typename的双重意义&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;用于在template指定模板形参。&lt;/li&gt;
&lt;li&gt;用于指定类内一些嵌套的类型名称。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;43. 学习处理模板化基类内的名称&lt;/h2&gt;
&lt;p&gt;如果我们继承一个模板类，我们想要调用基类继承而来的成员函数，可能会遇到麻烦&lt;/p&gt;
&lt;p&gt;假设以下的代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class Base {
public:
   void hello();  
};

template&amp;lt;typename T&amp;gt;
class Derived : public Base&amp;lt;T&amp;gt; {
public:
    void hello2() {
        hello();   // error! couldn&apos;t find it!
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之所以会出现这样的原因是，编译器不确定你是不是会全特化&lt;code&gt;Base&lt;/code&gt;class，全特化可能不实现成员函数了。因此，他对你继承的&lt;code&gt;template class&lt;/code&gt;不会做任何假设。比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;&amp;gt;
class Base&amp;lt;int&amp;gt; {
    public:
    void yes();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就没有hello函数了，对此我们可以有以下三种方式解决&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class Derived : public Base&amp;lt;T&amp;gt; {
    public:
    void hello2() {
        this-&amp;gt;hello();     // 假设hello可以被调用
    }
};

template&amp;lt;typename T&amp;gt;
class Derived : public Base&amp;lt;T&amp;gt; {
    public:
    using Base&amp;lt;T&amp;gt;::hello; // 告诉编译器，可以从Base中寻找该定义。揭露出命名。
    void hello2() {
        hello();     
    }
};

template&amp;lt;typename T&amp;gt;
class Derived : public Base&amp;lt;T&amp;gt; {
    public:
    
    void hello2() {
        Base&amp;lt;T&amp;gt;::hello();      // 指定hello的应用，但是这样子就会丧失多态性，因为不是用this调用的
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;44. 将于参数无关的代码抽离template&lt;/h2&gt;
&lt;p&gt;如果&lt;code&gt;template&lt;/code&gt;与参数无关，那么我们应该抽离，考虑如下函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, size_t n&amp;gt;
class Base {
    
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对这种代码，不同的n会生成不同的模板代码，因此我们需要将n与T分割开&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, size_t n&amp;gt;
class BaseV2 : public BaseV1&amp;lt;T&amp;gt; {
    
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;45.运用成员函数模板接受所有兼容类型&lt;/h2&gt;
&lt;p&gt;考虑shared_ptr,我们希望可以通过&lt;code&gt;shared_ptr&amp;lt;Bottom&amp;gt;&lt;/code&gt;初始化构造&lt;code&gt;shared_ptr&amp;lt;Up&amp;gt;&lt;/code&gt;，但是如果我们这样子写的话&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class shared_ptr {
  shared_ptr(const shared_ptr&amp;lt;T&amp;gt;&amp;amp; other);  
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样只能够&lt;code&gt;shared_ptr&amp;lt;Up&amp;gt;&lt;/code&gt;初始化构造&lt;code&gt;shared_ptr&amp;lt;Up&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;所以我们使用范化的构造函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class shared_ptr {
    template&amp;lt;typename U&amp;gt;
    shared_ptr(const shared_ptr&amp;lt;U&amp;gt;&amp;amp; other);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样子我们得到了很多的构造函数，超过了我们的要求，甚至可以用&lt;code&gt;shared_ptr&amp;lt;double&amp;gt;&lt;/code&gt;初始化构造&lt;code&gt;shared_ptr&amp;lt;Up&amp;gt;&lt;/code&gt;,为了对此加以限制。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class shared_ptr {
    template&amp;lt;typename U&amp;gt;
    shared_ptr(const shared_ptr&amp;lt;U&amp;gt;&amp;amp; other) : 
    data(other.get()) // add some restriction
    {}
    
    T* get();
    T* data;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过上述手法加以限制后，我们可以确定只有U可以隐式的转化为T时，我们才可以做成这样的事情。&lt;/p&gt;
&lt;p&gt;注意我们这里并没有加上explicit，因为指针的隐式转化是被允许的，因此shared_ptr也被允许隐式转化。&lt;/p&gt;
&lt;p&gt;同时注意泛化的成员模板函数，并不会对原来的生成规则产生影响，你可以将其视为一个普通的成员函数，而不是特殊的构造函数。&lt;/p&gt;
&lt;h2&gt;46. 需要类型转换时请为模板定义非成员函数&lt;/h2&gt;
&lt;p&gt;考虑以下代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class NumberType {
	NumberType(T val); 
};	

template&amp;lt;typename T&amp;gt;
const NumberType&amp;lt;T&amp;gt; operator*(const NumberType&amp;lt;T&amp;gt;&amp;amp; lhs, const NumberType&amp;lt;T&amp;gt;&amp;amp; rhs) {
    //....
} 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们调用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NumberType&amp;lt;int&amp;gt; a;
a * 3;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样是不会调用成功的，第二个参数也无法隐式转化。因为C++会先进行template推倒，再实例化，因此你需要将其声明为friend并提供定义&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class NumberType {
	NumberType(T val);
    friend
    const NumberType&amp;lt;T&amp;gt; operator*(const NumberType&amp;lt;T&amp;gt;&amp;amp; lhs, const NumberType&amp;lt;T&amp;gt;&amp;amp; rhs) {
    //....
	}     
};	
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样子在你声明&lt;code&gt;NumberType&amp;lt;int&amp;gt;&lt;/code&gt;时，就会实例化该friend函数，在你调用时就可以直接引进类型转化了。&lt;/p&gt;
&lt;h2&gt;47. 请使用traits classes表明类型信息&lt;/h2&gt;
&lt;p&gt;即类内根据std的规则&lt;code&gt;typedef&lt;/code&gt;一定的东西&lt;/p&gt;
&lt;h2&gt;48. 认识template元编程&lt;/h2&gt;
&lt;p&gt;nothing to say&lt;/p&gt;
&lt;h2&gt;49. 了解new-handler的行为&lt;/h2&gt;
&lt;p&gt;new-handler可以让你在内存无法分配至，指定一个函数，让其被调用。&lt;/p&gt;
&lt;h2&gt;50. 了解new 和 delete的合理替换时机&lt;/h2&gt;
&lt;p&gt;当你需要log，检查bug，测试性能等原因时，可以自定义new delete&lt;/p&gt;
&lt;h2&gt;51. 编写new和delete时要固守常规&lt;/h2&gt;
&lt;p&gt;例如，当用户需要new 0 byte时，需要返回1byte，或者如果无法分配内存就需要调用new handler等&lt;/p&gt;
&lt;h2&gt;52. 写了placement new 也要写placement delete&lt;/h2&gt;
&lt;p&gt;placement new是指定一个地方调用构造函数，new这个操作符&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;调用operator new 申请内存&lt;/li&gt;
&lt;li&gt;指定位置上调用构造函数&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;53. 不要轻忽编译器的警告&lt;/h2&gt;
&lt;h2&gt;54. 让自己熟悉标准库&lt;/h2&gt;
&lt;h2&gt;55. 让自己熟悉Boost&lt;/h2&gt;
</content:encoded><category>C++</category><category>Book</category><author>tang-hi</author></item><item><title>PQIVF(Prodcut Quantization)</title><link>https://tangdh.life/posts/vector-search/pqivf/</link><guid isPermaLink="true">https://tangdh.life/posts/vector-search/pqivf/</guid><description>Product Quantization是一种用于向量量化的方法，由Hervé Jégou和Olivier Chum于2011年在论文&lt;&lt;Product quantization for nearest neighbor search&gt;&gt;中首次提出</description><pubDate>Wed, 05 Apr 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;Product Quantization是一种用于向量量化的方法，由Hervé Jégou和Olivier Chum于2011年在论文&lt;a href=&quot;https://lear.inrialpes.fr/pubs/2011/JDS11/jegou_searching_with_quantization.pdf&quot;&gt;《Product quantization for nearest neighbor search》&lt;/a&gt;中首次提出。这篇论文解决了在大规模数据集上进行最近邻搜索的问题。相较于其他的ANN搜索算法通过牺牲一定的内存空间来减少搜索空间，PQ通过对向量进行量化压缩，使得所需要的内存空间大幅减小。&lt;/p&gt;
&lt;h3&gt;What is Product Quantization?&lt;/h3&gt;
&lt;p&gt;如果想要搞清楚PQ，那么首先得明白 &lt;strong&gt;Quantization&lt;/strong&gt; 的含义，Quantization即量化，这是一个从信号处理领域来的名词，按照Wikipedia的定义&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Quantization is the process of constraining an input from a continuous or otherwise large set of values to a discrete set (such as the integers).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;量化就是将一个集合映射到另一个离散的集合之中，那么PQ实际上就是将数据集中的每一个向量映射到另一个集合中，比如说整数集合。即&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/PQ1.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;通过这种方式我们可以用一个INT来表示整个向量，也就是说如果向量原来的维数为128.那么单个向量的内存占用就会从128 * 4 byte (float) 降低为 4 byte(int), 内存可以减少&lt;strong&gt;128&lt;/strong&gt;倍。&lt;strong&gt;但是量化要求我们的映射为满射，即被映射的集合中任一元素，原始集合中至少存在一个元素与之对应&lt;/strong&gt;。也就是说如果我们将向量映射到整个&lt;code&gt;LONG&lt;/code&gt;空间，我们会有&lt;code&gt;LONG_MAX * 8 byte&lt;/code&gt; 即&lt;strong&gt;18,446GB&lt;/strong&gt;的内存占用，这显然是不可接受的。那如果我们仅将向量映射到&lt;code&gt;UINT8&lt;/code&gt;,我们会有 &lt;code&gt;256 * 2 byte&lt;/code&gt; 即&lt;strong&gt;512B&lt;/strong&gt;的内存占用，内存问题看似解决了。但是如果我们数据集中有100万个向量，我们将其映射到0-255，平均一个整数有3906个向量与其对应，我们在搜索时无法区分这3906个向量与待查询向量之间的距离差别，这就会导致糟糕的召回率。因此选择一个合适的映射空间大小，使得内存占用以及召回率达到一个sweet point，这就是PQ所要达成的效果。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;请注意，Product Quantization并不是通过降维来减少空间大小。&lt;/p&gt;
&lt;p&gt;在Product Quantization中量化后向量的数量仍然保持不变。但是，压缩后的向量值现在被转换为一个小整数，你可以认为这个整数仅仅只是一个符号。通过将高维向量转化为一个符号，从而减少了向量的内存空间占用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;How Product Quantization Work?&lt;/h3&gt;
&lt;h3&gt;1. Split And Train&lt;/h3&gt;
&lt;p&gt;假设向量的维数为128,我们首先将该向量平均分为8份，每一个的维数为16。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/PQ2.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;如果我们有1000个向量，我们将每一份向量平均分为8份，V0, V1.... V7。那么我们会有1000个V0, 1000个V1,...1000个V7。我们分别对他们进行&lt;strong&gt;K-means&lt;/strong&gt;聚类，同时假定我们选定K-means的&lt;strong&gt;K&lt;/strong&gt;为256。那么我们在每一个Vi中会得到256个centroids。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/PQ3.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h3&gt;2. Encode&lt;/h3&gt;
&lt;p&gt;在得到centroids后，我们为每一个向量进行编码，我们首先分别对V0,V1,....V7进行编码，编码的规则相当简单。我们拿V0进行举例，首先我们遍历V0中每一个向量，并且选择距离当前向量最近的一个centroids作为其对应的编码值，如果向量A距离centroids42最近，那么A的编码值即为42。在完成V0,V1....V7的编码值后，我们将其拼接起来就会得到所有向量的编码值。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/PQ4.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h4&gt;Summary&lt;/h4&gt;
&lt;p&gt;目前PQ的构建部分已经讲完了，我们现在重新审视一下PQ这个算法，首先考虑PQ的映射空间大小，尽管我们单个Vi只有256个质心，也就是说无论向量有多少，它们只会映射到0-255,但是我们通过对向量进行split，使得整体的映射空间大小为 $256^M$ (M为向量被split的份数), 从而极大的扩充了映射空间的大小，那我们再考虑内存占用的大小，V0占用的空间大小为$N * 2 byte$ 整体的空间大小为$M * N * 2 byte$, 相比于原始的大小$ N * D * 4bytes$极大的减小了内存占用(D 为向量的维数)。因此PQ在保证了内存占用低的同时提供了一个较大的映射空间，从而保证了召回率。&lt;/p&gt;
&lt;h3&gt;3. Search&lt;/h3&gt;
&lt;p&gt;当我们已经对数据库中的所有向量都已经进行了量化，我们需要在用户给定任意向量后，从数据库中寻找与该向量最相似的N个向量，首先我们仍旧对query进行split。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/PQ5.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意我们不会对Query进行量化，仅仅只做Split，原因是这样子可以在距离计算时保证更高的精度，详情可以参考论文原文&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在Split后，我们会计算对应的Split部分与每一个centroids的距离，并将该距离进行保存。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/PQ6.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;随后我们遍历数据库中所有的向量，通过查表分别计算出Query中每一段与他们之间的距离，随后相加得到最终的距离，并最终选择距离最近的K个向量。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/PQ7.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h3&gt;4. IVF&lt;/h3&gt;
&lt;p&gt;尽管PQ大幅减少了我们所需要的内存，但是在搜索时，我们仍然需要与数据集中的每一个向量计算他们之间的距离，因此我们希望可以在保证召回率的同时，减少计算量。为此我们引入了IVF(Inverted File Index), 首先我们先对数据集中的向量进行k-means聚类，从而将数据集划分为k个小数据集，同时计算数据集中的向量与其对应的质心之间的残差，并对残差进行PQ压缩。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/PQ8.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
需要注意的是每一个centroid后面都会挂载属于这个分区的向量，但这向量不是数据集中的原始向量，而是原始向量与centroid的残差。之所以选择残差是因为这样可以降低数据集中的方差，从而在进行ANN搜索时，距离计算的误差也会更小，从而产生更好的搜索质量。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/PQ9.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;可以认为我们实际上是将整个数据集划分为几个更小的数据集后，再对每一个分区进行PQ压缩。当我们搜索时，我们会选择&lt;code&gt;probe&lt;/code&gt;个距离最近的分区(通过计算与centroid之间的距离决定),同样的计算Query与对应centroid的残差，并使用这个残差在该分区进行PQ搜索，在完成所有分区的搜索后，返回距离最近的K个向量。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/PQ10.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;这里之所以选择&lt;code&gt;probe&lt;/code&gt;个距离最近的分区而不是选择最近的那一个分区，是因为作者在论文中提到&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The query vector and its nearest neighbors are often not quantized to the same partition centroid, but to nearby ones&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此为了召回率我们会扩大一部分的搜索范围，因此probe的数量也体现了召回率与搜索速度之间的tradeoff。&lt;/p&gt;
&lt;h3&gt;5. Summary&lt;/h3&gt;
&lt;p&gt;PQ通过对向量进行量化，降低向量搜索时所使用的内存，同时通过对向量分段量化，扩展映射空间。而IVF在PQ的基础上进一步减少搜索空间，从而降低了搜索时间。&lt;/p&gt;
</content:encoded><category>vector search</category><author>tang-hi</author></item><item><title>HNSW (Hierarchical Navigable Small World)</title><link>https://tangdh.life/posts/vector-search/hnsw/</link><guid isPermaLink="true">https://tangdh.life/posts/vector-search/hnsw/</guid><description>HNSW是通过图的方式来解决向量搜索问题的算法，由Y.Malkov与D.Yashunin首次提出</description><pubDate>Sat, 25 Mar 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;HNSW是通过图的方式来解决向量搜索问题的算法，由Y.Malkov与D.Yashunin在&lt;a href=&quot;https://arxiv.org/pdf/1603.09320.pdf&quot;&gt;论文&lt;/a&gt;中首次提出。&lt;/p&gt;
&lt;p&gt;这一个Section安排如下&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;图拥有什么样的性质可以有效的找到最近的K个向量&lt;/li&gt;
&lt;li&gt;NSW(Navigable Small World)&lt;/li&gt;
&lt;li&gt;HNSW(Hierarchical Navigable Small World)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1. 图的性质&lt;/h3&gt;
&lt;p&gt;我们先直观的感受一下使用图的方式来表现向量空间。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/raw_vector.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;图中的点代表向量，我们可以看到，如果两个向量的距离较近，那么在图中这两个点之间的距离也会更近。当我们想要通过图的方式来解决向量搜索时，我们会希望从任一点出发可以到达图中其他所有的点，即这个图是一张联通图。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/compare_vec.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;但仅仅只是联通图，仍然无法做到快速有效的找到距离最近的K个向量。考虑如下的情况，A点与B点之间相隔较远，因此如果想要从A点到达B点需要途经许多点(代表着大量的计算)，同时我们可以看到点C与许多其他的点都有连接，因此如果我们从点C开始寻找距离查询向量最近的K个点，我们会计算大量无关的点(因为与点C相连的点，其中很多大概率是与结果无关的)。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/two_conn.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;综上所述，为了可以高效而准确的找到距离查询向量最近的K个向量。我们希望构建的图有以下几个性质&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;联通图(没有孤岛)&lt;/li&gt;
&lt;li&gt;距离较远的点,有边可以相连(long range edge)&lt;/li&gt;
&lt;li&gt;构建的图中边的数量不宜过多(大量的计算)&lt;/li&gt;
&lt;li&gt;距离相近的点，有边连接（保证召回率）
其中3,4是召回率与计算量的tradeoff。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2. NSW&lt;/h3&gt;
&lt;p&gt;NSW通过有效且简单的算法构建出满足上述要求的图，下面分别从构建以及查询两个方面来介绍NSW算法。&lt;/p&gt;
&lt;h4&gt;1. 构建&lt;/h4&gt;
&lt;p&gt;首先我们将通过随机的方式，将向量一个一个添加到图中，每一个新添加的点都会与当前图中距离该点最近的&lt;strong&gt;M&lt;/strong&gt;个点相连。之所以通过&lt;strong&gt;M&lt;/strong&gt;对相连的点的数量进行限制，是为了防止连接的边过多，从而影响查询效率。&lt;/p&gt;
&lt;p&gt;我们通过一个例子来描述构建的过程，假设我们将&lt;strong&gt;M&lt;/strong&gt;设置为3，并且已经将待加入的向量随机打乱。&lt;/p&gt;
&lt;p&gt;首先，添加点A，因为当前图中没有其他任何的点，所以我们只需要添加A，而不用作任何其他的操作。后面我们继续添加点B，此时图中只有点A，点的个数小于3,因此我们可以直接将两者相连。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/addB.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;类似的我们向图中加入点C，点D，我们会获得以下的图&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/addCD.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;随后我们继续添加点E，此时我们会找到当前图中距离点E最近的&lt;strong&gt;M&lt;/strong&gt;个点，即A，B，C并将其相互连接。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/addE.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;用相同的方式，我们继续添加点F，G，H，最终得到的图如下所示。&lt;/p&gt;
&lt;p&gt;我们逐个检查NSW图是否满足我们之前要求的性质&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;联通图，显而易见这是一张联通图&lt;/li&gt;
&lt;li&gt;距离较远的点有边可以相连，我们可以发现因为随机添加，最开始认为距离较近的点，比如A，D，随着添加的点越来越多，A，D相连的这条边成为了一条long range边。&lt;/li&gt;
&lt;li&gt;构建的图中，边的数量不宜过多。这一条因为我们始终用&lt;strong&gt;M&lt;/strong&gt;控制边的数量，所以也可以满足&lt;/li&gt;
&lt;li&gt;距离较近的点，有边连接。因为我们始终与距离最近的&lt;strong&gt;M&lt;/strong&gt;条边相连，因此也满足了该要求。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/addFH.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;因此，我们只需要随机添加向量，并且在随机添加的过程中，与当前最近的&lt;strong&gt;M&lt;/strong&gt;个点相连，我们就可以构建出一幅可以高效的进行ANN查询的图。下面我们讨论搜索的过程。&lt;/p&gt;
&lt;h4&gt;2. 搜索&lt;/h4&gt;
&lt;p&gt;因为我们通过NSW构建出的图,具有良好的特性,因此我们只需要使用简单的贪心算法就可以获得较好的搜索结果。在给定了一个query point后&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;我们在图中随机的选择一个点作为出发点(entry point)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我们计算每一个与该点相连的点，选出最近的一个点。&lt;/p&gt;
&lt;p&gt;a. 若该点即为entry point,搜索结束,返回entry point。&lt;/p&gt;
&lt;p&gt;b. 若该点不为entry point, 设置该点为entry point, 重复过程2&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下图为搜索的示意图,我们可以看到因为有long range，这一高速通道的存在，我们可以快速搜索到结果。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/nsw-search.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h3&gt;3.HNSW&lt;/h3&gt;
&lt;p&gt;尽管NSW已经可以很好的为我们解决ANN查询的问题，但其仍然有不足之处。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;搜索时，NSW无法区分long range与short range，从而无法先查询long range再查询short range。&lt;/li&gt;
&lt;li&gt;当数据的聚类效应特别明显时，即使我们乱序加入向量，cluster之间相互连接的边仍然十分稀疏，从而搜索结果容易陷入局部最优，同时效率也会比较低下。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此为了解决上述问题，HNSW作为NSW的改良版被提了出来。&lt;/p&gt;
&lt;h4&gt;1.构建&lt;/h4&gt;
&lt;p&gt;我们首先直观的感受HNSW图。我们可以看到hnsw相比于nsw多了层级的概念。我们从图中可以看到，level0中有全部的向量，随着层数的增加，向量的数量也相应的减少。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/hnsw.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;HNSW并不要求我们乱序插入向量，当我们向HNSW添加新的向量时，我们首先会通过一个指数衰减的概率函数，得到这个向量所处的最大层级(如果最大层级计算出来是3,那么level3, level2,level1,level0中都含有这个向量)。&lt;/p&gt;
&lt;p&gt;这就意味着，绝大多数的向量所处的最高层级都是level0, 同样我们也可以认为高层级是低层级的草图（抽样），因此高层级中的向量之间大概率是long range连接，低层级中的向量则是short range连接。这样子做给我们带来的好处就是搜索时，我们可以先寻找long range的边，再寻找short range的边，即先粗查再精查。从而尽可能减少搜索的次数。&lt;/p&gt;
&lt;p&gt;当得到这个向量所处的最大层级后，我们便需要将其添加到图中。假设，新增的向量为&lt;strong&gt;V&lt;/strong&gt;, 这个向量所处的最大层级为&lt;strong&gt;I&lt;/strong&gt;,HNSW的最高层级为&lt;strong&gt;J&lt;/strong&gt;。添加向量时因为需要从最高层级&lt;strong&gt;J&lt;/strong&gt;，一直走到最低层级0,我们将添加时所处的层级设置为&lt;strong&gt;C&lt;/strong&gt;。添加的过程可以分为3个阶段&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;J&lt;/strong&gt; &amp;gt;= &lt;strong&gt;C&lt;/strong&gt; &amp;gt; &lt;strong&gt;I&lt;/strong&gt;
这一阶段我们使用NSW的贪心算法，寻找距离最近的向量，随后在下一层级以这个点为搜索起点.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I&lt;/strong&gt; &amp;gt;= &lt;strong&gt;C&lt;/strong&gt; &amp;gt; 0
在这一阶段，我们不仅要找距离最近的向量，我们还需要将&lt;strong&gt;V&lt;/strong&gt;存放到这一级的图中。我们仍旧使用贪心算法寻找距离最近的向量，不同之处在于我们会维护一个动态的列表，保存距离&lt;strong&gt;V&lt;/strong&gt;最近的&lt;code&gt;efCounstruction&lt;/code&gt;个向量，&lt;code&gt;efConstruction&lt;/code&gt;为可调节的参数。当我们在这一层搜索完成后，我们会将这一动态列表作为&lt;code&gt;Candidate&lt;/code&gt;,并从中取出&lt;code&gt;M&lt;/code&gt;个向量，与其连接。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I&lt;/strong&gt; = 0
这一阶段，我们采用和第二阶段一样的策略，不同的是在level0,向量&lt;strong&gt;V&lt;/strong&gt;可以与最多&lt;strong&gt;2M&lt;/strong&gt;个向量进行连接。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下图为一个简单的示例，新增向量所处的最高层级为1。我们首先在level2中，寻找与其最近的点(黄色标示)，找到后，我们以这个点为起点在level1中寻找与其最近的&lt;code&gt;efCounstruction&lt;/code&gt;个点，随后与其中的&lt;strong&gt;M&lt;/strong&gt;个向量进行连接。最后当我们到达level0时，我们用上一层连接的&lt;strong&gt;M&lt;/strong&gt;个向量作为起点寻找符合要求的&lt;strong&gt;2M&lt;/strong&gt;个向量，并与其相连。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/new-insert.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;当我们在某一层中(&lt;strong&gt;I&lt;/strong&gt; &amp;gt;= &lt;strong&gt;C&lt;/strong&gt; &amp;gt;= 0)找到距离&lt;strong&gt;V&lt;/strong&gt;最近的&lt;code&gt;efConstruction&lt;/code&gt;向量后，我们需要从中挑选出&lt;strong&gt;M&lt;/strong&gt;个向量用以与&lt;strong&gt;V&lt;/strong&gt;连接。
一种简单的做法是直接从&lt;code&gt;efCounstruction&lt;/code&gt;中挑选出最近的&lt;strong&gt;M&lt;/strong&gt;个向量，但是这种做法当数据的聚类效果特别明显时，会导致不同cluster之间的连接十分稀疏，导致搜索陷入局部最优，并且查询效率降低。&lt;/p&gt;
&lt;p&gt;因此我们采用启发式的搜索方式，假定我们新增的向量为&lt;strong&gt;V&lt;/strong&gt;，挑选出的&lt;code&gt;efConstruction&lt;/code&gt;个向量为&lt;strong&gt;Candidate&lt;/strong&gt;,当前我们已经选择出需要连接的向量为&lt;strong&gt;Result&lt;/strong&gt;,启发式的算法为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while len(Candidate) &amp;gt; 0 and len(Result) &amp;lt; M:
    c = pop nearest element from Candidate to V
    for r in Result:
        lowest = min(lowest, distance(r, c))
    if dis(c, V) &amp;lt; lowest:
        Result += c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用一张图来描述这种情况,我们从C1,C2中决定哪个点应该作为下一个连接点时，我们会选择与&lt;code&gt;inserted&lt;/code&gt;之间的距离相比与其他&lt;code&gt;result&lt;/code&gt;更近的点，而不是距离&lt;code&gt;inserted&lt;/code&gt;更近的点。
按照论文中的说法，这可以帮助我们在高度聚类的数据中，取得更好的搜索效果以及效率。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The heuristic enhances the diversity of a vertex’s neighborhood and leads to better search efficiency for the case of highly clustered data.
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/her.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2.搜索&lt;/h4&gt;
&lt;p&gt;搜索的过程分为两个阶段&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;J&lt;/strong&gt; &amp;gt;= &lt;strong&gt;C&lt;/strong&gt; &amp;gt; 0
这一阶段我们使用NSW的贪心算法，在这一层中寻找距离最近的向量，随后在下一层级以这个点为搜索起点继续搜索.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I&lt;/strong&gt; = 0
这一阶段，我们仍旧使用贪心的搜索策略，不同之处在于，我们会维护一个距离最近的&lt;code&gt;efSearch&lt;/code&gt;个向量，并最终返回结果。
&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/search.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3.Summary&lt;/h4&gt;
&lt;p&gt;HNSW,通过引入层级的概念以及启发式搜索，解决了搜索时，NSW无法区分long range与short range，以及面对高度聚集的数据时，搜索效率的低下。&lt;/p&gt;
</content:encoded><category>vector search</category><author>tang-hi</author></item><item><title>文本相关性</title><link>https://tangdh.life/posts/ir/doc-relv/</link><guid isPermaLink="true">https://tangdh.life/posts/ir/doc-relv/</guid><description>文本相关性是信息检索和自然语言处理中的一个核心问题。在文本相关性中，我们希望能够量化文本之间的相似程度或相关程度，以便有效地处理和组织文本数据。</description><pubDate>Mon, 20 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;文本相关性是信息检索和自然语言处理中的一个核心问题。在文本相关性中，我们希望能够量化文本之间的相似程度或相关程度，以便有效地处理和组织文本数据。例如，在搜索引擎中，我们希望通过用户的查询来找到与查询相关的最相关的文档或网页。在文档分类和聚类中，我们希望将相似的文档放在一起，以便更好地管理和分析它们。在文本匹配和相似性匹配中，我们希望找到两个文本之间的相似度，以便评估它们之间的关系。&lt;/p&gt;
&lt;p&gt;这篇博客会介绍 &lt;strong&gt;TF-IDF&lt;/strong&gt; 以及 &lt;strong&gt;BM25&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;tf-idf&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;tf-idf&lt;/strong&gt;（Term Frequency-Inverse Document Frequency）是一种用于评估文档中单词重要性的统计方法，广泛应用于信息检索、自然语言处理等领域。&lt;/p&gt;
&lt;p&gt;他的整体公式如下&lt;/p&gt;
&lt;p&gt;$$
\text{tf-idf}(t,d,D) = \text{tf}(t,d) \cdot \text{idf}(t,D)
$$&lt;/p&gt;
&lt;p&gt;其中，$t$ 是指某个单词(term),$d$ 是指某个文档(document),$D$ 是指整个文档集合。tf 表示单词在文档中的频率(term frequency)。&lt;strong&gt;idf&lt;/strong&gt; 表示单词在整个文档集合中的逆文档频率(inverse document frequency)。&lt;/p&gt;
&lt;p&gt;它们的计算公式如下:&lt;/p&gt;
&lt;p&gt;$$
\text{tf}(t,d) = \frac{f_{t,d}}{\sum_{t&apos; \in d} f_{t&apos;,d}}
$$&lt;/p&gt;
&lt;p&gt;其中，$f_{t,d}$ 是指单词 $t$ 在文档 $d$ 中出现的次数。&lt;/p&gt;
&lt;p&gt;$$
\text{idf}(t,D) = \log{\frac{N}{|{d \in D : t \in d}|}}
$$&lt;/p&gt;
&lt;p&gt;其中，$N$ 是指整个文档集合中文档的总数，$|{d \in D : t \in d}|$ 是指包含单词 $t$ 的文档数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;tf-idf&lt;/strong&gt;考虑了一个单词在文档中的频率以及在整个文档集合中的频率，从而确定它在文档中的重要性。一个单词在某个文档中出现的次数越多，其重要性就越高（即&lt;strong&gt;tf&lt;/strong&gt;越高）,但是如果它在整个文集中出现的次数也很多，那么它的重要性就会降低(即&lt;strong&gt;idf&lt;/strong&gt;越低)&lt;/p&gt;
&lt;h4&gt;通过例子深入理解&lt;strong&gt;tf-idf&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;假设我们有一个包含以下 4 个文档的文档集合:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Doc 1: the cat in the hat&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Doc 2: the rat in the hat&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Doc 3: the cat and the rat&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Doc 4: the cat sat on the hat&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;现在，我们想要计算单词 &quot;cat&quot; 在文档集合中的 &lt;strong&gt;tf-idf&lt;/strong&gt; 值。首先，我们需要计算单词 &quot;cat&quot; 在每个文档中的 &lt;strong&gt;tf&lt;/strong&gt; 值。计算公式如下：&lt;/p&gt;
&lt;p&gt;$$\text{tf}(t,d) = \frac{f_{t,d}}{\sum_{t&apos; \in d} f_{t&apos;,d}}$$&lt;/p&gt;
&lt;p&gt;其中，$f_{t,d}$ 表示单词 $t$ 在文档 $d$ 中出现的次数，$\sum_{t&apos; \in d} f_{t&apos;,d}$ 表示文档 $d$ 中所有单词的出现次数之和。因此，单词 &quot;cat&quot; 在每个文档中的 &lt;strong&gt;tf&lt;/strong&gt; 值为:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tf(cat, Doc 1) = 1/5 = 0.2
tf(cat, Doc 2) = 0/5 = 0
tf(cat, Doc 3) = 1/6 = 0.1667
tf(cat, Doc 4) = 1/7 = 0.1429
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，我们需要计算单词 &quot;cat&quot; 在整个文档集合中的 &lt;strong&gt;idf&lt;/strong&gt; 值。计算公式如下：&lt;/p&gt;
&lt;p&gt;$$\text{idf}(t,D) = \log{\frac{N}{|{d \in D : t \in d}|}}$$&lt;/p&gt;
&lt;p&gt;其中，$N$ 表示文档集合中文档的总数，$|{d \in D : t \in d}|$ 表示包含单词 $t$ 的文档数。因此，单词 &quot;cat&quot; 在整个文档集合中的 &lt;strong&gt;idf&lt;/strong&gt; 值为：&lt;/p&gt;
&lt;p&gt;$$\text{idf}(cat, D) = \log{\frac{4}{3}} \approx 0.2877$$&lt;/p&gt;
&lt;p&gt;最后，我们可以计算单词 &quot;cat&quot; 在每个文档中的 &lt;strong&gt;tf-idf&lt;/strong&gt; 值，计算公式如下：&lt;/p&gt;
&lt;p&gt;$$\text{tf-idf}(t,d,D) = \text{tf}(t,d) \cdot \text{idf}(t,D)$$&lt;/p&gt;
&lt;p&gt;因此，单词 &quot;cat&quot; 在每个文档中的 &lt;strong&gt;tf-idf&lt;/strong&gt; 值为:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tf-idf(cat, Doc 1, D) = 0.2 * 0.2877 = 0.0575
tf-idf(cat, Doc 2, D) = 0 * 0.2877 = 0
tf-idf(cat, Doc 3, D) = 0.1667 * 0.2877 = 0.0481
tf-idf(cat, Doc 4, D) = 0.1429 * 0.2877 = 0.0412
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，我们就计算出了单词 &quot;cat&quot; 在每个文档中的 &lt;strong&gt;tf-idf&lt;/strong&gt; 值。可以看到，单词 &quot;cat&quot; 在 Doc 1 和 Doc 3中的 &lt;strong&gt;tf-idf&lt;/strong&gt; 值比较高，因为它们在文档中出现得比较少，并且在文档集合中出现的文档数也比较少，表明它们在文档集合中比较重要。&lt;/p&gt;
&lt;h3&gt;BM25&lt;/h3&gt;
&lt;p&gt;与 &lt;strong&gt;tf-idf&lt;/strong&gt; 相似，&lt;strong&gt;BM25&lt;/strong&gt; 也是基于词频的算法，但与 &lt;strong&gt;tf-idf&lt;/strong&gt; 不同的是，&lt;strong&gt;BM25&lt;/strong&gt; 引入了文档长度的因素，同时对词频的权重进行了调整。&lt;strong&gt;BM25&lt;/strong&gt; 的全称是 &lt;strong&gt;Best Matching 25&lt;/strong&gt;，它计算的是一个查询（query）与一个文档（document）之间的相似度得分。&lt;strong&gt;BM25&lt;/strong&gt; 基于以下三个因素来计算文档的得分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;查询项（query term）在文档中的出现次数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文档的长度（即包含的单词数）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询项的文档频率（即包含查询项的文档数量）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;BM25&lt;/strong&gt; 的公式如下：&lt;/p&gt;
&lt;p&gt;$$
\text{BM25}(q, d) = \sum_{i=1}^{|q|} \text{idf}(q_i) \cdot \frac{f(q_i, d) \cdot (k_1 + 1)}{f(q_i, d) + k_1 \cdot (1 - b + b \cdot \frac{|d|}{\text{avgdl}})}
$$&lt;/p&gt;
&lt;p&gt;其中，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$q$ 是查询项&lt;/li&gt;
&lt;li&gt;$d$ 是文档&lt;/li&gt;
&lt;li&gt;$|q|$ 是查询项的数量&lt;/li&gt;
&lt;li&gt;$q_i$ 是第 $i$ 个查询项&lt;/li&gt;
&lt;li&gt;$\text{idf}(q_i)$ 是查询项 $q_i$ 的逆文档频率（inverse document frequency），定义为 $\text{idf}(q_i) = \log{\frac{N - n(q_i) + 0.5}{n(q_i) + 0.5}}$
&lt;ul&gt;
&lt;li&gt;$N$ 是文档集合中的文档数&lt;/li&gt;
&lt;li&gt;$n(q_i)$ 是包含查询项 $q_i$ 的文档数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;$f(q_i, d)$ 是查询项 $q_i$ 在文档 $d$ 中出现的次数&lt;/li&gt;
&lt;li&gt;$|d|$ 是文档 $d$ 中的单词数&lt;/li&gt;
&lt;li&gt;$\text{avgdl}$ 是文档集合中所有文档的平均长度&lt;/li&gt;
&lt;li&gt;$k_1$ 和 $b$ 是常数，通常取 $k_1 = 1.2$，$b = 0.75$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 &lt;strong&gt;BM25&lt;/strong&gt; 中，查询项的权重由两个因素决定：逆文档频率（idf）和词频（tf）。与 &lt;strong&gt;tf-idf&lt;/strong&gt; 相似，&lt;strong&gt;idf&lt;/strong&gt; 用于衡量一个查询项的重要程度，&lt;strong&gt;tf&lt;/strong&gt; 用于衡量查询项在文档中的出现频率。不同之处在于，&lt;strong&gt;BM25&lt;/strong&gt; 引入了文档长度和查询项的文档频率来对词频进行加权。&lt;/p&gt;
&lt;p&gt;具体来说，当文档长度很小时，&lt;strong&gt;BM25&lt;/strong&gt; 对词频进行较大的加权，这可以帮助我们区分出现次数很少但重要的查询项；而当文档长度很大时，&lt;strong&gt;BM25&lt;/strong&gt; 对词频进行较小的加权，以避免受过多出现的常见查询项的影响。此外，&lt;strong&gt;BM25&lt;/strong&gt; 的常数参数 $k_1$ 和 $b$ 也可以根据实际情况进行调整，以获得更好的结果。&lt;/p&gt;
&lt;h4&gt;计算 BM25 的例子：&lt;/h4&gt;
&lt;p&gt;假设我们有一个文档集合，其中包含三个文档 $D_1, D_2, D_3$，它们的长度分别为 $|D_1|=100, |D_2|=200, |D_3|=300$。我们还有一个查询项 $q$，其中包含两个单词 $q_1$ 和 $q_2$。假设 $q_1$ 在 $D_1$ 中出现了 2 次，在 $D_2$ 中出现了 5 次，在 $D_3$ 中出现了 10 次；$q_2$ 在 $D_1$ 中出现了 3 次，在 $D_2$ 中出现了 1 次，在 $D_3$ 中没有出现。&lt;/p&gt;
&lt;p&gt;我们需要计算每个文档与查询项 $q$ 的 &lt;strong&gt;BM25&lt;/strong&gt; 得分。为了简化，我们假设 $k_1 = 1.2$，$b = 0.75$。&lt;/p&gt;
&lt;p&gt;首先，我们需要计算每个查询项的逆文档频率 $\text{idf}(q_i)$。根据公式，我们可以得到：&lt;/p&gt;
&lt;p&gt;$$\text{idf}(q_1) = \log{\frac{3 - 2 + 0.5}{2 + 0.5}} \approx 0.29$$&lt;/p&gt;
&lt;p&gt;$$\text{idf}(q_2) = \log{\frac{3 - 0 + 0.5}{0 + 0.5}} \approx 1.79$$&lt;/p&gt;
&lt;p&gt;接下来，我们需要计算每个文档与查询项 $q$ 的 BM25 得分。根据公式，我们可以得到：&lt;/p&gt;
&lt;p&gt;$$
\text{BM25}(q, D_1) = \text{idf}(q_1) \cdot \frac{2 \cdot (1.2 + 1)}{2 + 1.2 \cdot (1 - 0.75 + 0.75 \cdot \frac{100}{200})} + \text{idf}(q_2) \cdot \frac{3 \cdot (1.2 + 1)}{3 + 1.2 \cdot (1 - 0.75 + 0.75 \cdot \frac{100}{200})} \approx 0.78
$$&lt;/p&gt;
&lt;p&gt;$$
\text{BM25}(q, D_2) = \text{idf}(q_1) \cdot \frac{5 \cdot (1.2 + 1)}{5 + 1.2 \cdot (1 - 0.75 + 0.75 \cdot \frac{200}{200})} + \text{idf}(q_2) \cdot \frac{1 \cdot (1.2 + 1)}{1 + 1.2 \cdot (1 - 0.75 + 0.75 \cdot \frac{200}{200})} \approx 0.63
$$&lt;/p&gt;
&lt;p&gt;$$
\text{BM25}(q, D_3) = \text{idf}(q_1) \cdot \frac{10 \cdot (1.2 + 1)}{10 + 1.2 \cdot (1 - 0.75 + 0.75 \cdot \frac{300}{200})} + \text{idf}(q_2) \cdot \frac{0 \cdot (1.2 + 1)}{0 + 1.2 \cdot (1 - 0.75 + 0.75 \cdot \frac{300}{200})} \approx 0.69
$$&lt;/p&gt;
&lt;p&gt;因此，我们可以得到三个文档与查询项 $q$ 的 &lt;strong&gt;BM25&lt;/strong&gt; 得分分别为 0.78、0.63 和 0.69。&lt;/p&gt;
&lt;p&gt;这个例子说明了，虽然 $q_1$ 在 $D_3$ 中出现的次数最多，但是由于 $D_3$ 的长度较长，而且 $q_2$ 没有在 $D_3$ 中出现，因此 $D_1$ 的得分最高。这也说明了 &lt;strong&gt;BM25&lt;/strong&gt; 算法的优点之一，即克服了 &lt;strong&gt;tf-idf&lt;/strong&gt; 算法中常见查询项对结果的影响。&lt;/p&gt;
&lt;h4&gt;BM25的公式进一步解读&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;BM25&lt;/strong&gt;中的&lt;strong&gt;idf公式&lt;/strong&gt;与原版的&lt;strong&gt;idf公式&lt;/strong&gt;不一致&lt;/p&gt;
&lt;p&gt;传统的 &lt;strong&gt;idf&lt;/strong&gt; 公式是 $\log\frac{N}{df}$，其中 $N$ 是文档集合中文档的总数，$df$ 是包含查询项 $t$ 的文档数。而 &lt;strong&gt;BM25&lt;/strong&gt; 中使用的 &lt;strong&gt;idf&lt;/strong&gt; 公式是 $\log\frac{N-df+0.5}{df+0.5}$。这个公式与传统的 &lt;strong&gt;idf&lt;/strong&gt; 公式相比，主要做了两个改动：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;加入了平滑因子：在传统的 &lt;strong&gt;idf&lt;/strong&gt; 公式中，当某个查询项在文档集合中未出现时，其 &lt;strong&gt;idf&lt;/strong&gt; 值会变成负无穷。为了避免这种情况，&lt;strong&gt;BM25&lt;/strong&gt; 中的 &lt;strong&gt;idf&lt;/strong&gt; 公式加入了平滑因子 0.5。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;减少 &lt;strong&gt;idf&lt;/strong&gt; 的影响：在传统的 &lt;strong&gt;idf&lt;/strong&gt; 公式中，当某个查询项在很少的文档中出现时，其 &lt;strong&gt;idf&lt;/strong&gt; 值会很大，对结果产生过大的影响。&lt;strong&gt;BM25&lt;/strong&gt; 中的 &lt;strong&gt;idf&lt;/strong&gt; 公式通过减少 &lt;strong&gt;idf&lt;/strong&gt; 的影响，使得查询项在出现文档数较少时，不会对结果产生过大的影响。但是当文档出现的数量超过一半时，计算出的idf值为负数，Lucene中为了解决这个问题，更改了idf公式为$\log1+\frac{N-df+0.5}{df+0.5}$，从而防止了负数的产生。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;BM25&lt;/strong&gt;中的k是如何影响计算出的结果
$k$ 的值控制了词频对得分的影响程度，可以看作是一个词频的归一化因子。&lt;/p&gt;
&lt;p&gt;当 $k$ 的值较小时，词频的影响就相对较小，得分的变化范围也相对较小；当 $k$ 的值较大时，词频的影响就相对较大，得分的变化范围也相对较大。当 $k$ 的值等于 $0$ 时，相当于将文档中所有词项的词频都视为 $1$，此时得分只与文档与查询语句的匹配程度有关。同时，因为有$k$的存在，即使词频特别大，也不会对最终计算的结果有大的影响。即当词频达到一定程度，计算出的BM25的值并不会线性提升。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;BM25&lt;/strong&gt;中的b是如何影响计算出的结果
在 BM25 中，参数 $b$ 用来平衡文档长度对得分的影响。它控制了文档长度对得分的影响程度，可以看作是一个文档长度的归一化因子。$b$ 的取值会影响文档中的词项权重 $w_i$ 的大小，这里的 $w_i$ 是指包含词项 $i$ 的文档的 $i$ 的权重。当 $b$ 越大时，表示文档长度对词项权重的影响越大，这意味着文档中的词项权重 $w_i$ 会相应地趋于缩小；反之，当 $b$ 越小时，表示文档长度对词项权重的影响越小，这意味着文档中的词项权重 $w_i$ 会相应地趋于扩大。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;BM25&lt;/strong&gt;中的文档长度如何影响计算出的结果&lt;/p&gt;
&lt;p&gt;一篇文档如果所含的单词越少，那么 $\frac{|d|}{\text{avgdl}}$ 越小，从而导致最终的&lt;strong&gt;BM25&lt;/strong&gt;越大，因此文档字数越少，相关性越高.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;idf&lt;/strong&gt;为什么要用对数计算?
在计算文档中每个词项的逆文档频率时，使用对数函数的目的是将idf值的范围压缩到一个较小的区间内。由于文档的大小通常会很大，因此一个词项可能会出现几千甚至几十万次，这样计算得到的idf值就会非常大。使用对数函数可以将这些大数值压缩到一个较小的区间内，便于计算和处理。
此外，对数函数还能够使得低频词项的idf值更加突出。如果不使用对数函数，那么在某些情况下，一些低频词项的idf值可能会非常小，甚至可能会被忽略。而使用对数函数后，这些低频词项的idf值就会被放大，使得它们在检索时能够更好地区分文档的相关性。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;BM25F&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;BM25F&lt;/strong&gt;是&lt;strong&gt;BM25&lt;/strong&gt;算法的一种变体，它在&lt;strong&gt;BM25&lt;/strong&gt;的基础上增加了对多字段的支持。在&lt;strong&gt;BM25F&lt;/strong&gt;中，每个文档可以包含多个字段（例如标题、正文、标签等），每个字段都有一个权重。&lt;strong&gt;BM25F&lt;/strong&gt;通过将每个字段的得分相加来计算文档的相关性得分。&lt;strong&gt;BM25F&lt;/strong&gt;的公式如下：&lt;/p&gt;
&lt;p&gt;$score(D,Q) = \sum_{i=1}^{n}weight(q_i)\cdot IDF(q_i)\cdot \frac{f(q_i,D)\cdot (k_i + 1)}{f(q_i,D) + k_i\cdot (1 - b_i + b_i \cdot \frac{|D|}{avgdl_i})}$&lt;/p&gt;
&lt;p&gt;其中，$D$表示文档，$Q$表示查询，$n$表示查询中的词项数，$q_i$表示查询中的第$i$个词项，$weight(q_i)$表示第$i$个词项的权重，$IDF(q_i)$表示第$i$个词项的逆文档频率，$f(q_i,D)$表示文档$D$中第$i$个词项的出现频率，$k_i$和$b_i$分别表示第$i$个词项的参数$k$和$b$，$|D|$表示文档$D$的长度，$avgdl_i$表示包含第$i$个字段的所有文档的平均长度。&lt;/p&gt;
&lt;p&gt;在&lt;strong&gt;BM25F&lt;/strong&gt;中，每个词项的权重由其所在的字段的权重和全局权重两部分组成。全局权重表示该词项在整个文集中的重要性，字段权重则表示该词项在当前字段中的重要性。词项的权重可以通过以下公式计算：&lt;/p&gt;
&lt;p&gt;$weight(q_i) = weight_{field}(q_i)\cdot weight_{global}(q_i)$&lt;/p&gt;
&lt;p&gt;其中，$weight_{field}(q_i)$表示第$i$个词项在当前字段中的权重，$weight_{global}(q_i)$表示第$i$个词项在全局文档集中的权重。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BM25F&lt;/strong&gt;的优点是能够有效地处理多字段查询，可以更好地匹配查询和文档中不同字段的相关性。它可以通过调整字段的权重来对不同字段的重要性进行调整，从而提高搜索结果的准确性。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;tf-idf&lt;/strong&gt; 词频越高，词频在整个文档集中越稀少，值越高&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;BM25&lt;/strong&gt; 词频在整个文档集中越稀少，词频越高，&lt;strong&gt;文档的单词数越少&lt;/strong&gt;，值越高&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;BM25F&lt;/strong&gt; 词频在整个文档集中越稀少，词频越高， 文档的单词数越少，&lt;strong&gt;权重越高&lt;/strong&gt;，值越高&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><category>Full Text Search</category><author>tang-hi</author></item><item><title>Virtual 机制</title><link>https://tangdh.life/posts/c/virtual/</link><guid isPermaLink="true">https://tangdh.life/posts/c/virtual/</guid><description>这篇文章尝试使用较底层的视角来审视C++中虚函数是如何实现的</description><pubDate>Sat, 18 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;这篇文章会尝试使用&lt;code&gt;GDB&lt;/code&gt;来分析C++中虚函数的实现机制。希望可以帮助你更加透彻的理解C++的虚函数实现。&lt;/p&gt;
&lt;p&gt;我们用来测试的程序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
using namespace std;
struct Simple {
  int one;
};
struct Base {
  virtual void v1() {
    cout &amp;lt;&amp;lt; &quot;Base::V1&quot; &amp;lt;&amp;lt; endl;
  }

  virtual void v2() {
    cout &amp;lt;&amp;lt; &quot;Base::V2&quot; &amp;lt;&amp;lt; endl;
  }

  int one;

};


struct Derived : Base {
  void v1() override {
    cout &amp;lt;&amp;lt; &quot;Derived::v1&quot; &amp;lt;&amp;lt; endl;
  }

};

int main() {
  Base* derived = new Derived();
  Base* derived1 = new Derived();
  Base* base = new Base();
  Base* base1 = new Base();

  Simple* simple = new Simple();

  derived-&amp;gt;v1();
  derived-&amp;gt;v2();

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面我们将代码进行编译后，然后使用gdb进行分析&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;g++ virtual.cc --std=c++11 -g
gdb a.out
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们首先分别看一下&lt;code&gt;derived&lt;/code&gt;,&lt;code&gt;derived1&lt;/code&gt;,&lt;code&gt;base&lt;/code&gt;,&lt;code&gt;base1&lt;/code&gt;,&lt;code&gt;simple&lt;/code&gt;中的内容&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/pic/C++/virtual-add1.png&quot; alt=&quot;image-20230622221528352&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/pic/C++/virtual-add2.png&quot; alt=&quot;image-20230622222630329&quot; /&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;variable name&lt;/th&gt;
&lt;th&gt;address&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;derived&lt;/td&gt;
&lt;td&gt;0x55555556aeb0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;derived1&lt;/td&gt;
&lt;td&gt;0x55555556aed0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;base&lt;/td&gt;
&lt;td&gt;0x55555556aef0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;base1&lt;/td&gt;
&lt;td&gt;0x55555556af10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;simple&lt;/td&gt;
&lt;td&gt;0x55555556af30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;从这两张图，我们可以发现如下几件事&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当一个class有虚函数时，该class的对象中会有一个&lt;code&gt;vptr&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;该&lt;code&gt;vptr&lt;/code&gt;的大小为&lt;strong&gt;8byte&lt;/strong&gt;(0x55555556aeb8 - 0x55555556aeb0)&lt;/li&gt;
&lt;li&gt;该&lt;code&gt;vptr&lt;/code&gt;所指向的内容仅与class的类型有关，与对象&lt;strong&gt;无关&lt;/strong&gt; (derived.vptr == derived.vptr1)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们下面以&lt;strong&gt;derived&lt;/strong&gt;为例,看一下&lt;code&gt;vptr&lt;/code&gt;所指向的内容。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/pic/C++/vtable.png&quot; alt=&quot;image-20230622223541703&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们可以看到&lt;code&gt;vptr&lt;/code&gt;指向了一些东西，但具体是什么我们还不知道,但是我们可以发现这个地址的值&lt;code&gt;0x5555555553a6&lt;/code&gt;（小端写法）好像是一个地址，那么我们可以查看一下这个地址指向的是什么。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/pic/C++/vtable-content.png&quot; alt=&quot;image-20230622224641115&quot; /&gt;&lt;/p&gt;
&lt;p&gt;结果很明显，这里面的值指向的是函数&lt;code&gt;Derived::v1&lt;/code&gt;的定义,我们可以通过这个地址对该函数进行调用。我们再看一下其他的值。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/pic/C++/vtable-all.png&quot; alt=&quot;image-20230622225025776&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以结论很清楚，当你的class中含有虚函数时，编译器会为该类创建一个专属的&lt;code&gt;vtable&lt;/code&gt;,&lt;code&gt;vtable&lt;/code&gt;中存放着各个虚函数的实现，如果该类有自己的实现，那么指向的就是它自己的实现，否则指向父类的实现。然后当你创建一个类的对象时，编译器会将指向该&lt;code&gt;vtable&lt;/code&gt;的指针给到对象的&lt;code&gt;vptr&lt;/code&gt;中。&lt;/p&gt;
&lt;p&gt;我们最后再看一下，调用的过程。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;derived-&amp;gt;v1();&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/pic/C++/virtual-call1.png&quot; alt=&quot;image-20230622230204986&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt; derived-&amp;gt;v2();&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/pic/C++/virtual-call2.png&quot; alt=&quot;image-20230622230402086&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中&lt;code&gt;rbp&lt;/code&gt;为栈帧,其中&lt;code&gt;-0x38(%rbp)&lt;/code&gt;为获取&lt;code&gt;derived&lt;/code&gt;的地址，即&lt;code&gt;0x55555556aeb0&lt;/code&gt;,也就是&lt;code&gt;vptr&lt;/code&gt;的地址，随后通过&lt;code&gt;mov (%rax),%rax&lt;/code&gt;得到&lt;code&gt;vtable&lt;/code&gt;的地址并保存在&lt;code&gt;%rax&lt;/code&gt;中，因为调用的函数不同，因此&lt;code&gt;derived-&amp;gt;v2();&lt;/code&gt;的汇编需要将&lt;code&gt;%rax + 8&lt;/code&gt;得到对应的地址。然后通过&lt;code&gt;mov (%rax),%rdx&lt;/code&gt;得到需要调用的函数地址，最后通过&lt;code&gt;call *%rdx&lt;/code&gt;完成多态的函数调用。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;当当一个class有虚函数时，编译器会为该class对象生成一个&lt;code&gt;vptr&lt;/code&gt;，该&lt;code&gt;vptr&lt;/code&gt;的大小为&lt;strong&gt;8byte&lt;/strong&gt;,所指向的内容仅与class的类型有关，与对象&lt;strong&gt;无关&lt;/strong&gt;,这里面的值指向的是函数&lt;code&gt;Derived::v1&lt;/code&gt;的定义,我们可以通过这个地址对该函数进行调用。当实际调用时，编译器会根据你调用的函数不同，调整vtable所指的entry,最后根据&lt;code&gt;entry&lt;/code&gt;项中的地址，完成函数调用。&lt;/p&gt;
</content:encoded><category>C++</category><author>tang-hi</author></item><item><title>More Effective C++</title><link>https://tangdh.life/posts/books/more-effective-cpp/</link><guid isPermaLink="true">https://tangdh.life/posts/books/more-effective-cpp/</guid><description>这篇博客主要是用来加深自己对读过的书的记忆。写的内容可能只对我自己产生价值</description><pubDate>Sat, 11 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;这篇博客主要是用来加深自己对读过的书的记忆。写的内容可能只对我自己产生价值&lt;/p&gt;
&lt;h3&gt;Item 1: Distinguish between pointers and references&lt;/h3&gt;
&lt;p&gt;引用相较于指针&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优势&lt;/strong&gt; 他总是有效的，即没有null reference，指针则需要检查是否为空&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;劣势&lt;/strong&gt; 指针可以指向一个新的对象，引用不行。指针可以使用nullptr表示不存在，如果你需要该变量拥有不存在的语义，使用pointer。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt; 当你确认你需要指向某个东西，并且绝对不会改变指向其它东西，使用reference，不然的话使用pointer&lt;/p&gt;
&lt;h3&gt;Item 2: Prefer C++-style casts&lt;/h3&gt;
&lt;p&gt;C的转型，无法区分想做的是什么类型的转型，而且较难分辨，尽量使用C++的新式转型&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;static_cast&lt;/strong&gt; 基本拥有C旧式转型的相同威力与意义&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;cons_cast&lt;/strong&gt; 用于强转const属性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;dynamic_cast&lt;/strong&gt; 用于在继承体系中向下转型，转型失败时会以nullptr或者exception表现出来&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;reinterpret_cast&lt;/strong&gt; 用于转换二进制和序列化，或者函数指针的转型&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Item 3:  Never treat arrays polymorphically :skull:&lt;/h3&gt;
&lt;p&gt;数组类型不能被当作多态来进行传递，即&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void printBSTArray(const BST array[]);
class BalancedBST: public BST {};
printBSTArray(BalancedBST) // error!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why? 当你读取数组元素时，偏移是根据你申明的类型来进行计算的，但是子类的大小和父类基本都是不一致的，因此你实际使用的偏移是错误的，这是一个未定义行为！&lt;/p&gt;
&lt;h3&gt;Item 4:  Avoid gratuitous default constructors&lt;/h3&gt;
&lt;p&gt;如果一个类不借助外部的信息就无法正确初始化，那么就应该避免提供默认构造函数，但这会带来以下几个问题&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对于数组类型 &lt;strong&gt;A a[10]&lt;/strong&gt; 没有默认构造函数即无法生成，需要使用别的方式生成，例如使用指针数组，而不是对象数组&lt;/li&gt;
&lt;li&gt;对于一些基于模板的容器类型无法很好的兼容，因为他们可能假设你的类拥有默认构造函数&lt;/li&gt;
&lt;li&gt;如果virtual base class 缺乏默认构造函数，后续继承他的类都需要知道其意义(bad design)。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;结论，这是一个case by case的问题，根据实际情况进行抉择。&lt;/p&gt;
&lt;h3&gt;Item 5:   Be wary of user-defined conversion functions&lt;/h3&gt;
&lt;p&gt;对于自己定义转换函数需要格外的小心，因为他们可能导致非预期的函数调用，编译器会想尽办法帮你编译成功，因此可能在你未预料的地方给你进行了隐饰转换，解决办法&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;定义 **asType()**的成员函数，进行显式的类型转换&lt;/li&gt;
&lt;li&gt;使用&lt;strong&gt;explicit&lt;/strong&gt;去除单自变量的constructor的隐式转换&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Item 6:  Distinguish between prefix and postfix forms of increment and decrement operators&lt;/h3&gt;
&lt;p&gt;前置++返回引用，后置++返回const 对象(const 对象防止 a++++)&lt;/p&gt;
&lt;p&gt;后置++有一个临时变量的负担。&lt;/p&gt;
&lt;p&gt;prefer prefix&lt;/p&gt;
&lt;h3&gt;Item 7: Never overload &amp;amp;&amp;amp;, ||, or ,&lt;/h3&gt;
&lt;p&gt;这些符号是由短路特性，而且保证从左往右计算，如果你对其进行重载，函数传进来的参数是无法保证计算顺序的，会导致与常规理解不符，从而导致未定义行为。&lt;/p&gt;
&lt;h3&gt;Item 8:  Understand the different meanings of new and delete&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;new&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;分配内存&lt;/li&gt;
&lt;li&gt;在该内存上调用构造函数&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;operator new&lt;/strong&gt;  （void* operator new(size_t size))&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;返回一块原始的未初始化的内存&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;placement new&lt;/strong&gt; ( new (memory pointer) Type(args) )&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在memory pointer上调用构造函数&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;new []  和 operator new[]&lt;/strong&gt; 对应的数组版&lt;/p&gt;
&lt;p&gt;delete 与new对应，需要成对出现&lt;/p&gt;
&lt;p&gt;delete - new&lt;/p&gt;
&lt;p&gt;operator delete - operator new&lt;/p&gt;
&lt;h3&gt;Item 9: Use destructors to prevent resource leaks&lt;/h3&gt;
&lt;p&gt;因为有异常的存在，可能你释放资源之前就抛出了异常，导致资源泄漏。如果不断写catch会使代码乱七八糟，因此将资源释放放到析构函数中，即RAII&lt;/p&gt;
&lt;h3&gt;Item 10: Prevent resource leaks in constructors&lt;/h3&gt;
&lt;p&gt;如果contructor抛出异常，因为对象尚未完全构建完全，因此析构函数不会被调用，从而导致内存泄漏，解决办法为尽量使member不要是指针并且为智能指针。&lt;/p&gt;
&lt;h3&gt;Item 11: Prevent exceptions from leaving destructors&lt;/h3&gt;
&lt;p&gt;如果析构函数中抛出了异常有两个坏处1. 可能导致程序直接终止 2.导致析构函数需要执行的语句没有执行完，即内存泄漏，因此需要尽力避免析构函数抛出异常。&lt;/p&gt;
&lt;h3&gt;Item 12: Understand how throwing an exception differs from passing a parameter or calling a virtual function&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;异常类型永远会复制一份，无论捕获方式是什么&lt;/li&gt;
&lt;li&gt;被抛出作为exception的对象，其被允许的类型转化方式比被传递到函数的去的方式少&lt;/li&gt;
&lt;li&gt;异常比对是第一个成功就执行，而不是最佳匹配。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Item 13: Catch exceptions by reference&lt;/h3&gt;
&lt;p&gt;用指针捕获，容易导致传进来的指针已经失效，或者不知道该不该释放这个指针&lt;/p&gt;
&lt;p&gt;用值捕获，需要多复制一份且不支持多态&lt;/p&gt;
&lt;p&gt;用引用捕获，没有缺点！&lt;/p&gt;
&lt;h3&gt;Item 14: Use exception specifications judiciously&lt;/h3&gt;
&lt;p&gt;C++11基本不怎么使用了，仅用noexcept&lt;/p&gt;
&lt;h3&gt;Item 15: Understand the costs of exception handling&lt;/h3&gt;
&lt;p&gt;使用profile去检查性能的影响&lt;/p&gt;
&lt;h3&gt;Item 16: Remember the 80-20 rule&lt;/h3&gt;
&lt;p&gt;在真正关键的地方进行努力&lt;/p&gt;
&lt;h3&gt;Item 17: Consider using lazy evaluation&lt;/h3&gt;
&lt;p&gt;经典的计算机思想，仅在需要时计算。&lt;/p&gt;
&lt;h3&gt;Item 18: Amortize the cost of expected computations&lt;/h3&gt;
&lt;p&gt;将计算平坦到每一次调用中，例如你需要计算一个数组中的最大值，可以在每一次添加元素时，对最大值进行更新。&lt;/p&gt;
&lt;h3&gt;Item 19: Understand the origin of temporary objects&lt;/h3&gt;
&lt;p&gt;临时对象可能很耗成本，所以应该尽可能消除它们。例如reference to const 以及 value的地方就可能产生临时对象.&lt;/p&gt;
&lt;h3&gt;Item 20: Facilitate the return value optimization&lt;/h3&gt;
&lt;p&gt;详情看&lt;a href=&quot;../C++/rvo.md&quot;&gt;RVO&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Item 21:  Overload to avoid implicit type conversions&lt;/h3&gt;
&lt;p&gt;使用重载来消除隐式转换，从而消除临时变量，例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const UPInt operator+(const UPInt&amp;amp; lhs, // add UPInt
					  const UPInt&amp;amp; rhs); // and UPInt

const UPInt operator+(const UPInt&amp;amp; lhs, // add UPInt
					  int rhs); // and int

const UPInt operator+(int lhs, // add int and
					  const UPInt&amp;amp; rhs); // UPInt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样当执行 &lt;strong&gt;upi3 = upi1 + 10;&lt;/strong&gt; 就不会有因为类型转换而产生临时变量。&lt;/p&gt;
&lt;h3&gt;Item 22: Consider using op= instead of stand-alone op&lt;/h3&gt;
&lt;p&gt;复合版本即+=，一般效率高于+，因为不需要产生临时变量。&lt;/p&gt;
&lt;h3&gt;Item 23: Consider alternative libraries&lt;/h3&gt;
&lt;p&gt;这个没啥说的，有什么高性能库就用什么吧。&lt;/p&gt;
&lt;h3&gt;Item 24: Understand the costs of virtual functions, multiple inheritance, virtual base classes, and RTTI&lt;/h3&gt;
&lt;p&gt;这个也没啥说的，只有实际碰到才能知道。&lt;/p&gt;
&lt;h3&gt;Item 25: Virtualizing constructors and non-member functions&lt;/h3&gt;
&lt;p&gt;虚构造函数，实际就是一个虚static成员函数，在构造函数中调用，从而实现虚构造函数&lt;/p&gt;
&lt;p&gt;虚non-member函数，写一个虚函数做实际工作，再安排非虚函数对其进行调用。&lt;/p&gt;
&lt;h3&gt;Item 26: Limiting the number of objects of a class&lt;/h3&gt;
&lt;p&gt;设计一个Counted类，在内部进行计算，从而用户无感知&lt;/p&gt;
&lt;h3&gt;Item 27: Requiring or prohibiting heap-based objects&lt;/h3&gt;
&lt;p&gt;有一个hack的方式检查对象是否在heap中(利用程序的内存布局，但不具有可扩展性)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool onHeap(const void *address)
{
	char onTheStack; // local stack variable
	return address &amp;lt; &amp;amp;onTheStack;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们没有完美的方式来限制对象是否在heap中&lt;/p&gt;
&lt;h3&gt;Item 28: Smart pointers&lt;/h3&gt;
&lt;p&gt;C++11 已经支持了&lt;/p&gt;
&lt;h3&gt;Item 29: Reference counting&lt;/h3&gt;
&lt;p&gt;经典问题，不展开了&lt;/p&gt;
&lt;h3&gt;Item 30: Proxy classes&lt;/h3&gt;
&lt;p&gt;使用proxy对象来表示某些并不存在的对象，并且让用户无感知即为proxy classes&lt;/p&gt;
&lt;h3&gt;Item 31: Making functions virtual with respect to more than one object&lt;/h3&gt;
&lt;p&gt;multi dispatch，最佳解决手段，自己写虚表。&lt;/p&gt;
&lt;h3&gt;Item 32: Program in the future tense&lt;/h3&gt;
&lt;p&gt;时刻想着自己写的代码会被各种扩展，以及各种神奇的需求&lt;/p&gt;
&lt;h3&gt;Item 33: Make non-leaf classes abstract&lt;/h3&gt;
&lt;p&gt;专门抽象出Abstract类，让其他类来继承。&lt;/p&gt;
&lt;h3&gt;Item 34: Understand how to combine C++ and C in the same program&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#ifdef __cplusplus
extern &quot;C&quot; {
#endif
void drawLine(int x1, int y1, int x2, int y2); // 以这种方式避免编译器重命名
void twiddleBits(unsigned char bits);
void simulate(int iterations);
...
#ifdef __cplusplus
}
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you want to mix C++ and C in the same program, remember the following simple guidelines:&lt;/p&gt;
&lt;p&gt;■ Make sure the C++ and C compilers produce compatible object files.&lt;/p&gt;
&lt;p&gt;■ Declare functions to be used by both languages extern &quot;C&quot;.&lt;/p&gt;
&lt;p&gt;■ If at all possible, write main in C++.&lt;/p&gt;
&lt;p&gt;■ Always use delete with memory from new; always use free with memory from malloc.&lt;/p&gt;
&lt;p&gt;■ Limit what you pass between the two languages to data structures that compile under C; the C++ version of structs may contain nonvirtual member functions.&lt;/p&gt;
&lt;h3&gt;Item 35: Familiarize yourself with the language standard&lt;/h3&gt;
&lt;p&gt;熟悉语言标准！多看看RFC！&lt;/p&gt;
</content:encoded><category>C++</category><category>Book</category><author>tang-hi</author></item><item><title>Return Value Optimization</title><link>https://tangdh.life/posts/c/rvo/</link><guid isPermaLink="true">https://tangdh.life/posts/c/rvo/</guid><description>这篇文章是因为在油管上看了Jon Kalb在2018年的CppCon上做的演讲，深受启发，决定换一个视角来审视C++的RVO机制。</description><pubDate>Sun, 15 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;之所以写这篇文章是因为在油管上看了Jon Kalb在2018年的CppCon上做的&lt;a href=&quot;https://www.youtube.com/watch?v=IZbL-RGr_mk&amp;amp;list=PLhPcHQ7xzdwcG9OFqP4Xg_7nn6RGVFsw9&amp;amp;index=1&quot;&gt;演讲&lt;/a&gt;，深受启发，决定换一个视角来审视C++的RVO机制。&lt;/p&gt;
&lt;h3&gt;1. calling conventions&lt;/h3&gt;
&lt;h4&gt;1.1 返回值为int, float....&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;int simple() {
    return 1;
}

int main() {
	return 1 + simple();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述的代码经过编译后得到的汇编代码如下所示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;simple():
        push    rbp
        mov     rbp, rsp
        mov     eax, 1
        pop     rbp
        ret
main:
        push    rbp
        mov     rbp, rsp
        call    simple()
        add     eax, 1
        pop     rbp
        ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为是RVO，所以我们只关心 &lt;strong&gt;return value&lt;/strong&gt;，我们可以发现simple中的一条汇编语句&lt;code&gt;move eax 1&lt;/code&gt;,这条语句对应于simple中的&lt;code&gt;return 1;&lt;/code&gt;也就是说在C++中，我们会将需要返回的值存在&lt;code&gt;rax&lt;/code&gt;寄存器中。当然前提是rax可以放下需要返回的值。&lt;/p&gt;
&lt;h4&gt;1.2 返回值为struct类型&lt;/h4&gt;
&lt;p&gt;如果返回值为struct类型，也就是rax不一定可以放的下该类型应该怎么办？&lt;/p&gt;
&lt;p&gt;观察下面的代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct BigObject {
    int data[6];
};

BigObject big() {
    return BigObject{1,2,3,5,6,7};
}

int main() {
	BigObject bo = big();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该代码经过编译后得到的汇编代码如下所示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;big():
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax], 1
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax+4], 2
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax+8], 3
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax+12], 5
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax+16], 6
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax+20], 7
        mov     rax, QWORD PTR [rbp-8]
        pop     rbp
        ret
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 32
        lea     rax, [rbp-32]
        mov     rdi, rax
        call    big()
        mov     eax, 0
        leave
        ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到仍旧是将返回值存入&lt;code&gt;rax&lt;/code&gt;中，只不过这里的&lt;code&gt;rax&lt;/code&gt;更像是一个指针，通过offset将对应的值存入&lt;code&gt;mov     DWORD PTR [rax+4], 2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;整个调用过程我们用两张图来进行总结&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/pic/stack.png&quot; alt=&quot;simple&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/pic/stack-big.png&quot; alt=&quot;big-call&quot; /&gt;&lt;/p&gt;
&lt;p&gt;整个调用过程就算不太了解也没有关系，我们只需要记住函数的返回值一定是存在&lt;code&gt;rax&lt;/code&gt;中，区别在于是把&lt;code&gt;rax&lt;/code&gt;当作int这种标量，还是当作指针对待。&lt;/p&gt;
&lt;h3&gt;2 使用RAX实现RVO&lt;/h3&gt;
&lt;p&gt;RVO实际上就是在函数返回时，将原本需要进行的拷贝操作省略掉，那么怎么实现呢？通过上面的描述，我们知道返回值实际都在&lt;code&gt;rax&lt;/code&gt;中，那么只要我们在调用函数前，自己开辟一块空间（在栈帧中），然后将这块空间的地址给到 &lt;code&gt;rax&lt;/code&gt;，等到函数返回时，我们就无须对返回的临时变量进行拷贝，因为返回值已经在&lt;code&gt;rax&lt;/code&gt;（我们开辟的空间）中了，我们可以直接使用。&lt;/p&gt;
&lt;p&gt;还是用一张图来总结这个过程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/pic/stack-summary.png&quot; alt=&quot;summary&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3.RVO 的适用场景&lt;/h3&gt;
&lt;p&gt;当粗略的了解了RVO的实现原理后，我们便可以，从另一种视角对RVO的适用场景进行审视。&lt;/p&gt;
&lt;h4&gt;3.1 unamed rvo :white_check_mark:&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;Foo URVO() { return Foo(); }
Foo foo = URVO();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种场景下，因为整个返回值都是临时变量，所以我们可以直接在开辟的空间中进行构造，无需拷贝。因此这种场景下，RVO是可以被使用的。&lt;/p&gt;
&lt;h4&gt;3.1 named rvo :white_check_mark:&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;Foo NRVO() {
  Foo foo;
  return foo;
}
Foo foo = NRVO();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种场景下，返回值是一个局部变量，但是我们可以在开辟的空间中直接对局部变量进行构造。因此这种场景下，RVO是可以被使用的。&lt;/p&gt;
&lt;h4&gt;3.3 named rvo with compile-time condition :white_check_mark:&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;Foo NRVO_Compile_BRANCH(int x) {
  Foo foo;
  if (x % 2 == 0) {
    return foo;
  } else {
    return foo;
  }
}

Foo foo = NRVO_Compile_BRANCH();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种场景下，返回值是一个局部变量，并且不论条件变量如何，我们都明确只返回那一个局部变量(编译期即可确定)，因此我们可以直接在开辟的空间中构造局部变量，rvo适用。&lt;/p&gt;
&lt;h4&gt;3.4 named rvo with run-time condition :x:&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;Foo NRVO_RUNTIME_BRANCH(int x) {
  Foo foo, foo1;
  if (x % 2 == 0) {
    return foo;
  }
  return foo1;
}

Foo foo = NRVO_RUNTIME_BRANCH();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种场景下，我们有两个局部变量，且这两个局部变量都有可能成为返回值，只有在runtime我们才能确定，因此我们无法直接在开辟的空间中进行构造（因为只有运行到return时，我们才知道那一个是返回值，而这时候该值早就已经构造好了）,只能通过拷贝构造函数进行生成，rvo不适用。&lt;/p&gt;
&lt;h4&gt;3.5 return global variable :white_check_mark:&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;Foo Global_FOO() { return global_foo; }

Foo foo = Global_FOO();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尽管很多博客文章都说这种场景下，不会使用RVO，但是经过测试结果显示，虽然我们返回的是全局变量，该变量早就已经构造完成，有它专属的物理地址，但是我们依然可以在返回地址处直接使用拷贝构造函数进行生成。rvo适用。&lt;/p&gt;
&lt;p&gt;以下是我做的实验&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Foo {
  Foo() : data(0), id(++version) {
    ++object_create;
    cout &amp;lt;&amp;lt; &quot;Foo ctor, version :&quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; endl;
  }

  Foo(const Foo &amp;amp;rhs) : data(rhs.data), id(++version), aaaa(rhs.aaaa) {
    ++object_create;
    cout &amp;lt;&amp;lt; &quot;Foo copy ctor, version: &quot; &amp;lt;&amp;lt; rhs.id &amp;lt;&amp;lt; &quot; -&amp;gt; &quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; endl;
  }


  Foo &amp;amp;operator=(const Foo &amp;amp;rhs) {
    data = rhs.data;
    cout &amp;lt;&amp;lt; &quot;Foo copy assign version: &quot; &amp;lt;&amp;lt; rhs.id &amp;lt;&amp;lt; &quot; -&amp;gt; &quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; endl;
    return *this;
  }


  ~Foo() { cout &amp;lt;&amp;lt; &quot;Foo destory version: &quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; endl; }

  /* data */
  int data;
  int id;
};

Foo global_foo;
Foo foo1 = Global_FOO();
---------------------------------------------------------------
g++ -o enable -O0 -std=c++98 &amp;amp; ./enable
Foo copy ctor, version: 1 -&amp;gt; 1
Foo destory version: 1
create 1 objects

g++ -o disable -O0 -std=c++98 -fno-elide-constructors &amp;amp; ./disable
Foo copy ctor, version: 1 -&amp;gt; 1
Foo copy ctor, version: 1 -&amp;gt; 2
Foo destory version: 1
Foo destory version: 2
create 2 objects
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到开启rvo时的确少调用一次拷贝构造函数,当然其实这也可以认为是对unamed的rvo优化，而不是global的。&lt;/p&gt;
&lt;h4&gt;3.6 return parameter :white_check_mark:&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;Foo Return_Para(Foo foo) { return foo; }
Foo foo = Return_Para();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种场景下，和上一个场景很相似，尽管都需要对参数进行一次拷贝，但是RVO可以在返回时进行优化直接拷贝到新开辟的空间中，从而相比与禁止RVO少调用一次拷贝构造函数。&lt;/p&gt;
&lt;p&gt;这次我们通过汇编代码进行论证&lt;/p&gt;
&lt;p&gt;开启RVO的汇编代码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/pic/compiler1.png&quot; alt=&quot;开启RVO&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/pic/compiler2.png&quot; alt=&quot;开启RVO&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到整个过程中仅仅只调用了一次拷贝构造函数。&lt;/p&gt;
&lt;p&gt;关闭RVO的汇编代码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/pic/compiler3.png&quot; alt=&quot;closervo&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/pic/compiler4.png&quot; alt=&quot;closervo&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到一共调用了两次拷贝构造函数，这证明了RVO确实在发生作用。&lt;/p&gt;
&lt;h4&gt;3.7 return by move :x:&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;Foo Return_BY_MOVE() {
  Foo foo;
  return std::move(foo);
}
Foo foo = Return_BY_MOVE();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个没有什么好说的，C++标准不允许，当你使用&lt;strong&gt;std::move&lt;/strong&gt;时，会禁用RVO。&lt;/p&gt;
&lt;h4&gt;3.8 一个没啥用的发现&lt;/h4&gt;
&lt;p&gt;当你的class没有自己写拷贝构造函数，并且里面的成员变量都是没有自己定义的拷贝构造函数，这时候开启RVO，编译器甚至不会给你生成拷贝构造函数。&lt;/p&gt;
&lt;p&gt;测试代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Foo {
  Foo() : data(0), id(++version) {
    ++object_create;
    cout &amp;lt;&amp;lt; &quot;Foo ctor, version :&quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; endl;
  }

//   Foo(const Foo &amp;amp;rhs) : data(rhs.data), id(++version) {
//     ++object_create;
//     cout &amp;lt;&amp;lt; &quot;Foo copy ctor, version: &quot; &amp;lt;&amp;lt; rhs.id &amp;lt;&amp;lt; &quot; -&amp;gt; &quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; endl;
//   }

//   Foo &amp;amp;operator=(const Foo &amp;amp;rhs) {
//     data = rhs.data;
//     cout &amp;lt;&amp;lt; &quot;Foo copy assign version: &quot; &amp;lt;&amp;lt; rhs.id &amp;lt;&amp;lt; &quot; -&amp;gt; &quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; endl;
//     return *this;
//   }

  ~Foo() { cout &amp;lt;&amp;lt; &quot;Foo destory version: &quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; endl; }

  /* data */
  int data;
  int id;
// std::vector&amp;lt;int&amp;gt; vec;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开启RVO生成的汇编代码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/pic/compiler5.png&quot; alt=&quot;开启rvo&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到完全就是寄存器和堆栈的运算。&lt;/p&gt;
&lt;p&gt;当你关闭RVO，生成的汇编代码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/pic/compiler6.png&quot; alt=&quot;closervo&quot; /&gt;&lt;/p&gt;
&lt;p&gt;编译器会为你生成拷贝构造函数，并且被调用。&lt;/p&gt;
&lt;p&gt;但如果&lt;strong&gt;你的class没有自己写拷贝构造函数，并且里面的成员变量都是没有自己定义的拷贝构造函数&lt;/strong&gt;这两个条件有一个没满足，编译器都会为你生成拷贝构造函数。&lt;/p&gt;
&lt;p&gt;总结，RVO只要被开启，当你返回时基本总是会被使用，即直接在开辟的新空间中直接进行生成，从而节省了一次拷贝。但对于某些特殊的情况，例如返回参数，返回全局变量时，对这种对象的拷贝是无法被省略的。&lt;/p&gt;
&lt;h3&gt;RVO与std::move&lt;/h3&gt;
&lt;p&gt;当std::move参与到rvo时，情况又会有点微妙。&lt;/p&gt;
&lt;p&gt;先说一个非常一般，并且绝大多数都对的结论，&lt;strong&gt;当class可以被move，那么当你返回时，如果可以直接构造那么直接构造，如果不能，调用移动构造函数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;其实用一句话说，你return的值会被当作右值处理，要么使用RVO，要么使用移动构造函数，但也有例外。&lt;/p&gt;
&lt;p&gt;我们先看官网文档&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/pic/doc.png&quot; alt=&quot;doc&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注意这里加粗的意思是说如果我们return的类型和函数申明的返回类型对不上，那么就会把返回值看作左值也就是会调用拷贝构造函数。例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Foo {
  Foo() : data(0), id(++version) {
    ++object_create;
    cout &amp;lt;&amp;lt; &quot;Foo ctor, version :&quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; endl;
  }

  Foo(const Foo &amp;amp;rhs) : data(rhs.data), id(++version), aaaa(rhs.aaaa) {
    ++object_create;
    cout &amp;lt;&amp;lt; &quot;Foo copy ctor, version: &quot; &amp;lt;&amp;lt; rhs.id &amp;lt;&amp;lt; &quot; -&amp;gt; &quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; endl;
  }

  Foo(Foo &amp;amp;&amp;amp;rhs) : data{rhs.data}, id{++version} {
    cout &amp;lt;&amp;lt; &quot;Foo move ctor, version: &quot; &amp;lt;&amp;lt; rhs.id &amp;lt;&amp;lt; &quot; -&amp;gt; &quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; endl;
  }

  Foo &amp;amp;operator=(const Foo &amp;amp;rhs) {
    data = rhs.data;
    cout &amp;lt;&amp;lt; &quot;Foo copy assign version: &quot; &amp;lt;&amp;lt; rhs.id &amp;lt;&amp;lt; &quot; -&amp;gt; &quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; endl;
    return *this;
  }

  Foo &amp;amp;operator=(Foo &amp;amp;&amp;amp;rhs) {
    data = rhs.data;
    cout &amp;lt;&amp;lt; &quot;Foo move assign version: &quot; &amp;lt;&amp;lt; rhs.id &amp;lt;&amp;lt; &quot; -&amp;gt; &quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; endl;
    return *this;
  }

  ~Foo() { cout &amp;lt;&amp;lt; &quot;Foo destory version: &quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; endl; }

  /* data */
  int data;
  int id;
  Complex complex;
  std::vector&amp;lt;int&amp;gt; aaaa;
};

struct FOOS : public FOO {
    
}

FOO return_derived() {
    FOOS foos;
    return foos; // treat is as lvalue
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为FOOS并不完全是FOO，所以与FOO(FOO &amp;amp;&amp;amp;rhs)对不上，因此会将返回值视作左值，导致RVO，move都无法使用。&lt;/p&gt;
&lt;p&gt;至此，我对于RVO的总结就全部完成了。&lt;/p&gt;
</content:encoded><category>C++</category><author>tang-hi</author></item><item><title>Map Reduce</title><link>https://tangdh.life/posts/paper/mapreduce/</link><guid isPermaLink="true">https://tangdh.life/posts/paper/mapreduce/</guid><description>MapReduce本质上是为了处理大数据而诞生的框架，它含有两个原语，分别是Map和Reduce(从函数式编程中借鉴过来的概念)，而这两个原语因为抽象程度高，因此可以相互组合完成大部分的大数据处理任务</description><pubDate>Tue, 15 Nov 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot;
href=&quot;https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css&quot;
integrity=&quot;sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ&quot;
crossorigin=&quot;anonymous&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://research.google/pubs/pub62/&quot;&gt;MapReduce&lt;/a&gt;是谷歌在2004年发表的论文,根据它在论文中的描述&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;MapReduce is a programming model and an associated implementation for processing and generating large data sets. Users specify a map function that processes a key/value pair to generate a set of intermediate key/value pairs, and a reduce function that merges all intermediate values associated with the same intermediate key. Many real world tasks are expressible in this model, as shown in the paper.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;MapReduce本质上是为了处理大数据而诞生的框架，它含有两个原语，分别是Map和Reduce(从函数式编程中借鉴过来的概念)，而这两个原语因为抽象程度高，因此可以相互组合完成大部分的大数据处理任务&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Map: 将一组数据转化为另一组数据,可以将这个任务看作输入为一个单一元素，输出为一个tuple的建值对&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// ele是你需要处理的原始数据
// 输出为你根据原始数据生成的键值对
func map(String ele) {
    return (generateKey(ele), generateValue(ele))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Reduce: 将多个Map任务的结果按Key聚合后作为输入, 对该输入进行计算后输出最终结果&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 输入中的Key为多个Map任务中产生的一个Key
// values为map任务产生的所有(k,v)中属于这个key的value集合
// 即 values = values + (value_1 | if (key_1, value_1) key_1 = key)
func reduce(Key key, List&amp;lt;Value&amp;gt; values) {
    result := process(values)
    return (key, values)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;MapReduce解决了什么问题&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;通过分布式的方法解决大数据处理的问题&lt;/li&gt;
&lt;li&gt;Fault Tolerance (可以部署在商用服务器上，容忍一定的机器损坏)&lt;/li&gt;
&lt;li&gt;程序员只需专注于编写数据的处理程序(即map和reduce这两个函数),无需关注分布式的问题,便可以让其进行分布式计算&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;MapReduce的实现&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Many different implementations of the MapReduce interface are possible. The right choice depends on the environment. For example, one implementation may be suitable for a small shared-memory machine, another for a large NUMA multi-processor, and yet another for an even larger collection of networked machines&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;根据论文中的描述,MapReduce只是一个计算模型，你可以按照你自己的需要，设计并实现最适合你需求的架构.在这里我们介绍谷歌所使用的架构.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center&quot;&amp;gt;
&amp;lt;img src=&quot;https://hayesx-1302722143.cos.ap-singapore.myqcloud.com/img/MapReduce.png&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h4&gt;角色描述&lt;/h4&gt;
&lt;p&gt;从图中我们可以看出在整个MapReduce中有三种不同的角色&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;InputFile: 待处理的输入文件&lt;/li&gt;
&lt;li&gt;Master: 调度整个任务的执行，并且检测Worker是否存活&lt;/li&gt;
&lt;li&gt;Worker: 听从Master调度,并执行用户指定的Map或者Reduce任务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们先详细介绍这三种不同的角色，然后描述MapReduce的总体流程&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;InputFile&lt;br /&gt;
因为MapReduce的应用场景是大数据处理，所以输入的文件较大，往往是无法完全放在内存之中，因此我们需要将输入的文件分割为
大小相等的文件块(split1, split2, split3....),在谷歌的实现中,文件分割后，每份大小为16-64MB&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Master&lt;br /&gt;
Master是一个较为特殊的角色，全局仅有一个Master,它有以下的几个职责&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;监听Worker状态,当Worker处于Idle状态时，给他分配任务(map/reduce)&lt;/li&gt;
&lt;li&gt;通过心跳探活Worker,当Worker宕机时,执行容灾操作，即重新执行.&lt;/li&gt;
&lt;li&gt;提供给Worker需要的信息,以使其正常运行.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Worker&lt;br /&gt;
Worker是实际执行用户编写程序的角色，它听从Master的调度（执行map或者reduce）,并且从Master那里获得执行程序所需要的一切信息
并将执行结果反馈给Master.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;总体流程&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;用户提交需要计算的任务,和输入文件&lt;/li&gt;
&lt;li&gt;Master接收到任务后，任务会被分解为M个Map任务，R个Reduce任务.&lt;/li&gt;
&lt;li&gt;Master不断选择处于Idle状态的机器，并让他们执行Map任务，直至Map任务被全部执行完成.&lt;/li&gt;
&lt;li&gt;被调度执行Map任务的Worker会读取对应的文件分片，并对该文件进行解析后作为用户Map程序的输入，然后将Map输出的键值对缓存在内存之中,最终写在本地磁盘上.
写在磁盘上的键值对文件，会根据Key划分为R个文件. ie.(K % R)&lt;/li&gt;
&lt;li&gt;当Map执行完成后会向Master汇报执行完成，并且将所有键值对文件的位置告知Master&lt;/li&gt;
&lt;li&gt;当所有Map任务完成后，Master会选择状态处于Idle的Worker,让其执行Reduce任务，同时会告知他所需要处理的键值对文件位置&lt;/li&gt;
&lt;li&gt;当Worker被调度执行Reduce任务时, 他首先会发起一个rpc来读取键值对文件,当他将所有文件读取完毕后,他对键值对进行排序,这样子相同键的键值对就会聚集在一起
如果键值对文件过大，无法全部保存在内存之中，那么需要进行外排序.Worker最后将具有相同键的值聚合在一起形成(Key, list Value)传给Reduce函数进行计算,并将结果
写在文件中&lt;/li&gt;
&lt;li&gt;当所有的Reduce执行完成后,MapReduce也就执行结束了.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;MapReduce如何解决了那些问题&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;通过分布式的方法解决大数据处理的问题&lt;br /&gt;
通过一台Master来调度多台Worker可以实现分布式计算，同时我们可以注意到Master中需要记录的Worker信息所需要的存储空间较小
因此可以使用上千的Worker来同时计算,而不会给Master带来太大的负担.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Fault Tolerance&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Master Fail&lt;br /&gt;
Master会周期写内部数据的checkpoint,如果Master宕机，一个新的备份机器可以通过读取checkpoint来恢复状态.
我们可以发现根据以上的设计,Master宕机后就算丢失了一些任务的进度，例如,不知道map_3任务已经执行完成,但是通过
重新执行，对于最后结果的正确性并没有影响.&lt;/li&gt;
&lt;li&gt;Worker Fail&lt;br /&gt;
如果Worker宕机，那么该Worker完成的所有Map任务全部设置为取消，并且需要全部重新执行,还没有执行完的Map和Reduce任务也全部取消,并需要全部执行.
之所以完成的所有Map任务需要全部重新执行,是因为Map任务的结果写在本地磁盘上，当机器宕机时，这些结果就全部不可获取了，因此需要全部重新执行.
而执行完成的Reduce任务不需要重新执行，是因为Reduce任务的结果写在了分布式文件系统上.我们可以发现，只需要通过简单的重新执行，便可以保证即使机器宕机
仍然可以完成分布式计算.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;程序员只需专注于编写数据的处理程序(即map和reduce这两个函数),无需关注分布式的问题,便可以让其进行分布式计算&lt;br /&gt;
根据MapReduce的设计，程序员唯一需要做的就是map和reduce这两个函数，其他的分布式调度，容灾等策略均在MapReduce内部完成.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;MapReduce的优化&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;任务的粒度&lt;/strong&gt;:MapReduce一般会划分为M个Map任务和R个Reduce任务,M和R的选择一般会远大于机器数量，这样有利于负载均衡，同时如果机器宕机的话，也可以快速恢复.论文中给的例子是
当有2000台机器时，M=200,000 R=5000&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backup机制&lt;/strong&gt;: 实际生产环境中，我们经常会遇到长尾效应,即有某几台机器执行的任务特别慢，从而拖累了整体任务的进度.MapReduce通过Backup机制来解决.即同时给多个机器发出同样的任务
任意一台机器返回结果即视为任务结束.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Combiner函数&lt;/strong&gt;: combiner函数是在Map执行后再执行的函数。举一个例子，word count中，因为map函数会产生大量的(the,1),这些数据都会通过网络发送给Reduce
这加大了无谓的网络带宽.因此使用combiner函数可以在map后聚合这些数据，再传给reduce减少网络带宽.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跳过Bad Records&lt;/strong&gt;: 用户编写的Map和Reduce函数可能存在Bug,这就导致当Master给Worker分配任务时，会将该机器打挂，而后Master
再让其他worker重新执行，再次打挂Worker，最坏的可能是把整个集群打挂,因此谷歌在启动MapReduce时，会注册相应的signal handler,当特定的signal被捕获时，例如segment fault等,
会给Master发送一条UDP，当Master发现相同的UDP &amp;gt;= 1时，就会拒绝再次调度对应的map/reduce任务了.&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><category>Distributed System</category><author>tang-hi</author></item></channel></rss>