Skip to content

Commit f810a42

Browse files
committed
feat(digest): add traits for hashing many messages at once
A hash processes one message serially: each block depends on the last, so a single message cannot be spread across SIMD lanes. Independent messages have no such dependency, so several can be hashed side by side, one per lane. This is a different axis of parallelism from ParBlocks, which processes several blocks of one stream. Add a feature-gated `multi` module structured as two layers, each hiding one detail from the layer above: MultiUpdateBackend hides the implementation. One backend is one way of doing the work - AVX2, AVX-512, portable, or a future instruction set - with a lane count fixed by the hardware it targets. MultiUpdateCore and MultiFixedOutputCore hide the lane count. A caller asks to hash some number of messages, a property of its workload rather than of the machine; the core selects a backend for the current CPU, splits the batch across it, and handles the remainder. update_blocks takes whole blocks, which keeps absorption free of copies: blocks are read where the caller already keeps them, and lanes are borrowed separately so messages need not be contiguous. A shared prefix is absorbed once for all lanes. Only finalize takes a sub-block tail, and the only bytes copied are the final padded blocks, which do not exist in the message. Message count, block counts, and tail length are const generics, so lanes advance in step by construction: unequal lengths fail to compile rather than panicking, and the API is infallible.
1 parent 9a0012f commit f810a42

1 file changed

Lines changed: 180 additions & 39 deletions

File tree

digest/src/multi.rs

Lines changed: 180 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,193 @@
1-
//! Multi-buffer (multi-message) hashing.
1+
//! Hashing many independent messages at once.
22
//!
3-
//! [`MultiDigest`] hashes many **independent messages of one compile-time length `N`** at
4-
//! once — one per SIMD lane — running the compression across all lanes simultaneously.
5-
//! This is a different axis of parallelism from block-level `ParBlocks` (which processes
6-
//! multiple blocks of a *single* stream): here each lane is a distinct message.
3+
//! An ordinary hash reads one message and produces one digest. The compression function
4+
//! inside it is serial — block *n* depends on block *n−1* — so a single message cannot be
5+
//! spread across SIMD lanes. But *different* messages are completely independent of one
6+
//! another, so several of them can be hashed side by side, one per lane, with the same
7+
//! instructions driving all lanes at once. That is what this module exposes.
78
//!
8-
//! The message length `N` and batch size `B` are const generics, so equal length and the
9-
//! one-output-per-message count are enforced by the type system rather than checked at
10-
//! run time — `&[&[u8; N]; B] -> [Output; B]` cannot mismatch. Messages are passed *by
11-
//! reference* (`&[u8; N]`), so they need not be contiguous in memory. The lane count is
12-
//! deliberately *not* exposed: it is the hardware detail (AVX2 vs AVX-512 width) that an
13-
//! implementation abstracts over via its own runtime dispatch.
9+
//! This is a different kind of parallelism from
10+
//! [`ParBlocks`](crate::common::ParBlocks), which processes several blocks of a *single*
11+
//! stream. Here each lane is a separate message.
12+
//!
13+
//! # Two layers, each hiding one detail
14+
//!
15+
//! Two unrelated things must be kept apart, and each layer hides one of them from the layer
16+
//! above:
17+
//!
18+
//! 1. **[`MultiUpdateBackend`] hides the implementation.** One backend is one concrete way
19+
//! of doing the work — an AVX2 routine, an AVX-512 routine, a plain portable one, or
20+
//! some future instruction set. Each has a lane count fixed by the hardware it targets.
21+
//! An algorithm provides as many backends as it has implementations.
22+
//!
23+
//! 2. **[`MultiUpdateCore`] / [`MultiFixedOutputCore`] hide the lane count.** Callers ask
24+
//! to hash some number of messages; that number is a property of their workload and has
25+
//! nothing to do with how wide the machine is. The core picks a backend suitable for the
26+
//! current CPU, splits the messages across it, and handles any leftover that does not
27+
//! fill a full set of lanes. Above this layer, lane counts never appear.
28+
//!
29+
//! These mirror the layering this crate already uses for ordinary hashing:
30+
//!
31+
//! | layer | hides | here | single-stream analogue |
32+
//! |-------|-------|------|------------------------|
33+
//! | 1 | the implementation | [`MultiUpdateBackend`] | [`BlockCipherEncBackend`] |
34+
//! | 2 | the lane count | [`MultiUpdateCore`], [`MultiFixedOutputCore`] | [`UpdateCore`], [`FixedOutputCore`] |
35+
//!
36+
//! [`BlockCipherEncBackend`]: https://docs.rs/cipher
37+
//! [`UpdateCore`]: crate::block_api::UpdateCore
38+
//! [`FixedOutputCore`]: crate::block_api::FixedOutputCore
39+
//!
40+
//! # Blocks in, a final tail at the end
41+
//!
42+
//! [`MultiUpdateCore::update_blocks`] accepts whole blocks and nothing else. A whole block
43+
//! can be read straight from wherever the caller already keeps it, so no message data is
44+
//! copied into a staging buffer, and each lane is borrowed separately, so the messages do
45+
//! not need to sit next to each other in memory. Only [`finalize_fixed_core`] takes a
46+
//! sub-block tail — that is the last call, so it can pad without breaking the zero-copy
47+
//! rule for the bulk of the message.
48+
//!
49+
//! This matters most when a message is built from pieces — say a domain-separation tag
50+
//! followed by a large payload. With a byte-oriented API the tag and payload must be
51+
//! concatenated somewhere before hashing, which copies the payload. Here the tag is
52+
//! absorbed with [`update_blocks_shared`](MultiUpdateCore::update_blocks_shared) (one copy
53+
//! for all lanes, not one per lane) and the payload is absorbed where it already lives.
54+
//!
55+
//! The only bytes this module ever copies are the one or two final padded blocks, which do
56+
//! not exist in the message and so must be built during finalization.
57+
//!
58+
//! [`finalize_fixed_core`]: MultiFixedOutputCore::finalize_fixed_core
59+
//!
60+
//! # What the types guarantee
61+
//!
62+
//! Every call supplies the same amount of data for every lane — the arguments are arrays of
63+
//! equal-length pieces (`[&[u8; N]; MSGS]`) — so all lanes advance in step by construction.
64+
//! No length check is needed at run time, and unequal lengths fail to compile rather than
65+
//! panicking.
66+
//!
67+
//! # Example
68+
//!
69+
//! Hashing a batch of fixed-size records, each prefixed by a shared tag, without copying
70+
//! any record. `Hash` here is some algorithm implementing [`MultiDigest`]:
71+
//!
72+
//! ```ignore
73+
//! use digest::multi::{MultiDigest, MultiFixedOutputCore, MultiUpdateCore};
74+
//!
75+
//! const BATCH: usize = 64;
76+
//!
77+
//! let mut core = Hash::multi_core::<BATCH>();
78+
//!
79+
//! // A prefix shared by every message: stored once, absorbed once.
80+
//! core.update_blocks_shared::<1>(&tag_block);
81+
//!
82+
//! // Each record is absorbed where it already lives; nothing is copied.
83+
//! core.update_blocks::<RECORD_BLOCKS>(&records);
84+
//!
85+
//! // Finish: no trailing bytes in this example, so the tail is empty.
86+
//! let mut out = core::array::from_fn(|_| Default::default());
87+
//! core.finalize_fixed_core::<0>(&[&[]; BATCH], &mut out);
88+
//! ```
1489
1590
use crate::array::{Array, ArraySize};
91+
use crate::common::{Block, BlockSizeUser};
92+
use crate::typenum::Unsigned;
1693
use crate::{Digest, Output, OutputSizeUser};
1794

