git/list[1] front-page[2] threads[3] people[4] search[5] about
wed 2026-10-07 17:00 UTC

[PATCH v3 0/6] last-modified: use the pathspec's Bloom key to pre-filter commits

From
Toon Claes <toon@iotcl.com>
Date
Aug 31, 2026, 15:18 UTC
Message-ID
<20260831-toon-speed-up-last-modified-v3-0-2bbb864acf93@iotcl.com>
In-Reply-To
<20260807-toon-speed-up-last-modified-v2-0-7d87bbdeaf9b@iotcl.com>

We have received a report[1] git-last-modified(1) is slow compared to git-log(1) if you want to find the last commit for all entries in a directory. For example running the following command on ziglang/zig[2]:

   $ git last-modified -t --max-depth=0 $OID -- doc/langref/
Turns out to find results about 2.5 times slower than:
   $ git log --name-status -c --format=commit%x00%H %P%x00" \
       --parents --no-renames -t -z $OID -- :(literal)doc/langref

Now the latter needs some post-processing to come to the same results, the total solution still is faster than integrating git-last-modified(1).

After some research we've discovered the Bloom filters aren't used optimally. But it turns out the code powering git-log(1) can fairly easy be reused. We do this in a few steps:

 - Patch 1 & 2 prepare revision.[ch] to expose the helper to check if
   revs maybe changes in Bloom filter.
 - Patch 3 & 4 prepare a similar helper, but this one is needed when
   git-last-modified(1) is called with `--show-trees`.
 - Patch 5 uses these helpers in git-last-modified(1).
 - Patch 6 is a bonus change, which optimizes when working with wildcard
   pathspecs.

Below are benchmarks on the ziglang/zig repository for the `doc/langref/` directory (with commit-graphs written using `--changed-paths`):

    Benchmark 1: master: last-modified -z -t
      Time (mean ± σ):      61.9 ms ±   1.8 ms    [User: 57.1 ms, System: 4.0 ms]
      Range (min … max):    58.5 ms …  68.9 ms    150 runs
    Benchmark 2: HEAD: last-modified -z -t
      Time (mean ± σ):      31.8 ms ±   1.3 ms    [User: 27.1 ms, System: 4.2 ms]
      Range (min … max):    29.7 ms …  35.6 ms    150 runs
    Benchmark 3: git log -t
      Time (mean ± σ):      22.1 ms ±   1.2 ms    [User: 16.7 ms, System: 5.0 ms]
      Range (min … max):    20.1 ms …  26.6 ms    150 runs
    Summary
      git log -t ran
        1.44 ± 0.10 times faster than HEAD: last-modified -z -t
        2.80 ± 0.18 times faster than master: last-modified -z -t

