Files
sharpcompress/tests/SharpCompress.Performance/rar-crypto-profiling.md
Adam Hathcock f63cd81a65 Reduce allocations in encrypted RAR reading
Reuse bounded buffers and transform AES blocks in place. Remove per-block arrays and plaintext queues, avoid per-byte header allocations, and dispose owned crypto transforms.

Add encrypted RAR benchmarks, regression tests, and before/after profiling results. Full .NET 10 suite passes (2,446 tests); all library targets build and net48 tests compile.
2026-10-20 10:43:28 +01:00

66 lines
3.8 KiB
Markdown

# Encrypted RAR allocation profiling
Baseline: `e61a335d` (the RAR5 HMAC allocation fix), compared with the buffered
decryption follow-up. Measurements were taken on macOS 27.0.1 ARM64 / .NET 10.0.13,
Release mode, with tiered compilation disabled. These are not Alpine results.
## Allocation-focused measurements
An isolated harness exercised `RarCryptoWrapper` over 1 MiB of AES-CBC ciphertext
with a fixed key (excluding PBKDF2), plus repeated `RarHeaderFactory` scans of
existing in-memory fixtures. Each workload was warmed up, then measured five
times. Tables show median elapsed time and median managed bytes allocated using
process-wide counters, including wrapper/reader construction and disposal but
excluding fixture/encryption/output-buffer setup. No other workloads ran during
the final measurements. Header scans reused a factory to exercise warm KDF caches.
| Workload | Before bytes | After bytes | Before ms | After ms |
| --- | ---: | ---: | ---: | ---: |
| Decrypt 1 MiB, sync, 1-byte reads | 5,244,392 | 1,416 | 7.972 | 4.010 |
| Decrypt 1 MiB, async, 1-byte reads | 7,865,832 | 1,416 | 15.281 | 8.147 |
| Decrypt 1 MiB, sync, 16-byte reads | 5,244,392 | 1,416 | 6.330 | 2.899 |
| Decrypt 1 MiB, async, 16-byte reads | 12,584,424 | 4,720,008 | 8.346 | 5.929 |
| Decrypt 1 MiB, sync, 8192-byte reads | 5,260,960 | 9,632 | 6.705 | 0.110 |
| Decrypt 1 MiB, async, 8192-byte reads | 5,275,296 | 18,512 | 8.132 | 0.138 |
| RAR5 encrypted headers, sync, 100 scans | 2,425,456 | 1,403,056 | 1.331 | 1.169 |
| RAR5 encrypted headers, async, 100 scans | 2,552,656 | 1,530,256 | 3.250 | 2.983 |
| RAR4 encrypted headers, sync, 1 scan | 34,908,464 | 34,903,512 | 98.269 | 105.439 |
| RAR4 encrypted headers, async, 1 scan | 34,909,488 | 34,904,768 | 97.998 | 106.596 |
| Unencrypted RAR5 headers, sync, 100 scans | 518,608 | 518,608 | 0.217 | 0.236 |
| Unencrypted RAR5 headers, async, 100 scans | 597,808 | 597,808 | 2.289 | 2.340 |
Async decryption here uses the `Task<int>` array overload. Returning a non-cached
integer such as 16 still allocates a Task per call; the `Memory<byte>` overload
uses `ValueTask<int>` and avoids that conversion. This distinction matters when
interpreting the remaining allocations.
## Changes and remaining opportunities
- Transform AES blocks in place rather than allocating two arrays per block.
- Batch file-data transformations in a reusable buffer bounded to 8 KiB; start
with 16 bytes to avoid imposing the larger allocation on tiny entries.
- Remove plaintext queues, per-byte header-result arrays, per-block ciphertext
arrays, and the pooled-copy adapter for `Memory<byte>` reads.
- Dispose owned crypto transforms and per-header readers deterministically.
Native memory retention was not quantified by these managed allocation counters.
- Leave the RAR3/4 KDF unchanged: its large working array dominates the roughly
35 MB per encrypted-header scan. The measured legacy workloads were slightly
slower, not faster. Any KDF rewrite needs separate correctness/performance work.
- Primitive-read arrays and fixed KDF/cache setup costs remain secondary targets.
## Repeatable repository benchmarks
`RarEncryptedBenchmarks` provides warm/cold header parsing, async warm parsing,
and sync/async archive extraction, with encrypted-header, encrypted-data-only,
and unencrypted fixtures. Load fixture bytes outside measured iterations.
```sh
dotnet run --project tests/SharpCompress.Performance -c Release -- --filter '*RarEncryptedBenchmarks*'
```
All 15 benchmark cases were smoke-tested with one warmup and one measured
invocation; those smoke-test timings are not statistically meaningful and are
not the before/after measurements above. Dedicated regression tests cover
bounded allocations, CRC/read counts, block boundaries, large and fragmented
reads, span/memory overloads, stream ownership, and transform disposal.