Skip to content

fix: resolve delta trees lazily in a lock-free pool - #2852

Merged
Sebastian Thiel (Byron) merged 2 commits into
mainfrom
delta-tree-parallelism
Jul 30, 2026
Merged

fix: resolve delta trees lazily in a lock-free pool#2852
Sebastian Thiel (Byron) merged 2 commits into
mainfrom
delta-tree-parallelism

Conversation

@Byron

@Byron Sebastian Thiel (Byron) commented Jul 29, 2026

Copy link
Copy Markdown
Member

Tasks

This section is maintained by humans. Do not edit it automatically.

  • refackiew

Everything below this line was generated by Codex GPT-5.

Created by Codex on behalf of Byron. Byron will review before this is ready to merge.

Summary

  • resolve delta children lazily from immutable shared bases instead of retaining the internal-node frontier
  • use lock-free local deques plus a shared root injector so idle workers can help the last expensive root
  • recycle the final base reference, keeping a linear chain to roughly two object buffers per active worker
  • propagate gix's parallel feature to the new gix-pack scheduler

Regression coverage

  • proves internal siblings are not all materialized before descending
  • proves two workers concurrently resolve children from a single remaining root

Benchmarks

Git index-pack thread selection

Git index-pack accepts --threads=<n> even though the option is omitted from its short usage text. With no explicit value or pack.threads configuration, it starts from the online CPU count, uses all CPUs below four, uses three threads for four or five CPUs, halves the count from six through 39 CPUs, and caps it at 20 from 40 CPUs onward. This 16-core machine therefore defaults to eight resolver threads. Existing unqualified Git measurements below have been relabeled as -t8 (default). Git multiplies its delta-base cache limit by the selected thread count.

phpstan pathological pack

The pathological phpstan pack contains 100.7k objects and decodes to 174.6 GB.

For this comparison Git's aggregate delta-base cache allowance is kept at or above the default eight-thread allowance of 8 × 96 MiB. Because Git multiplies core.deltaBaseCacheLimit by its worker count, the per-thread limits are 768, 384, 192, 96, and 96 MiB for 1, 2, 4, 8, and 16 threads respectively. The 16-thread run therefore retains Git's default 96 MiB per worker and has a 1.5 GiB aggregate allowance.

Git index-pack scaling:

Threads Cache/worker Aggregate cache Wall time Speedup vs -t1 Parallel efficiency Incremental scaling User CPU time Max RSS Peak memory footprint
1 768 MiB 768 MiB 304.51 s 1.00x 100.0% 301.16 s 1.47 GB 1.45 GB
2 384 MiB 768 MiB 168.06 s 1.81x 90.6% 1.81x for 2x threads 304.74 s 1.19 GB 1.17 GB
4 192 MiB 768 MiB 98.80 s 3.08x 77.1% 1.70x from -t2 for 2x threads 306.05 s 1.55 GB 1.55 GB
8 96 MiB 768 MiB 65.41 s 4.65x 58.2% 1.51x from -t4 for 2x threads 317.78 s 1.91 GB 1.90 GB
16 96 MiB 1.5 GiB 52.79 s 5.77x 36.1% 1.24x from -t8 for 2x threads 378.49 s 2.34 GB 2.33 GB

Same-thread wall-time comparison:

Threads Git index-pack This PR gix speedup vs Git
1 304.51 s 283.41 s 1.07x
4 98.80 s 71.94 s 1.37x
8 65.41 s 36.42 s 1.80x
16 52.79 s 22.83 s 2.31x

gix results:

Metric Baseline gix This PR, -t1 This PR, -t4 This PR, -t8 This PR, -t16
Wall time 98.53 s 283.41 s 71.94 s 36.42 s 22.83 s
Resolver time 98.38 s 283.30 s 71.82 s 36.27 s 22.70 s
User CPU time 301.79 s 289.67 s 293.27 s 296.01 s 344.48 s
Max RSS 19.81 GB 7.20 GB 7.42 GB 7.86 GB 8.90 GB
Peak memory footprint 13.14 GB 0.68 GB 0.75 GB 1.17 GB 2.16 GB

