“忍者”的成功与失败

2020-05-13 03:27:57

大约九年前,我发布了“忍者”,这是一个几乎可以与make相媲美的构建系统。当时我有点不好意思分享我的辅助项目,但从那时起,它就变得很受欢迎。

我再也不能随便列出所有的用户了,但是一些使用忍者的大项目包括:

Android把它用在系统的一些我一直不太理解的大组件上;

所有介子项目,这似乎越来越成为自由软件世界中使用的构建系统;

许多其他使用忍者和CMake的项目(例如,SWIFT编程语言的构建说明告诉您安装忍者)。

到目前为止,忍者是我最成功的开源项目,这取决于你如何量化成功。(我的其他项目,如Chrome拥有更多用户,但我只负责部分Chrome;忍者也为合作者做出了重要贡献,但感觉更像是我的。)。我在2011年发布了“忍者”,在2014年放弃了“忍者”项目的所有权,自那以后,它再次交给了第三个维护者,所以现在我在故事中的角色差不多结束了,我想在这里反思一下我学到了什么。

如果我用一句话来总结我学到的东西,那就是:我们谈论编程就像是写代码,但代码最终不如架构重要,架构最终不如社会问题重要。

也就是说,作为程序员,我们喜欢谈论问题,就好像它们主要是技术问题一样--我如何优化这个循环,以便从这项服务中挤出更多的钱呢?&根据我的经验,当我的经验表明,技术几乎总是次要于更大的图景因素时。从那以后,我看到了同样的观察,换句话说,许多人描述了从初级工程师到高级工程师的历程,这对我的职业生涯来说总是非常正确的,所以我也希望在这次回顾中能对这意味着什么给予一些洞察力。

忍者具体做的事情非常简单;给出了要求的描述,我希望一个称职的本科生,也许是系统课程的学生,能够在没有太多帮助的情况下炮制出它的基本版本。总而言之,用户给了忍者一个忍者构建文件,该文件(省略了一些细节)包含了你希望忍者运行的所有命令,以及每个命令消耗和产生的文件。忍者加载此文件,检查各种文件的修改时间戳,并并行执行使所有内容保持最新所需的命令。与make相比(它做的事情差不多),忍者在它的输入构建语言中提供了更少的功能,并且主要是围绕着让它做的几件事情非常快来构建的。

忍者所做的几件事是:(1)解析和解释该构建文件;(2)检查其输入的修改时间;(3)执行所需的命令。我们的目标是尽可能快地进入步骤3,即使是在大型(>;100k输入文件)项目中也是如此,这样做是仔细但很小的优化的集合。举一个小例子,忍者小心地尽可能早地将每个输入文件路径映射到一个唯一的内存中对象,然后在这些对象之间使用指针比较来测试路径相等(有效地,内嵌字符串)。我为“开源软件的性能”这本书写了一个关于忍者的章节,它讲述了一些低级的忍者故事,你可以在网上读到快速的技术细节。

多年来,许多人都重写了忍者。这是一个足够小的项目,可以尝试用你最喜欢的语言来实现,这是一件很有趣的事情。例如,llbuild和Shake都支持忍者文件作为输入,而武士则几乎是一个文件一个文件地重新实现(代码更少,但功能更少,而且没有测试(!))。忍者是相当容易实现的乐趣的20%,剩下的80%是";只是";一些繁琐的细节。据我所知,没有人实现得更快。

有些忍者的碎片是费了很大力气才拿到的,然后显然是回想起来的。我认为很多数学都是如此,一旦你把想法提炼到它们的本质,它们就会变得显而易见。力量来自于正确的思考问题的方式。我主要是跌跌撞撞地通过忍者的设计,但一旦我在它的另一边,我来看,我意外地击中了一些好的设计空间。这里有几个例子。

图形表示法。Make不能很好地处理构建规则生成多个文件的情况。我不知道make在内部是如何构造的,但我猜它将构建结构表示为文件之间的图形,因为这就是输入语法的样子,而该结构将生成该行为。Ninsainstead在文件和命令之间使用二分图,其中文件节点是命令节点的边,命令节点的边返回到文件。这种表示法更好地捕捉了构建的结构:如果命令的任何输入发生更改,命令就会过期,并且在运行时更新所有输出。(唯一的图形不变量是给定文件最多可以有一条输入边。)。对于这种情况的另一个后果,请注意,命令行本身可以被认为是输入到命令节点,因为如果命令行标志更改,命令就会过期(因此它的输出也会过期)。