Comparing HEAD to master, there is about 1.95x speedup on running `git last-modified -z -t. `git log -t` is still slightly faster though.

But without `-t` the speedup is even bigger:
    Benchmark 1: master: last-modified -z
      Time (mean ± σ):      60.7 ms ±   4.5 ms    [User: 56.5 ms, System: 3.8 ms]
      Range (min … max):    57.5 ms …  96.2 ms    150 runs
    Benchmark 2: HEAD: last-modified -z
      Time (mean ± σ):      16.2 ms ±   1.4 ms    [User: 13.3 ms, System: 2.7 ms]
      Range (min … max):    13.9 ms …  20.4 ms    212 runs
    Benchmark 3: git log (no -t)
      Time (mean ± σ):      22.0 ms ±   3.7 ms    [User: 16.8 ms, System: 4.9 ms]
      Range (min … max):    18.7 ms …  37.6 ms    150 runs
    Summary
      HEAD: last-modified -z ran
        1.35 ± 0.25 times faster than git log (no -t)
        3.74 ± 0.42 times faster than master: last-modified -z

This makes sense because without `-t` we can use the Bloom filter more optimally.

Similar timings are seen across a few other repositories (like GitLab's monolith gitlab-org/gitlab).

[1]: https://lore.kernel.org/git/17f356ff-7bfb-47f5-b714-62a95cc8b821@codeberg.org/ [2]: https://codeberg.org/ziglang/zig

---
Changes in v3:
- Add trace2 "bloom_queries" and use it in test to verify top-level
  wildcard behavior.
- Link to v2: https://patch.msgid.link/20260807-toon-speed-up-last-modified-v2-0-7d87bbdeaf9b@iotcl.com
Changes in v2:
- Make the public helper revs_maybe_changed_in_bloom() return a bool
  instead of a tristate.
- Keep the bloom_keyvecs_nr precondition before get_bloom_filter() and
  return early from the key vector loop.
- Add commits 3 & 4 to add helper used with `--show-trees`.
- Use Bloom filter correctly with `--show-trees` and add test to prove.
- Rerun benchmarks to compare results with and without `--show-trees`.
- Link to v1: https://patch.msgid.link/20260717-toon-speed-up-last-modified-v1-0-410418f18614@iotcl.com
---
Toon Claes (6):
      revision: move bloom keyvec precondition into function
      revision: expose check for paths maybe changed in Bloom filter
      bloom: add helper to check if any key in a vector is present
      revision: add Bloom check that includes parent directories
      last-modified: check pathspec against Bloom filter first
      last-modified: keep per-path Bloom filters for wildcard pathspecs
 bloom.c                  | 12 +++++++++++
 bloom.h                  | 11 ++++++++++
 builtin/last-modified.c  | 28 ++++++++++++++++++++++++++
 revision.c               | 52 +++++++++++++++++++++++++++++++++++++-----------
 revision.h               | 20 +++++++++++++++++++
 t/t8020-last-modified.sh | 47 +++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 158 insertions(+), 12 deletions(-)
Range-diff versus v2:
1:  a98bbaad50 = 1:  ac58e4a3cd revision: move bloom keyvec precondition into function
2:  ebf4f65fab = 2:  43375b505e revision: expose check for paths maybe changed in Bloom filter
3:  7a3c14fe87 = 3:  41e3a19fde bloom: add helper to check if any key in a vector is present
4:  145c95a2fa = 4:  a648612927 revision: add Bloom check that includes parent directories
5:  9dc5a0be79 = 5:  0e8721fe75 last-modified: check pathspec against Bloom filter first
6:  83036c2fe4 ! 6:  e7997e0a9b last-modified: keep per-path Bloom filters for wildcard pathspecs
    @@ Commit message
         Restore `bloom_filter_settings` after prepare_revision_walk() so the
         per-path check keeps working for wildcard pathspecs.
     
    +    This change isn't having any effect on the output, but only has an
    +    impact on performance. Add a "bloom_queries" trace2 counter that records
    +    how often the per-path Bloom check runs, and a test that asserts the
    +    count increments as appropriate for a top-level wildcard pathspec.
    +
         Signed-off-by: Toon Claes <toon@iotcl.com>
     
      ## builtin/last-modified.c ##
    +@@
    + #include "quote.h"
    + #include "repository.h"
    + #include "revision.h"
    ++#include "trace2.h"
    + 
    + /* Remember to update object flag allocation in object.h */
    + #define PARENT1 (1u<<16) /* used instead of SEEN */
    +@@ builtin/last-modified.c: struct last_modified {
    + 
    + 	/* 'scratch' to avoid allocating a bitmap every process_parent() */
    + 	struct bitmap *scratch;
    ++
    ++	unsigned int count_bloom_filter_queries;
    + };
    + 
    + static struct bitmap *active_paths_for(struct last_modified *lm, struct commit *c)
    +@@ builtin/last-modified.c: static bool maybe_changed_path(struct last_modified *lm,
    + 	if (!filter)
    + 		return true;
    + 
    ++	lm->count_bloom_filter_queries++;
    ++
    + 	/*
    + 	 * With --show-trees we also track the tree entries containing the
    + 	 * paths, so a change to any of those parent directories matters too.
     @@ builtin/last-modified.c: static int last_modified_run(struct last_modified *lm)
      
      	prepare_revision_walk(&lm->rev);
    @@ builtin/last-modified.c: static int last_modified_run(struct last_modified *lm)
      	max_count = lm->rev.max_count;
      
      	init_active_paths_for_commit(&lm->active_paths);
    +@@ builtin/last-modified.c: static int last_modified_run(struct last_modified *lm)
    + 	if (hashmap_get_size(&lm->paths))
    + 		BUG("paths remaining beyond boundary in last-modified");
    + 
    ++	trace2_data_intmax("last-modified", lm->rev.repo, "bloom_queries",
    ++			   lm->count_bloom_filter_queries);
    ++
    + 	clear_prio_queue(&not_queue);
    + 	clear_prio_queue(&queue);
    + 	clear_active_paths_for_commit(&lm->active_paths);
    +
    + ## t/t8020-last-modified.sh ##
    +@@ t/t8020-last-modified.sh: test_expect_success 'last-modified with Bloom filters and --show-trees' '
    + 	)
    + '
    + 
    ++test_expect_success 'last-modified with Bloom filters and top-level wildcard' '
    ++	test_when_finished rm -rf wildcard &&
    ++	git init wildcard &&
    ++	(
    ++		cd wildcard &&
    ++		test_commit base-c a.c &&
    ++		test_commit base-h a.h &&
    ++		test_commit touch-c a.c &&
    ++		mkdir d &&
    ++		test_commit sub-c d/b.c &&
    ++
    ++		git commit-graph write --reachable --changed-paths &&
    ++		GIT_TRACE2_PERF="$(pwd)/off.perf" \
    ++			git -c core.commitGraph=false last-modified -r HEAD \
    ++			-- "*.c" >expect &&
    ++		test_grep "data .* bloom_queries:0$" off.perf &&
    ++
    ++		GIT_TRACE2_PERF="$(pwd)/on.perf" \
    ++			git -c core.commitGraph=true last-modified -r HEAD \
    ++			-- "*.c" >actual &&
    ++		test_grep "data .* bloom_queries:2$" on.perf &&
    ++
    ++		test_cmp expect actual
    ++	)
    ++'
    ++
    + test_expect_success 'cannot run last-modified on two commits' '
    + 	test_must_fail git last-modified HEAD HEAD~1 2>err &&
    + 	test_grep "last-modified can only operate on one commit at a time" err

--- base-commit: c73e85354c275c9d409b26445089bc16940fc527 change-id: 20260716-toon-speed-up-last-modified-b04ea1f21831

Previous: Toon ClaesNext: Toon Claes
Message 3 of 22 in “revision: move bloom keyvec precondition into function”
  1. 1/6 revision: move bloom keyvec precondition into functionToon Claes, Aug 31, 2026
  2. 2/6 revision: expose check for paths maybe changed in Bloom filterToon Claes, Aug 31, 2026
  3. 0/6 last-modified: use the pathspec's Bloom key to pre-filter commitsToon Claes, Aug 31, 2026
  4. 3/6 bloom: add helper to check if any key in a vector is presentToon Claes, Aug 31, 2026
  5. 4/6 revision: add Bloom check that includes parent directoriesToon Claes, Aug 31, 2026
  6. 5/6 last-modified: check pathspec against Bloom filter firstToon Claes, Aug 31, 2026
  7. 6/6 last-modified: keep per-path Bloom filters for wildcard pathspecsToon Claes, Aug 31, 2026
  8. Junio C HamanoAug 31, 2026
  9. Junio C HamanoSep 1, 2026
  10. Toon ClaesSep 1, 2026
  11. 0/6 last-modified: use the pathspec's Bloom key to pre-filter commitsToon Claes, Sep 1, 2026
  12. 1/6 revision: move bloom keyvec precondition into functionToon Claes, Sep 1, 2026
  13. 2/6 revision: expose check for paths maybe changed in Bloom filterToon Claes, Sep 1, 2026
  14. 3/6 bloom: add helper to check if any key in a vector is presentToon Claes, Sep 1, 2026
  15. 4/6 revision: add Bloom check that includes parent directoriesToon Claes, Sep 1, 2026
  16. 5/6 last-modified: check pathspec against Bloom filter firstToon Claes, Sep 1, 2026
  17. 6/6 last-modified: keep per-path Bloom filters for wildcard pathspecsToon Claes, Sep 1, 2026
  18. Junio C HamanoSep 1, 2026
  19. Patrick SteinhardtSep 10, 2026
  20. Patrick SteinhardtSep 10, 2026
  21. Patrick SteinhardtSep 10, 2026
  22. Patrick SteinhardtSep 10, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.