gix resolver scaling and memory growth:

Threads Resolver time Speedup vs -t1 Parallel efficiency Incremental scaling Max RSS vs -t1 Peak footprint vs -t1
1 283.30 s 1.00x 100.0% 1.00x 1.00x
4 71.82 s 3.94x 98.6% 3.94x for 4x threads 1.03x 1.10x
8 36.27 s 7.81x 97.6% 1.98x from -t4 for 2x threads 1.09x 1.72x
16 22.70 s 12.48x 78.0% 1.60x from -t8 for 2x threads 1.24x 3.18x

gix scaling is nearly linear through eight workers. Doubling from eight to sixteen workers yields another 1.60x speedup at 79.9% incremental efficiency, while max RSS rises by 13% and peak footprint by 85%.

At the nearest wall-time operating points, gix -t4 takes 71.94 s versus Git -t8 at 65.41 s. gix reports 7.42 GB max RSS versus Git's 1.91 GB because its RSS includes clean file-backed pages from the 6.9 GB pack mapping, while Git reads through bounded pread() buffers. Charged peak footprint is 0.75 GB for gix versus 1.90 GB for Git, 61% lower.

Commands:

/usr/bin/time -lp /Users/byron/dev/github.com/git/git/bin-wrappers/git -c core.deltaBaseCacheLimit=768m index-pack --verify --threads=1 /Users/byron/dev/github.com/phpstan/phpstan/.git/objects/pack/pack-fc5a93b6903e792e5c65ce2f8f02054775c35c94.pack
/usr/bin/time -lp /Users/byron/dev/github.com/git/git/bin-wrappers/git -c core.deltaBaseCacheLimit=384m index-pack --verify --threads=2 /Users/byron/dev/github.com/phpstan/phpstan/.git/objects/pack/pack-fc5a93b6903e792e5c65ce2f8f02054775c35c94.pack
/usr/bin/time -lp /Users/byron/dev/github.com/git/git/bin-wrappers/git -c core.deltaBaseCacheLimit=192m index-pack --verify --threads=4 /Users/byron/dev/github.com/phpstan/phpstan/.git/objects/pack/pack-fc5a93b6903e792e5c65ce2f8f02054775c35c94.pack
/usr/bin/time -lp /Users/byron/dev/github.com/git/git/bin-wrappers/git -c core.deltaBaseCacheLimit=96m index-pack --verify --threads=8 /Users/byron/dev/github.com/phpstan/phpstan/.git/objects/pack/pack-fc5a93b6903e792e5c65ce2f8f02054775c35c94.pack
/usr/bin/time -lp /Users/byron/dev/github.com/git/git/bin-wrappers/git -c core.deltaBaseCacheLimit=96m index-pack --verify --threads=16 /Users/byron/dev/github.com/phpstan/phpstan/.git/objects/pack/pack-fc5a93b6903e792e5c65ce2f8f02054775c35c94.pack
/usr/bin/time -lp target/release/gix -t1 -v free pack verify /Users/byron/dev/github.com/phpstan/phpstan/.git/objects/pack/pack-fc5a93b6903e792e5c65ce2f8f02054775c35c94.idx
/usr/bin/time -lp target/release/gix -t4 -v free pack verify /Users/byron/dev/github.com/phpstan/phpstan/.git/objects/pack/pack-fc5a93b6903e792e5c65ce2f8f02054775c35c94.idx
/usr/bin/time -lp target/release/gix -t8 -v free pack verify /Users/byron/dev/github.com/phpstan/phpstan/.git/objects/pack/pack-fc5a93b6903e792e5c65ce2f8f02054775c35c94.idx
/usr/bin/time -lp target/release/gix -t16 -v free pack verify /Users/byron/dev/github.com/phpstan/phpstan/.git/objects/pack/pack-fc5a93b6903e792e5c65ce2f8f02054775c35c94.idx

Linux best-case pack:

Git index-pack on the Linux best-case pack:

Threads Wall time Speedup vs -t1 Parallel efficiency Incremental scaling User CPU time Max RSS Peak memory footprint
1 156.65 s 1.00x 100.0% 153.38 s 0.79 GB 0.77 GB
4 60.48 s 2.59x 64.8% 2.59x for 4x threads 164.45 s 1.02 GB 0.99 GB
8 45.48 s 3.44x 43.1% 1.33x from -t4 for 2x threads 170.33 s 1.04 GB 1.02 GB
16 42.97 s 3.65x 22.8% 1.06x from -t8 for 2x threads 190.96 s 1.10 GB 1.08 GB

Same-thread wall-time comparison:

Threads Git index-pack This PR gix speedup vs Git
1 156.65 s 137.48 s 1.14x
4 60.48 s 36.75 s 1.65x
8 45.48 s 19.81 s 2.30x
16 42.97 s 13.16 s 3.27x

Commands:

/usr/bin/time -lp /Users/byron/dev/github.com/git/git/bin-wrappers/git index-pack --threads=1 --verify -v /Users/byron/dev/github.com/GitoxideLabs/gitoxide/tests/fixtures/repos/linux.git/objects/pack/pack-3ee05b0f4e4c2cb59757c95c68e2d13c0a491289.pack
/usr/bin/time -lp /Users/byron/dev/github.com/git/git/bin-wrappers/git index-pack --threads=4 --verify -v /Users/byron/dev/github.com/GitoxideLabs/gitoxide/tests/fixtures/repos/linux.git/objects/pack/pack-3ee05b0f4e4c2cb59757c95c68e2d13c0a491289.pack
/usr/bin/time -lp /Users/byron/dev/github.com/git/git/bin-wrappers/git index-pack --threads=8 --verify -v /Users/byron/dev/github.com/GitoxideLabs/gitoxide/tests/fixtures/repos/linux.git/objects/pack/pack-3ee05b0f4e4c2cb59757c95c68e2d13c0a491289.pack
/usr/bin/time -lp /Users/byron/dev/github.com/git/git/bin-wrappers/git index-pack --threads=16 --verify -v /Users/byron/dev/github.com/GitoxideLabs/gitoxide/tests/fixtures/repos/linux.git/objects/pack/pack-3ee05b0f4e4c2cb59757c95c68e2d13c0a491289.pack
Baseline gix This PR, -t1 This PR, -t4 This PR, -t8 This PR, -t16
wall time 13.68 s 137.48 s 36.75 s 19.81 s 13.16 s
resolver time 11.81 s 135.58 s 34.92 s 17.92 s 11.33 s
user CPU time 138.36 s 142.86 s 146.56 s 178.05 s
max RSS 3.54 GB 2.74 GB 2.76 GB 2.89 GB 2.97 GB
peak memory footprint 1.96 GB 1.76 GB 1.76 GB 1.76 GB 1.76 GB

Resolver scaling and memory growth:

Threads Resolver time Speedup vs -t1 Parallel efficiency Incremental scaling Max RSS vs -t1 Peak footprint vs -t1
1 135.58 s 1.00× 100% 1.00× 1.00×
4 34.92 s 3.88× 97.1% 3.88× for 4× threads 1.01× 1.00×
8 17.92 s 7.57× 94.6% 1.95× from -t4 for 2× threads 1.06× 1.00×
16 11.33 s 11.97× 74.8% 1.58× from -t8 for 2× threads 1.09× 1.00×

Scaling is nearly linear through eight workers. Doubling from eight to sixteen yields another 1.58× speedup at 79.1% incremental efficiency. Across the entire one-to-sixteen-thread range, max RSS rises by 9% while peak footprint remains effectively unchanged.

At -t16, the best-case pack is slightly faster than baseline: 13.16 s versus 13.68 s wall time and 11.33 s versus 11.81 s resolver time. It also uses less memory: 2.97 GB versus 3.54 GB max RSS and 1.76 GB versus 1.96 GB peak footprint.

Commands:

/usr/bin/time -lp target/release/gix -t1 -v free pack verify /Users/byron/dev/github.com/GitoxideLabs/gitoxide/tests/fixtures/repos/linux.git/objects/pack/pack-3ee05b0f4e4c2cb59757c95c68e2d13c0a491289.idx
/usr/bin/time -lp target/release/gix -t4 -v free pack verify /Users/byron/dev/github.com/GitoxideLabs/gitoxide/tests/fixtures/repos/linux.git/objects/pack/pack-3ee05b0f4e4c2cb59757c95c68e2d13c0a491289.idx
/usr/bin/time -lp target/release/gix -t8 -v free pack verify /Users/byron/dev/github.com/GitoxideLabs/gitoxide/tests/fixtures/repos/linux.git/objects/pack/pack-3ee05b0f4e4c2cb59757c95c68e2d13c0a491289.idx
/usr/bin/time -lp target/release/gix -t16 -v free pack verify /Users/byron/dev/github.com/GitoxideLabs/gitoxide/tests/fixtures/repos/linux.git/objects/pack/pack-3ee05b0f4e4c2cb59757c95c68e2d13c0a491289.idx

SHA-1 sibling-pack comparison

This is the original SHA-1 sibling of the 10.9M-object SHA-256 benchmark pack below, with the same compressed payload and delta topology and 146.7 GB of decoded data. It is distinct from the 7.6M-object tests/fixtures/repos/linux.git pack above, whose -t16 wall time is 13.16 s.

Metric Git index-pack, -t8 (default) This PR, -t1 This PR, -t4 This PR, -t8 This PR, -t16
Wall time 125.11 s 212.57 s 56.58 s 30.60 s 20.74 s
Resolver time 210.35 s 54.36 s 28.33 s 18.51 s
User CPU time 323.69 s 215.06 s 222.58 s 231.29 s 281.72 s
Max RSS 5.42 GB 4.89 GB 5.18 GB 5.29 GB 5.46 GB
Peak memory footprint 1.81 GB 2.54 GB 2.54 GB 2.54 GB 2.54 GB
Wall speedup vs Git 1.00x 0.59x 2.21x 4.09x 6.03x

Resolver scaling:

Threads Resolver time Speedup vs -t1 Parallel efficiency Incremental scaling
1 210.35 s 1.00x 100.0%
4 54.36 s 3.87x 96.7% 3.87x for 4x threads
8 28.33 s 7.42x 92.8% 1.92x from -t4 for 2x threads
16 18.51 s 11.36x 71.0% 1.53x from -t8 for 2x threads

Commands:

/usr/bin/time -lp /Users/byron/dev/github.com/git/git/bin-wrappers/git -C /Users/byron/dev/git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-sha256.git index-pack --verify -v /Users/byron/dev/git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-sha256.git/objects/pack/pack-6f64e91eccd8f68e174dc2c01416b392c66867e7.pack
/usr/bin/time -lp target/release/gix --object-hash sha1 -t1 -v free pack verify /Users/byron/dev/git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-sha256.git/objects/pack/pack-6f64e91eccd8f68e174dc2c01416b392c66867e7.idx
/usr/bin/time -lp target/release/gix --object-hash sha1 -t4 -v free pack verify /Users/byron/dev/git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-sha256.git/objects/pack/pack-6f64e91eccd8f68e174dc2c01416b392c66867e7.idx
/usr/bin/time -lp target/release/gix --object-hash sha1 -t8 -v free pack verify /Users/byron/dev/git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-sha256.git/objects/pack/pack-6f64e91eccd8f68e174dc2c01416b392c66867e7.idx
/usr/bin/time -lp target/release/gix --object-hash sha1 -t16 -v free pack verify /Users/byron/dev/git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-sha256.git/objects/pack/pack-6f64e91eccd8f68e174dc2c01416b392c66867e7.idx

SHA-256 hasher comparison

To isolate object hashing from scheduling, the 10.9M-object Linux pack was converted by preserving its compressed payload and delta topology byte-for-byte, replacing the SHA-1 pack trailer with SHA-256, and regenerating the index in a SHA-256 repository. It decodes to 146.7 GB. This is a pack-verification fixture rather than a valid converted repository: commit and tree payloads still contain SHA-1 references.

