Ninja: 使用文件特征而不是时间戳的选项

创建于 2018-08-14  ·  15评论  ·  资料来源: ninja-build/ninja

如其他问题所述,使用时间戳来确定是否重建某些内容可能会出现问题。 虽然时间戳方便且相对较快,但通常希望根据文件本身的某些固有特征(例如其内容的哈希)来关键构建决策。

我经常使用 git,只是更改分支会触发重建,这很烦人。 理想情况下,我可以从我当前的工作分支切换到其他分支,不接触任何文件,然后切换回原始分支,根本不需要重建任何东西。 据我所知,如果构建系统使用时间戳,这是不可能的。 使用文件哈希可以解决这个特定问题。

我了解使用文件哈希或其他此类固有文件特性可能会减慢 Ninja 的速度,因此使用它们应该是一种选择。

feature

最有用的评论

我也在工作中使用了散列,并取得了巨大的成功。 它基于 #929,但带有一堆补丁,如https://github.com/moroten/ninja/commits/hashed 所示。 hash_input = 1对选定的规则非常方便。 我的分支仍然包含一个错误,其中文件stat过于频繁, O(n^2)而不是O(n) 。 该错误与虚假边缘有关。

一个问题是如何处理虚假边缘。 我使用假边缘来分组例如头文件。 因此,我的实现通过虚假边缘递归迭代。 它还与#1021 中的错误有关。

另一个想法是让哈希成为忍者的第一类成员,即将哈希移动到构建日志中。 使用 SHA256 将是增加对 Bazel 的远程执行 API 支持的一步。 C++ 实现可以在https://gitlab.com/bloomberg/recc/ 找到。 那不是很好吗?

不幸的是,整理出虚假边缘的语义可能会破坏向后兼容性。

所有15条评论

929有一个实现。 即使它每天成功地用于(在一个 fork 中)数千次构建,它也没有被考虑用于合并。

929 是单线程的,因此可能会像ccache或其他解决方案一样慢。 另外我认为这应该是一个命令行标志,因此它不需要更改构建定义。

它不能(或至少不应该)是命令行标志,因为散列将适用于所有规则。 散列例如链接规则的所有输入是昂贵的,因此是不希望的,而源文件及其已知的依赖关系是很好的候选者。 对于这种区别,它必须是构建描述的一部分。 此外,要使用该功能,您应该始终一致地使用该标志。 不只是有时。

响应单线程参数:是的,它向单线程循环添加指令。 实际上,只有当单个线程的工作量超过它可以工作的量时才重要(即完成的规则​​多于一个线程可以处理的量(depslog+hashlog+...))。 只有这样散列才会疼。 否则,单线程循环无论如何都会等待作业完成。 即使使用 -j1000 实验,我们也从未见过使用散列的忙碌忍者。 (对于快速完成的规则​​,散列无论如何对安全时间都不感兴趣。)

还要考虑:使用 murmur 散列的散列相当快,即使是大型源文件也只需要几毫秒即可散列。 此外,在编译器看到源文件(和依赖项)之后立即发生散列。 因此它们通常是从文件系统缓存中读取的。
由于在构建期间发生散列(与执行规则并行),因此总体构建时间通常不会受到可测量的影响。

最后,#929 中的实现是可选的,对于不使用该功能的人来说是免费的(除了 if 语句)。

散列例如链接规则的所有输入是昂贵的,因此是不希望的,而源文件及其已知的依赖关系是很好的候选者。

我会说,特别需要对链接器的输入进行散列处理,因为它通常会导致链接被完全跳过(例如,格式更改或注释更改时)。 随着目标文件的编译逐步完成,哈希计算可以在构建运行时进行(正如您所指出的)。

如果它真的太慢(例如使用大型静态库),我们可以考虑只为纯输入而不是中间文件实现哈希。 这至少可以解决“切换 Git 分支导致完全重建”的情况。

此外,要使用该功能,您应该始终一致地使用该标志。 不只是有时。

我会说这是一个优势:如果我在单个分支上工作并且想要快速迭代,我不会使用哈希。 如果我要比较不同的功能分支,我会使用哈希。

此外,要使用该功能,您应该始终一致地使用该标志。 不只是有时。

我会说这是一个优势:如果我在单个分支上工作并且想要快速迭代,我不会使用哈希。 如果我要比较不同的功能分支,我会使用哈希。

但是接下来需要一种从非散列切换到散列的方法,这意味着需要对文件的当前状态进行散列,因此下一次重建可以使用散列(可能不存在或已出如果您没有通过标志,则为日期)。

