kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 3f86328e70e2af348da45ab24097ca30e09db9d3
parent c89c5909d47ae0b854d9018f0bec63db7c559277
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Thu, 11 Jun 2026 09:45:19 -0700

perf(link): hardware SHA-256 image-id / build-id (−27% on large links)

Replace the two-stream FNV-1a image-id fold with the linker's existing
HW ARMv8 SHA-256 primitive (core/sha256, already used for the Mach-O
code signature — no second SHA impl). The id stays content-derived and
deterministic; only its bytes change. The single digest still feeds
both the ELF .note.gnu.build-id and the Mach-O LC_UUID, so they stay
in lockstep.

Output delta vs the old binary is exactly 48 bytes (16B LC_UUID + the
32B code-sig page hash covering it); nothing structural moves. This was
the one PERF.md lever fenced off solely because it changes UUID/build-id
bytes (so it is excluded from the byte-identical gate by design); it is
gated instead on determinism (stable id across runs) + run-correctness
(links load and execute) + the full non-link byte-identical gate.

Measured kit ld best-of-7 on symbol-count=64000 (objects prebuilt
untimed): 118ms -> 86ms (-27%). The win scales with image bytes; the
image-id fold was the residual top frame on byte/symbol-heavy links.

Diffstat:
Msrc/link/link_image_id.c | 62+++++++++++++++++++++++++++-----------------------------------
1 file changed, 27 insertions(+), 35 deletions(-)

diff --git a/src/link/link_image_id.c b/src/link/link_image_id.c @@ -1,10 +1,14 @@ /* link_image_id_compute: format-agnostic 16-byte identity hash for a * resolved LinkImage. * - * Two FNV-1a 64-bit streams with different seeds produce 128 bits. The - * mix covers each segment's vaddr, file_size, and post-shift bytes, so - * the digest changes if either content or layout shifts. Determinism - * (no time / random component) is intentional — reproducible builds. + * A single SHA-256 stream is fed each segment's vaddr, file_size, and + * post-shift bytes, so the digest changes if either content or layout + * shifts. The 16-byte image id is the first half of the 32-byte digest. + * The SHA-256 core uses the AArch64 crypto extension where the host has + * it (the same hardware path that hashes the Mach-O code signature), so + * the per-byte fold is far cheaper than the old twin-FNV mix — this was + * the residual top frame on pathologically large links. Determinism (no + * time / random component) is intentional — reproducible builds. * * Wrapped per format: * - ELF .note.gnu.build-id (link_emit_elf) @@ -15,47 +19,35 @@ * sees the same bytes. */ #include "core/core.h" +#include "core/sha256.h" #include "link/link_internal.h" -static u64 fnv1a64(const u8* data, size_t n, u64 seed) { - const u64 PRIME = 0x100000001b3ull; - u64 h = seed; - size_t i; - for (i = 0; i < n; ++i) { - h ^= (u64)data[i]; - h *= PRIME; +/* sha256_update takes a u32 length; feed large segments in chunks so a + * >4 GiB segment can never truncate the stream. */ +static void image_id_update(Sha256* s, const u8* data, size_t n) { + while (n) { + u32 chunk = n > 0x40000000u ? 0x40000000u : (u32)n; + sha256_update(s, data, chunk); + data += chunk; + n -= chunk; } - return h; } void link_image_id_compute(const LinkImage* img, u8 out[LINK_IMAGE_ID_BYTES]) { - const u64 SEED_LO = 0xcbf29ce484222325ull; - const u64 SEED_HI = 0x14650fb0739d0383ull; - const u64 PRIME = 0x100000001b3ull; - u64 lo = SEED_LO, hi = SEED_HI; + Sha256 s; + sha256_init(&s); u32 i; for (i = 0; i < img->nsegments; ++i) { const LinkSegment* seg = &img->segments[i]; u64 vaddr = seg->vaddr; u64 fsz = seg->file_size; - lo = fnv1a64((const u8*)&vaddr, sizeof vaddr, lo); - lo = fnv1a64((const u8*)&fsz, sizeof fsz, lo); - hi = fnv1a64((const u8*)&vaddr, sizeof vaddr, hi); - hi = fnv1a64((const u8*)&fsz, sizeof fsz, hi); - /* Fold both digest streams over the segment bytes in a single pass: each - * byte is read once and the two independent multiply chains issue in - * parallel. Bit-for-bit identical to two separate fnv1a64() passes (the - * dominant cost for large images), but ~2x faster. */ - if (img->segment_bytes[i] && fsz) { - const u8* data = img->segment_bytes[i]; - size_t n = (size_t)fsz, j; - for (j = 0; j < n; ++j) { - u8 b = data[j]; - lo = (lo ^ (u64)b) * PRIME; - hi = (hi ^ (u64)b) * PRIME; - } - } + sha256_update(&s, (const u8*)&vaddr, (u32)sizeof vaddr); + sha256_update(&s, (const u8*)&fsz, (u32)sizeof fsz); + if (img->segment_bytes[i] && fsz) + image_id_update(&s, img->segment_bytes[i], (size_t)fsz); } - for (i = 0; i < 8; ++i) out[i] = (u8)(lo >> (i * 8)); - for (i = 0; i < 8; ++i) out[8 + i] = (u8)(hi >> (i * 8)); + u8 digest[SHA256_DIGEST_LEN]; + sha256_final(&s, digest); + /* Image id is the first 16 bytes of the 256-bit digest. */ + for (i = 0; i < LINK_IMAGE_ID_BYTES; ++i) out[i] = digest[i]; }