The original Git build used the portable SHA256_BLK backend, while gix's RustCrypto SHA-256 dispatches to ARMv8 SHA-2 instructions on this machine. To avoid disadvantaging Git, the comparison was rerun with Git cf5497b14c built with SHA1_DC for SHA-1 and SHA256_OPENSSL from OpenSSL 3.6.3 for SHA-256. At -t8, changing only Git's SHA-256 backend reduced wall time from 144.15 s to 40.45 s (3.56x faster) and user CPU time from 471.94 s to 99.81 s (4.73x less). All Git SHA-256 comparison values below use the OpenSSL build; the SHA-1 measurements above remain from the original SHA1_DC build.

Git index-pack SHA-256 scaling:

Threads Wall time Speedup vs -t1 Parallel efficiency Incremental scaling User CPU time System CPU time Max RSS Peak memory footprint
1 87.85 s 1.00x 100.0% 84.23 s 3.57 s 1.21 GB 1.20 GB
4 44.05 s 1.99x 49.9% 1.99x for 4x threads 91.60 s 9.63 s 1.47 GB 1.46 GB
8 40.45 s 2.17x 27.1% 1.09x from -t4 for 2x threads 99.81 s 39.19 s 1.57 GB 1.57 GB
16 48.68 s 1.80x 11.3% 0.83x from -t8 for 2x threads 108.46 s 204.81 s 1.69 GB 1.69 GB

Same-thread wall-time comparison:

Threads Git index-pack This PR gix speedup vs Git
1 87.85 s 72.47 s 1.21x
4 44.05 s 21.83 s 2.02x
8 40.45 s 12.58 s 3.22x
16 48.68 s 8.67 s 5.61x

gix SHA-256 results:

Metric This PR, -t1 This PR, -t4 This PR, -t8 This PR, -t16
Wall time 72.47 s 21.83 s 12.58 s 8.67 s
Resolver time 70.31 s 19.69 s 10.43 s 6.51 s
User CPU time 72.71 s 81.31 s 85.88 s 97.04 s
Max RSS 5.18 GB 5.29 GB 5.39 GB 5.61 GB
Peak memory footprint 2.54 GB 2.54 GB 2.54 GB 2.54 GB

gix resolver scaling:

Threads Resolver time Speedup vs -t1 Parallel efficiency Incremental scaling
1 70.31 s 1.00x 100.0%
4 19.69 s 3.57x 89.3% 3.57x for 4x threads
8 10.43 s 6.74x 84.3% 1.89x from -t4 for 2x threads
16 6.51 s 10.80x 67.5% 1.60x from -t8 for 2x threads

SHA-1 versus SHA-256 resolver time on the identical compressed pack:

Threads SHA-1 resolver SHA-256 resolver SHA-256 speedup
1 210.35 s 70.31 s 2.99x
4 54.36 s 19.69 s 2.76x
8 28.33 s 10.43 s 2.72x
16 18.51 s 6.51 s 2.84x

SHA-256 is 2.72x to 2.99x faster across the measured thread counts with identical work distribution and delta data, showing that the slow SHA-1 implementation, rather than the scheduler, dominates the absolute regression.

Commands:

/usr/bin/time -lp /Users/byron/dev/github.com/git/git.sha256-openssl/bin-wrappers/git index-pack --verify --object-format=sha256 --threads=1 /Users/byron/dev/git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-sha256-pack.git/objects/pack/pack-sha256-linux.pack
/usr/bin/time -lp /Users/byron/dev/github.com/git/git.sha256-openssl/bin-wrappers/git index-pack --verify --object-format=sha256 --threads=4 /Users/byron/dev/git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-sha256-pack.git/objects/pack/pack-sha256-linux.pack
/usr/bin/time -lp /Users/byron/dev/github.com/git/git.sha256-openssl/bin-wrappers/git index-pack --verify --object-format=sha256 --threads=8 /Users/byron/dev/git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-sha256-pack.git/objects/pack/pack-sha256-linux.pack
/usr/bin/time -lp /Users/byron/dev/github.com/git/git.sha256-openssl/bin-wrappers/git index-pack --verify --object-format=sha256 --threads=16 /Users/byron/dev/git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-sha256-pack.git/objects/pack/pack-sha256-linux.pack
/usr/bin/time -lp target/release/gix --object-hash sha256 -t1 -v free pack verify /Users/byron/dev/git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-sha256-pack.git/objects/pack/pack-sha256-linux.idx
/usr/bin/time -lp target/release/gix --object-hash sha256 -t4 -v free pack verify /Users/byron/dev/git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-sha256-pack.git/objects/pack/pack-sha256-linux.idx
/usr/bin/time -lp target/release/gix --object-hash sha256 -t8 -v free pack verify /Users/byron/dev/git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-sha256-pack.git/objects/pack/pack-sha256-linux.idx
/usr/bin/time -lp target/release/gix --object-hash sha256 -t16 -v free pack verify /Users/byron/dev/git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-sha256-pack.git/objects/pack/pack-sha256-linux.idx