副警长的日志。要获得正确的C头依赖关系,您需要使用由C编译器生成的额外依赖关系数据。它在书中的章节中有更多的描述。我记得,我一直在为是否引入数据库以及如何使之与我追求简单的愿望相协调而苦苦挣扎,直到我最终发现了一种最终相当紧凑的表示格式。(不幸的是,它在某些重要方面仍然是错误的,但哦,好吧。)。

端到端/仅限崩溃。忍者不是一个持久的守护进程,而是在每次执行时从头开始执行所有工作。这是故意的,混合了端到端原则和仅限崩溃软件的见解,也就是说:考虑到你有时需要从头开始运行忍者,如果你让它变得更快,那么你就不需要在网上构建第二个代码路径了。这句话的意思是:考虑到你有时需要从头开始运行忍者,如果你让它变得更快,那么你就不需要在网上构建第二个代码路径。那些可以驻留在内存中的项目最终往往会让它们的创业表现萎靡不振。

文件状态。程序员有时希望构建工具驻留在内存中的原因是,这样他们就可以缓存磁盘上文件的状态。但实际上,内核已经将这些信息缓存到内存中,并将其再次缓存到用户端,这并不能为您节省太多;从Linux获取文件状态是非常快的。忍者甚至只用一条线就能做到这一点。在一台十年前还很快的机器上,你可以在10毫秒内处理30K个文件。(一个编程笑话:一半的性能问题是通过引入缓存来解决的,另一半则是通过删除一个缓存来解决的。)

数量级。经验法则是,你可以通过优化将规模扩大2倍,但要将规模扩大10倍,你需要重新架构。忍者是围绕Chrome的构建而设计的,当时大约有30k个构建步骤。如今,它被用在可能不需要它的小得多的环境中(参见下面关于速度的讨论),以及像Android版本这样规模较大的环境,它无法扩展,很可能需要另一种方法。

指定不足和指定过多。忍者并行执行命令,因此它需要用户提供足够的信息才能正确执行。但在另一个极端,它也不强制要求它有一个完整的建设图景。您可以在这个bug中看到关于这个动态的一个特别讨论(请搜索以查看我的评论)。你必须经常在正当性和方便性或表现之间妥协,当你沿着这个连续体选择一个点时,你应该是有意的。我发现有些程序员在考虑这一动态时很僵硬,在某种程度上很明显,其中一个问题占主导地位,但在我的经验中,相互作用相当微妙;例如,如果程序员最终避免了后者,那么一个以正确性换取便利性的工具总体上可能会产生一个更直接的生态系统,而不是一个更正确但不太方便的替代方案。(这可能是Haskell不太成功的一个原因。现在我从事编程语言工作,我经常看到这种动态的表现。)。

当人们想到构建系统时,他们想到的是广泛的特征,如此广泛,以至于构建系统谈论自身的方式有时在不同的工具中甚至是不可比较的。这些工具的营销文本经常谈到输入语法是多么的用户友好。

忍者的洞察力(在回顾中发现)是,所有这些工具,无论是什么高级功能,最终都必须构建某种类型的动作图:它们想要保持最新的文件,以及要执行的命令。忍者只实现动作图,并让用户选择顶部的另一个生成器程序。

我最初发明这两个程序Split只是因为它正好适合我当时正在做的项目(Chrome),但从那以后我就开始把它视为忍者的主要贡献。