18-
/// A stateless multi-buffer kernel: hash exactly `Lanes` messages of length `N` at once.
95+
/// Number of messages a [`MultiUpdateBackend`] processes at once.
96+
///
97+
/// The multi-message analogue of [`ParBlocksSizeUser`](crate::common::ParBlocksSizeUser).
98+
pub trait LanesSizeUser {
99+
/// Number of lanes (messages processed simultaneously).
100+
type LanesSize: ArraySize;
101+
102+
/// Return the lane count.
103+
#[inline(always)]
104+
#[must_use]
105+
fn lanes() -> usize {
106+
Self::LanesSize::USIZE
107+
}
108+
}
109+
110+
/// One run of `BLOCKS` blocks per lane, borrowed from each lane's own memory.
111+
///
112+
/// The multi-message analogue of [`ParBlocks`](crate::common::ParBlocks).
113+
pub type LaneBlocks<'a, T, const BLOCKS: usize> =
114+
Array<&'a [Block<T>; BLOCKS], <T as LanesSizeUser>::LanesSize>;
115+
116+
/// One chaining value per lane.
117+
pub type LaneStates<T> =
118+
Array<<T as MultiUpdateBackend>::State, <T as LanesSizeUser>::LanesSize>;
119+
120+
/// A stateless fixed-width multi-buffer kernel, e.g. an AVX2 8-lane or AVX-512 16-lane
121+
/// implementation of one compression function.
19122
///
20-
/// A single algorithm may provide several backend types, one per SIMD width it supports
21-
/// (e.g. an AVX2 and an AVX-512 backend); [`MultiDigest::multi_digest`] selects among
22-
/// them at runtime.
23-
pub trait MultiDigestBackend: OutputSizeUser {
24-
/// Number of messages processed per batch. Must be at least 1 (a zero-lane backend
25-
/// is meaningless; drivers divide the batch by this count).
26-
type Lanes: ArraySize;
27-
28-
/// Hash `Lanes` messages of length `N`, writing digest `i` into `out[i]`. Equal
29-
/// length is guaranteed by the type: every message is a `&[u8; N]`.
30-
fn multi_digest_lanes<const N: usize>(
123+
/// Backends absorb whole blocks only; padding, length accounting, and digest output all
124+
/// belong to the core, so a single backend serves every variant of an algorithm that
125+
/// differs only in IV or truncation (e.g. SHA-256 and SHA-224).
126+
pub trait MultiUpdateBackend: BlockSizeUser + LanesSizeUser {
127+
/// One lane's chaining value, e.g. `[u32; 8]` for SHA-256.
128+
///
129+
/// All backends of a given algorithm must agree on this type, so that a core can hold
130+
/// the state independently of which backend the current CPU selects.
131+
type State: Copy + Default;
132+
133+
/// Compress `BLOCKS` blocks into each lane's chaining value, reading each lane's
134+
/// blocks in place.
135+
fn update_blocks<const BLOCKS: usize>(
31136
&self,
32-
msgs: &Array<&[u8; N], Self::Lanes>,
33-
out: &mut Array<Output<Self>, Self::Lanes>,
137+
state: &mut LaneStates<Self>,
138+
msgs: &LaneBlocks<'_, Self, BLOCKS>,
34139
);
35140
}
36141

37-
/// Hash many independent, equal-length messages at once.
142+
/// Absorbs whole blocks for `MSGS` independent messages.
143+
///
144+
/// The multi-message analogue of [`UpdateCore`](crate::block_api::UpdateCore). The SIMD
145+
/// lane width does not appear here: an implementation selects a [`MultiUpdateBackend`]
146+
/// for the current CPU and splits `MSGS` across it.
147+
pub trait MultiUpdateCore<const MSGS: usize>: BlockSizeUser + Sized {
148+
/// Absorb `BLOCKS` blocks into each lane, read in place from that lane's memory.
149+
fn update_blocks<const BLOCKS: usize>(&mut self, msgs: &[&[Block<Self>; BLOCKS]; MSGS]);
150+
151+
/// Absorb the same `BLOCKS` blocks into every lane — a shared prefix such as a domain
152+
/// tag or transcript header — without materializing `MSGS` copies of it.
153+
fn update_blocks_shared<const BLOCKS: usize>(&mut self, blocks: &[Block<Self>; BLOCKS]);
154+
}
155+
156+
/// Pads and writes fixed-size digests for `MSGS` messages.
157+
///
158+
/// The multi-message analogue of
159+
/// [`FixedOutputCore`](crate::block_api::FixedOutputCore), which likewise receives the
160+
/// leftover bytes separately from the absorbed blocks.
161+
///
162+
/// `update_blocks` takes whole blocks so absorption stays zero-copy; `finalize` is the last
163+
/// call, so it may take a loose, non-block-sized tail and let the core pad it. The tail is
164+
/// one byte slice per lane, all the same length (fewer than one block) — the length is the
165+
/// slice length, so no separate count is needed, and equal length keeps the lanes in step.
166+
pub trait MultiFixedOutputCore<const MSGS: usize>: MultiUpdateCore<MSGS> + OutputSizeUser {
167+
/// Absorb each lane's `TAIL` trailing bytes, pad, and write the digests. Equal length
168+
/// across lanes is guaranteed by the type; `TAIL` must be shorter than one block.
169+
fn finalize_fixed_core<const TAIL: usize>(
170+
&mut self,
171+
tails: &[&[u8; TAIL]; MSGS],
172+
out: &mut [Output<Self>; MSGS],
173+
);
174+
175+
/// Absorb the same trailing bytes into every lane, pad, and write the digests. The
176+
/// final padded block is then identical in all lanes, so it is built once.
177+
fn finalize_fixed_core_shared<const TAIL: usize>(
178+
&mut self,
179+
tail: &[u8; TAIL],
180+
out: &mut [Output<Self>; MSGS],
181+
);
182+
}
183+
184+
/// Hash many independent messages at once.
185+
///
186+
/// The multi-message analogue of [`Digest`].
38187
pub trait MultiDigest: Digest {
39-
/// Hash `B` messages of length `N`, returning digest `i` for message `i`.
40-
///
41-
/// Equal length (`&[u8; N]`) and the one-output-per-message count (`[_; B]`) are both
42-
/// carried by the types and need no run-time check; the result is written directly
43-
/// into the returned array (`sret`), so no digest is copied. Messages are borrowed
44-
/// individually, so they need not be contiguous.
45-
///
46-
/// The implementation selects a [`MultiDigestBackend`] for the current CPU, splits the
47-
/// batch into `Lanes`-sized groups for it, and hashes any `< Lanes` remainder with the
48-
/// scalar [`Digest`].
49-
fn multi_digest<const N: usize, const B: usize>(msgs: &[&[u8; N]; B]) -> [Output<Self>; B]
50-
where
51-
Self: Sized;
188+
/// The block-oriented core backing a batch of `MSGS` messages.
189+
type MultiCore<const MSGS: usize>: MultiFixedOutputCore<MSGS, OutputSize = Self::OutputSize>;
190+
191+
/// Create the block-oriented core for `MSGS` messages.
192+
fn multi_core<const MSGS: usize>() -> Self::MultiCore<MSGS>;
52193
}

0 commit comments

Comments
 (0)