Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Resource profiles: fused (default) vs small-tables

dstu-core builds in one of two resource profiles, chosen by a Cargo feature. Both produce byte-identical output — same DSTU 7624/7564/8845 math, same test vectors pass either way (see docs/DECISIONS.md D-35/D-38/D-39). The only difference is a straight trade: flash/ROM footprint against throughput.

  • fused (default, no feature flag needed) — precomputed S-box+MDS lookup tables. Fast, costs real flash.
  • small-tables (--features dstu-core/small-tables) — the same math computed on the fly via GF(2^8) multiplication, no big tables. Small, costs real speed.

Pick fused unless you have a specific, measured flash budget that doesn’t fit it — see “Which one do I need?” below.

Memory: what each profile actually compiles in

All numbers are const table data linked into the binary — measured directly off hazmat::tables.rs/hazmat::strumok.rs, not estimated.

Table setfusedsmall-tables
Kalyna/Kupyna S-boxes (SBOXES, SBOXES_DEC)2.0 KB2.0 KB
Kalyna/Kupyna MDS matrices (MDS_MATRIX, MDS_INV_MATRIX)— (unused)0.13 KB
Kalyna/Kupyna precomputed MDS tables (MDS_TABLE, MDS_INV_TABLE)32.0 KBnot compiled
Kalyna/Kupyna fused S-box+MDS tables (SBOX_MDS, SBOX_MDS_DEC)32.0 KBnot compiled
Kalyna + Kupyna subtotal66.0 KB~2.1 KB
Strumok T0..T716.0 KBnot compiled
Strumok MUL_ALPHA/MUL_ALPHA_INV (not swappable — different math, needed either way)4.0 KB4.0 KB
Strumok subtotal (reuses the Kalyna/Kupyna S-box/matrix above, adds nothing extra)20.0 KB~4.0 KB
All three algorithms, one binary~86 KB~6.1 KB

That’s a real, measured difference, not just a theoretical one: a release build of uacrypt (all three algorithms linked in) is ~75 KB smaller under small-tables.

A second, separate fused-only cost as of T-172/D-161 (docs/DECISIONS.md): Kalyna’s interior round sequence is now a genuine compile-time unroll (no loop at all, unroll_rounds!) under fused, in exchange for a real 21-35% speed win on four of its five variants. This is not a const-table cost like the numbers above — it’s compiled code (.text), and it’s real, measured the same way as this doc’s own linked-uacrypt-binary method above (not a raw rlib object-code sum, which overestimates — an earlier pass got this wrong first, corrected same session, see D-161): a release uacrypt.exe grew +71.1 KB (+4.17%) under fused. Deliberately not applied to small-tables, which keeps Kalyna’s old runtime loop specifically so this profile’s whole reason to exist (smallest possible code) isn’t undercut — small-tables’s own binary only grew +9.2 KB (+0.56%) (an unrelated, minor side effect of NR becoming a const generic everywhere, kept for both profiles to avoid two parallel function signatures). Net effect: the fused-vs-small-tables gap on a release uacrypt.exe widened from ~60.5 KB to ~122.4 KB. If you’re on small-tables for a real measured flash budget, this unroll never applies to you either way — but note the profile split is no longer only about which table data links in (see D-161’s “scope of what small-tables now means” note): it now also picks which Kalyna round-sequence code compiles, correctness-identical either way but a real, additive-Cargo-feature-wide performance choice, not just a flash one.

What this means depending on your target: on a 32-bit MCU with memory-mapped flash (ARM Cortex-M, Xtensa/RISC-V — the fused tables live in flash and cost zero RAM, only flash space). On AVR (Harvard architecture), a const table copies into SRAM at startup unless placed in PROGMEM with AVR-specific code — small-tables avoids that problem entirely by not having a table to place.

RAM/stack: what each mode costs beyond the table data above

A different axis from the flash/const-table split above, and the same for both profilesfused/small-tables only swap which table data is linked in; they don’t change any struct layout or working-set size. Numbers below are computed from the actual struct definitions and array literal dimensions in the current tree (size_of-equivalent arithmetic, cross-checked against the source lines cited), not measured with a memory profiler — a weaker claim than the table above’s “measured directly off hazmat::tables.rs”, stated as such rather than inherited.

Key-schedule storage is MAX_NB-sized regardless of variant — the same oversizing pattern docs/TASKS.md T-128 fixed on the compute side (round functions), still present on the storage side: RoundKeys (hazmat::kalyna.rs) is [[Column; MAX_NB]; ROUND_KEYS_LEN] = 19 * 8 * 8 = 1216 bytes, the same for every variant — a Kalyna128_128 caller pays the identical footprint a Kalyna512_512 caller does, even though 128-128’s real round-key material is a quarter the size. ExpandedKey (the cached-schedule type every kalyna_variant! invocation produces) holds two (round_keys + dec_keys) = 2432 bytes per live instance, again independent of variant. Not flagged as a problem to fix here — just a real number the resource-constrained cases from the sizing table below should account for.