一方面,它让我可以让忍者变得愚蠢但快速,因为任何昂贵的东西(比如*.c&34;的全局)都会被强制放入生成器中。与其他一次完成所有工作的构建系统相比,忍者的设计有效地迫使你在计算完动作图后将其快照到磁盘上。(#34;GLOB for*.C&34;;GLOB for*.C&34;GLOB)与其他一次完成所有工作的构建系统相比,忍者的设计有效地迫使你在计算完动作图后将其快照到磁盘。看待这一点的另一种方式是,它有效地让您跨构建缓存动作图。

另一方面,这也意味着忍者在非常灵活的方式中很有用,因为生成器可以像用户想要的那样高级别(测试是通过在整个源码树中查找名称中带有“Test”的文件来找到的)。重要的是,它迫使使用忍者的开发者决定他们将支付什么费用。如果他们的生成器程序想要在磁盘上到处寻找文件,这是受欢迎的,但这样对他们来说,为什么他们的构建速度很慢就会更明显了。

(我应该在这里指出,生成器和生成的动作图之间的清晰分离并不像我想象的那么容易。忍者最终有很多繁琐的细节,都在努力解决工作属于哪一层的问题,但很难把学到的东西写下来。)

忍者设计的这一方面具有讽刺意味的是,没有任何东西可以阻止其他任何人这样做。例如,Xcode或Visual Studio sbuild系统也可以做同样的事情:提前做大量工作,然后对结果进行快照,以便快速重新执行。我认为,很少有人能做到这一点的原因是,它太诱人了,无法混为一谈。

忍者的近亲是make,它试图包含所有这些面向程序员的功能(使用全局绑定、可变扩展、子化、函数等)。这就产生了一种编程语言,它太弱了,不能表达所有需要的功能(比如Autotools),但仍然足够强大,可以让人们编写速度很慢的Makefile。这隐约是格林斯潘的第十条规则,我在“忍者”中极力想要避免这一点。

默认情况下,忍者并行执行所需的命令。make也可以做到这一点;忍者为此能力借用了相同的标志名称(-j),只是使用了不同的默认值。然而,因为make缺省为串行运行命令,所以相对容易编写一个未指定依赖项的Makefile,因此并行执行它是不安全的。事实上,甚至有一些商业供应商提供某种Makefile加速器工具,帮助人们发现和修复未指定的依赖项。

相反,因为忍者总是并行执行命令(即使是在单核系统上),所以它最终会暴露出像之前这样的错误。这意味着使用忍者构建的程序通常可以安全地并行构建。(忍者没有什么花哨的系统来检测你做错了什么,它只会更经常地造成错误的建筑。)。相反,用户经常忘记或不知道要创建的标志,这使得它也可以并行运行。这让人很尴尬,因为它只是一面旗帜,但仅仅因为它的默认值,对于不小心的用户来说,忍者实际上会比Make快两倍甚至更多。我们得到的教训是,如果你的用户没有真正看到,世界上所有的优化都无关紧要。

在这篇文章中,我已经谈过几次性能,重要的是要注意,在构建系统中有很多不同类型的性能度量需要关注。例如,当我从头开始时,构建需要多长时间?";忍者只专注于大型代码库中增量构建的编辑-编译周期,也就是说,您已经运行了一个构建,您编辑了一个文件,然后运行下一个构建。

当我写忍者的时候,我记得火焰(又名巴泽尔)速度很快,但我已经有很多年没有用过它了。因为这段记忆,我不断尝试让忍者变得更快,试图赶上我对Blaze的记忆。很久以前,我发现Blaze在我所关心的速度度量上并不是特别快;因为Blaze是一个Java程序,即使让BlavingBlaze打印它的帮助,输出也相当慢。(#34;Help;#34;Help)。

我专注于增量构建可能有点傻,但我坚信迭代时间对程序员满意度有很大影响,而忍者正是在编辑-编译循环中使用的,其中1秒和4秒的差异非常关键。我个人认为我比普通程序员对延迟更敏感,但我也相信程序员会感觉到延迟,即使他们没有注意到这一点,它也会影响他们的行为方式。(谷歌最近在这个领域做了一些研究,这在一定程度上证实了我的看法,在这里,我希望他们能公开发表!)。

要向用户传达FAST的许多可能的解释是非常困难的。忍者手册试图警告人们不要在小程序中使用它。从字面上看,引言后的第二段说,如果你的项目很小,忍者对速度的影响可能不会引起注意,并建议使用不同的构建系统。不幸的是,忍者列表卖得很快,而且忍者列表经常有用户试图将其用于他们的微型应用程序,但用户对其功能匮乏感到沮丧。

虽然忍者关注的是增量重建性能,但一些用户报告说,忍者也提高了他们的端到端构建性能。这是无意的,但这是因为忍者(同样是由于几乎什么都不做)在运行这些构建时消耗的CPU非常少,而无论出于什么原因,类似的程序在运行时消耗了更多的CPU,这从底层构建中夺走了CPU。

在我的帖子中,我深入探讨了速度有很多方面需要考虑,而最终重要的是用户对速度的感知,在我的帖子中,快速浏览器意味着什么。忍者的输出非常简洁:对于大多数成功的构建,它打印单行。相反,其他的构建系统倾向于打印一堆(通常是无缘无故的彩色的)输出,上面有关于它正在经历的构建的各个阶段的计时号,这让它们感觉很重。忍者,由于很少说话,让人感觉更像它不在那里。

我开发忍者的初衷是为了与Chrome古怪的一次性构建系统协同工作,后来就到此为止了。不知何故,一个名叫PeterCollingbourne的善良陌生人找到了忍者,并做了工作,将其插入到更受欢迎的CMake构建系统中。忍者的设计非常适合CMake,但(一如既往)有很多细节需要解决,彼得做了大部分,最初是使用忍者来处理LLVM。这不仅仅是CMake,还需要在忍者中建立新的语义。如果有人要为忍者在现实世界中的成功负责,彼得应该受到赞扬。

CMake的作者最终接管了这个集成,我为我对他们的支持有多么糟糕而感到难过;他们对我非常友好和耐心,但我从来没有真正有时间回答他们的请求或顾虑。布拉德,如果你读了这个,我很抱歉!直到今天,我实际上一直在使用CMake,我从来没有时间为它担心。

因为忍者的激励项目也是Chrome和Chrome目标Windows,所以我们让它在Windows上运行。(这是“忍者”的另一部分,主要由一位贡献者撰写。)。

在技术层面上,支持Windows基本上是一个很大的麻烦。在Linux代码不能按原样工作的地方,它要么需要无趣的抽象,要么需要重大的重新设计。举一个前者的例子,在不同的平台上,派生进程和捕获它们的输出是非常不同的,但主要是因为您需要学习完全不同的API。举一个后者的例子,忍者的设计集中依赖于这样一个特性,即你可以快速获得内核缓存文件的最后修改时间,而这在Windows上并不是这样。

但就开发人员而言,Windows仍然是一个巨大的平台,这些开发人员急需工具。潜在的动力是,当有人为Linux开发了一个漂亮的工具时,冲动是想分享它,但当他们为Windows开发工具时,冲动是想把它卖了,因此,因为Windows上没有那么多免费可用的工具。

早期的忍者用户中有这么多是Windowsuser,这让我感到惊讶,但回想起来,这一点是显而易见的:即使每一百名Windows开发人员中只有一人关心忍者,Windows上的人如此之多,以至于他们最终会出现在Windows上。(在Linux Chrome早期,有时我们是这样谈论这个问题的:即使我们让所有使用Chrome的桌面Linux用户都使用Chrome,但就人类总数而言,他们只希望获得额外5%的Windows用户。您可以不同意该数字的具体值,但希望您能理解我的观点。)

我提到我偶然发现了忍者的设计。我后悔没有在构建之前花更多的时间进行研究,但是我打算整个项目只是一个周末的演示,而不是一件严肃的事情。(相关的,请原谅我这个令人尴尬的名字。)。因为我开始意识到在建造一件东西的时候真正理解设计空间是多么的重要。我现在发现自己注意到程序员讨论相关工作是多么罕见,这现在让我发疯。

我在上面使用的术语(动作图)不是我对忍者的看法,而是取自谷歌的构建系统(#34;Blaze&34;/";Bazel&34;Bazel";Bazel";Bazel&34;)。在Bazel中,他们明确讨论了如何生成目标图(更高级的用户概念,如库和二进制),以及如何生成操作图(命令)。

我在上面写了一点关于如何将命令行文本视为命令的输入,就像文件一样。这是更广泛的增量计算概念的一个具体实例,它不仅涵盖构建系统,而且还涵盖UI中的增量。我的朋友Rado在过去一年左右的时间里一直在阅读这方面的研究(!)。正在撰写一系列试图总结这一观点的博客文章,请注意。简街博客已经做了一些工作来总结这一领域;正如你在那里看到的,它甚至与我们最近在Reaction中发现的UI建设的复兴有联系。

精彩的文章#34;build SystemsàlaCarte";讨论了构建系统上下文中的增量计算。我希望这张纸在我写“忍者”之前就已经存在了。

我想通过稍微谈一谈如何成为一个开源的维护者来结束这个话题。正如你可能在其他地方读到的那样,它最终并不是特别有趣。(这场演讲是硬碰硬的P。

..