git/list[1] front-page[2] threads[3] people[4] search[5] about
 

Re: [PATCH 02/30] read-cache: add index.computeHash config option

From
Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Date
Nov 17, 2022, 16:13 UTC
Message-ID
<221117.8635ahik7e.gmgdl@evledraar.gmail.com>
In-Reply-To
<030d76f52af654470026b0c4b1dfba2b6c996885.1667846164.git.gitgitgadget@gmail.com>
On Mon, Nov 07 2022, Derrick Stolee via GitGitGadget wrote:
Show 7 quoted lines
> Summary
>   'without hash' ran
>     1.78 ± 0.76 times faster than 'with hash'
>
> These performance benefits are substantial enough to allow users the
> ability to opt-in to this feature, even with the potential confusion
> with older 'git fsck' versions.
The 0.76 part of that is probably just fs caches etc. screwing things
up. I tried it on a ramdisk with CFLAGS=-O3:
	
	$ hyperfine -L v false,true './git -c index.computeHash={v} -C /dev/shm/linux update-index --force-write' -w 1 -r 10
	Benchmark 1: ./git -c index.computeHash=false -C /dev/shm/linux update-index --force-write
	  Time (mean ± σ):      13.3 ms ±   0.3 ms    [User: 7.1 ms, System: 6.1 ms]
	  Range (min … max):    12.7 ms …  13.6 ms    10 runs
	 
	Benchmark 2: ./git -c index.computeHash=true -C /dev/shm/linux update-index --force-write
	  Time (mean ± σ):      34.8 ms ±   0.4 ms    [User: 28.9 ms, System: 5.8 ms]
	  Range (min … max):    34.2 ms …  35.1 ms    10 runs
	 
	Summary
	  './git -c index.computeHash=false -C /dev/shm/linux update-index --force-write' ran
	    2.62 ± 0.07 times faster than './git -c index.computeHash=true -C /dev/shm/linux update-index --force-write'
I also see that if I compile with OPENSSL_SHA1=Y, then:
	
	$ hyperfine -L v false,true './git -c index.computeHash={v} -C /dev/shm/linux update-index --force-write' 
	Benchmark 1: ./git -c index.computeHash=false -C /dev/shm/linux update-index --force-write
	  Time (mean ± σ):      14.0 ms ±   1.3 ms    [User: 7.7 ms, System: 6.2 ms]
	  Range (min … max):    13.1 ms …  21.7 ms    206 runs
	 
	  Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet PC without any interferences from other programs. It might help to use the '--warmup' or '--prepare' 
	options.
	 
	Benchmark 2: ./git -c index.computeHash=true -C /dev/shm/linux update-index --force-write
	  Time (mean ± σ):      21.0 ms ±   1.0 ms    [User: 15.0 ms, System: 6.0 ms]
	  Range (min … max):    20.1 ms …  28.4 ms    138 runs
	 
	  Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet PC without any interferences from other programs. It might help to use the '--warmup' or '--prepare' 
	options.
	 
	Summary
	  './git -c index.computeHash=false -C /dev/shm/linux update-index --force-write' ran
	    1.50 ± 0.15 times faster than './git -c index.computeHash=true -C /dev/shm/linux update-index --force-write'

Which, FWIW is something worth considering. I.e. when we introduced sha1dc we did so with the "big hammer" of the existing hashing API, which is all or nothing, and we pick the hash when we compile git.

But that left a lot of things slower for no good reason, e.g. when we do this hashing of the trailers. So if we could just compile with two implementations, and give users the choice of "use the faster hash when you're not communicating with other git repos" we could make things faster in some cases, without the potential format interop issues.

Show 7 quoted lines
> From: Derrick Stolee <derrickstolee@github.com>
> [...]
> +index.computeHash::
> +	When enabled, compute the hash of the index file as it is written
> +	and store the hash at the end of the content. This is enabled by
> +	default.
> ++

If we have a boolean option it makes sense to make its name reflect the opt-in nature. So "index.skipHash". Then just say "If enabled", and skip the "this is enabled by default, and then later this code:

Show 5 quoted lines
> +	int compute_hash;
> [...]
> +	if (!git_config_get_maybe_bool("index.computehash", &compute_hash) &&
> +	    !compute_hash)
> +		f->skip_hash = 1;
Can just become:
	git_config_get_maybe_bool("index.skipHash", &f->skip_hash);

I.e. git_config_get_maybe_bool() leaves the passed-in dest value alone if it doesn't have it in the config, and you only use this "compute_hash" as an inverted version of "skip_hash".

Show 28 quoted lines
> +If you disable `index.computHash`, then older Git clients may report that
> +your index is corrupt during `git fsck`.
> diff --git a/read-cache.c b/read-cache.c
> index 32024029274..f24d96de4d3 100644
> --- a/read-cache.c
> +++ b/read-cache.c
> @@ -1817,6 +1817,8 @@ static int verify_hdr(const struct cache_header *hdr, unsigned long size)
>  	git_hash_ctx c;
>  	unsigned char hash[GIT_MAX_RAWSZ];
>  	int hdr_version;
> +	int all_zeroes = 1;
> +	unsigned char *start, *end;
>  
>  	if (hdr->hdr_signature != htonl(CACHE_SIGNATURE))
>  		return error(_("bad signature 0x%08x"), hdr->hdr_signature);
> @@ -1827,10 +1829,23 @@ static int verify_hdr(const struct cache_header *hdr, unsigned long size)
>  	if (!verify_index_checksum)
>  		return 0;
>  
> +	end = (unsigned char *)hdr + size;
> +	start = end - the_hash_algo->rawsz;
> +	while (start < end) {
> +		if (*start != 0) {
> +			all_zeroes = 0;
> +			break;
> +		}
> +		start++;
> +	}
Didn't you just re-invent oidread()? :)

Just to narrate my way through this. Before we called verify_hdr() we did:

        hdr = (const struct cache_header *)mmap;
        if (verify_hdr(hdr, mmap_size) < 0)

So, we mmap()'d the index on disk, and whe "hdr" is the struct version of this data, we then cast that back to an "unsigned char *" here, because we're interested in just the raw bytes.

Then we "jump to the end" here, and start iterating over the rawsz at the end, because we're just reading if we have a null_oid().

Then, right after that verify_hdr() call, the veriy next thing we'll do is:
	oidread(&istate->oid, (const unsigned char *)hdr + mmap_size - the_hash_algo->rawsz);

So, maybe I'm missing some subtlety still, and some of this is existing baggage in the pre-image (we used to have the sha1 in the struct, a *long* time ago).