Validation

  • cargo test -p gix-pack
  • cargo clippy -p gix-pack --all-targets --all-features -- -D warnings
  • cargo check -p gix-pack --no-default-features --features sha1
  • cargo check -p gix --no-default-features --features parallel,sha1
  • cargo fmt --all -- --check

The scheduling and bounded-live-base approach was informed by Git's builtin/index-pack.c at cf5497b14c; the implementation is independent and lock-free.

Fixes #2424.

@Byron
Sebastian Thiel (Byron) force-pushed the delta-tree-parallelism branch 4 times, most recently from b20d2d5 to 03cfb1e Compare July 29, 2026 19:05
@Byron
Sebastian Thiel (Byron) marked this pull request as ready for review July 30, 2026 09:27
<!-- agent -->
--- Summary of findings

The lazy, stealable resolver fixes the pathological phpstan pack without
regressing the Linux best case. At 16 threads, phpstan falls from the
98.38-second baseline resolver time to 22.70 seconds, while the Linux
best-case resolver improves slightly from 11.81 to 11.33 seconds. Charged
peak memory on phpstan falls from 13.14 GB to 2.16 GB, and the Linux best
case falls from 1.96 GB to 1.76 GB.

Across the measured packs, gix resolver speedup at 16 threads ranges from
10.80x to 12.48x over its new serial path. On phpstan, gix is 1.07x faster
than Git at one thread and 2.31x faster at 16 threads. On the identical
10.9M-object Linux payload, gix's SHA-256 resolver is 2.72x to 2.99x faster
than its SHA-1 resolver, confirming that the slow SHA-1 implementation
dominates much of the remaining absolute cost.

The Git SHA-256 comparison was initially unfair: the local Git build used
the portable SHA256_BLK backend while RustCrypto selected ARMv8 SHA-2
instructions. Rebuilding Git with OpenSSL reduced its eight-thread wall
time from 144.15 to 40.45 seconds and user CPU time from 471.94 to
99.81 seconds. All final Git SHA-256 comparisons use that faster backend.

--- Pathological phpstan pack

The pack contains 100.7k objects but expands to 174.6 GB because one root
ends in a deep, expensive delta tree. The previous resolver takes
98.53 seconds wall clock and has a 13.14 GB peak memory footprint.

The new resolver takes 283.41, 71.94, 36.42, and 22.83 seconds wall clock
at 1, 4, 8, and 16 threads. Its resolver speedups are 3.94x, 7.81x, and
12.48x relative to one thread, remaining nearly linear through eight
threads. Peak memory footprint is 0.68, 0.75, 1.17, and 2.16 GB
respectively. Thus the fastest run is 4.31x faster than the old wall time
while using 84% less charged peak memory.

Git was measured at 1, 2, 4, 8, and 16 threads while keeping its aggregate
delta-base cache allowance at least as large as the default eight-thread
allowance of 8 times 96 MiB. Git takes 304.51, 168.06, 98.80, 65.41, and
52.79 seconds, for 5.77x one-to-sixteen-thread scaling. At equal thread
counts, gix is 1.07x, 1.37x, 1.80x, and 2.31x faster at 1, 4, 8, and
16 threads.

