Reduce Deflate read and Huffman scratch allocations
Use a ValueTask read core and memory-based compressed-input refills on modern targets, replace per-block Huffman output arrays with scalar parameters, and decode directly into array-backed caller memory with a pooled fallback.
Preserve generated sync array-read semantics and clear codec output references after reads. Add slice, fallback, cancellation, failure, tree-validation, and read-ahead coverage; document staged ZIP profiles.
Validation: 2,989 .NET 10 tests passed; all library targets, net48 tests, and performance project compile; formatting checks pass.
2026-10-20 09:56:54 +01:00
|
|
|
|
# ZIP allocation and throughput profile
|
|
|
|
|
|
|
|
|
|
|
|
## Scope
|
|
|
|
|
|
|
|
|
|
|
|
Profiled `master` at **`7ddb0121`**, using Release net10.0, .NET 10.0.13 and
|
|
|
|
|
|
Apple M4/macOS ARM64. The checkout was rebuilt and the library copied before
|
|
|
|
|
|
measurement. The initial diagnostic results below are the baseline; the
|
|
|
|
|
|
allocation-optimization follow-up records changes and staged measurements
|
|
|
|
|
|
separately.
|
|
|
|
|
|
|
|
|
|
|
|
The payload is identical to the RAR case: four 50 MiB files alternating 64 KiB
|
|
|
|
|
|
seeded-random and structured regions, **200 MiB extracted**. Native `7zz` produced
|
|
|
|
|
|
two ordinary, unencrypted, single-volume ZIPs:
|
|
|
|
|
|
|
|
|
|
|
|
| Compression | Archive bytes | SHA-256 |
|
|
|
|
|
|
| --- | ---: | --- |
|
|
|
|
|
|
| Deflate, level 5, one thread | 109,948,058 | `5e8bbcc810ac6835a39bc9090a56c4ed186b3c0b56bc4c9ae15de01791b829b6` |
|
|
|
|
|
|
| Stored | 209,715,758 | `bebeab9c94dc4ed2fc9c5d6d5c4716b60c94a1e4b0859b5f532cbaabe266a977` |
|
|
|
|
|
|
|
|
|
|
|
|
Python's ZIP reader independently checked all entry CRCs before profiling. The
|
|
|
|
|
|
same archived bytes are used by SharpCompress and `System.IO.Compression`.
|
|
|
|
|
|
This is not a Zip64, encryption, split-archive or ZIP-writing evaluation.
|
|
|
|
|
|
|
|
|
|
|
|
## Method
|
|
|
|
|
|
|
|
|
|
|
|
- Each scenario runs in a fresh process, with two warmups and seven measured
|
|
|
|
|
|
extractions. Tables show medians for the complete 200 MiB payload.
|
|
|
|
|
|
- SharpCompress Archive and Reader APIs are measured synchronously and
|
|
|
|
|
|
asynchronously. Async cases use async open/enumeration/entry-stream APIs.
|
|
|
|
|
|
The .NET 10 reference uses `ZipArchive.CreateAsync` and `ZipArchiveEntry.OpenAsync`.
|
|
|
|
|
|
- All paths use the same explicit CRC32 pass and verify each entry's CRC, length
|
|
|
|
|
|
and the overall entry count. A separate raw control omits CRC computation but
|
|
|
|
|
|
still verifies lengths. These are raw entry-stream drains, not high-level
|
|
|
|
|
|
extraction-to-directory APIs.
|
|
|
|
|
|
- Memory input and the caller's 64 KiB output buffer are allocated outside the
|
|
|
|
|
|
measurement window. Output is discarded. Each extraction opens a new archive.
|
|
|
|
|
|
- File input uses a 4 KiB FileStream buffer with `SequentialScan` and
|
|
|
|
|
|
`Asynchronous`, including for sync tests. Filesystem caches are warm; these
|
|
|
|
|
|
numbers are not cold-disk results.
|
|
|
|
|
|
- `LeaveStreamOpen = true` and `EnableParallelism = false` are explicit. The
|
|
|
|
|
|
harness verifies the caller's source remains readable after archive disposal.
|
|
|
|
|
|
- Allocation deltas use `GC.GetTotalAllocatedBytes(true)` around extraction and
|
|
|
|
|
|
disposal, including the 40-byte stopwatch but excluding logging and CPU-counter
|
|
|
|
|
|
queries. These are managed allocations, not retained memory or process RSS.
|
|
|
|
|
|
- Separate EventPipe traces repeat ten measured extractions after warmup. No
|
|
|
|
|
|
builds, tests, RAR profiles or native sampling overlap timed ZIP measurements.
|
|
|
|
|
|
|
|
|
|
|
|
## Deflate extraction with CRC validation
|
|
|
|
|
|
|
|
|
|
|
|
| Input / API | Allocated bytes | Elapsed |
|
|
|
|
|
|
| --- | ---: | ---: |
|
|
|
|
|
|
| Memory / SharpCompress Archive sync | 445,104 | 241.34 ms |
|
|
|
|
|
|
| Memory / SharpCompress Reader sync | 445,312 | 244.18 ms |
|
|
|
|
|
|
| Memory / SharpCompress Archive async | 2,127,304 | 245.39 ms |
|
|
|
|
|
|
| Memory / SharpCompress Reader async | 1,160,112 | 246.71 ms |
|
|
|
|
|
|
| Memory / .NET ZipArchive sync | 8,816 | 175.84 ms |
|
|
|
|
|
|
| Memory / .NET ZipArchive async | 10,808 | 175.38 ms |
|
|
|
|
|
|
| File / SharpCompress Archive sync | 449,640 | 250.90 ms |
|
|
|
|
|
|
| File / SharpCompress Reader sync | 449,600 | 254.09 ms |
|
|
|
|
|
|
| File / SharpCompress Archive async | 4,799,624 | 274.25 ms |
|
|
|
|
|
|
| File / SharpCompress Reader async | 3,135,352 | 268.42 ms |
|
|
|
|
|
|
| File / .NET ZipArchive sync | 13,104 | 184.96 ms |
|
|
|
|
|
|
| File / .NET ZipArchive async | 3,884,928 | 214.04 ms |
|
|
|
|
|
|
|
|
|
|
|
|
On memory-backed input, async Archive allocates about **1.68 MB more than sync**;
|
|
|
|
|
|
async Reader allocates about **0.71 MB more**. File-backed async allocations also
|
|
|
|
|
|
include suspended state machines. The .NET reference itself allocates substantially
|
|
|
|
|
|
more with asynchronous file input, so not all file-backed overhead is specific to
|
|
|
|
|
|
SharpCompress.
|
|
|
|
|
|
|
|
|
|
|
|
## Stored and checksum controls
|
|
|
|
|
|
|
|
|
|
|
|
| Memory-backed API | Deflate without CRC | Stored with CRC | Stored without CRC |
|
|
|
|
|
|
| --- | ---: | ---: | ---: |
|
|
|
|
|
|
| SharpCompress Archive sync | 163.15 ms | 82.70 ms | 4.00 ms |
|
|
|
|
|
|
| SharpCompress Reader sync | 165.96 ms | 83.91 ms | 4.42 ms |
|
|
|
|
|
|
| SharpCompress Archive async | 166.06 ms | 102.63 ms | 4.36 ms |
|
|
|
|
|
|
| SharpCompress Reader async | 168.62 ms | 83.11 ms | 4.16 ms |
|
|
|
|
|
|
| .NET ZipArchive sync | 103.18 ms | 82.70 ms | 3.65 ms |
|
|
|
|
|
|
| .NET ZipArchive async | 101.73 ms | 83.15 ms | 3.96 ms |
|
|
|
|
|
|
|
|
|
|
|
|
The Deflate-only gap persists without the common CRC pass: roughly **163–169 ms
|
|
|
|
|
|
versus 102–103 ms**. Stored ZIP is much closer, pointing to the codec/read chain
|
|
|
|
|
|
rather than a general ZIP-container bottleneck. The stored async Archive CRC
|
|
|
|
|
|
timing was higher in this process; it is not evidence of a separate codec cost.
|
|
|
|
|
|
|
|
|
|
|
|
Memory-backed stored extraction allocates only about **8–11 KB** per operation.
|
|
|
|
|
|
The common CRC pass costs roughly 75–85 ms on this host and dominates stored
|
|
|
|
|
|
extraction, so CRC-inclusive timings alone obscure codec performance.
|
|
|
|
|
|
|
|
|
|
|
|
## Allocation findings
|
|
|
|
|
|
|
|
|
|
|
|
### 1. Task-returning compressed-input read chain
|
|
|
|
|
|
|
|
|
|
|
|
Memory-backed allocation samples attribute about **1.7 MB/extraction** to
|
|
|
|
|
|
`Task<int>` in async Archive and about **0.7 MB** in async Reader. The principal
|
|
|
|
|
|
Archive chain is:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
ZlibBaseStream.ReadAsync(byte[], ...)
|
|
|
|
|
|
-> ReadOnlySubStream.ReadAsync(byte[], ...)
|
|
|
|
|
|
-> SharpCompressStream.ReadAsync(byte[], ...)
|
|
|
|
|
|
-> SourceStream.ReadAsync(byte[], ...)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs` refills its default
|
|
|
|
|
|
16 KiB compressed-input buffer through byte-array/Task overloads, even on modern
|
|
|
|
|
|
targets. The wrappers already have Memory/ValueTask overloads, but this call path
|
|
|
|
|
|
does not select them. Archive has more wrappers than Reader, explaining much of
|
|
|
|
|
|
the allocation difference.
|
|
|
|
|
|
|
|
|
|
|
|
**First candidate:** carry Memory/ValueTask reads through the compressed-input
|
|
|
|
|
|
path and provide a ValueTask-based codec read core. Preserve legacy/generated
|
|
|
|
|
|
sync behavior, input limits, stream ownership and cancellation. Savings must be
|
|
|
|
|
|
measured; existing Task-returning public overloads still need compatible adapters.
|
|
|
|
|
|
|
|
|
|
|
|
### 2. Per-block Huffman scratch arrays
|
|
|
|
|
|
|
|
|
|
|
|
Both sync and async traces attribute roughly **0.4 MB/extraction** to `int[]`
|
|
|
|
|
|
allocations in `InflateBlocks.Process`, accounting for most sync allocations.
|
|
|
|
|
|
In `src/SharpCompress/Compressors/Deflate/Inflate.cs`, dynamic-tree construction
|
|
|
|
|
|
allocates four single-element arrays (`bl`, `bd`, `tl`, `td`) for output values.
|
|
|
|
|
|
Fixed-tree construction also uses small arrays and array wrappers.
|
|
|
|
|
|
|
|
|
|
|
|
**Second candidate:** replace these temporary array-shaped outputs with ref/out
|
|
|
|
|
|
values, or bounded reusable/stack scratch. Renting a separate pool buffer for
|
|
|
|
|
|
each single integer would add overhead rather than address the representation.
|
|
|
|
|
|
The large Huffman workspace and sliding window are already pooled; do not count
|
|
|
|
|
|
them as fresh per-block allocations. Initialization and corrupt-input behavior
|
|
|
|
|
|
must remain intact.
|
|
|
|
|
|
|
|
|
|
|
|
### 3. Memory reads rent and copy decoded output
|
|
|
|
|
|
|
|
|
|
|
|
`ZlibBaseStream.ReadAsync(Memory<byte>)` currently rents an array, delegates to
|
|
|
|
|
|
the byte-array overload, copies decoded bytes into the caller's memory, and
|
|
|
|
|
|
returns the rental on every read. This is mostly a copy/dispatch cost after
|
|
|
|
|
|
warmup, not the dominant fresh-array allocation source.
|
|
|
|
|
|
|
|
|
|
|
|
**Third candidate:** decode directly into array-backed caller memory, retaining
|
|
|
|
|
|
a correct pooled fallback for non-array memory. A future core can eliminate the
|
|
|
|
|
|
intermediate Task as well. Merely changing callers to the array overload is
|
|
|
|
|
|
**not** an allocation fix: a controlled probe measured:
|
|
|
|
|
|
|
|
|
|
|
|
| Async Deflate / memory input | Memory overload | Array overload |
|
|
|
|
|
|
| --- | ---: | ---: |
|
|
|
|
|
|
| Archive allocated | 2,127,304 B | 2,357,704 B |
|
|
|
|
|
|
| Reader allocated | 1,160,112 B | 1,620,912 B |
|
|
|
|
|
|
|
|
|
|
|
|
Array-overload probes were slightly faster in some runs but allocated more
|
|
|
|
|
|
through additional Task-returning wrappers. Fix the library core rather than
|
|
|
|
|
|
recommending this as a caller-side allocation optimization.
|
|
|
|
|
|
|
|
|
|
|
|
### Caller-buffer sensitivity
|
|
|
|
|
|
|
|
|
|
|
|
| Async Deflate / memory input | 16 KiB caller buffer | 64 KiB | 256 KiB |
|
|
|
|
|
|
| --- | ---: | ---: | ---: |
|
|
|
|
|
|
| Archive allocated | 2,818,504 B | 2,127,304 B | 1,954,504 B |
|
|
|
|
|
|
| Reader allocated | 1,851,360 B | 1,160,112 B | 987,312 B |
|
|
|
|
|
|
|
|
|
|
|
|
Larger caller buffers reduce per-output-read Tasks but leave the compressed-input
|
|
|
|
|
|
chain in place. Throughput stayed around 245–250 ms; increasing output buffers
|
|
|
|
|
|
alone is not a solution to the underlying allocation cost. Caller-buffer storage
|
|
|
|
|
|
is outside these allocation deltas and larger buffers have a memory tradeoff.
|
|
|
|
|
|
|
|
|
|
|
|
## CPU interpretation and lower-priority work
|
|
|
|
|
|
|
|
|
|
|
|
Managed traces place work in the inflater, `InflateFast`, CRC and table setup.
|
|
|
|
|
|
They also attribute a surprisingly large leaf share to `ZeroMemoryInternal`
|
|
|
|
|
|
under inflater/tree code. A separate macOS native sample did **not** corroborate
|
|
|
|
|
|
a dominant native memset hotspot and could not resolve most JIT frames. Do not
|
|
|
|
|
|
interpret that managed leaf percentage as proof that clearing costs half the CPU,
|
|
|
|
|
|
or remove required initialization based on it. Exact allocations and untraced
|
|
|
|
|
|
timings are the stronger evidence here.
|
|
|
|
|
|
|
|
|
|
|
|
File traces include Monitor waits and suspended read state machines; sampled
|
|
|
|
|
|
thread time is not exact instruction CPU time. All nine EventPipe traces report
|
|
|
|
|
|
zero lost events. The shared analyzer has too few relevant CPU samples for some
|
|
|
|
|
|
.NET-reference traces; that does not mean the reference consumes no CPU.
|
|
|
|
|
|
|
|
|
|
|
|
For four entries, metadata-only opens allocate about **6.4–7.0 KB** in
|
|
|
|
|
|
SharpCompress and take **16–33 microseconds**. The .NET reference takes about
|
|
|
|
|
|
3–4 microseconds. This is negligible beside full extraction, so metadata object
|
|
|
|
|
|
pooling is not the first target for this workload.
|
|
|
|
|
|
|
|
|
|
|
|
The next CPU investigation should isolate the managed inflater from stream and
|
|
|
|
|
|
output-copy overhead, then measure hot-loop changes with CRC disabled as well as
|
|
|
|
|
|
enabled. The .NET reference is a useful native-backed comparison, not a promise
|
|
|
|
|
|
that a pure managed implementation can match every timing.
|
|
|
|
|
|
|
|
|
|
|
|
## Validation and artifacts
|
|
|
|
|
|
|
|
|
|
|
|
- Every measured CRC-enabled extraction verifies four entry CRCs and 200 MiB of
|
|
|
|
|
|
output. Raw controls still verify entry lengths and total output.
|
|
|
|
|
|
- Independent ZIP validation passed for both generated archives.
|
|
|
|
|
|
- **1,002 ZIP/Deflate targeted tests pass** on .NET 10 Release.
|
|
|
|
|
|
- The initial investigation made no library changes. Implementation changes are
|
|
|
|
|
|
described separately below; staging and commits remain under user control.
|
|
|
|
|
|
|
|
|
|
|
|
Artifacts are outside the repository:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
/private/var/folders/6c/j56r6kqn5yj7q1_c6vm5c0t80000gn/T/opencode/zip-200mb-profile/
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`manifest.json` records the commit, archive hashes and library hash. `prepare.py`
|
|
|
|
|
|
creates/validates archives and snapshots the already-built library. `run.py`
|
|
|
|
|
|
reproduces the matrix, controls, buffer sweep and traces. `probe.py` compares
|
|
|
|
|
|
read overloads; `native-sample.py` performs the separate native-stack cross-check.
|
|
|
|
|
|
Harness sources, frozen library, logs, traces and analysis JSON are retained.
|
|
|
|
|
|
Tracing uses `dotnet-trace` 10.0.750501 and the shared TraceEvent analyzer under
|
|
|
|
|
|
`rar-100mb-profile/analyzer/`; its RAR-named phase marker is reused by the ZIP
|
|
|
|
|
|
harness and does not imply RAR decoding. Open Speedscope traces at
|
|
|
|
|
|
<https://www.speedscope.app/>.
|
|
|
|
|
|
|
|
|
|
|
|
## Allocation-optimization follow-up
|
|
|
|
|
|
|
|
|
|
|
|
The follow-up saves four independent library snapshots before measurement:
|
|
|
|
|
|
`baseline`, `reads` (target 1), `trees` (targets 1–2), and `direct` (targets 1–3).
|
|
|
|
|
|
They use the original ZIPs, caller buffers, CRC checks and harness boundaries.
|
|
|
|
|
|
Artifacts and the comparison script are in the artifact directory's
|
|
|
|
|
|
`optimization/` subdirectory; `library-hashes.json` identifies each snapshot.
|
|
|
|
|
|
These exploratory snapshots precede the final generated-sync overload correction.
|
|
|
|
|
|
The `final` snapshot and `final.py` revalidate the actual retained code; use the
|
|
|
|
|
|
`verified-*` logs for the final before/after comparison, not exploratory timings.
|
|
|
|
|
|
|
|
|
|
|
|
### Retained implementation
|
|
|
|
|
|
|
|
|
|
|
|
1. **Async read chain:** compressed-input refills select Memory/ValueTask
|
|
|
|
|
|
overloads on modern targets. A shared ValueTask read core avoids materializing
|
|
|
|
|
|
a Task for memory-based output reads. Public byte-array signatures remain
|
|
|
|
|
|
unchanged, and generated sync reads retain byte-array I/O. Legacy targets keep
|
|
|
|
|
|
their existing byte-array refill path.
|
|
|
|
|
|
2. **Huffman outputs:** fixed and dynamic tree builders use scalar/ref/out
|
|
|
|
|
|
parameters instead of allocating single-element arrays and array wrappers for
|
|
|
|
|
|
each block. Existing bit-length initialization and invalid-tree checks remain.
|
|
|
|
|
|
The large pooled workspace and sliding window are unchanged.
|
|
|
|
|
|
3. **Output memory:** array-backed memory decodes directly into the requested
|
|
|
|
|
|
array segment. Non-array memory retains the pooled fallback and returns its
|
|
|
|
|
|
rental in `finally`. The codec's output-array reference is cleared after reads,
|
|
|
|
|
|
including cancellation/failure, so caller memory or returned rentals are not
|
|
|
|
|
|
retained by the decoder between operations.
|
|
|
|
|
|
|
|
|
|
|
|
No public API signatures or format support change. These shared codec changes
|
|
|
|
|
|
also require ZIP, GZip, Zlib and legacy-target validation; they do not introduce
|
|
|
|
|
|
a new sync Span read override or a new global mutable codec pool.
|
|
|
|
|
|
|
|
|
|
|
|
### Allocation savings by stage
|
|
|
|
|
|
|
|
|
|
|
|
For the same memory-backed 200 MiB extraction with CRCs and a 64 KiB caller buffer:
|
|
|
|
|
|
|
|
|
|
|
|
| API | Baseline | Target 1 | Targets 1–2 | All three targets |
|
|
|
|
|
|
| --- | ---: | ---: | ---: | ---: |
|
|
|
|
|
|
| Archive sync | 445,104 B | 445,104 B | 34,448 B | 34,448 B |
|
|
|
|
|
|
| Reader sync | 445,312 B | 445,312 B | 34,656 B | 34,656 B |
|
|
|
|
|
|
| Archive async | 2,127,304 B | 445,896 B | 35,240 B | 35,240 B |
|
|
|
|
|
|
| Reader async | 1,160,112 B | 445,872 B | 35,216 B | 35,216 B |
|
|
|
|
|
|
|
|
|
|
|
|
Target 1 removes the recurring Task allocation premium. Target 2 removes
|
|
|
|
|
|
**410,656 B/extraction** of tree scratch in this workload. Target 3 avoids the
|
|
|
|
|
|
per-output-read rental/copy but adds no further warm allocation reduction.
|
|
|
|
|
|
The final build reproduces these allocation totals while preserving byte-array
|
|
|
|
|
|
I/O for generated sync reads.
|
|
|
|
|
|
|
|
|
|
|
|
### Final before/after comparison
|
|
|
|
|
|
|
|
|
|
|
|
| Deflate / API | Baseline allocated | Final allocated | Baseline elapsed | Final elapsed |
|
|
|
|
|
|
| --- | ---: | ---: | ---: | ---: |
|
|
|
|
|
|
| Memory / Archive sync | 445,104 B | 34,448 B | 279.58 ms | 273.74 ms |
|
|
|
|
|
|
| Memory / Reader sync | 445,312 B | 34,656 B | 266.99 ms | 259.80 ms |
|
|
|
|
|
|
| Memory / Archive async | 2,127,304 B | 35,240 B | 287.89 ms | 265.48 ms |
|
|
|
|
|
|
| Memory / Reader async | 1,160,112 B | 35,216 B | 272.96 ms | 264.22 ms |
|
|
|
|
|
|
| File / Archive sync | 450,280 B | 38,984 B | 280.99 ms | 277.79 ms |
|
|
|
|
|
|
| File / Reader sync | 449,600 B | 38,944 B | 268.66 ms | 274.04 ms |
|
|
|
|
|
|
| File / Archive async | 4,799,632 B | 2,897,656 B | 288.79 ms | 286.93 ms |
|
|
|
|
|
|
| File / Reader async | 3,133,968 B | 2,129,952 B | 291.82 ms | 302.76 ms |
|
|
|
|
|
|
|
|
|
|
|
|
Memory-backed allocation savings are approximately **92% sync**, **98% async
|
|
|
|
|
|
Archive**, and **97% async Reader**. File-backed async allocations remain affected
|
|
|
|
|
|
by suspension and scheduling: their remaining samples are primarily async state
|
|
|
|
|
|
machine boxes rather than the removed per-block arrays and completed-read Tasks.
|
|
|
|
|
|
|
|
|
|
|
|
Two additional process pairs reversed execution order with CRC computation
|
|
|
|
|
|
disabled. Medians across their 14 measured extractions per version:
|
|
|
|
|
|
|
|
|
|
|
|
| Memory / raw Deflate | Baseline elapsed | Final elapsed |
|
|
|
|
|
|
| --- | ---: | ---: |
|
|
|
|
|
|
| Archive sync | 178.85 ms | 173.08 ms |
|
|
|
|
|
|
| Archive async | 178.33 ms | 173.01 ms |
|
|
|
|
|
|
| Reader async | 182.00 ms | 175.60 ms |
|
|
|
|
|
|
|
|
|
|
|
|
The repeat suggests a modest 3–4% improvement for these memory-backed cases,
|
|
|
|
|
|
not a general throughput guarantee. File results vary in both directions.
|
|
|
|
|
|
Target 3's isolated timing benefit is small/variable, so it is retained as copy
|
|
|
|
|
|
elimination without claiming a separate universal speedup. These changes do not
|
|
|
|
|
|
establish that the original native-backed .NET inflater throughput gap is closed.
|
|
|
|
|
|
|
|
|
|
|
|
Final async allocation totals are unchanged across 16, 64 and 256 KiB caller
|
|
|
|
|
|
buffers: **35,240 B Archive / 35,216 B Reader**. The former buffer-size-dependent
|
|
|
|
|
|
Task allocation cost is gone. Stored controls retain their original allocation
|
|
|
|
|
|
totals; the changes target the Deflate decoder.
|
|
|
|
|
|
|
|
|
|
|
|
### Final traces and validation
|
|
|
|
|
|
|
|
|
|
|
|
All six exploratory and four final EventPipe traces report zero lost events.
|
|
|
|
|
|
The baseline memory-backed Archive trace samples approximately 1.7 MB/extraction
|
|
|
|
|
|
of `Task<int>` and 0.4 MB of `int[]`. Final memory traces no longer sample the
|
|
|
|
|
|
recurring Task sites and have only four/five allocation ticks across ten measured
|
|
|
|
|
|
extractions. Some per-codec setup arrays remain; sparse samples and harness JSON
|
|
|
|
|
|
initialization do not support a detailed percentage split at this smaller total.
|
|
|
|
|
|
|
|
|
|
|
|
- **1,018 targeted ZIP/Deflate/GZip/Zlib tests** and **2,989 complete .NET 10
|
|
|
|
|
|
Release tests** pass.
|
|
|
|
|
|
- All six library targets, net48 tests and the performance project compile
|
|
|
|
|
|
without warnings/errors. net48 tests are not executed on macOS.
|
|
|
|
|
|
- New tests cover fixed/stored block transitions and read-ahead, empty and
|
|
|
|
|
|
oversubscribed trees, sliced array memory, non-array memory, fragmented and
|
|
|
|
|
|
genuinely suspended input, cancellation, corrupt input, I/O failures, caller
|
|
|
|
|
|
memory reuse and clearing codec output references. A many-read allocation
|
|
|
|
|
|
guard checks that the non-array fallback reuses its rental.
|
|
|
|
|
|
- Emitted sync code was inspected. The explicit `SYNC_ONLY` branch keeps the
|
|
|
|
|
|
original `Read(byte[], offset, count)` input call; a regression source that
|
|
|
|
|
|
rejects Span reads protects this behavior.
|
|
|
|
|
|
- CSharpier formatting/check and Git whitespace checks pass.
|
|
|
|
|
|
- No staging or commit is performed by this follow-up.
|
2026-10-20 10:57:04 +01:00
|
|
|
|
|
|
|
|
|
|
## Ring-buffer follow-up (`c78cc86`)
|
|
|
|
|
|
|
|
|
|
|
|
The original matrix used seekable memory/file inputs, including for Reader API
|
|
|
|
|
|
measurements. It therefore missed the non-seekable ring-buffer path: switching
|
|
|
|
|
|
Deflate refills to memory-based reads selected a wrapper implementation that
|
|
|
|
|
|
allocated a temporary array for every fresh read and buffered replay.
|
|
|
|
|
|
|
|
|
|
|
|
Commit `c78cc86` accesses array-backed memory directly when reading/writing the
|
|
|
|
|
|
ring buffer. Non-array memory uses `ArrayPool<byte>` rentals returned in
|
|
|
|
|
|
`finally` blocks. Underlying reads retain the Memory/ValueTask overload.
|
|
|
|
|
|
|
|
|
|
|
|
Reran the 24-case Deflate/stored, memory/file, SharpCompress/.NET matrix with the
|
|
|
|
|
|
same archives, two warmups, seven measured extractions, CRC/size checks and
|
|
|
|
|
|
64 KiB output buffer. Added non-seekable Reader cases by wrapping each input in
|
|
|
|
|
|
a forward-only delegate. Each extraction still checks that the borrowed source
|
|
|
|
|
|
remains readable. Builds and traces did not overlap timed measurements.
|
|
|
|
|
|
|
|
|
|
|
|
### Non-seekable Reader comparison
|
|
|
|
|
|
|
|
|
|
|
|
Both versions use the extended harness. "Before" is the saved pre-fix
|
|
|
|
|
|
`optimization/final-library` snapshot; `manifest.json` records both DLL hashes.
|
|
|
|
|
|
Allocation values are bytes per complete 200 MiB extraction, not retained memory.
|
|
|
|
|
|
|
|
|
|
|
|
| Async Reader / input | Before allocated | Fixed allocated | Before elapsed | Fixed elapsed |
|
|
|
|
|
|
| --- | ---: | ---: | ---: | ---: |
|
|
|
|
|
|
| Deflate / memory | 110,149,184 B | 35,256 B | 249.45 ms | 243.41 ms |
|
|
|
|
|
|
| Deflate / file | 114,237,408 B | 4,226,200 B | 275.71 ms | 265.28 ms |
|
|
|
|
|
|
| Stored / memory | 209,808,104 B | 10,784 B | 94.71 ms | 86.58 ms |
|
|
|
|
|
|
| Stored / file | 211,697,560 B | 1,943,216 B | 161.20 ms | 150.70 ms |
|
|
|
|
|
|
|
|
|
|
|
|
The removed allocation volume tracks the compressed payload size. Stored reads
|
|
|
|
|
|
benefit too because they share the wrapper's memory-based read implementation.
|
|
|
|
|
|
File-backed async reads retain suspension/state-machine allocations; the fix is
|
|
|
|
|
|
not a claim of allocation-free file extraction. Sync non-seekable allocations
|
|
|
|
|
|
are unchanged: **34,696 B / 38,984 B** for Deflate memory/file and
|
|
|
|
|
|
**10,224 B / 14,512 B** for stored memory/file. Timing differences are descriptive
|
|
|
|
|
|
single-run medians, not a universal speedup guarantee.
|
|
|
|
|
|
|
|
|
|
|
|
### Seekable Deflate rerun
|
|
|
|
|
|
|
|
|
|
|
|
| API / input | Fixed allocated | Fixed elapsed |
|
|
|
|
|
|
| --- | ---: | ---: |
|
|
|
|
|
|
| Archive sync / memory | 34,448 B | 243.22 ms |
|
|
|
|
|
|
| Reader sync / memory | 34,656 B | 240.99 ms |
|
|
|
|
|
|
| Archive async / memory | 35,240 B | 241.85 ms |
|
|
|
|
|
|
| Reader async / memory | 35,216 B | 241.97 ms |
|
|
|
|
|
|
| Archive sync / file | 38,984 B | 250.24 ms |
|
|
|
|
|
|
| Reader sync / file | 38,944 B | 249.78 ms |
|
|
|
|
|
|
| Archive async / file | 2,898,728 B | 262.74 ms |
|
|
|
|
|
|
| Reader async / file | 2,130,928 B | 265.69 ms |
|
|
|
|
|
|
|
|
|
|
|
|
Seekable-memory allocation totals match the previous final profile exactly.
|
|
|
|
|
|
Async memory allocations also remain **35,240 B Archive / 35,216 B Reader**
|
|
|
|
|
|
across the 16/64/256 KiB output-buffer sweep. The full matrix, raw controls and
|
|
|
|
|
|
non-seekable comparisons contain 49 timed scenarios in total.
|
|
|
|
|
|
|
|
|
|
|
|
Three EventPipe traces cover pre-fix/fixed non-seekable async Reader and fixed
|
|
|
|
|
|
seekable async Reader. All report zero lost events. The pre-fix non-seekable
|
|
|
|
|
|
trace samples approximately **1.10 GB of `byte[]`** over ten measured
|
|
|
|
|
|
extractions under `SharpCompressStream.ReadWithRingBufferAsync`; the fixed
|
|
|
|
|
|
non-seekable trace has four allocation ticks and no sampled `byte[]` hotspot.
|
|
|
|
|
|
Exact allocation totals above come from GC counters, not sampled trace sums.
|
|
|
|
|
|
|
|
|
|
|
|
Artifacts are in `zip-200mb-profile/ring-buffer-c78cc86/` under the temporary
|
|
|
|
|
|
artifact root documented above. `run.py` reproduces the matrix and traces;
|
|
|
|
|
|
`summary.json`, individual logs, `manifest.json`, library snapshots and trace
|
|
|
|
|
|
analyses preserve the results. Validation before committing the fix: **2,996
|
|
|
|
|
|
.NET 10 Release tests pass**, all library targets build without warnings/errors,
|
|
|
|
|
|
and CSharpier and Git whitespace checks pass.
|