But isn't this equivalent?:
	
	diff --git a/read-cache.c b/read-cache.c
	index f24d96de4d3..39b5b8419f5 100644
	--- a/read-cache.c
	+++ b/read-cache.c
	@@ -1812,13 +1812,14 @@ int verify_index_checksum;
	 /* Allow fsck to force verification of the cache entry order. */
	 int verify_ce_order;
	 
	-static int verify_hdr(const struct cache_header *hdr, unsigned long size)
	+static int verify_hdr(const char *const mmap, const size_t size,
	+		      const struct cache_header **hdrp, struct object_id *oid)
	 {
	+	const struct cache_header *hdr = (const struct cache_header *)mmap;
	 	git_hash_ctx c;
	 	unsigned char hash[GIT_MAX_RAWSZ];
	 	int hdr_version;
	-	int all_zeroes = 1;
	-	unsigned char *start, *end;
	+	const unsigned char *end = (unsigned char *)mmap + size;
	 
	 	if (hdr->hdr_signature != htonl(CACHE_SIGNATURE))
	 		return error(_("bad signature 0x%08x"), hdr->hdr_signature);
	@@ -1826,20 +1827,12 @@ static int verify_hdr(const struct cache_header *hdr, unsigned long size)
	 	if (hdr_version < INDEX_FORMAT_LB || INDEX_FORMAT_UB < hdr_version)
	 		return error(_("bad index version %d"), hdr_version);
	 
	+	*hdrp = hdr;
	+	oidread(oid, end - the_hash_algo->rawsz);
	+
	 	if (!verify_index_checksum)
	 		return 0;
	-
	-	end = (unsigned char *)hdr + size;
	-	start = end - the_hash_algo->rawsz;
	-	while (start < end) {
	-		if (*start != 0) {
	-			all_zeroes = 0;
	-			break;
	-		}
	-		start++;
	-	}
	-
	-	if (all_zeroes)
	+	if (is_null_oid(oid))
	 		return 0;
	 
	 	the_hash_algo->init_fn(&c);
	@@ -2358,11 +2351,8 @@ int do_read_index(struct index_state *istate, const char *path, int must_exist)
	 			mmap_os_err());
	 	close(fd);
	 
	-	hdr = (const struct cache_header *)mmap;
	-	if (verify_hdr(hdr, mmap_size) < 0)
	+	if (verify_hdr(mmap, mmap_size, &hdr, &istate->oid) < 0)
	 		goto unmap;
	-
	-	oidread(&istate->oid, (const unsigned char *)hdr + mmap_size - the_hash_algo->rawsz);
	 	istate->version = ntohl(hdr->hdr_version);
	 	istate->cache_nr = ntohl(hdr->hdr_entries);
	 	istate->cache_alloc = alloc_nr(istate->cache_nr);

I.e. we just make the verify function be in charge of populating our "oid", which we can do that early, as we'd error out later in the function if it doesn't match.

We could avoid the "hdrp" there, but if we're doing the cast it's probably good for readability to just do it once.

Show 7 quoted lines
> +test_expect_success 'index.computeHash config option' '
> +	(
> +		rm -f .git/index &&
> +		git -c index.computeHash=false add a &&
> +		git fsck
> +	)
> +'

You can skip the subshell here, but for a non-RFC let's leave the test in a nice state for the next test someone adds, so maybe:

	test_when_finished "rm -rf repo" &&
	git clone . repo &&
	[...]
Lastly, on this again:
> These performance benefits are substantial enough to allow users the
> ability to opt-in to this feature, even with the potential confusion
> with older 'git fsck' versions.

Isn't an unstated major caveat here that it's not "an older verison", but if you on *your version* set the config to "true" your index doesn't have a hash, so it's persisted until you wipe the index?

