{"article":{"slug":"solving-for-faster-sha-1-collision-detection","title":"Solving for faster SHA-1 collision detection","subtitle":null,"summary":"tl;dr: I discovered collision-detecting SHA-1 is slow and decided to build my own. sha1dc is a rewrite of SHA-1 with collision detection, whose code generator uses a solver to fit collision tests into SIMD lanes. It runs at 68–81% of plain SHA-1's speed where the existing crate runs at 28–29%, and can make git pack verification twice as fast.","content_type":"blog_post","language":"en","canonical_url":"https://sam.dev/blog/faster-sha1-collision-detection","author":{"name":"Sam Reis","url":"https://sam.dev/","person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"sam.dev","url":"https://sam.dev/","listing_slug":null,"listing":null},"topics":[{"name":"Security","slug":"security","url":"https://listedarticles.com/topics/security"},{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"Performance","slug":"performance","url":"https://listedarticles.com/topics/performance"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":2227,"reading_minutes":10,"published_at":"2026-09-20T12:00:00.000Z","added_at":"2026-09-24T06:18:13.806Z","updated_at":"2026-09-24T06:18:13.806Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/solving-for-faster-sha-1-collision-detection","markdown_url":"https://listedarticles.com/articles/solving-for-faster-sha-1-collision-detection.md","example":false,"citation":"Sam Reis, sam.dev. \"Solving for faster SHA-1 collision detection.\" 20 Sept 2026. https://sam.dev/blog/faster-sha1-collision-detection (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://sam.dev/blog/faster-sha1-collision-detection"},"body_markdown":"# Solving for faster SHA-1 collision detection\n\n**tl;dr:** I discovered collision-detecting SHA-1 is slow and decided to build\nmy own. `sha1dc` is a rewrite of SHA-1 with\ncollision detection, whose code generator uses a solver to fit collision tests\ninto SIMD lanes. It runs at 68–81% of plain SHA-1's speed where the existing\ncrate runs at 28–29%, and can make git pack verification twice as fast.\n\n## How git uses SHA-1 and why it has to be slow(er)\n\nAs part of working on Enroute, I'm currently deep into optimizing the performance of a git server backend. One thing you do a lot in a git server is accepting pack files from git clients. These pack files are untrusted input and have to be validated, which includes checking the SHA-1 hash of the included git objects.\n\nI'm using `gitoxide` and as it\nturns out, pack verification can be quite slow. Verifying the pack of a bare\ngit/git clone (421,292 objects, 305 MiB\ncompressed, 7.7 GiB inflated) on my M4 takes 12.5 seconds, while spending\n**84%** of that in SHA-1.\n\nUnderneath, it uses a crate called `sha1-checked`, a SHA-1 library with\ncollision detection, running at about 900 MiB/s on that machine. In contrast,\nplain `sha1` with the M4's SHA-1 hardware instructions sits at about 3 GB/s.\n\nWait, collision detection? Yes: What makes SHA-1 difficult for untrusted input is that it is known to be cryptographically broken because chosen-prefix collisions are practical. Ideally, we would of course all be using SHA-256 for our git objects, but migrations...\n\nLuckily, this security issue can be mitigated by detecting those manufactured collisions within the SHA-1 state space, which is what git is doing. Using this approach, when a git server detects a collision, it refuses to accept these objects from the client.\n\nOn the not so lucky side, this is obviously quite slow! And thus I set out to see if I could make things faster and improve the performance of SHA-1 with collision detection.\n\n## Low-hanging fruit\n\nTo start with, I had a closer look at `sha1-checked`, and found something to do\nstraight away: it had no hardware acceleration on the detecting path. Modern\n`arm64` and `x86_64` CPUs have SHA-1 instructions and I initially expected that\nit would try and use them.\n\nHowever, the catch with the hardware instructions in this case is that collision detection runs off the internal SHA-1 message schedule and hash state, which the hardware instructions make difficult to access.\n\nWhat I changed is to spill the schedule to a buffer as it is expanded, run the\nhappy path through the hardware, and only fall back to a scalar recomputation\nfor the rare block that looks suspicious. This roughly doubled throughput on\nboth architectures: 928 → 1996 MB/s on Apple Silicon, ~300 → ~640 MB/s on an\nAMD EPYC with `sha_ni`.\n\nAt the time, I wrapped this up into a pull\nrequest to `sha1-checked`,\nwhich is currently open, waiting on the `0.11` release before review.\n\nBut then I became curious to see how much better we could do.\n\n## The wall of constants\n\nWith the simple fix of the way, the next bottleneck quickly became visible.\n\nAs far as I could deduct from the code at this point (we'll get to the theory in a minute), collision detection needs to do two things per block. First it runs a cheap filter: about 150 tests on individual bits of the expanded message, each of which rules out some of the known attack patterns. The filter keeps a mask with one bit per pattern and clears bits as tests fail. Then, only if the mask is still non-zero, comes an expensive recomputation of the block that settles the question for good.\n\nOn ordinary data about 95% of blocks leave the filter with an empty mask, so the recomputation almost never runs. Without the filter, hashing would crawl along at around 40 MiB/s, and the filter itself is where a collision-detecting SHA-1 spends the time it spends beyond plain SHA-1.\n\nHowever, there was a problem. Here is what this code looks like\nin `sha1-checked`, which seems to be a more or less direct\ntranslation of the original C code, which in turn was generated\nby a tool from the data files of the research paper behind it:\n\nThat goes on for about 475 lines, after 540 lines of hex tables. It is certainly correct based on the test coverage, but at least to me it is also completely opaque.\n\nI simply could not see how to make it faster without understanding where the numbers came from, and there was nothing in the code to help with that.\n\n## Reading the paper\n\nSo I went back to the source: Stevens and Shumow's paper on speeding up detection explains what the filter is testing. The paper itself can be a bit dry, but they also have slides and a video presentation.\n\nIt turns out that SHA-1 collision attacks are built from patterns of message\ndifferences called *disturbance vectors*, and the paper selects the 32 cheapest\nto attack with (32 because the mask is a 32-bit integer). What the detector\ntries to figure out is whether a block could be part of an attack along any of\nthose 32 vectors.\n\nFor each vector, the paper derives seven to fifteen so-called *unavoidable bit\nconditions*, which are relations between pairs of bits of the expanded message\nthat must hold if an attack along that vector is in progress. Each one is cheap\nto check, and if it fails, that vector can be crossed off.\n\nUsing these conditions, a small program from the paper's tools repository turns them into checks. It enumerates every linear combination of a vector's conditions and greedily picks, at each step, the relation that covers the most vectors not yet covered, breaking ties by how cheap the relation is to test: fewest active bits, then fewest distinct bit positions, then the smallest distance between the two words.\n\nHere is why the generator works: A condition says that two particular bits of the expanded message must be equal (or must differ). Relations like that chain: if bit A must equal bit B, and bit B must equal bit C, then A must equal C. So for each disturbance vector there is not one list of pairs to check but a whole family of equivalent lists, and the generator gets to choose among them.\n\nThe paper's generator chooses pairs that many vectors have in common, so that one statement can serve several of them at once. That is the choice that minimises statements and it is best for scalar computation.\n\nHowever, could SIMD units change what a good choice looks like?\n\nA few SSE2 or NEON instructions can compare four pairs of bits at once, but only if the four pairs can be processed simultaneously, meaning if they have the same distance between the two words, the same bit positions within them, and so on.\n\n## Starting over\n\nAt that point, a plan started to form: Instead of trying to hand-tune implementations for a given architecture, I wanted to take these theoretical foundations and introduce them to the world of SIMD.\n\nAnd so, `sha1dc` was born. It is a bottom-up\nrebuild of SHA-1 with collision detection in Rust. It uses the SHA-1\ninstructions on `x86_64` and `arm64` for the hashing itself, and generates\n`neon`, `sse2` and `avx2` forms of the collision check.\n\nUnderneath, it uses a code generator that can be aimed at different vector units to deterministically generate code tuned to their specific characteristics.\n\n### How it works\n\nWhen vectorizing the unavoidable bit conditions, every vector group is equally expensive to execute, but the effectiveness varies: The first few groups buy a lot, because each one rules out a large share of blocks for some disturbance vector. After that the returns shrink, for two reasons: Most disturbance vectors are already ruled out on most blocks, so another group barely changes anything. And second, the conditions that fit four to a group begin to run out; the leftovers would mostly fill one lane out of four.\n\nThat is why `sha1dc` generates checks in two parts, with the split between them\nrepresenting the break-even point:\n\n- The **prefix** runs on every block, unconditionally. Its unit is a*group* :\none pair of vector loads covering up to 4 or 8 conditions of one shape at\nconsecutive words.\n- The **tail** is a cascading sequence of scalar conditions, very similar to\nthe fully scalar implementation, which only runs when a block hasn't been\ndisqualified previously by the prefix.\n\nTo accomplish this, `sha1dc` uses a solver. The solver's job is to decide which\nconditions go into the vector prefix, and in which groups, so that the prefix\nrules out as many blocks as it can for the number of groups it is allowed.\nWhatever it does not rule out falls to the scalar tail. The tension is that a\ngroup only pays for its full four lanes when four conditions of the same shape\nline up at consecutive words, and the conditions that line up best are not\nnecessarily the ones that rule out the most blocks.\n\nThink of a random block being hashed. Each independent condition a vector has\nin the prefix is a coin flip: the two bits either match or they do not. So a\nvector with `r` such conditions survives with probability `2^(−r)`, and summing\nthat over the 32 vectors gives the expected number of survivors, which is how\nthe solver knows statistically how often the tail has to run.\n\nThat number keeps improving with every group, but throughput does not: measured on the M4, it peaks well before the tail-entry rate bottoms out. The way this trade-off is encoded is via a budget for each instruction set: The solver receives a limit on the number of prefix groups which is set so that it maximizes throughput.\n\n### How it's tested\n\nWhen it comes to testing, I am a big fan of fuzzing and property tests to suss\nout problems that fixed test batteries won't find. So naturally there are\nplenty of property tests in `sha1dc`.\n\nHowever, purely random inputs have a big blind spot in this case: we only progress to the checks in the tail when none of the prefix checks have triggered, and given that we're optimizing specifically for the effectiveness of prefix checks, this becomes extremely difficult to trigger with random inputs.\n\nLuckily, this also can be solved for: because SHA-1 seeds its internal buffer by linearly expanding the input block, a condition on two of its bits is an equation on the block's bits. Keeping one vector alive means satisfying at most 15 such equations, with 512 bits to play with. Computed once, you can fill the hundreds of unconstrained bits with random values, and out comes a fresh block that satisfies every condition, as many times as you like.\n\nThe tests in `sha1dc` generate 64 such witnesses per vector, 2,048 in all, from\na single seed, so that the checks behind even the rarest vector run.\n\n## Final results\n\nThe first version of `sha1dc` is now\npublished on `crates.io`, and it narrows the gap to plain SHA-1 to 19–32%,\ndepending on the instruction set.\n\n### Microbenchmarking\n\nMeasuring 16 KiB of pseudo-random input against two baselines:\n\n1. The `sha1` crate, which does no detection but has hardware-acceleration.\n2. The `sha1-checked` crate, which does detection but has no\nhardware-acceleration.\n\nThroughput in MiB/s, and as a fraction of the `sha1` row:\n\n| implementation | Apple M4 | Xeon Platinum 8488C | Graviton4 | \n|---|---|---|---|\n| `sha1` | 2979 (100%) | 1887 (100%) | 1616 (100%) | \n| **`sha1dc`** | **2400 (81%)** | **1285 (68%)** | **1286 (80%)** | \n| `sha1-checked` | 856 (29%) | 523 (28%) | 462 (29%) | \n| `sha1-checked` + PR #910 | 1712 (57%) | 877 (46%) | 980 (61%) | \n\nGiven that both `sha1` and `sha1dc` use the machine's SHA-1 instructions, the\ngap between them is, all else being equal, the cost of collision detection in\n`sha1dc`: 19% to 32%, depending on the machine.\n\nThe gap to `sha1-checked` is bigger, 2.5× to 2.8×, with most of it due to\nhardware acceleration. When adding my PR against `sha1-checked`, it gives a\nclearer picture: the hardware SHA-1 instructions take it to 46% to 61% of plain\nSHA-1, and the vector check takes it the rest of the way, another 1.3× to 1.5×\non top.\n\n### Real world performance\n\nThis all started with wanting to make `git` faster, so how are we doing? We're\nmeasuring gitoxide, which normally\ngets its SHA-1 from `gix-hash`, which wraps `sha1-checked`. For testing,\n`sha1dc` was swapped in. All tests ran on my M4 machine, and I picked the\nfastest of five runs.\n\n| operation | `sha1-checked` | **`sha1dc`** | speedup | \n|---|---|---|---|\n| `pack verify` , all cores | 2.33 s | **1.09 s** | 2.13× | \n| `pack verify` , one thread | 12.55 s | **6.26 s** | 2.01× | \n| `index-pack` , all cores | 4.23 s | **2.79 s** | 1.52× | \n| `index-pack` , one thread | 14.72 s | **8.02 s** | 1.84× | \n\nThe numbers from the start of this post move the way they should: SHA-1 with\ndetection goes from 84% of the single-threaded `pack verify` to 73%, with zlib\ninflation rising from 11% to 18% as the hashing shrinks around it.\n\n## What's next\n\nWhile we're already using this in Enroute, I would also\nlove to upstream this into `gitoxide` itself so that the whole Rust-based `git`\necosystem can benefit.\n\nThat said, the single biggest thing in the `pack verify` profile is still SHA-1,\nso this journey is far from over. Stay tuned!","body_html":"<h1 id=\"solving-for-faster-sha-1-collision-detection\">Solving for faster SHA-1 collision detection</h1>\n<p><strong>tl;dr:</strong> I discovered collision-detecting SHA-1 is slow and decided to build\nmy own. <code>sha1dc</code> is a rewrite of SHA-1 with\ncollision detection, whose code generator uses a solver to fit collision tests\ninto SIMD lanes. It runs at 68–81% of plain SHA-1&#39;s speed where the existing\ncrate runs at 28–29%, and can make git pack verification twice as fast.</p>\n<h2 id=\"how-git-uses-sha-1-and-why-it-has-to-be-slow-er\">How git uses SHA-1 and why it has to be slow(er)</h2>\n<p>As part of working on Enroute, I&#39;m currently deep into optimizing the performance of a git server backend. One thing you do a lot in a git server is accepting pack files from git clients. These pack files are untrusted input and have to be validated, which includes checking the SHA-1 hash of the included git objects.</p>\n<p>I&#39;m using <code>gitoxide</code> and as it\nturns out, pack verification can be quite slow. Verifying the pack of a bare\ngit/git clone (421,292 objects, 305 MiB\ncompressed, 7.7 GiB inflated) on my M4 takes 12.5 seconds, while spending\n<strong>84%</strong> of that in SHA-1.</p>\n<p>Underneath, it uses a crate called <code>sha1-checked</code>, a SHA-1 library with\ncollision detection, running at about 900 MiB/s on that machine. In contrast,\nplain <code>sha1</code> with the M4&#39;s SHA-1 hardware instructions sits at about 3 GB/s.</p>\n<p>Wait, collision detection? Yes: What makes SHA-1 difficult for untrusted input is that it is known to be cryptographically broken because chosen-prefix collisions are practical. Ideally, we would of course all be using SHA-256 for our git objects, but migrations...</p>\n<p>Luckily, this security issue can be mitigated by detecting those manufactured collisions within the SHA-1 state space, which is what git is doing. Using this approach, when a git server detects a collision, it refuses to accept these objects from the client.</p>\n<p>On the not so lucky side, this is obviously quite slow! And thus I set out to see if I could make things faster and improve the performance of SHA-1 with collision detection.</p>\n<h2 id=\"low-hanging-fruit\">Low-hanging fruit</h2>\n<p>To start with, I had a closer look at <code>sha1-checked</code>, and found something to do\nstraight away: it had no hardware acceleration on the detecting path. Modern\n<code>arm64</code> and <code>x86_64</code> CPUs have SHA-1 instructions and I initially expected that\nit would try and use them.</p>\n<p>However, the catch with the hardware instructions in this case is that collision detection runs off the internal SHA-1 message schedule and hash state, which the hardware instructions make difficult to access.</p>\n<p>What I changed is to spill the schedule to a buffer as it is expanded, run the\nhappy path through the hardware, and only fall back to a scalar recomputation\nfor the rare block that looks suspicious. This roughly doubled throughput on\nboth architectures: 928 → 1996 MB/s on Apple Silicon, ~300 → ~640 MB/s on an\nAMD EPYC with <code>sha_ni</code>.</p>\n<p>At the time, I wrapped this up into a pull\nrequest to <code>sha1-checked</code>,\nwhich is currently open, waiting on the <code>0.11</code> release before review.</p>\n<p>But then I became curious to see how much better we could do.</p>\n<h2 id=\"the-wall-of-constants\">The wall of constants</h2>\n<p>With the simple fix of the way, the next bottleneck quickly became visible.</p>\n<p>As far as I could deduct from the code at this point (we&#39;ll get to the theory in a minute), collision detection needs to do two things per block. First it runs a cheap filter: about 150 tests on individual bits of the expanded message, each of which rules out some of the known attack patterns. The filter keeps a mask with one bit per pattern and clears bits as tests fail. Then, only if the mask is still non-zero, comes an expensive recomputation of the block that settles the question for good.</p>\n<p>On ordinary data about 95% of blocks leave the filter with an empty mask, so the recomputation almost never runs. Without the filter, hashing would crawl along at around 40 MiB/s, and the filter itself is where a collision-detecting SHA-1 spends the time it spends beyond plain SHA-1.</p>\n<p>However, there was a problem. Here is what this code looks like\nin <code>sha1-checked</code>, which seems to be a more or less direct\ntranslation of the original C code, which in turn was generated\nby a tool from the data files of the research paper behind it:</p>\n<p>That goes on for about 475 lines, after 540 lines of hex tables. It is certainly correct based on the test coverage, but at least to me it is also completely opaque.</p>\n<p>I simply could not see how to make it faster without understanding where the numbers came from, and there was nothing in the code to help with that.</p>\n<h2 id=\"reading-the-paper\">Reading the paper</h2>\n<p>So I went back to the source: Stevens and Shumow&#39;s paper on speeding up detection explains what the filter is testing. The paper itself can be a bit dry, but they also have slides and a video presentation.</p>\n<p>It turns out that SHA-1 collision attacks are built from patterns of message\ndifferences called <em>disturbance vectors</em>, and the paper selects the 32 cheapest\nto attack with (32 because the mask is a 32-bit integer). What the detector\ntries to figure out is whether a block could be part of an attack along any of\nthose 32 vectors.</p>\n<p>For each vector, the paper derives seven to fifteen so-called *unavoidable bit\nconditions*, which are relations between pairs of bits of the expanded message\nthat must hold if an attack along that vector is in progress. Each one is cheap\nto check, and if it fails, that vector can be crossed off.</p>\n<p>Using these conditions, a small program from the paper&#39;s tools repository turns them into checks. It enumerates every linear combination of a vector&#39;s conditions and greedily picks, at each step, the relation that covers the most vectors not yet covered, breaking ties by how cheap the relation is to test: fewest active bits, then fewest distinct bit positions, then the smallest distance between the two words.</p>\n<p>Here is why the generator works: A condition says that two particular bits of the expanded message must be equal (or must differ). Relations like that chain: if bit A must equal bit B, and bit B must equal bit C, then A must equal C. So for each disturbance vector there is not one list of pairs to check but a whole family of equivalent lists, and the generator gets to choose among them.</p>\n<p>The paper&#39;s generator chooses pairs that many vectors have in common, so that one statement can serve several of them at once. That is the choice that minimises statements and it is best for scalar computation.</p>\n<p>However, could SIMD units change what a good choice looks like?</p>\n<p>A few SSE2 or NEON instructions can compare four pairs of bits at once, but only if the four pairs can be processed simultaneously, meaning if they have the same distance between the two words, the same bit positions within them, and so on.</p>\n<h2 id=\"starting-over\">Starting over</h2>\n<p>At that point, a plan started to form: Instead of trying to hand-tune implementations for a given architecture, I wanted to take these theoretical foundations and introduce them to the world of SIMD.</p>\n<p>And so, <code>sha1dc</code> was born. It is a bottom-up\nrebuild of SHA-1 with collision detection in Rust. It uses the SHA-1\ninstructions on <code>x86_64</code> and <code>arm64</code> for the hashing itself, and generates\n<code>neon</code>, <code>sse2</code> and <code>avx2</code> forms of the collision check.</p>\n<p>Underneath, it uses a code generator that can be aimed at different vector units to deterministically generate code tuned to their specific characteristics.</p>\n<h3 id=\"how-it-works\">How it works</h3>\n<p>When vectorizing the unavoidable bit conditions, every vector group is equally expensive to execute, but the effectiveness varies: The first few groups buy a lot, because each one rules out a large share of blocks for some disturbance vector. After that the returns shrink, for two reasons: Most disturbance vectors are already ruled out on most blocks, so another group barely changes anything. And second, the conditions that fit four to a group begin to run out; the leftovers would mostly fill one lane out of four.</p>\n<p>That is why <code>sha1dc</code> generates checks in two parts, with the split between them\nrepresenting the break-even point:</p>\n<ul><li><p>The <strong>prefix</strong> runs on every block, unconditionally. Its unit is a<em>group</em> :</p><p>one pair of vector loads covering up to 4 or 8 conditions of one shape at\nconsecutive words.</p></li><li><p>The <strong>tail</strong> is a cascading sequence of scalar conditions, very similar to</p><p>the fully scalar implementation, which only runs when a block hasn&#39;t been\ndisqualified previously by the prefix.</p></li></ul>\n<p>To accomplish this, <code>sha1dc</code> uses a solver. The solver&#39;s job is to decide which\nconditions go into the vector prefix, and in which groups, so that the prefix\nrules out as many blocks as it can for the number of groups it is allowed.\nWhatever it does not rule out falls to the scalar tail. The tension is that a\ngroup only pays for its full four lanes when four conditions of the same shape\nline up at consecutive words, and the conditions that line up best are not\nnecessarily the ones that rule out the most blocks.</p>\n<p>Think of a random block being hashed. Each independent condition a vector has\nin the prefix is a coin flip: the two bits either match or they do not. So a\nvector with <code>r</code> such conditions survives with probability <code>2^(−r)</code>, and summing\nthat over the 32 vectors gives the expected number of survivors, which is how\nthe solver knows statistically how often the tail has to run.</p>\n<p>That number keeps improving with every group, but throughput does not: measured on the M4, it peaks well before the tail-entry rate bottoms out. The way this trade-off is encoded is via a budget for each instruction set: The solver receives a limit on the number of prefix groups which is set so that it maximizes throughput.</p>\n<h3 id=\"how-it-s-tested\">How it&#39;s tested</h3>\n<p>When it comes to testing, I am a big fan of fuzzing and property tests to suss\nout problems that fixed test batteries won&#39;t find. So naturally there are\nplenty of property tests in <code>sha1dc</code>.</p>\n<p>However, purely random inputs have a big blind spot in this case: we only progress to the checks in the tail when none of the prefix checks have triggered, and given that we&#39;re optimizing specifically for the effectiveness of prefix checks, this becomes extremely difficult to trigger with random inputs.</p>\n<p>Luckily, this also can be solved for: because SHA-1 seeds its internal buffer by linearly expanding the input block, a condition on two of its bits is an equation on the block&#39;s bits. Keeping one vector alive means satisfying at most 15 such equations, with 512 bits to play with. Computed once, you can fill the hundreds of unconstrained bits with random values, and out comes a fresh block that satisfies every condition, as many times as you like.</p>\n<p>The tests in <code>sha1dc</code> generate 64 such witnesses per vector, 2,048 in all, from\na single seed, so that the checks behind even the rarest vector run.</p>\n<h2 id=\"final-results\">Final results</h2>\n<p>The first version of <code>sha1dc</code> is now\npublished on <code>crates.io</code>, and it narrows the gap to plain SHA-1 to 19–32%,\ndepending on the instruction set.</p>\n<h3 id=\"microbenchmarking\">Microbenchmarking</h3>\n<p>Measuring 16 KiB of pseudo-random input against two baselines:</p>\n<ol><li>The <code>sha1</code> crate, which does no detection but has hardware-acceleration.</li><li><p>The <code>sha1-checked</code> crate, which does detection but has no</p><p>hardware-acceleration.</p></li></ol>\n<p>Throughput in MiB/s, and as a fraction of the <code>sha1</code> row:</p>\n<div class=\"table-wrap\"><table><thead><tr><th>implementation</th><th>Apple M4</th><th>Xeon Platinum 8488C</th><th>Graviton4</th></tr></thead><tbody><tr><td><code>sha1</code></td><td>2979 (100%)</td><td>1887 (100%)</td><td>1616 (100%)</td></tr><tr><td><strong><code>sha1dc</code></strong></td><td><strong>2400 (81%)</strong></td><td><strong>1285 (68%)</strong></td><td><strong>1286 (80%)</strong></td></tr><tr><td><code>sha1-checked</code></td><td>856 (29%)</td><td>523 (28%)</td><td>462 (29%)</td></tr><tr><td><code>sha1-checked</code> + PR #910</td><td>1712 (57%)</td><td>877 (46%)</td><td>980 (61%)</td></tr></tbody></table></div>\n<p>Given that both <code>sha1</code> and <code>sha1dc</code> use the machine&#39;s SHA-1 instructions, the\ngap between them is, all else being equal, the cost of collision detection in\n<code>sha1dc</code>: 19% to 32%, depending on the machine.</p>\n<p>The gap to <code>sha1-checked</code> is bigger, 2.5× to 2.8×, with most of it due to\nhardware acceleration. When adding my PR against <code>sha1-checked</code>, it gives a\nclearer picture: the hardware SHA-1 instructions take it to 46% to 61% of plain\nSHA-1, and the vector check takes it the rest of the way, another 1.3× to 1.5×\non top.</p>\n<h3 id=\"real-world-performance\">Real world performance</h3>\n<p>This all started with wanting to make <code>git</code> faster, so how are we doing? We&#39;re\nmeasuring gitoxide, which normally\ngets its SHA-1 from <code>gix-hash</code>, which wraps <code>sha1-checked</code>. For testing,\n<code>sha1dc</code> was swapped in. All tests ran on my M4 machine, and I picked the\nfastest of five runs.</p>\n<div class=\"table-wrap\"><table><thead><tr><th>operation</th><th><code>sha1-checked</code></th><th><strong><code>sha1dc</code></strong></th><th>speedup</th></tr></thead><tbody><tr><td><code>pack verify</code> , all cores</td><td>2.33 s</td><td><strong>1.09 s</strong></td><td>2.13×</td></tr><tr><td><code>pack verify</code> , one thread</td><td>12.55 s</td><td><strong>6.26 s</strong></td><td>2.01×</td></tr><tr><td><code>index-pack</code> , all cores</td><td>4.23 s</td><td><strong>2.79 s</strong></td><td>1.52×</td></tr><tr><td><code>index-pack</code> , one thread</td><td>14.72 s</td><td><strong>8.02 s</strong></td><td>1.84×</td></tr></tbody></table></div>\n<p>The numbers from the start of this post move the way they should: SHA-1 with\ndetection goes from 84% of the single-threaded <code>pack verify</code> to 73%, with zlib\ninflation rising from 11% to 18% as the hashing shrinks around it.</p>\n<h2 id=\"what-s-next\">What&#39;s next</h2>\n<p>While we&#39;re already using this in Enroute, I would also\nlove to upstream this into <code>gitoxide</code> itself so that the whole Rust-based <code>git</code>\necosystem can benefit.</p>\n<p>That said, the single biggest thing in the <code>pack verify</code> profile is still SHA-1,\nso this journey is far from over. Stay tuned!</p>","headings":[{"level":1,"text":"Solving for faster SHA-1 collision detection","id":"solving-for-faster-sha-1-collision-detection"},{"level":2,"text":"How git uses SHA-1 and why it has to be slow(er)","id":"how-git-uses-sha-1-and-why-it-has-to-be-slow-er"},{"level":2,"text":"Low-hanging fruit","id":"low-hanging-fruit"},{"level":2,"text":"The wall of constants","id":"the-wall-of-constants"},{"level":2,"text":"Reading the paper","id":"reading-the-paper"},{"level":2,"text":"Starting over","id":"starting-over"},{"level":3,"text":"How it works","id":"how-it-works"},{"level":3,"text":"How it's tested","id":"how-it-s-tested"},{"level":2,"text":"Final results","id":"final-results"},{"level":3,"text":"Microbenchmarking","id":"microbenchmarking"},{"level":3,"text":"Real world performance","id":"real-world-performance"},{"level":2,"text":"What's next","id":"what-s-next"}]}}