|
1 | | -//! Multi-buffer (multi-message) hashing. |
| 1 | +//! Hashing many independent messages at once. |
2 | 2 | //! |
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. |
7 | 8 | //! |
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 | +//! ``` |
14 | 89 |
|
15 | 90 | use crate::array::{Array, ArraySize}; |
| 91 | +use crate::common::{Block, BlockSizeUser}; |
| 92 | +use crate::typenum::Unsigned; |
16 | 93 | use crate::{Digest, Output, OutputSizeUser}; |
17 | 94 |
|
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. |
19 | 122 | /// |
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>( |
31 | 136 | &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>, |
34 | 139 | ); |
35 | 140 | } |
36 | 141 |
|
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`]. |
38 | 187 | 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>; |
52 | 193 | } |
0 commit comments