kit

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

commit 8e8000ef0e4b077ea707cf17e46d23b2695af928
parent 30873143c411b1f4c5fd73db6ecb79b0b0fb4c32
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 10 Jun 2026 19:05:02 -0700

perf: hardware SHA-256 + fused build-id hash; trim codegen type-query path

Linker: symbol-count was the only axis kit lost to ld64 (2.00x). For large
outputs the link was ~85% two full-image content hashes -- the mandatory macOS
codesig (SHA-256/page) and the LC_UUID/build-id (two byte-at-a-time FNV passes).

- src/core/sha256.c: ARMv8 crypto-extension block (sha256h/h2/su0/su1 inline
  asm, no arm_neon.h so it survives -nostdinc), gated on __ARM_FEATURE_SHA2
  (always set on Apple Silicon). Portable scalar fallback otherwise. Plus a
  memcpy-free bulk update path. Bit-identical digest; ~5-8x throughput.
- src/link/link_image_id.c: fuse the two FNV-1a passes into one (each byte read
  once, two multiply chains in parallel). Bit-identical, ~2x.

256k-symbol link 1.12s -> 0.46s; symbol-count 2.00x -> 0.89x clang (now faster
than ld64 across the axis).

Codegen: on body-size, cg_type_get was 9.6% self-time and memset 10%.

- type_cg_builtin fetched two structs by value per operand (the whole builtin
  id table + the target spec) to read one field. New public
  kit_cg_builtin_type_id() single-id accessor; target spec fetched lazily.
- cg_type_get: drop the redundant per-lookup cg_api_init_builtins and inline the
  already-initialized cg_api_get fast path.

body-size ~2.36s -> ~2.06s (~13%); the type-query frames leave the top-10. Hot
path is now the lexer/pp token pump (documented next target).

Verified bit-identical: hash + 17-size kit-hash-vs-shasum differential across
block boundaries, link/macho/elf/dwarf/dist/tools/smoke/cg-api/toy/parse/opt/
isa/debug all green, end-to-end compile+link+run correct. doc/plan/PERF.md
Round 4.

Diffstat:
Mdoc/plan/PERF.md | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minclude/kit/cg.h | 5+++++
Mlang/c/type/type.c | 36++++++++++++++++++++----------------
Msrc/cg/type.c | 13+++++++++++--
Msrc/core/sha256.c | 200++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/link/link_image_id.c | 14++++++++++++--
6 files changed, 309 insertions(+), 22 deletions(-)