Same pattern on KupynaCore after T-134 (docs/DECISIONS.md D-85): T-134 made Kupyna’s compute path (sub_shift_mix/compress and friends) const-generic over COLUMNS, the direct analogue of T-128’s Kalyna fix above — but, same as T-128 for Kalyna, deliberately left KupynaCore’s own storage untouched (advisor()’s explicit scope call during T-134: genericizing the struct itself buys no throughput, since its fields are touched once per update, not once per round). h/ buffer are still MAX_COLUMNS(16)-sized regardless of variant: [[u8; ROWS]; MAX_COLUMNS] + [u8; MAX_BLOCK_BYTES] = 128 + 128 = 256 bytes per live Kupyna256Hasher/Kupyna512Hasher (or KupynaCore inside kupyna_kmac/kupyna_kdf), even though Kupyna-256’s real working state is half that width. Const-genericizing KupynaCore itself would halve this to 128 bytes for Kupyna-256 specifically - flagged in D-85 as a real memory win worth a separate follow-up task, not pursued as part of T-134’s throughput-only scope.

GCM/GMAC’s field multiply builds a transient 16-entry comb table on the stack, once per call to poly_mul_wide (hazmat::gf2m_wide.rs, T-125’s 4-bit-window comb method, docs/DECISIONS.md D-76) — new since this doc was first written, and genuinely a stack cost, not a flash one (freed when the call returns, never linked into the binary as const data):

Field width (m)Used byt: [[u64; $limbs2]; 16]Total incl. a_wide/acc scratch
128 (Gf2m128, $limbs2=4)Kalyna128-* GCM/GMAC16 × 4 × 8 = 512 B~576 B
256 (Gf2m256, $limbs2=8)Kalyna256-* GCM/GMAC16 × 8 × 8 = 1024 B~1152 B
512 (Gf2m512, $limbs2=16)Kalyna512-512 GCM/GMAC16 × 16 × 8 = 2048 B~2304 B

This is on the call stack of whatever calls Gf2m*::multiply — one block’s worth of GCM’s Horner accumulation, or GMAC’s equivalent — not held for the construction’s lifetime, and it applies identically to crypto_secretbox/crypto_secretstream too (both built on Kalyna256_256Gcm, so they pay the m=256/~1152 B figure during every chunk’s tag computation). Kalyna-XTS is the contrasting case: T-126 replaced its once-per-block tweak-doubling with double() — a handful of u64 shift/XOR locals, no comb table at all — so XTS’s own stack cost is negligible regardless of variant, unlike GCM/GMAC’s.

crypto_secretstream’s PushState/PullState hold only a 32-byte subkey, not a cached ExpandedKey — the smallest persistent state of any construction in this crate, at the cost of re-running Kalyna256_256Gcm::new(&self.subkey) (a full 2432-byte-schedule expansion, transient during the call) on every push/pull chunk rather than once per stream. A deliberate space/time trade in the current implementation, not a bug — noted here since it’s directly relevant to “how much RAM does this mode cost,” not proposed as a change.

uacrypt’s own I/O buffering (CLI-layer, not dstu-core): SECRETSTREAM_CHUNK_BYTES/ DIGEST_STREAM_CHUNK_BYTES/SIGN_STREAM_CHUNK_BYTES/STRUMOK_STREAM_CHUNK_BYTES are all 8 KiB (crates/uacrypt/src/lib.rs) — encrypt/decrypt double-buffers two chunks (cur+next) for its rekey-lookahead logic, ~16 KiB peak; the others single-buffer, ~8 KiB peak. Separate from these: DIGEST_BENCH_CHUNK_BYTES (1 MiB) is the --iterations-benchmark path only, sized for throughput measurement, not real single-pass use (D-42’s own “each streaming command picks a chunk size matched to its own constraint” convention).

Speed: what that costs you

Measured with a real built binary (uacrypt, release build), one process per number, same methodology as docs/PERFORMANCE.md’s canonical binary-level comparison (docs/DECISIONS.md D-34) — not a theoretical estimate. Ryzen 5 PRO 4650U dev machine, Windows. One run each (not the full multi-baseline criterion protocol docs/PERFORMANCE.md uses for cross-implementation claims) — good enough to size the trade-off, not a certified regression baseline.

Algorithmfusedsmall-tablesfused is…
Kalyna-128-128 encrypt (cached schedule)124.0 MB/s5.9 MB/s~21x faster
Kalyna-512-512 encrypt (cached schedule)86.4 MB/s3.6 MB/s~24x faster
Kalyna-512-512 decrypt (cached schedule)75.6 MB/s3.8 MB/s~20x faster
Kupyna-256 (64 KB message)92.3 MB/s2.4 MB/s~39x faster
Kupyna-512 (64 KB message)74.4 MB/s1.8 MB/s~43x faster
Strumok-256 (64 KB, cached)610.6 MB/s135.9 MB/s~4.5x faster
Strumok-512 (64 KB, cached)562.4 MB/s139.1 MB/s~4.0x faster