At the nearest wall-time operating points, gix at four threads takes
71.94 seconds with a 0.75 GB peak footprint, while Git at eight threads
takes 65.41 seconds with a 1.90 GB peak footprint. gix therefore uses
61% less charged working memory near Git-matching throughput.

--- Linux best-case pack

The 7.6M-object fixture expands to 95.6 GB and guards the already-friendly
case. The old implementation takes 13.68 seconds wall clock and
11.81 seconds in the resolver. The new implementation takes 137.48,
36.75, 19.81, and 13.16 seconds wall clock at 1, 4, 8, and 16 threads;
resolver scaling reaches 11.97x at 16 threads.

The 16-thread result is slightly faster than the old implementation, with
11.33 seconds in the resolver, and lowers peak footprint from 1.96 to
1.76 GB. Peak footprint remains effectively constant from one through
16 threads. Git takes 156.65, 60.48, 45.48, and 42.97 seconds at the same
thread counts, so gix's advantage grows from 1.14x to 3.27x.

--- SHA-1 and SHA-256 sibling packs

The sibling packs contain 10.9M objects, expand to 146.7 GB, and preserve
the same compressed payload and delta topology. This isolates hashing from
work distribution.

For SHA-1, gix takes 212.57, 56.58, 30.60, and 20.74 seconds wall clock at
1, 4, 8, and 16 threads. Resolver scaling reaches 11.36x at 16 threads.
The default eight-thread Git run takes 125.11 seconds, while gix takes
30.60 seconds at eight threads and 20.74 seconds at 16.

For SHA-256, gix takes 72.47, 21.83, 12.58, and 8.67 seconds wall clock.
Resolver scaling reaches 10.80x at 16 threads. Against OpenSSL-backed Git,
gix is 1.21x, 2.02x, 3.22x, and 5.61x faster at equal thread counts.
Git improves from 87.85 seconds at one thread to 40.45 seconds at eight,
then regresses to 48.68 seconds at 16 with 204.81 seconds of system CPU.

--- Hasher and scheduler interpretation

On identical pack data, gix SHA-256 resolver time is 70.31 seconds at one
thread versus 210.35 seconds for SHA-1, and 6.51 seconds at 16 threads
versus 18.51 seconds for SHA-1. SHA-256 is therefore 2.99x faster
serially and 2.84x faster at 16 threads, with a 2.72x to 2.99x advantage
throughout the measured range.

The similar 10.80x to 12.48x gix scaling across pathological, best-case,
SHA-1, and SHA-256 packs argues against lock contention being the main
high-thread limitation. The remaining flattening is consistent with
finite parallel work, scheduling overhead, memory bandwidth, and hashing
cost.

--- Memory interpretation

The resolver no longer materializes the unresolved internal-node frontier.
It resolves a child only when a worker starts it, shares immutable bases
between sibling tasks, and recycles the final base reference. A linear
chain therefore needs roughly two object buffers per active worker instead
of retaining every intermediate base.

macOS max RSS includes clean file-backed pages. gix maps the pack, so its
reported RSS can include most of a multi-gigabyte pack even when those
pages are reclaimable; Git reads through bounded pread buffers. Peak memory
footprint better represents charged working memory here. On phpstan, gix
max RSS grows from 7.20 to 8.90 GB across 1 to 16 threads, but charged
footprint grows from only 0.68 to 2.16 GB. On the Linux best case,
footprint stays at 1.76 GB across the same range.

--- Implementation

Use lock-free local deques and a shared root injector so idle workers can
steal branches from the last expensive root without every worker hoarding
roots. Enable this resolver through the gix parallel feature.

Regression tests prove that internal siblings are not all materialized
before descent and that two workers can concurrently resolve children from
one remaining root. The scheduler follows the proven Git index-pack
principles of bounded live delta bases and keeping independent delta work
available, informed by builtin/index-pack.c at cf5497b14c.
Only then will it use its parallel implementation.
@Byron
Sebastian Thiel (Byron) merged commit 4a6cf9d into main Jul 30, 2026
32 checks passed
@Byron
Sebastian Thiel (Byron) deleted the delta-tree-parallelism branch July 30, 2026 09:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

phpstan/phpstan pack resolves with a single thread only

1 participant