我猜散列仍然首先使用时间戳,因此如果时间戳匹配,则无需比较散列。 这意味着,前几个构建可能会不必要地重新编译某些文件,但这不应该经常发生(毕竟大多数时候时间戳启发式是正确的)。

我也在工作中使用了散列,并取得了巨大的成功。 它基于 #929,但带有一堆补丁,如https://github.com/moroten/ninja/commits/hashed 所示。 hash_input = 1对选定的规则非常方便。 我的分支仍然包含一个错误,其中文件stat过于频繁, O(n^2)而不是O(n) 。 该错误与虚假边缘有关。

一个问题是如何处理虚假边缘。 我使用假边缘来分组例如头文件。 因此,我的实现通过虚假边缘递归迭代。 它还与#1021 中的错误有关。

另一个想法是让哈希成为忍者的第一类成员,即将哈希移动到构建日志中。 使用 SHA256 将是增加对 Bazel 的远程执行 API 支持的一步。 C++ 实现可以在https://gitlab.com/bloomberg/recc/ 找到。 那不是很好吗?

不幸的是,整理出虚假边缘的语义可能会破坏向后兼容性。

同时,我为我在 Chromium 中切换分支的用例设计了一个解决方案(这特别痛苦); 我使用的 Go 小程序和脚本在这里: https ://github.com/bromite/mtool

随意调整它以适应您的用例,如果它适用于 Chromium,我敢打赌它也适用于较小的构建项目(而且我的运行时间可以忽略不计)。 唯一的缺点是,如果您按原样使用我在那里发布的脚本,它将在每个 git 存储库父目录中到处乱扔.mtool文件,但没有什么全局gitignore无法解决的。

有趣的是,我使用git ls-files --stage输出来满足散列需求; 如果您的构建依赖于非索引文件,则可以(但效率较低)要求 git 对非索引文件进行哈希处理。

功能方面的人希望实现这里讨论的功能 ninja 可以在内部执行相同的操作(不依赖 git)并获得类似的性能结果。

快速浏览后,除了输入文件之外,这些 ninja 补丁似乎没有对编译器命令行进行哈希处理。 我错过了什么吗?

命令行已经被 ninja 散列并存储在构建日志中。

我猜散列仍然首先使用时间戳,因此如果时间戳匹配,则无需比较散列。 这意味着,前几个构建可能会不必要地重新编译某些文件,但这不应该经常发生(毕竟大多数时候时间戳启发式是正确的)。

那将是非常错误的。 如果时间戳匹配,可以省略比较哈希; 构建系统可以假设依赖项已被修改,如果它没有真正修改(仅触及),那么构建将是次优但正确的。 但是,如果时间戳匹配,仍然有可能修改了依赖项并且它的时间戳被强制重置(编辑:或者目标被触摸并因此比它的依赖项更新)。 如果不通过比较哈希进行双重检查,这将导致不正确的构建。

我猜 Ninja 已经假设并在时间戳匹配时跳过所有工作。 因此,在您的示例中,无论如何这将是一个不正确的构建。

我不知道(可能是由于我的无知)任何工具会产生不同的输出并保留以前的时间戳。 为什么要这样做?

恕我直言,当时间戳匹配时跳过哈希检查是一个非常有效的优化。

哈希检查将简单地避免一些“假脏”重建,同时保持 Ninja 已经提供的现有语义。

问题不是错误的脏重建,而是错误的清洁非重建。 git checkout 会触及它覆盖的所有内容。 它可以使目标比依赖项更新(是的,人们出于各种正当理由提交生成的代码)。 在这种情况下,哈希检查将防止错误清洁的非重建。

@rulatir我想我基本上明白你在说什么:) 我想这就是 Bazel 和其他基于哈希检查的构建系统真正反对树内目标输出的原因之一。

虽然,如果构建系统会检查目标是否比以前已知的时间更新,并在必要时重建,这个问题不会得到解决吗?

@rulatir我想我基本上明白你在说什么:) 我想这就是 Bazel 和其他基于哈希检查的构建系统真正反对树内目标输出的原因之一。

哈希如何取决于文件的位置?

虽然,如果构建系统会检查目标是否比以前已知的时间更新,并在必要时重建,这个问题不会得到解决吗?

我知道使用时间戳的主要好处是避免需要维护一个单独的数据库来跟踪“以前已知的”版本签名。 如果你愿意放弃这个好处,为什么这些签名不应该是哈希?

此页面是否有帮助?
0 / 5 - 0 等级