pack-objects:避免毫无意义的oe_map_new_pack() 调用
签字人:Jeff King
审核人:Derrick Stolee
由于43fa44fa3b (pack-objects: move in_pack out of struct object_entry, 2018-04-14),我们使用一个复杂的系统来节省一些每个对象的内存。
每个object_entry 结构都有一个 10 位字段来存储它所在包的索引。我们使用在程序开始时初始化的 packing_data->in_pack_by_idx, 将这些索引映射到指针。
如果我们有 2^10 个或更多包,那么我们将创建一个包指针数组,每个对象一个。这是packing_data->in_pack。
到目前为止一切顺利。但是还有另一个棘手的情况:如果在我们初始化in_pack_by_idx, 之后有一个新包到达,它还没有索引。我们通过调用oe_map_new_pack() 来解决这个问题,它只是即时切换到不太理想的in_pack 机制,为已经看到的对象分配数组并回填它。
但即使我们已经切换到它,这种逻辑也会起作用(无论是因为我们确实看到了一个新包,还是因为我们一开始就有太多包)。结果不会产生错误的结果,但速度很慢。会发生什么:
但该功能没有任何有用的工作要做。
我们已经在使用in_pack,所以它只是无用地遍历对象的完整列表,试图回填in_pack。
我们最终为近 1000 个包执行此操作(每个包都可能由多个对象触发)。每次触发时,我们可能会迭代多达 500k 个对象。所以在绝对最坏的情况下,这是对象数量的二次方。
解决方案很简单:如果我们已经转换为使用in_pack,,则无需费心检查包是否有索引,因为根据定义,我们不会使用它。所以我们可以将“包是否有有效的索引”检查推到条件的那一半,我们知道我们将要使用它。
遗憾的是,p5303 中的当前测试没有注意到这个问题,因为它的最大值为 1000 包。如果我们在 2000 包时添加一个新测试,它确实显示出改进:
Test HEAD^ HEAD
----------------------------------------------------------------------
5303.12: repack (2000) 26.72(39.68+0.67) 15.70(28.70+0.66) -41.2%
但是,这些多包测试用例运行起来相当昂贵,因此添加越来越大的数字并没有吸引力。相反,我们可以通过使用GIT_TEST_FULL_IN_PACK_ARRAY, 更轻松地展示它,这迫使我们进入绝对最坏的情况:没有包有索引,所以我们将毫无意义地为每个对象触发oe_map_new_pack(),使其真正成为二次方。
以下是数字(git.git),其中包含对 p5303 的更改:
Test HEAD^ HEAD
----------------------------------------------------------------------
5303.3: rev-list (1) 2.05(1.98+0.06) 2.06(1.99+0.06) +0.5%
5303.4: repack (1) 33.45(33.46+0.19) 2.75(2.73+0.22) -91.8%
5303.6: rev-list (50) 2.07(2.01+0.06) 2.06(2.01+0.05) -0.5%
5303.7: repack (50) 34.21(35.18+0.16) 3.49(4.50+0.12) -89.8%
5303.9: rev-list (1000) 2.87(2.78+0.08) 2.88(2.80+0.07) +0.3%
5303.10: repack (1000) 41.26(51.30+0.47) 10.75(20.75+0.44) -73.9%
同样,这些改进对于 1-pack 的情况是不现实的(因为在现实世界中,全阵列解决方案不会发挥作用),但测试更复杂的代码路径会更有用。
在我们研究这个问题的同时,我们还要调整一件事:在oe_map_new_pack() 中,我们调用REALLOC_ARRAY(pack->in_pack)。但除非我们第一次回填它,否则我们永远不会期望到达这里,在这种情况下它将是NULL。
因此,为了清楚起见,让我们将其切换为 ALLOC_ARRAY(),并添加一个 BUG() 来记录期望。不幸的是,这段代码在测试套件中没有得到很好的覆盖,因为它本质上是活泼的(只有在我们重新打包的过程中,如果其他人添加了新包,它才会生效)。