Strumok’s absolute numbers above predate 2026-07-27’s batched/fixed-index apply_keystream rewrite (docs/TASKS.md T-135, docs/DECISIONS.md D-86) — both columns’ real throughput is now substantially higher (the fused column’s own criterion numbers moved by roughly -53 to -65% in time, i.e. ~2.2-2.8x higher MB/s, at message sizes at or above the new 128-byte bulk threshold; small-tables gets the same batching/indexing win independently of table size, so its own absolute number moved too, direction unmeasured here). Not re-measured in this table this pass — the ratio conclusion below (Strumok’s fused-vs-small-tables gap being much smaller than Kalyna/ Kupyna’s, because only T-substitution is swapped) should still roughly hold since both columns share the same rewrite, but treat the two absolute MB/s figures above as stale until re-measured.

Why Strumok’s gap is so much smaller than Kalyna/Kupyna’s: Kalyna and Kupyna’s entire round is the S-box+MDS step that the profile swaps out, so the whole cipher slows down by roughly the same factor. Strumok’s T-substitution is only one part of its per-word cost (LFSR feedback, mul_alpha, state update all stay identical either way) — the parts that don’t change dilute the slowdown from the part that does.

Reproducing: cargo build -p uacrypt --release [--features dstu-core/small-tables], then the same kalyna-block/kupyna-digest/strumok-crypt commands docs/PERFORMANCE.md’s “Reproducing” notes document.

DSTU 4145 verify: the same flag, a different kind of trade (T-151/D-108)

Everything above this point is a flash/ROM-const-table trade. hazmat::dstu4145::curve163’s verify_combine (the s*G + r*Q step DSTU 4145 signature verification needs) reuses this same small-tables feature and the same polarity (default = faster, small-tables = smaller/simpler), but for a genuinely different reason: no new const table is added here at all. The default profile’s faster path (projective/López-Dahab coordinates + Shamir’s trick) computes its one small lookup table ({Infinity, G, Q, G+Q}, 4 points) fresh on every verify call - nothing new is linked into the binary. What small-tables actually buys for this one primitive is a smaller, already-longer-audited code path (the classic constant-time ladder, called twice, no new projective-coordinate arithmetic compiled in at all) rather than fewer flash bytes - a code-size/ audit-surface trade, not the memory-table trade every other row in this document describes. See docs/DECISIONS.md D-108 for the full design and why.

Profileverify ops/s
Default (fast path)~16,850-17,055
small-tables (classic ladder)~8,850-9,040
Default is…~1.9x faster

Updated 2026-08-09 (docs/TASKS.md T-198, docs/DECISIONS.md D-184) - both profiles now go through hazmat::gf2m163’s hardware-clmul dispatch (FieldElement::multiply(), orthogonal to which verify_combine algorithm wraps it), so both absolute numbers jumped by roughly the same factor versus the original D-108/T-153 measurements (239.31/120.06, then 524.01/328.20) - the relative gap between the two profiles stayed close to its original ~1.9-2.0x the whole time, since the hardware path accelerates the field multiply underneath both equally. See docs/PERFORMANCE.md’s own T-198 section for the full history and reproduction command.

sign/verifying_key() (which multiply by a secret scalar - the ephemeral nonce or private key) are unaffected by either profile: scalar_multiply itself was deliberately left unchanged, in every build configuration. (Both still benefit from the same T-198 hardware dispatch - see docs/PERFORMANCE.md.)

Which one do I need?

A quick sizing guide by target, from docs/DECISIONS.md D-35’s survey of typical hardware — flash budget is what actually decides this, not a chip-family label:

TargetTypical flashFits fused (~86 KB tables)?Use
Desktop / server / Raspberry PiMBs+yes, triviallyfused (default)
ESP32 / ESP32-S3 / ESP32-C34 MB+yes, triviallyfused (default)
STM32 F1/F3/G4/F4/F7/H7 (mid-range and up)64 KB – 2 MByesfused (default)
STM32 L0/F0/G0 entry-level (e.g. L011F4, F030F4)16–64 KBnosmall-tables
Arduino Mega (ATmega2560, AVR)256 KB flash, 8 KB SRAMtables fit flash, but AVR copies const to SRAM unless placed in PROGMEM — not done here yetsmall-tables, and even then only once PROGMEM placement exists (docs/TASKS.md Phase 4)
Arduino Uno (ATmega328P, AVR)32 KB flash, 2 KB SRAMno — smaller than even small-tables’s footprint would need with room left for codenot viable yet either way (stretch goal, docs/TASKS.md Phase 4)

If you’re not memory-constrained, don’t reach for small-tables — you’d be trading a large, measured speed loss for a save you don’t need.

How to build each

# fused (default) - what you get with no extra flags
cargo build -p dstu-core --release
cargo build -p uacrypt --release

# small-tables
cargo build -p dstu-core --release --no-default-features --features small-tables
cargo build -p uacrypt --release --features dstu-core/small-tables

Both profiles pass the exact same test suite (official DSTU vectors, proptest round-trips) — cargo test --features dstu-core/small-tables — see docs/DECISIONS.md D-39 for why one test suite covering both is sufficient rather than needing separate verification per profile.