Previous: Derrick StoleeNext: Derrick Stolee via GitGitGadget
Message 6 of 56 in “[RFC] extensions.refFormat and packed-refs v2 file format”
  1. 00/30 [RFC] extensions.refFormat and packed-refs v2 file formatDerrick Stolee via GitGitGadget, Nov 7, 2022
  2. 01/30 hashfile: allow skipping the hash functionDerrick Stolee via GitGitGadget, Nov 7, 2022
  3. 02/30 read-cache: add index.computeHash config optionDerrick Stolee via GitGitGadget, Nov 7, 2022
  4. Elijah NewrenNov 11, 2022
  5. Derrick StoleeNov 14, 2022
  6. Ævar Arnfjörð BjarmasonNov 17, 2022
  7. 03/30 extensions: add refFormat extensionDerrick Stolee via GitGitGadget, Nov 7, 2022
  8. Elijah NewrenNov 11, 2022
  9. Derrick StoleeNov 16, 2022
  10. 06/30 refs: allow loose files without packed-refsDerrick Stolee via GitGitGadget, Nov 7, 2022
  11. 07/30 chunk-format: number of chunks is optionalDerrick Stolee via GitGitGadget, Nov 7, 2022
  12. 04/30 config: fix multi-level bulleted listDerrick Stolee via GitGitGadget, Nov 7, 2022
  13. 05/30 repository: wire ref extensions to ref backendsDerrick Stolee via GitGitGadget, Nov 7, 2022
  14. 08/30 chunk-format: document trailing table of contentsDerrick Stolee via GitGitGadget, Nov 7, 2022
  15. 09/30 chunk-format: store chunk offset during writeDerrick Stolee via GitGitGadget, Nov 7, 2022
  16. 11/30 chunk-format: parse trailing table of contentsDerrick Stolee via GitGitGadget, Nov 7, 2022
  17. 10/30 chunk-format: allow trailing table of contentsDerrick Stolee via GitGitGadget, Nov 7, 2022
  18. 13/30 packed-backend: extract add_write_error()Derrick Stolee via GitGitGadget, Nov 7, 2022
  19. 12/30 refs: extract packfile format to new fileDerrick Stolee via GitGitGadget, Nov 7, 2022
  20. 14/30 packed-backend: extract iterator/updates mergeDerrick Stolee via GitGitGadget, Nov 7, 2022
  21. 16/30 config: add config values for packed-refs v2Derrick Stolee via GitGitGadget, Nov 7, 2022
  22. 15/30 packed-backend: create abstraction for writing refsDerrick Stolee via GitGitGadget, Nov 7, 2022
  23. 17/30 packed-backend: create shell of v2 writesDerrick Stolee via GitGitGadget, Nov 7, 2022
  24. 18/30 packed-refs: write file format version 2Derrick Stolee via GitGitGadget, Nov 7, 2022
  25. 19/30 packed-refs: read file format v2Derrick Stolee via GitGitGadget, Nov 7, 2022
  26. 20/30 packed-refs: read optional prefix chunksDerrick Stolee via GitGitGadget, Nov 7, 2022
  27. 21/30 packed-refs: write prefix chunksDerrick Stolee via GitGitGadget, Nov 7, 2022
  28. 22/30 packed-backend: create GIT_TEST_PACKED_REFS_VERSIONDerrick Stolee via GitGitGadget, Nov 7, 2022
  29. 24/30 t5312: allow packed-refs v2 formatDerrick Stolee via GitGitGadget, Nov 7, 2022
  30. 23/30 t1409: test with packed-refs v2Derrick Stolee via GitGitGadget, Nov 7, 2022
  31. 26/30 t3210: require packed-refs v1 for some testsDerrick Stolee via GitGitGadget, Nov 7, 2022
  32. 25/30 t5502: add PACKED_REFS_V1 prerequisiteDerrick Stolee via GitGitGadget, Nov 7, 2022
  33. 27/30 t*: skip packed-refs v2 over http testsDerrick Stolee via GitGitGadget, Nov 7, 2022
  34. 28/30 ci: run GIT_TEST_PACKED_REFS_VERSION=2 in some buildsDerrick Stolee via GitGitGadget, Nov 7, 2022
  35. 29/30 p1401: create performance test for ref operationsDerrick Stolee via GitGitGadget, Nov 7, 2022
  36. 30/30 refs: skip hashing when writing packed-refs v2Derrick Stolee via GitGitGadget, Nov 7, 2022
  37. Derrick StoleeNov 9, 2022
  38. Elijah NewrenNov 11, 2022
  39. Derrick StoleeNov 14, 2022
  40. Elijah NewrenNov 15, 2022
  41. Derrick StoleeNov 16, 2022
  42. Elijah NewrenNov 17, 2022
  43. Junio C HamanoNov 18, 2022
  44. Elijah NewrenNov 19, 2022
  45. Taylor BlauNov 19, 2022
  46. Derrick StoleeNov 30, 2022
  47. Han-Wen NienhuysNov 28, 2022
  48. Derrick StoleeNov 30, 2022
  49. Phillip WoodNov 30, 2022
  50. Taylor BlauNov 30, 2022
  51. Han-Wen NienhuysNov 30, 2022
  52. Sean AllredNov 30, 2022
  53. Derrick StoleeDec 1, 2022
  54. Han-Wen NienhuysDec 2, 2022
  55. Ævar Arnfjörð BjarmasonDec 2, 2022
  56. Junio C HamanoNov 30, 2022

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.