diff --git a/doc/plan/PERF.md b/doc/plan/PERF.md @@ -75,6 +75,69 @@ Quick wire-check: `KIT_CC_BENCH_SIZES='8 16 32' KIT_CC_BENCH_SAMPLE=0 make bench ## Findings +### Round 4 — CPU work: linker output hashing + the codegen type-query path + +> 2026-06-10, M1, clang-built `PROFILE=1` kit. With memory + IO handled +> (Rounds 2–3), the remaining cost is **CPU in the actual passes**. Profiling +> the largest input of each axis (multi-second runs so `sample` captures a real +> call graph) surfaced two concentrated hotspots. + +**Linker — `symbol-count` was the only axis kit lost to the system linker +(2.00× ld64).** For a large output the link was ~85 % two full-image content +hashes: `compute_codesig` (the mandatory macOS ad-hoc code signature — SHA-256 +over every 4 KB page) at ~58 %, and `link_image_id_compute` (the LC_UUID / +build-id — **two** byte-at-a-time FNV-1a passes over the whole image) at ~27 %. +kit hashed with a portable scalar SHA-256 while ld64 uses the ARMv8 crypto +extension. Three fixes, all output-bit-identical: + +1. **Hardware SHA-256** (`src/core/sha256.c`). Added an ARMv8 crypto-extension + block (`sha256h`/`h2`/`su0`/`su1` inline asm, no `arm_neon.h` so it survives + the freestanding `-nostdinc` build) gated on `defined(__aarch64__) && + defined(__ARM_FEATURE_SHA2)` — always set on Apple Silicon, so the macOS + linking host (the codesig consumer) is covered with zero runtime detection. + ~5–8× the scalar throughput; same digest. Linux/FreeBSD aarch64 could gain it + behind a HWCAP probe (deferred — keeps core free of platform detection). +2. **memcpy-free `sha256_update`.** The old update staged every byte through a + 64-byte buffer; now full blocks are absorbed straight from the caller — a + whole pass over the data removed (helps every SHA user: codesig, CAS, pkg). +3. **Fused FNV in `link_image_id_compute`.** One pass updating both 64-bit + streams (each byte read once, the two multiply chains issue in parallel) + instead of two passes — bit-identical, ~2× on that 27 %. + +Result: 256k-symbol link **1.12 s → 0.46 s**; benchmark `symbol-count` +**2.00× → 0.89× clang** (per-symbol 3897 ns → 1472 ns), i.e. kit's linker is now +faster than ld64 across the whole axis. The residual large-link top frame is the +image-id FNV (now ~31 %, but only on pathologically large outputs); switching it +to the hardware SHA would help further at the cost of changing UUID/build-id +bytes — deferred. + +**Codegen — the type-query path.** On `body-size` (pure per-statement codegen), +`cg_type_get` was 9.6 % self-time and `_platform_memset` 10 %. Two causes, both +plumbing waste in the hot per-operand path: + +- `type_cg_builtin` (frontend type lowering) fetched **two structs by value per + call** — `kit_cg_builtin_types` builds + copies the whole ~15-entry id table, + `kit_compiler_target_spec` memset+copies the spec — to read one field. Added a + public `kit_cg_builtin_type_id(c, which)` single-id accessor and made the + target-spec fetch lazy (only the `long`/`long double` cases need it). Killed + the by-value-copy memsets (19 → 0 sampled, memset 10.2 % → 6.3 %). +- `cg_type_get` called `cg_api_init_builtins` redundantly per lookup (cg_api_get + already guarantees init) and went through an un-inlined `cg_api_get` call; + inlined the already-initialized fast path (load + index, no call). + +Result: `body-size` **~2.36 s → ~2.06 s (~13 %)**; `cg_type_get`/`cg_api_get` +fall out of the top-10. The hot frame is now the **lexer/pp token pump** +(`lex_next`, `peek`, `pp_next_raw`) — in every compile's path and the clear next +target (left untouched here: intricate, correctness-sensitive, diffuse gains). + +Verified: hash (24 checks + a 17-size `kit hash` vs `shasum` differential across +every block boundary), link 124, macho 80, elf 41, dwarf, dist 182, tools 45, +smoke-x64/rv64, cg-api, toy 1392, parse, opt, isa, debug — all green; +compile+link+run end-to-end correct. (Pre-existing, unrelated: +`test-driver-cc/cc-print-search-dirs-hosted` greps `<sysroot>/include` but the +driver emits `<sysroot>/usr/include` for a Linux sysroot — untouched by this +work.) + ### Round 3 — bounded preprocessor memory (O(expansions) → O(depth)) > 2026-06-10. Round 2's note proposed a heap block free-list to kill the diff --git a/include/kit/cg.h b/include/kit/cg.h @@ -151,6 +151,11 @@ typedef struct KitCgRecordDesc { * operations, comparisons, conversions, and ABI extension attributes. */ KIT_API KitCgBuiltinTypes kit_cg_builtin_types(KitCompiler*); +/* Single builtin id. Equivalent to kit_cg_builtin_types(c).id[which] but + * without materializing (and copying by value) the whole table -- the hot + * type-lowering path queries one builtin at a time. */ +KIT_API KitCgTypeId kit_cg_builtin_type_id(KitCompiler*, KitCgBuiltinType which); + /* Interned structural types. Address space 0 is the normal target data * address space. */ KIT_API KitCgTypeId kit_cg_type_func(KitCompiler*, KitCgFuncSig sig); diff --git a/lang/c/type/type.c b/lang/c/type/type.c @@ -512,44 +512,48 @@ int type_is_signed_integer(const Type* t) { } static KitCgTypeId type_cg_builtin(KitCompiler* c, TypeKind kind) { - KitCgBuiltinTypes b = kit_cg_builtin_types(c); - KitTargetSpec target = kit_compiler_target_spec(c); + /* Hot path: one builtin id per query, no whole-table copy. The target spec + * (a by-value struct) is only consulted for the two data-model-dependent + * widths, so it is fetched lazily inside those cases rather than per call. */ switch (kind) { case TY_VOID: - return b.id[KIT_CG_BUILTIN_VOID]; + return kit_cg_builtin_type_id(c, KIT_CG_BUILTIN_VOID); case TY_BOOL: - return b.id[KIT_CG_BUILTIN_BOOL]; + return kit_cg_builtin_type_id(c, KIT_CG_BUILTIN_BOOL); case TY_CHAR: case TY_SCHAR: case TY_UCHAR: - return b.id[KIT_CG_BUILTIN_I8]; + return kit_cg_builtin_type_id(c, KIT_CG_BUILTIN_I8); case TY_SHORT: case TY_USHORT: - return b.id[KIT_CG_BUILTIN_I16]; + return kit_cg_builtin_type_id(c, KIT_CG_BUILTIN_I16); case TY_INT: case TY_UINT: - return b.id[KIT_CG_BUILTIN_I32]; + return kit_cg_builtin_type_id(c, KIT_CG_BUILTIN_I32); case TY_LONG: case TY_ULONG: - return b.id[kit_target_uses_lp64(target) ? KIT_CG_BUILTIN_I64 - : KIT_CG_BUILTIN_I32]; + return kit_cg_builtin_type_id( + c, kit_target_uses_lp64(kit_compiler_target_spec(c)) + ? KIT_CG_BUILTIN_I64 + : KIT_CG_BUILTIN_I32); case TY_LLONG: case TY_ULLONG: - return b.id[KIT_CG_BUILTIN_I64]; + return kit_cg_builtin_type_id(c, KIT_CG_BUILTIN_I64); case TY_INT128: case TY_UINT128: - return b.id[KIT_CG_BUILTIN_I128]; + return kit_cg_builtin_type_id(c, KIT_CG_BUILTIN_I128); case TY_FLOAT: - return b.id[KIT_CG_BUILTIN_F32]; + return kit_cg_builtin_type_id(c, KIT_CG_BUILTIN_F32); case TY_DOUBLE: - return b.id[KIT_CG_BUILTIN_F64]; + return kit_cg_builtin_type_id(c, KIT_CG_BUILTIN_F64); case TY_LDOUBLE: /* `long double` is IEEE-754 binary128 on targets that follow the quad * psABI (RISC-V, aarch64-linux, wasm32); elsewhere it aliases `double`. * See kit_target_long_double_is_binary128. */ - if (kit_target_long_double_is_binary128(target)) - return b.id[KIT_CG_BUILTIN_F128]; - return b.id[KIT_CG_BUILTIN_F64]; + return kit_cg_builtin_type_id( + c, kit_target_long_double_is_binary128(kit_compiler_target_spec(c)) + ? KIT_CG_BUILTIN_F128 + : KIT_CG_BUILTIN_F64); default: break; } diff --git a/src/cg/type.c b/src/cg/type.c @@ -186,9 +186,12 @@ const CgType* cg_type_get(Compiler* c, KitCgTypeId id) { off = id & CG_API_TYPE_SEG_MASK; if (seg == CG_API_TYPE_BUILTIN_SEG) { if (off >= KIT_CG_BUILTIN_COUNT) return NULL; - s = cg_api_get(c); + /* Fast path: the per-compiler state (with its builtin table) is created + * on first use and then stays put, so inline the already-initialized case + * -- the hot per-op type query becomes a load + index instead of a call + * into cg_api_get. cg_api_get still handles lazy first-time creation. */ + s = c->cg_api ? (CgApiState*)c->cg_api : cg_api_get(c); if (!s) return NULL; - cg_api_init_builtins(c, s); return &s->builtins[off]; } e = api_type_from_id(c, id); @@ -625,6 +628,12 @@ KitCgBuiltinTypes kit_cg_builtin_types(KitCompiler* c) { return out; } +KitCgTypeId kit_cg_builtin_type_id(KitCompiler* c, KitCgBuiltinType which) { + (void)c; + if ((u32)which >= KIT_CG_BUILTIN_COUNT) return KIT_CG_TYPE_NONE; + return builtin_id(which); +} + KitCgTypeId kit_cg_type_ptr(KitCompiler* c, KitCgTypeId pointee, uint32_t address_space) { KitCgTypeId id; diff --git a/src/core/sha256.c b/src/core/sha256.c @@ -2,6 +2,13 @@ #include <string.h> +/* Use the AArch64 SHA-256 crypto extension when the compiler knows the host + * has it (always true on Apple Silicon). SHA256_K is shared: the scalar core + * indexes it and the hardware core loads round-key vectors from it. */ +#if defined(__aarch64__) && defined(__ARM_FEATURE_SHA2) +#define SHA256_HW 1 +#endif + static const u32 SHA256_K[64] = { 0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u, 0x3956c25bu, 0x59f111f1u, 0x923f82a4u, 0xab1c5ed5u, 0xd807aa98u, 0x12835b01u, @@ -17,7 +24,9 @@ static const u32 SHA256_K[64] = { 0x682e6ff3u, 0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u, 0x90befffau, 0xa4506cebu, 0xbef9a3f7u, 0xc67178f2u}; +#ifndef SHA256_HW static u32 sha256_rotr32(u32 v, u32 n) { return (v >> n) | (v << (32 - n)); } +#endif void sha256_init(Sha256* s) { s->h[0] = 0x6a09e667u; @@ -32,6 +41,7 @@ void sha256_init(Sha256* s) { s->total = 0; } +#ifndef SHA256_HW static void sha256_block(Sha256* s, const u8* p) { u32 w[64]; for (u32 i = 0; i < 16; ++i) @@ -71,10 +81,182 @@ static void sha256_block(Sha256* s, const u8* p) { s->h[6] += g; s->h[7] += hh; } +#endif /* !SHA256_HW */ + +/* Absorb `nblocks` complete 64-byte blocks starting at `p`. + * + * On AArch64 hosts that the compiler knows have the SHA-256 crypto extension + * (Apple Silicon always does), this dispatches to a vectorized inline-asm core + * that is ~5-8x the scalar throughput -- the same hardware path ld64 uses, and + * the reason kit's Mach-O code-signature hash (a SHA-256 over every output + * page) stops dominating large links. Output is identical to the scalar core; + * only the instruction selection differs. Other hosts (and AArch64 builds + * without the feature compiled in) use the portable block below. Linux/FreeBSD + * AArch64 could gain the same speedup behind a runtime HWCAP probe -- deferred + * to keep core free of platform feature-detection. */ +#ifdef SHA256_HW +/* The fully unrolled 64-round asm template is one string literal past C99's + * 4095-char minimum; clang/gcc both accept it, so silence -Wpedantic here. */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Woverlength-strings" +static void sha256_blocks(Sha256* s, const u8* p, size_t nblocks) { + if (!nblocks) return; + __asm__ volatile( + "ld1 {v0.4s, v1.4s}, [%[st]] \n" /* ABCD / EFGH */ + "1: \n" + "ld1 {v4.4s, v5.4s, v6.4s, v7.4s}, [%[d]], #64\n" /* MSG0..MSG3 */ + "mov v2.16b, v0.16b \n" /* save ABCD */ + "mov v3.16b, v1.16b \n" /* save EFGH */ + "rev32 v4.16b, v4.16b \n" + "rev32 v5.16b, v5.16b \n" + "rev32 v6.16b, v6.16b \n" + "rev32 v7.16b, v7.16b \n" + "mov x8, %[k] \n" + "ld1 {v19.4s}, [x8], #16 \n" + "add v16.4s, v4.4s, v19.4s \n" /* TMP0 = M0+K0 */ + /* rounds 0-3 */ + "sha256su0 v4.4s, v5.4s \n" + "mov v18.16b, v0.16b \n" + "ld1 {v19.4s}, [x8], #16 \n" + "add v17.4s, v5.4s, v19.4s \n" + "sha256h q0, q1, v16.4s \n" + "sha256h2 q1, q18, v16.4s \n" + "sha256su1 v4.4s, v6.4s, v7.4s \n" + /* rounds 4-7 */ + "sha256su0 v5.4s, v6.4s \n" + "mov v18.16b, v0.16b \n" + "ld1 {v19.4s}, [x8], #16 \n" + "add v16.4s, v6.4s, v19.4s \n" + "sha256h q0, q1, v17.4s \n" + "sha256h2 q1, q18, v17.4s \n" + "sha256su1 v5.4s, v7.4s, v4.4s \n" + /* rounds 8-11 */ + "sha256su0 v6.4s, v7.4s \n" + "mov v18.16b, v0.16b \n" + "ld1 {v19.4s}, [x8], #16 \n" + "add v17.4s, v7.4s, v19.4s \n" + "sha256h q0, q1, v16.4s \n" + "sha256h2 q1, q18, v16.4s \n" + "sha256su1 v6.4s, v4.4s, v5.4s \n" + /* rounds 12-15 */ + "sha256su0 v7.4s, v4.4s \n" + "mov v18.16b, v0.16b \n" + "ld1 {v19.4s}, [x8], #16 \n" + "add v16.4s, v4.4s, v19.4s \n" + "sha256h q0, q1, v17.4s \n" + "sha256h2 q1, q18, v17.4s \n" + "sha256su1 v7.4s, v5.4s, v6.4s \n" + /* rounds 16-19 */ + "sha256su0 v4.4s, v5.4s \n" + "mov v18.16b, v0.16b \n" + "ld1 {v19.4s}, [x8], #16 \n" + "add v17.4s, v5.4s, v19.4s \n" + "sha256h q0, q1, v16.4s \n" + "sha256h2 q1, q18, v16.4s \n" + "sha256su1 v4.4s, v6.4s, v7.4s \n" + /* rounds 20-23 */ + "sha256su0 v5.4s, v6.4s \n" + "mov v18.16b, v0.16b \n" + "ld1 {v19.4s}, [x8], #16 \n" + "add v16.4s, v6.4s, v19.4s \n" + "sha256h q0, q1, v17.4s \n" + "sha256h2 q1, q18, v17.4s \n" + "sha256su1 v5.4s, v7.4s, v4.4s \n" + /* rounds 24-27 */ + "sha256su0 v6.4s, v7.4s \n" + "mov v18.16b, v0.16b \n" + "ld1 {v19.4s}, [x8], #16 \n" + "add v17.4s, v7.4s, v19.4s \n" + "sha256h q0, q1, v16.4s \n" + "sha256h2 q1, q18, v16.4s \n" + "sha256su1 v6.4s, v4.4s, v5.4s \n" + /* rounds 28-31 */ + "sha256su0 v7.4s, v4.4s \n" + "mov v18.16b, v0.16b \n" + "ld1 {v19.4s}, [x8], #16 \n" + "add v16.4s, v4.4s, v19.4s \n" + "sha256h q0, q1, v17.4s \n" + "sha256h2 q1, q18, v17.4s \n" + "sha256su1 v7.4s, v5.4s, v6.4s \n" + /* rounds 32-35 */ + "sha256su0 v4.4s, v5.4s \n" + "mov v18.16b, v0.16b \n" + "ld1 {v19.4s}, [x8], #16 \n" + "add v17.4s, v5.4s, v19.4s \n" + "sha256h q0, q1, v16.4s \n" + "sha256h2 q1, q18, v16.4s \n" + "sha256su1 v4.4s, v6.4s, v7.4s \n" + /* rounds 36-39 */ + "sha256su0 v5.4s, v6.4s \n" + "mov v18.16b, v0.16b \n" + "ld1 {v19.4s}, [x8], #16 \n" + "add v16.4s, v6.4s, v19.4s \n" + "sha256h q0, q1, v17.4s \n" + "sha256h2 q1, q18, v17.4s \n" + "sha256su1 v5.4s, v7.4s, v4.4s \n" + /* rounds 40-43 */ + "sha256su0 v6.4s, v7.4s \n" + "mov v18.16b, v0.16b \n" + "ld1 {v19.4s}, [x8], #16 \n" + "add v17.4s, v7.4s, v19.4s \n" + "sha256h q0, q1, v16.4s \n" + "sha256h2 q1, q18, v16.4s \n" + "sha256su1 v6.4s, v4.4s, v5.4s \n" + /* rounds 44-47 */ + "sha256su0 v7.4s, v4.4s \n" + "mov v18.16b, v0.16b \n" + "ld1 {v19.4s}, [x8], #16 \n" + "add v16.4s, v4.4s, v19.4s \n" + "sha256h q0, q1, v17.4s \n" + "sha256h2 q1, q18, v17.4s \n" + "sha256su1 v7.4s, v5.4s, v6.4s \n" + /* rounds 48-51 (schedule complete) */ + "mov v18.16b, v0.16b \n" + "ld1 {v19.4s}, [x8], #16 \n" + "add v17.4s, v5.4s, v19.4s \n" + "sha256h q0, q1, v16.4s \n" + "sha256h2 q1, q18, v16.4s \n" + /* rounds 52-55 */ + "mov v18.16b, v0.16b \n" + "ld1 {v19.4s}, [x8], #16 \n" + "add v16.4s, v6.4s, v19.4s \n" + "sha256h q0, q1, v17.4s \n" + "sha256h2 q1, q18, v17.4s \n" + /* rounds 56-59 */ + "mov v18.16b, v0.16b \n" + "ld1 {v19.4s}, [x8], #16 \n" + "add v17.4s, v7.4s, v19.4s \n" + "sha256h q0, q1, v16.4s \n" + "sha256h2 q1, q18, v16.4s \n" + /* rounds 60-63 */ + "mov v18.16b, v0.16b \n" + "sha256h q0, q1, v17.4s \n" + "sha256h2 q1, q18, v17.4s \n" + /* fold block result back into running state */ + "add v0.4s, v0.4s, v2.4s \n" + "add v1.4s, v1.4s, v3.4s \n" + "subs %[n], %[n], #1 \n" + "b.ne 1b \n" + "st1 {v0.4s, v1.4s}, [%[st]] \n" + : [d] "+r"(p), [n] "+r"(nblocks) + : [st] "r"(s->h), [k] "r"(SHA256_K) + : "x8", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v16", "v17", + "v18", "v19", "memory", "cc"); +} +#pragma GCC diagnostic pop +#else +static void sha256_blocks(Sha256* s, const u8* p, size_t nblocks) { + while (nblocks--) { + sha256_block(s, p); + p += 64; + } +} +#endif void sha256_update(Sha256* s, const u8* data, u32 n) { s->total += n; - while (n) { + /* Top off a partially filled block first. */ + if (s->buflen) { u32 take = 64u - s->buflen; if (take > n) take = n; memcpy(s->buf + s->buflen, data, take); @@ -82,10 +264,24 @@ void sha256_update(Sha256* s, const u8* data, u32 n) { data += take; n -= take; if (s->buflen == 64) { - sha256_block(s, s->buf); + sha256_blocks(s, s->buf, 1); s->buflen = 0; } } + /* Bulk: absorb full blocks straight from the caller's buffer -- no staging + * memcpy, which on a multi-MB hash (e.g. the Mach-O code signature) is a + * whole extra pass over the data avoided. */ + if (n >= 64u) { + size_t nb = n >> 6; + sha256_blocks(s, data, nb); + data += nb * 64u; + n -= (u32)(nb * 64u); + } + /* Stash the sub-block tail. */ + if (n) { + memcpy(s->buf + s->buflen, data, n); + s->buflen += n; + } } void sha256_final(Sha256* s, u8 out[SHA256_DIGEST_LEN]) { diff --git a/src/link/link_image_id.c b/src/link/link_image_id.c @@ -31,6 +31,7 @@ static u64 fnv1a64(const u8* data, size_t n, u64 seed) { 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; u32 i; for (i = 0; i < img->nsegments; ++i) { @@ -41,9 +42,18 @@ void link_image_id_compute(const LinkImage* img, u8 out[LINK_IMAGE_ID_BYTES]) { 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) { - lo = fnv1a64(img->segment_bytes[i], (size_t)fsz, lo); - hi = fnv1a64(img->segment_bytes[i], (size_t)fsz, hi); + 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; + } } } for (i = 0; i < 8; ++i) out[i] = (u8)(lo >> (i * 8));