Pooled Static Hash API
This page documents the static HashData and TryHashData one-shot methods available on every hash algorithm in this package, explains the pooling strategy behind them, and shows expected allocation savings compared to creating a new instance for each call.
Motivation
Every hash algorithm in this package extends System.Security.Cryptography.HashAlgorithm. Creating a new instance for each hash operation allocates the full internal state on the managed heap — from 72 bytes for the compact SHA-2 variants up to ~2.9 KB for BLAKE3. In high-throughput scenarios (network request validation, file integrity pipelines, blockchain transaction processing) this results in measurable GC pressure.
The standard solution for .NET's built-in algorithms (e.g. System.Security.Cryptography.SHA256.HashData(...)) is a static one-shot method backed by a thread-local or pooled instance. This package provides the same pattern for every supported algorithm.
API
Each algorithm exposes two static methods:
// Write result into a caller-provided buffer — zero heap allocation on the hot path
public static bool TryHashData(ReadOnlySpan<byte> source, Span<byte> destination, out int bytesWritten);
// Allocate and return a new byte[] — convenience overload
public static byte[] HashData(ReadOnlySpan<byte> source);
Algorithms with configurable output sizes (Streebog, Kupyna, LSH-256, LSH-512) expose an additional hashSizeBytes parameter:
public static bool TryHashData(ReadOnlySpan<byte> source, Span<byte> destination, int hashSizeBytes, out int bytesWritten);
public static byte[] HashData(ReadOnlySpan<byte> source, int hashSizeBytes);
Example — zero-allocation path
Span<byte> hash = stackalloc byte[32];
if (SHA256.TryHashData(data, hash, out int written))
{
// 'hash' contains the SHA-256 result — no heap allocation occurred
}
Example — convenience allocating overload
byte[] digest = Blake3.HashData(data);
Example — parameterised output size
// Compute a Streebog-256 digest (32-byte output)
byte[] digest256 = Streebog.HashData(data, hashSizeBytes: 32);
// Compute a Kupyna-384 digest (48-byte output)
Span<byte> buf = stackalloc byte[48];
Kupyna.TryHashData(data, buf, hashSizeBytes: 48, out _);
Pooling Implementation
IResettable on the base class
HashAlgorithm implements Microsoft.Extensions.ObjectPool.IResettable. TryReset() delegates to the existing Initialize() method, which every concrete hash already overrides to return the algorithm state to its initial value. This means pooled reuse is safe with no additional work in the concrete classes.
// HashAlgorithm base class
public bool TryReset()
{
Initialize();
return true;
}
Generic pool — HashAlgorithmPool<T>
Algorithms with a parameter-free default constructor (everything except the size-parameterised regional algorithms) use a DefaultObjectPool<T> backed by the IResettable contract:
// Equivalent internal implementation
private static readonly ObjectPool<SHA256> _pool = new DefaultObjectPool<SHA256>(
new DefaultPooledObjectPolicy<SHA256>());
public static bool TryHashData(ReadOnlySpan<byte> source, Span<byte> destination, out int bytesWritten)
{
var hasher = _pool.Get();
try
{
return hasher.TryComputeHash(source, destination, out bytesWritten);
}
finally
{
_pool.Return(hasher); // calls TryReset() → Initialize()
}
}
The pool is a static field on HashAlgorithmPool<T>, so it is initialised once per concrete type and shared across all threads for the lifetime of the application.
Delegate pool — HashAlgorithmPool.CreatePool<T>(Func<T>)
Algorithms that require constructor arguments to select an output size (Streebog, Kupyna, Lsh256, Lsh512) maintain one pool per supported variant. A lightweight DelegatePoolPolicy<T> wraps the factory delegate:
// Internal initialisation in e.g. Streebog
private static readonly ObjectPool<Streebog> _pool256 =
HashAlgorithmPool.CreatePool(() => new Streebog(32));
private static readonly ObjectPool<Streebog> _pool512 =
HashAlgorithmPool.CreatePool(() => new Streebog(64));
Return calls TryReset() via the same IResettable path, so the pooled instance is ready for the next caller.
Exclusions
The following variants are intentionally excluded from pooling because their per-call state (key material, customisation string, or output size) cannot be expressed through a parameter-free reset:
| Variant | Reason |
|---|---|
Blake2b.CreateKeyed(...) |
Key is baked into the IV during Initialize() |
Blake2s.CreateKeyed(...) |
Same as above |
Blake3.CreateKeyed(...) |
Key material stored in the algorithm state |
Blake3 derive-key mode |
Different mode flag changes the IV entirely |
cSHAKE128 / cSHAKE256 |
Function-name and customisation-string bytes prepended at Initialize() |
TurboSHAKE128/256 with custom domain |
Domain separator is a constructor parameter |
For these variants, create instances explicitly with using and reuse them per-scope where appropriate.
Allocation Savings
The following table shows the allocation avoided per HashData call by reusing a pooled instance instead of creating a new one. Values are managed heap bytes, derived from the object state and buffer sizes listed in the benchmarks page. Managed object overhead (~24 bytes per object header + method-table pointer on 64-bit) is included in the Instance overhead column.
Actual saving equals Instance state + Internal arrays + Object overhead. Static lookup tables are not included because they are shared across all instances and loaded only once.
SHA-2 Family
| Algorithm | Output | Instance state | Internal arrays | Object overhead | Saving per call |
|---|---|---|---|---|---|
| SHA-224 | 28 B | 32 B (uint[8]) | 64 B (byte[64] buffer) | 24 B | 120 B |
| SHA-256 | 32 B | 32 B (uint[8]) | 64 B (byte[64] buffer) | 24 B | 120 B |
| SHA-384 | 48 B | 64 B (ulong[8]) | 128 B (byte[128] buffer) | 24 B | 216 B |
| SHA-512 | 64 B | 64 B (ulong[8]) | 128 B (byte[128] buffer) | 24 B | 216 B |
| SHA-512/224 | 28 B | 64 B (ulong[8]) | 128 B (byte[128] buffer) | 24 B | 216 B |
| SHA-512/256 | 32 B | 64 B (ulong[8]) | 128 B (byte[128] buffer) | 24 B | 216 B |
SHA-3 / Keccak / SHAKE Family
| Algorithm | Output | Instance state | Internal arrays | Object overhead | Saving per call |
|---|---|---|---|---|---|
| SHA3-224 | 28 B | 200 B (Keccak state) | 144 B (byte[rate]) | 24 B | 368 B |
| SHA3-256 | 32 B | 200 B | 136 B | 24 B | 360 B |
| SHA3-384 | 48 B | 200 B | 104 B | 24 B | 328 B |
| SHA3-512 | 64 B | 200 B | 72 B | 24 B | 296 B |
| SHAKE128 (default 32 B) | 32 B | 200 B | 168 B | 24 B | 392 B |
| SHAKE256 (default 64 B) | 64 B | 200 B | 136 B | 24 B | 360 B |
| TurboSHAKE128 (default 32 B) | 32 B | 200 B | 168 B | 24 B | 392 B |
| TurboSHAKE256 (default 64 B) | 64 B | 200 B | 136 B | 24 B | 360 B |
| KT128 (default 32 B) | 32 B | 200 B | 168 B | 24 B | 392 B |
| KT256 (default 64 B) | 64 B | 200 B | 136 B | 24 B | 360 B |
| Keccak-256 | 32 B | 200 B | 136 B | 24 B | 360 B |
| Keccak-384 | 48 B | 200 B | 104 B | 24 B | 328 B |
| Keccak-512 | 64 B | 200 B | 72 B | 24 B | 296 B |
BLAKE Family
| Algorithm | Output | Instance state | Internal arrays | Object overhead | Saving per call |
|---|---|---|---|---|---|
| BLAKE2b (unkeyed, default 64 B) | 64 B | 64 B (ulong[8]) | 128 B (byte[128]) | 24 B | 216 B |
| BLAKE2s (unkeyed, default 32 B) | 32 B | 32 B (uint[8]) | 64 B (byte[64]) | 24 B | 120 B |
| BLAKE3 (default 32 B) | 32 B | ~1 KB (CV stack) + ~1 KB (chunk) | 768 B | 24 B | ~2,840 B |
BLAKE3 benefits the most from pooling due to its large Merkle-tree internal state.
Ascon Family
| Algorithm | Output | Instance state | Internal arrays | Object overhead | Saving per call |
|---|---|---|---|---|---|
| Ascon-Hash256 | 32 B | 40 B (ulong[5]) | 8 B (byte[8]) | 24 B | 72 B |
| Ascon-XOF128 (default 32 B) | 32 B | 40 B (ulong[5]) | 8 B (byte[8]) | 24 B | 72 B |
Regional Algorithms
| Algorithm | Output | Instance state | Internal arrays | Object overhead | Saving per call |
|---|---|---|---|---|---|
| RIPEMD-160 | 20 B | 20 B (uint[5]) | 64 B (byte[64]) | 24 B | 108 B |
| SM3 | 32 B | 32 B (uint[8]) | 64 B (byte[64]) | 24 B | 120 B |
| Whirlpool | 64 B | 64 B (ulong[8]) | 64 B (byte[64]) | 24 B | 152 B |
| Streebog-256 | 32 B | 192 B (3 × ulong[8]) | 64 B (byte[64]) | 24 B | 280 B |
| Streebog-512 | 64 B | 192 B (3 × ulong[8]) | 64 B (byte[64]) | 24 B | 280 B |
| Kupyna-256 | 32 B | 64 B (ulong[8] state) + 64 B (scratch) | 64 B (byte[64]) | 24 B | 216 B |
| Kupyna-384 | 48 B | 128 B (ulong[16] state) + 128 B (scratch) | 128 B (byte[128]) | 24 B | 408 B |
| Kupyna-512 | 64 B | 128 B (ulong[16] state) + 128 B (scratch) | 128 B (byte[128]) | 24 B | 408 B |
| LSH-256/224 | 28 B | 256 B (CV + submsg) | 128 B (byte[128]) | 24 B | 408 B |
| LSH-256/256 | 32 B | 256 B (CV + submsg) | 128 B (byte[128]) | 24 B | 408 B |
| LSH-512/224 | 28 B | 512 B (CV + submsg) | 256 B (byte[256]) | 24 B | 792 B |
| LSH-512/256 | 32 B | 512 B (CV + submsg) | 256 B (byte[256]) | 24 B | 792 B |
| LSH-512/384 | 48 B | 512 B (CV + submsg) | 256 B (byte[256]) | 24 B | 792 B |
| LSH-512/512 | 64 B | 512 B (CV + submsg) | 256 B (byte[256]) | 24 B | 792 B |
Legacy Algorithms
| Algorithm | Output | Instance state | Internal arrays | Object overhead | Saving per call |
|---|---|---|---|---|---|
| SHA-1 (obsolete) | 20 B | 20 B (uint[5]) + 320 B (uint[80] W) | 64 B (byte[64]) | 24 B | 428 B |
| MD5 (obsolete) | 16 B | 16 B (uint[4]) | 64 B (byte[64]) | 24 B | 104 B |
SHA-1 and MD5 are marked
[Obsolete]and included for legacy interoperability only. The staticHashDataandTryHashDatamethods on these classes preserve the same[Obsolete]attribute so that callers receive the same compiler warning at the call site.
Usage Recommendations
| Scenario | Recommended API |
|---|---|
| Single call, result as byte array | byte[] d = SHA256.HashData(data) |
| Single call, zero allocation | SHA256.TryHashData(data, dest, out _) |
| Multiple calls in a loop | SHA256.TryHashData(...) — pool shared across all iterations |
| Streaming / chunked input | Create instance with SHA256.Create(), call AppendData / GetHashAndReset |
| Keyed hash (MAC mode) | Blake2b.CreateKeyed(...) or KMAC128 — pooling not applicable |
| Custom output length | Streebog.HashData(data, 32) / Kupyna.TryHashData(data, dest, 64, out _) |