Remove BinaryReader and BinaryWriter references from ZipArchive - #103153

Merged
carlossanlop merged 14 commits into
dotnet:mainfrom
edwardneal:ziparchive-stream-read-write
Jan 23, 2025
Merged

Remove BinaryReader and BinaryWriter references from ZipArchive#103153
carlossanlop merged 14 commits into
dotnet:mainfrom
edwardneal:ziparchive-stream-read-write

Conversation

@edwardneal

@edwardnealedwardneal commented Jun 7, 2024

Copy link
Copy Markdown
Contributor

Relates to #83909, #31460.

This removes all references to BinaryReader and BinaryWriter from ZipArchive and ZipArchiveEntry. It also changes the way that the ZIP central directory header is read from the source stream and makes one tweak to the way that the EOCD header is detected.

I've removed BinaryReader and BinaryWriter for three reasons:

  • They were being used to read and write to the stream field-by-field - so every file could generate nearly two dozen individual writes to the backing stream.
  • There are no async implementations on these two objects, so these need to be implemented (or their usage removed) in order to add any future async support for ZipArchive.
  • Removes some allocations & GC pressure.

The second change is to adjust the way that the ZIP central directory header is read. Previously, this was read from the source stream file-by-file. This PR now reads from the source in 4KB blocks and tries to read the headers from there. This is much faster. I've chosen not to implement it when writing the CD headers because they contain dynamic data and I didn't want to copy buffers around; I'm open to doing so.

The detection of the end-of-central-directory header is very similar too. It was already doing something similar, but with only 16 bytes at a time. I've tweaked this to read 4KB block instead, and changed the way it searches for the EOCD signature to use an intrinsic rather than byte-by-byte bit shuffling.

In both cases, I've picked 4KB because it feels like a small enough buffer to not make a massive difference to wait times, and it aligns with the 4KB buffer on FileStream (which I imagine would be the most common use case.)

There are performance improvements across the board. To benchmark this, I used a test wrapping stream which simulates the worst case - an Xms Thread.Sleep on every Read and Write call. Results are below, but in short:

Reads

  • 12-13% reduction in execution time as a baseline, rising to 18-19% as the number of entries in the archive increases.
  • 33-36% reduction in memory usage
  • When latency is introduced, as the number of entries in the archive increases, the reduction in execution time becomes more pronounced - 99.6%

Creation

  • Execution time is almost identical assuming no latency. As latency increases, the execution time reduces by around 82%
  • 10% reduction in memory usage
Benchmark results - Reads
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevMedianRatioRatioSDGen0Gen1AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-11.280 μs0.0178 μs0.0338 μs1.269 μs1.000.000.4482-1.84 KB1.00
BenchmarkCoreRun.NET 9.02-11.131 μs0.0104 μs0.0081 μs1.133 μs0.870.030.2861-1.17 KB0.64
BenchmarkBaseline.NET 8.02021.412 μs0.5187 μs1.5211 μs21.083 μs1.000.000.4578-1.88 KB1.00
BenchmarkCoreRun.NET 9.0202.871 μs0.0318 μs0.0265 μs2.871 μs0.130.010.2937-1.21 KB0.65
BenchmarkBaseline.NET 8.021716,595.380 μs2,212.8744 μs2,069.9241 μs716,359.000 μs1.000.00--2.64 KB1.00
BenchmarkCoreRun.NET 9.02162,065.810 μs280.6085 μs248.7521 μs62,120.450 μs0.090.00--1.29 KB0.49
BenchmarkBaseline.NET 8.025716,582.567 μs2,484.5321 μs2,324.0329 μs716,860.600 μs1.000.00--2.64 KB1.00
BenchmarkCoreRun.NET 9.02562,190.336 μs212.1850 μs198.4780 μs62,244.822 μs0.090.00--1.29 KB0.49
BenchmarkBaseline.NET 8.010-13.925 μs0.0296 μs0.0247 μs3.922 μs1.000.001.7929-7.33 KB1.00
BenchmarkCoreRun.NET 9.010-13.452 μs0.0683 μs0.0786 μs3.424 μs0.880.031.1864-4.85 KB0.66
BenchmarkBaseline.NET 8.010098.469 μs5.5739 μs16.0820 μs92.865 μs1.000.001.7090-7.37 KB1.00
BenchmarkCoreRun.NET 9.01005.902 μs0.1166 μs0.2011 μs5.830 μs0.070.011.1902-4.89 KB0.66
BenchmarkBaseline.NET 8.01012,966,581.267 μs5,120.4172 μs4,789.6414 μs2,967,184.400 μs1.000.00--8.09 KB1.00
BenchmarkCoreRun.NET 9.010162,189.399 μs283.9015 μs265.5616 μs62,213.289 μs0.020.00--4.97 KB0.61
BenchmarkBaseline.NET 8.01052,965,424.293 μs5,345.5963 μs5,000.2741 μs2,965,234.300 μs1.000.00--8.09 KB1.00
BenchmarkCoreRun.NET 9.010562,184.896 μs320.7364 μs300.0170 μs62,224.356 μs0.020.00--4.97 KB0.61
BenchmarkBaseline.NET 8.025-19.367 μs0.1231 μs0.1028 μs9.356 μs1.000.004.2114-17.22 KB1.00
BenchmarkCoreRun.NET 9.025-17.661 μs0.0472 μs0.0394 μs7.654 μs0.820.012.7771-11.34 KB0.66
BenchmarkBaseline.NET 8.0250189.773 μs1.5383 μs1.2010 μs190.032 μs1.000.004.1504-17.26 KB1.00
BenchmarkCoreRun.NET 9.02509.558 μs0.1294 μs0.1210 μs9.529 μs0.050.002.7771-11.38 KB0.66
BenchmarkBaseline.NET 8.02517,184,576.887 μs6,870.6351 μs6,426.7964 μs7,183,295.200 μs1.0000.00--17.98 KB1.00
BenchmarkCoreRun.NET 9.025162,115.901 μs231.8962 μs216.9159 μs62,113.444 μs0.0090.00--11.46 KB0.64
BenchmarkBaseline.NET 8.02557,176,659.979 μs7,168.4172 μs6,354.6151 μs7,177,295.900 μs1.0000.00--17.98 KB1.00
BenchmarkCoreRun.NET 9.025562,154.444 μs243.2540 μs227.5399 μs62,233.989 μs0.0090.00--11.46 KB0.64
BenchmarkBaseline.NET 8.050-118.166 μs0.2379 μs0.2225 μs18.171 μs1.000.008.4229-34.48 KB1.00
BenchmarkCoreRun.NET 9.050-114.800 μs0.1155 μs0.1080 μs14.796 μs0.810.015.6152-22.95 KB0.67
BenchmarkBaseline.NET 8.0500368.723 μs3.1490 μs2.9455 μs367.986 μs1.000.008.3008-34.52 KB1.00
BenchmarkCoreRun.NET 9.050017.192 μs0.2263 μs0.2117 μs17.291 μs0.050.005.61520.030522.98 KB0.67
BenchmarkBaseline.NET 8.050114,205,316.860 μs11,442.8220 μs10,703.6228 μs14,203,486.100 μs1.0000.00--35.24 KB1.00
BenchmarkCoreRun.NET 9.050162,229.530 μs282.4781 μs264.2302 μs62,229.311 μs0.0040.00--23.06 KB0.65
BenchmarkBaseline.NET 8.050514,211,658.147 μs9,950.6110 μs9,307.8077 μs14,212,125.500 μs1.0000.00--35.24 KB1.00
BenchmarkCoreRun.NET 9.050562,237.983 μs341.3432 μs319.2927 μs62,250.889 μs0.0040.00--23.06 KB0.65
Benchmark results - Reads (scaling up by number of ZipArchiveEntry children)
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevRatioRatioSDGen0Gen1Gen2AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-11.276 μs0.0162 μs0.0152 μs1.000.000.4482--1.84 KB1.00
BenchmarkCoreRun.NET 9.02-11.120 μs0.0116 μs0.0129 μs0.880.010.2861--1.17 KB0.64
BenchmarkBaseline.NET 8.010-14.527 μs0.0259 μs0.0229 μs1.000.001.7929--7.33 KB1.00
BenchmarkCoreRun.NET 9.010-13.964 μs0.0315 μs0.0295 μs0.880.011.1826--4.85 KB0.66
BenchmarkBaseline.NET 8.025-110.897 μs0.0787 μs0.0698 μs1.000.004.2114--17.22 KB1.00
BenchmarkCoreRun.NET 9.025-18.824 μs0.0499 μs0.0466 μs0.810.012.7771--11.34 KB0.66
BenchmarkBaseline.NET 8.050-121.540 μs0.1920 μs0.1604 μs1.000.008.4229--34.48 KB1.00
BenchmarkCoreRun.NET 9.050-117.341 μs0.1089 μs0.1019 μs0.810.015.6152--22.95 KB0.67
BenchmarkBaseline.NET 8.0100-143.626 μs0.4626 μs0.6016 μs1.000.0016.96780.1221-69.46 KB1.00
BenchmarkCoreRun.NET 9.0100-133.674 μs0.1899 μs0.1683 μs0.770.0111.3525--46.59 KB0.67
BenchmarkBaseline.NET 8.0250-1109.428 μs1.3621 μs1.2074 μs1.000.0041.25980.1221-168.87 KB1.00
BenchmarkCoreRun.NET 9.0250-186.518 μs1.4735 μs1.2305 μs0.790.0127.34386.7139-112.02 KB0.66
BenchmarkBaseline.NET 8.0500-1222.703 μs1.4561 μs1.3621 μs1.000.0083.25200.2441-340.65 KB1.00
BenchmarkCoreRun.NET 9.0500-1171.378 μs1.2795 μs1.0684 μs0.770.0155.41990.2441-227.16 KB0.67
BenchmarkBaseline.NET 8.01000-1476.601 μs8.2338 μs6.8756 μs1.000.00133.789172.2656-686.68 KB1.00
BenchmarkCoreRun.NET 9.01000-1361.508 μs1.1992 μs1.0630 μs0.760.0190.820347.3633-459.91 KB0.67
BenchmarkBaseline.NET 8.010000-112,294.500 μs244.9925 μs547.9609 μs1.000.001171.8750687.5000265.62506879.98 KB1.00
BenchmarkCoreRun.NET 9.010000-110,722.270 μs212.4330 μs504.8696 μs0.870.06843.7500578.1250234.37504614.13 KB0.67
Benchmark results - Creation
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevMedianRatioRatioSDGen0Gen1AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-117.17 μs0.289 μs0.256 μs17.09 μs1.000.000.5798-2.44 KB1.00
BenchmarkCoreRun.NET 9.02-114.20 μs0.214 μs0.179 μs14.26 μs0.830.010.5188-2.15 KB0.88
BenchmarkBaseline.NET 8.02048.28 μs0.883 μs2.115 μs47.69 μs1.000.000.5493-2.48 KB1.00
BenchmarkCoreRun.NET 9.02020.52 μs0.390 μs0.848 μs20.22 μs0.420.030.5188-2.19 KB0.88
BenchmarkBaseline.NET 8.0211,153,370.77 μs3,033.808 μs2,837.826 μs1,153,076.60 μs1.000.00--3.24 KB1.00
BenchmarkCoreRun.NET 9.021202,163.82 μs810.531 μs758.172 μs202,433.70 μs0.180.00--2.43 KB0.75
BenchmarkBaseline.NET 8.0251,154,271.62 μs2,629.954 μs2,460.061 μs1,153,689.70 μs1.000.00--3.24 KB1.00
BenchmarkCoreRun.NET 9.025202,071.53 μs987.541 μs923.747 μs201,793.43 μs0.180.00--2.43 KB0.75
BenchmarkBaseline.NET 8.010-159.75 μs0.906 μs0.803 μs59.52 μs1.000.003.2959-13.5 KB1.00
BenchmarkCoreRun.NET 9.010-159.42 μs0.269 μs0.238 μs59.40 μs0.990.012.9297-12.21 KB0.90
BenchmarkBaseline.NET 8.0100226.82 μs5.396 μs15.395 μs227.63 μs1.000.003.1738-13.54 KB1.00
BenchmarkCoreRun.NET 9.010098.33 μs1.223 μs1.084 μs98.45 μs0.480.032.9297-12.25 KB0.90
BenchmarkBaseline.NET 8.01015,274,439.79 μs6,901.351 μs6,455.528 μs5,273,943.90 μs1.000.00--14.26 KB1.00
BenchmarkCoreRun.NET 9.0101950,228.86 μs2,438.419 μs2,161.595 μs950,750.30 μs0.180.00--12.97 KB0.91
BenchmarkBaseline.NET 8.01055,276,480.03 μs5,844.084 μs5,466.560 μs5,279,086.70 μs1.000.00--14.26 KB1.00
BenchmarkCoreRun.NET 9.0105949,421.14 μs2,346.035 μs2,194.483 μs948,990.50 μs0.180.00--12.97 KB0.91
BenchmarkBaseline.NET 8.025-1148.85 μs2.834 μs3.264 μs149.14 μs1.000.007.5684-31.16 KB1.00
BenchmarkCoreRun.NET 9.025-1147.24 μs1.313 μs1.229 μs146.78 μs0.990.036.8359-28 KB0.90
BenchmarkBaseline.NET 8.0250490.65 μs5.097 μs4.257 μs491.34 μs1.000.006.8359-31.2 KB1.00
BenchmarkCoreRun.NET 9.0250214.95 μs1.275 μs1.065 μs214.92 μs0.440.006.8359-28.04 KB0.90
BenchmarkBaseline.NET 8.025113,010,754.08 μs7,587.337 μs7,097.199 μs13,008,804.00 μs1.000.00--31.92 KB1.00
BenchmarkCoreRun.NET 9.02512,353,101.94 μs4,536.628 μs3,788.290 μs2,352,594.90 μs0.180.00--28.76 KB0.90
BenchmarkBaseline.NET 8.025513,008,020.51 μs8,158.693 μs7,631.646 μs13,008,868.30 μs1.000.00--31.92 KB1.00
BenchmarkCoreRun.NET 9.02552,356,024.47 μs4,256.167 μs3,981.221 μs2,356,131.80 μs0.180.00--28.76 KB0.90
BenchmarkBaseline.NET 8.050-1294.44 μs1.924 μs1.800 μs294.02 μs1.000.0015.1367-62.7 KB1.00
BenchmarkCoreRun.NET 9.050-1297.19 μs4.039 μs3.580 μs297.07 μs1.010.0113.67190.976656.41 KB0.90
BenchmarkBaseline.NET 8.0500981.38 μs4.847 μs4.534 μs980.08 μs1.000.0013.6719-62.74 KB1.00
BenchmarkCoreRun.NET 9.0500428.33 μs3.014 μs2.672 μs428.36 μs0.440.0013.67190.488356.45 KB0.90
BenchmarkBaseline.NET 8.050125,900,673.22 μs15,093.982 μs14,118.920 μs25,902,252.70 μs1.000.00--63.46 KB1.00
BenchmarkCoreRun.NET 9.05014,699,400.51 μs5,271.756 μs4,931.204 μs4,697,546.80 μs0.180.00--57.17 KB0.90
BenchmarkBaseline.NET 8.050525,891,853.07 μs12,417.690 μs11,615.515 μs25,891,875.90 μs1.000.00--63.46 KB1.00
BenchmarkCoreRun.NET 9.05054,698,669.15 μs7,373.175 μs6,896.872 μs4,699,045.70 μs0.180.00--57.17 KB0.90
Benchmark header
BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3593/23H2/2023Update/SunValley3)
Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Baseline : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
CoreRun : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2

NB: because this changes the number of reads/writes to a stream, it'll have an impact on the tests for #102704. I'll change these depending on the order the PRs are merged in.

We now read the data into a (sometimes stack-allocated) byte array and slice it up with BinaryPrimitives.
This reduces the number of reads and writes required to read and write a ZipArchive. It also makes future work to enable async APIs easier, since BinaryReader and BinaryWriter lack this support.
Also changed approach to reading central file directory headers. Rather than performing X reads per header, we read 4KB of data at a time and look for all applicable headers in that data. This should improve performance when dealing with many small files.
This allowed the removal of the ArchiveReader property from ZipArchive.
Now pooling the file IO buffers and the temporary buffers for extra fields of the CD file header (which would otherwise be allocated and deallocated in a loop.)
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Jun 7, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-io-compression
See info in area-owners.md if you want to be subscribed.

This handles 64x entries with 19-character filenames (and thus, 65-byte file headers.) As a result, it straddles two 4KB read buffers.
Also corrected the seek logic while reading the central directory header
@carlossanlop

Copy link
Copy Markdown
Contributor

@edwardneal the main branch is currently only taking changes for RC1 that will go into .NET 9. This change currently does not meet the bar for merging it now, but we would love to consider taking it for .NET 10. The RC1 branch will get snapped from main on August 14th, and we can consider merging this PR after that date, when main will start pointing to .NET 10.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick partial review. Also needs resolving the merge conflict so we get proper CI results.

Comment threadsrc/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs Outdated
Comment threadsrc/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs Outdated
@edwardneal

edwardneal commented Aug 8, 2024

Copy link
Copy Markdown
ContributorAuthor

Thanks @carlossanlop - I've addressed the merge conflict and made those test changes.

I've got a number of post-merge test failures to deal with - I'll look at those shortly, ready for review post-14th.
Edit: these should now be resolved, all tests are passing. It's ready for review.

The buffer returned from the ArrayPool contained older data (including a ZIP header.) When reading the last chunk of the file (i.e a chunk which was less than BackwardsSeekingBufferSize) the buffer's Span wasn't resized to account for this.
SeekBackwardsToSignature would thus find the older data, try to seek beyond the end of the stream and fail to read the file.
@carlossanlop

Copy link
Copy Markdown
Contributor

I fetched your branch and all the relevant tests passed locally: System.IO.Compression, System.IO.Compression.ZipFile and System.IO.Packaging.

I merged the latest bits in main into this branch, as the last CI results were already stale and the build info was gone. Let's see what comes out of this.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left more feedback for you to consider. Thank you so much for this change.

Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs Outdated
Reads and writes are now performed using a new set of field lengths and locations.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@edwardneal thanks for applying the suggestions, I'm happy with how this is looking! I left a (hopefully) final batch of comments for you to consider.

After addressing those comments, I'd like to run a CI run that doesn't get executed by default, which will test your code in mobile platforms. If we don't see anything concerning there, I think we can merge it.

If it's in your possibilities, we could try to get this merged before the Code Complete day for .NET 10 Preview 1, which is Monday January 27th. Let me know if this works for you, otherwise I can take over for the above two final steps.

Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.cs Outdated
Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.cs Outdated
Formatting change; added one comment to Zip64EndOfCentralDirectoryLocator.SignatureConstantBytes; clarified comment on ZipHelper.SeekBackwardsAndRead
Added comment to SeekBackwardsToStream.
Lingering references to SignatureConstantBytes.Length.
Added two asserts to CanReadLargeCentralDirectoryHeader test, verifying that the archive entry metadata is in a sensible state.
@carlossanlop

Copy link
Copy Markdown
Contributor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you @edwardneal for this PR.

The CI failures in the default legs are all marked as expected. The runtime-extra-platforms legs unfortunately are plagued with exit 137 for all assemblies, so we'll have to verify the results there at a later time after that issue gets resolved.

@carlossanlop
carlossanlop merged commit 77fc88a into dotnet:mainJan 23, 2025
@edwardneal
edwardneal deleted the ziparchive-stream-read-write branch January 23, 2025 04:17
@edwardneal

Copy link
Copy Markdown
ContributorAuthor

@carlossanlop I've seen several outerloop tests failing on main when dealing with Zip64 structures. I'm pretty sure they came from this PR and I'm going to trace them down this evening - sorry for missing these.

@carlossanlop

Copy link
Copy Markdown
Contributor

Okay, thank you. We might have to revert both to avoid risking the preview.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.IO.Compressioncommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@edwardneal@carlossanlop@bartonjs
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Remove BinaryReader and BinaryWriter references from ZipArchive - #103153

Merged
carlossanlop merged 14 commits into
dotnet:mainfrom
edwardneal:ziparchive-stream-read-write
Jan 23, 2025
Merged

Remove BinaryReader and BinaryWriter references from ZipArchive#103153
carlossanlop merged 14 commits into
dotnet:mainfrom
edwardneal:ziparchive-stream-read-write

Conversation

@edwardneal

@edwardnealedwardneal commented Jun 7, 2024

Copy link
Copy Markdown
Contributor

Relates to #83909, #31460.

This removes all references to BinaryReader and BinaryWriter from ZipArchive and ZipArchiveEntry. It also changes the way that the ZIP central directory header is read from the source stream and makes one tweak to the way that the EOCD header is detected.

I've removed BinaryReader and BinaryWriter for three reasons:

  • They were being used to read and write to the stream field-by-field - so every file could generate nearly two dozen individual writes to the backing stream.
  • There are no async implementations on these two objects, so these need to be implemented (or their usage removed) in order to add any future async support for ZipArchive.
  • Removes some allocations & GC pressure.

The second change is to adjust the way that the ZIP central directory header is read. Previously, this was read from the source stream file-by-file. This PR now reads from the source in 4KB blocks and tries to read the headers from there. This is much faster. I've chosen not to implement it when writing the CD headers because they contain dynamic data and I didn't want to copy buffers around; I'm open to doing so.

The detection of the end-of-central-directory header is very similar too. It was already doing something similar, but with only 16 bytes at a time. I've tweaked this to read 4KB block instead, and changed the way it searches for the EOCD signature to use an intrinsic rather than byte-by-byte bit shuffling.

In both cases, I've picked 4KB because it feels like a small enough buffer to not make a massive difference to wait times, and it aligns with the 4KB buffer on FileStream (which I imagine would be the most common use case.)

There are performance improvements across the board. To benchmark this, I used a test wrapping stream which simulates the worst case - an Xms Thread.Sleep on every Read and Write call. Results are below, but in short:

Reads

  • 12-13% reduction in execution time as a baseline, rising to 18-19% as the number of entries in the archive increases.
  • 33-36% reduction in memory usage
  • When latency is introduced, as the number of entries in the archive increases, the reduction in execution time becomes more pronounced - 99.6%

Creation

  • Execution time is almost identical assuming no latency. As latency increases, the execution time reduces by around 82%
  • 10% reduction in memory usage
Benchmark results - Reads
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevMedianRatioRatioSDGen0Gen1AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-11.280 μs0.0178 μs0.0338 μs1.269 μs1.000.000.4482-1.84 KB1.00
BenchmarkCoreRun.NET 9.02-11.131 μs0.0104 μs0.0081 μs1.133 μs0.870.030.2861-1.17 KB0.64
BenchmarkBaseline.NET 8.02021.412 μs0.5187 μs1.5211 μs21.083 μs1.000.000.4578-1.88 KB1.00
BenchmarkCoreRun.NET 9.0202.871 μs0.0318 μs0.0265 μs2.871 μs0.130.010.2937-1.21 KB0.65
BenchmarkBaseline.NET 8.021716,595.380 μs2,212.8744 μs2,069.9241 μs716,359.000 μs1.000.00--2.64 KB1.00
BenchmarkCoreRun.NET 9.02162,065.810 μs280.6085 μs248.7521 μs62,120.450 μs0.090.00--1.29 KB0.49
BenchmarkBaseline.NET 8.025716,582.567 μs2,484.5321 μs2,324.0329 μs716,860.600 μs1.000.00--2.64 KB1.00
BenchmarkCoreRun.NET 9.02562,190.336 μs212.1850 μs198.4780 μs62,244.822 μs0.090.00--1.29 KB0.49
BenchmarkBaseline.NET 8.010-13.925 μs0.0296 μs0.0247 μs3.922 μs1.000.001.7929-7.33 KB1.00
BenchmarkCoreRun.NET 9.010-13.452 μs0.0683 μs0.0786 μs3.424 μs0.880.031.1864-4.85 KB0.66
BenchmarkBaseline.NET 8.010098.469 μs5.5739 μs16.0820 μs92.865 μs1.000.001.7090-7.37 KB1.00
BenchmarkCoreRun.NET 9.01005.902 μs0.1166 μs0.2011 μs5.830 μs0.070.011.1902-4.89 KB0.66
BenchmarkBaseline.NET 8.01012,966,581.267 μs5,120.4172 μs4,789.6414 μs2,967,184.400 μs1.000.00--8.09 KB1.00
BenchmarkCoreRun.NET 9.010162,189.399 μs283.9015 μs265.5616 μs62,213.289 μs0.020.00--4.97 KB0.61
BenchmarkBaseline.NET 8.01052,965,424.293 μs5,345.5963 μs5,000.2741 μs2,965,234.300 μs1.000.00--8.09 KB1.00
BenchmarkCoreRun.NET 9.010562,184.896 μs320.7364 μs300.0170 μs62,224.356 μs0.020.00--4.97 KB0.61
BenchmarkBaseline.NET 8.025-19.367 μs0.1231 μs0.1028 μs9.356 μs1.000.004.2114-17.22 KB1.00
BenchmarkCoreRun.NET 9.025-17.661 μs0.0472 μs0.0394 μs7.654 μs0.820.012.7771-11.34 KB0.66
BenchmarkBaseline.NET 8.0250189.773 μs1.5383 μs1.2010 μs190.032 μs1.000.004.1504-17.26 KB1.00
BenchmarkCoreRun.NET 9.02509.558 μs0.1294 μs0.1210 μs9.529 μs0.050.002.7771-11.38 KB0.66
BenchmarkBaseline.NET 8.02517,184,576.887 μs6,870.6351 μs6,426.7964 μs7,183,295.200 μs1.0000.00--17.98 KB1.00
BenchmarkCoreRun.NET 9.025162,115.901 μs231.8962 μs216.9159 μs62,113.444 μs0.0090.00--11.46 KB0.64
BenchmarkBaseline.NET 8.02557,176,659.979 μs7,168.4172 μs6,354.6151 μs7,177,295.900 μs1.0000.00--17.98 KB1.00
BenchmarkCoreRun.NET 9.025562,154.444 μs243.2540 μs227.5399 μs62,233.989 μs0.0090.00--11.46 KB0.64
BenchmarkBaseline.NET 8.050-118.166 μs0.2379 μs0.2225 μs18.171 μs1.000.008.4229-34.48 KB1.00
BenchmarkCoreRun.NET 9.050-114.800 μs0.1155 μs0.1080 μs14.796 μs0.810.015.6152-22.95 KB0.67
BenchmarkBaseline.NET 8.0500368.723 μs3.1490 μs2.9455 μs367.986 μs1.000.008.3008-34.52 KB1.00
BenchmarkCoreRun.NET 9.050017.192 μs0.2263 μs0.2117 μs17.291 μs0.050.005.61520.030522.98 KB0.67
BenchmarkBaseline.NET 8.050114,205,316.860 μs11,442.8220 μs10,703.6228 μs14,203,486.100 μs1.0000.00--35.24 KB1.00
BenchmarkCoreRun.NET 9.050162,229.530 μs282.4781 μs264.2302 μs62,229.311 μs0.0040.00--23.06 KB0.65
BenchmarkBaseline.NET 8.050514,211,658.147 μs9,950.6110 μs9,307.8077 μs14,212,125.500 μs1.0000.00--35.24 KB1.00
BenchmarkCoreRun.NET 9.050562,237.983 μs341.3432 μs319.2927 μs62,250.889 μs0.0040.00--23.06 KB0.65
Benchmark results - Reads (scaling up by number of ZipArchiveEntry children)
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevRatioRatioSDGen0Gen1Gen2AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-11.276 μs0.0162 μs0.0152 μs1.000.000.4482--1.84 KB1.00
BenchmarkCoreRun.NET 9.02-11.120 μs0.0116 μs0.0129 μs0.880.010.2861--1.17 KB0.64
BenchmarkBaseline.NET 8.010-14.527 μs0.0259 μs0.0229 μs1.000.001.7929--7.33 KB1.00
BenchmarkCoreRun.NET 9.010-13.964 μs0.0315 μs0.0295 μs0.880.011.1826--4.85 KB0.66
BenchmarkBaseline.NET 8.025-110.897 μs0.0787 μs0.0698 μs1.000.004.2114--17.22 KB1.00
BenchmarkCoreRun.NET 9.025-18.824 μs0.0499 μs0.0466 μs0.810.012.7771--11.34 KB0.66
BenchmarkBaseline.NET 8.050-121.540 μs0.1920 μs0.1604 μs1.000.008.4229--34.48 KB1.00
BenchmarkCoreRun.NET 9.050-117.341 μs0.1089 μs0.1019 μs0.810.015.6152--22.95 KB0.67
BenchmarkBaseline.NET 8.0100-143.626 μs0.4626 μs0.6016 μs1.000.0016.96780.1221-69.46 KB1.00
BenchmarkCoreRun.NET 9.0100-133.674 μs0.1899 μs0.1683 μs0.770.0111.3525--46.59 KB0.67
BenchmarkBaseline.NET 8.0250-1109.428 μs1.3621 μs1.2074 μs1.000.0041.25980.1221-168.87 KB1.00
BenchmarkCoreRun.NET 9.0250-186.518 μs1.4735 μs1.2305 μs0.790.0127.34386.7139-112.02 KB0.66
BenchmarkBaseline.NET 8.0500-1222.703 μs1.4561 μs1.3621 μs1.000.0083.25200.2441-340.65 KB1.00
BenchmarkCoreRun.NET 9.0500-1171.378 μs1.2795 μs1.0684 μs0.770.0155.41990.2441-227.16 KB0.67
BenchmarkBaseline.NET 8.01000-1476.601 μs8.2338 μs6.8756 μs1.000.00133.789172.2656-686.68 KB1.00
BenchmarkCoreRun.NET 9.01000-1361.508 μs1.1992 μs1.0630 μs0.760.0190.820347.3633-459.91 KB0.67
BenchmarkBaseline.NET 8.010000-112,294.500 μs244.9925 μs547.9609 μs1.000.001171.8750687.5000265.62506879.98 KB1.00
BenchmarkCoreRun.NET 9.010000-110,722.270 μs212.4330 μs504.8696 μs0.870.06843.7500578.1250234.37504614.13 KB0.67
Benchmark results - Creation
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevMedianRatioRatioSDGen0Gen1AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-117.17 μs0.289 μs0.256 μs17.09 μs1.000.000.5798-2.44 KB1.00
BenchmarkCoreRun.NET 9.02-114.20 μs0.214 μs0.179 μs14.26 μs0.830.010.5188-2.15 KB0.88
BenchmarkBaseline.NET 8.02048.28 μs0.883 μs2.115 μs47.69 μs1.000.000.5493-2.48 KB1.00
BenchmarkCoreRun.NET 9.02020.52 μs0.390 μs0.848 μs20.22 μs0.420.030.5188-2.19 KB0.88
BenchmarkBaseline.NET 8.0211,153,370.77 μs3,033.808 μs2,837.826 μs1,153,076.60 μs1.000.00--3.24 KB1.00
BenchmarkCoreRun.NET 9.021202,163.82 μs810.531 μs758.172 μs202,433.70 μs0.180.00--2.43 KB0.75
BenchmarkBaseline.NET 8.0251,154,271.62 μs2,629.954 μs2,460.061 μs1,153,689.70 μs1.000.00--3.24 KB1.00
BenchmarkCoreRun.NET 9.025202,071.53 μs987.541 μs923.747 μs201,793.43 μs0.180.00--2.43 KB0.75
BenchmarkBaseline.NET 8.010-159.75 μs0.906 μs0.803 μs59.52 μs1.000.003.2959-13.5 KB1.00
BenchmarkCoreRun.NET 9.010-159.42 μs0.269 μs0.238 μs59.40 μs0.990.012.9297-12.21 KB0.90
BenchmarkBaseline.NET 8.0100226.82 μs5.396 μs15.395 μs227.63 μs1.000.003.1738-13.54 KB1.00
BenchmarkCoreRun.NET 9.010098.33 μs1.223 μs1.084 μs98.45 μs0.480.032.9297-12.25 KB0.90
BenchmarkBaseline.NET 8.01015,274,439.79 μs6,901.351 μs6,455.528 μs5,273,943.90 μs1.000.00--14.26 KB1.00
BenchmarkCoreRun.NET 9.0101950,228.86 μs2,438.419 μs2,161.595 μs950,750.30 μs0.180.00--12.97 KB0.91
BenchmarkBaseline.NET 8.01055,276,480.03 μs5,844.084 μs5,466.560 μs5,279,086.70 μs1.000.00--14.26 KB1.00
BenchmarkCoreRun.NET 9.0105949,421.14 μs2,346.035 μs2,194.483 μs948,990.50 μs0.180.00--12.97 KB0.91
BenchmarkBaseline.NET 8.025-1148.85 μs2.834 μs3.264 μs149.14 μs1.000.007.5684-31.16 KB1.00
BenchmarkCoreRun.NET 9.025-1147.24 μs1.313 μs1.229 μs146.78 μs0.990.036.8359-28 KB0.90
BenchmarkBaseline.NET 8.0250490.65 μs5.097 μs4.257 μs491.34 μs1.000.006.8359-31.2 KB1.00
BenchmarkCoreRun.NET 9.0250214.95 μs1.275 μs1.065 μs214.92 μs0.440.006.8359-28.04 KB0.90
BenchmarkBaseline.NET 8.025113,010,754.08 μs7,587.337 μs7,097.199 μs13,008,804.00 μs1.000.00--31.92 KB1.00
BenchmarkCoreRun.NET 9.02512,353,101.94 μs4,536.628 μs3,788.290 μs2,352,594.90 μs0.180.00--28.76 KB0.90
BenchmarkBaseline.NET 8.025513,008,020.51 μs8,158.693 μs7,631.646 μs13,008,868.30 μs1.000.00--31.92 KB1.00
BenchmarkCoreRun.NET 9.02552,356,024.47 μs4,256.167 μs3,981.221 μs2,356,131.80 μs0.180.00--28.76 KB0.90
BenchmarkBaseline.NET 8.050-1294.44 μs1.924 μs1.800 μs294.02 μs1.000.0015.1367-62.7 KB1.00
BenchmarkCoreRun.NET 9.050-1297.19 μs4.039 μs3.580 μs297.07 μs1.010.0113.67190.976656.41 KB0.90
BenchmarkBaseline.NET 8.0500981.38 μs4.847 μs4.534 μs980.08 μs1.000.0013.6719-62.74 KB1.00
BenchmarkCoreRun.NET 9.0500428.33 μs3.014 μs2.672 μs428.36 μs0.440.0013.67190.488356.45 KB0.90
BenchmarkBaseline.NET 8.050125,900,673.22 μs15,093.982 μs14,118.920 μs25,902,252.70 μs1.000.00--63.46 KB1.00
BenchmarkCoreRun.NET 9.05014,699,400.51 μs5,271.756 μs4,931.204 μs4,697,546.80 μs0.180.00--57.17 KB0.90
BenchmarkBaseline.NET 8.050525,891,853.07 μs12,417.690 μs11,615.515 μs25,891,875.90 μs1.000.00--63.46 KB1.00
BenchmarkCoreRun.NET 9.05054,698,669.15 μs7,373.175 μs6,896.872 μs4,699,045.70 μs0.180.00--57.17 KB0.90
Benchmark header
BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3593/23H2/2023Update/SunValley3)
Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Baseline : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
CoreRun : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2

NB: because this changes the number of reads/writes to a stream, it'll have an impact on the tests for #102704. I'll change these depending on the order the PRs are merged in.

We now read the data into a (sometimes stack-allocated) byte array and slice it up with BinaryPrimitives.
This reduces the number of reads and writes required to read and write a ZipArchive. It also makes future work to enable async APIs easier, since BinaryReader and BinaryWriter lack this support.
Also changed approach to reading central file directory headers. Rather than performing X reads per header, we read 4KB of data at a time and look for all applicable headers in that data. This should improve performance when dealing with many small files.
This allowed the removal of the ArchiveReader property from ZipArchive.
Now pooling the file IO buffers and the temporary buffers for extra fields of the CD file header (which would otherwise be allocated and deallocated in a loop.)
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Jun 7, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-io-compression
See info in area-owners.md if you want to be subscribed.

This handles 64x entries with 19-character filenames (and thus, 65-byte file headers.) As a result, it straddles two 4KB read buffers.
Also corrected the seek logic while reading the central directory header
@carlossanlop

Copy link
Copy Markdown
Contributor

@edwardneal the main branch is currently only taking changes for RC1 that will go into .NET 9. This change currently does not meet the bar for merging it now, but we would love to consider taking it for .NET 10. The RC1 branch will get snapped from main on August 14th, and we can consider merging this PR after that date, when main will start pointing to .NET 10.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick partial review. Also needs resolving the merge conflict so we get proper CI results.

Comment threadsrc/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs Outdated
Comment threadsrc/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs Outdated
@edwardneal

edwardneal commented Aug 8, 2024

Copy link
Copy Markdown
ContributorAuthor

Thanks @carlossanlop - I've addressed the merge conflict and made those test changes.

I've got a number of post-merge test failures to deal with - I'll look at those shortly, ready for review post-14th.
Edit: these should now be resolved, all tests are passing. It's ready for review.

The buffer returned from the ArrayPool contained older data (including a ZIP header.) When reading the last chunk of the file (i.e a chunk which was less than BackwardsSeekingBufferSize) the buffer's Span wasn't resized to account for this.
SeekBackwardsToSignature would thus find the older data, try to seek beyond the end of the stream and fail to read the file.
@carlossanlop

Copy link
Copy Markdown
Contributor

I fetched your branch and all the relevant tests passed locally: System.IO.Compression, System.IO.Compression.ZipFile and System.IO.Packaging.

I merged the latest bits in main into this branch, as the last CI results were already stale and the build info was gone. Let's see what comes out of this.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left more feedback for you to consider. Thank you so much for this change.

Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs Outdated
Reads and writes are now performed using a new set of field lengths and locations.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@edwardneal thanks for applying the suggestions, I'm happy with how this is looking! I left a (hopefully) final batch of comments for you to consider.

After addressing those comments, I'd like to run a CI run that doesn't get executed by default, which will test your code in mobile platforms. If we don't see anything concerning there, I think we can merge it.

If it's in your possibilities, we could try to get this merged before the Code Complete day for .NET 10 Preview 1, which is Monday January 27th. Let me know if this works for you, otherwise I can take over for the above two final steps.

Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.cs Outdated
Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.cs Outdated
Formatting change; added one comment to Zip64EndOfCentralDirectoryLocator.SignatureConstantBytes; clarified comment on ZipHelper.SeekBackwardsAndRead
Added comment to SeekBackwardsToStream.
Lingering references to SignatureConstantBytes.Length.
Added two asserts to CanReadLargeCentralDirectoryHeader test, verifying that the archive entry metadata is in a sensible state.
@carlossanlop

Copy link
Copy Markdown
Contributor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you @edwardneal for this PR.

The CI failures in the default legs are all marked as expected. The runtime-extra-platforms legs unfortunately are plagued with exit 137 for all assemblies, so we'll have to verify the results there at a later time after that issue gets resolved.

@carlossanlop
carlossanlop merged commit 77fc88a into dotnet:mainJan 23, 2025
@edwardneal
edwardneal deleted the ziparchive-stream-read-write branch January 23, 2025 04:17
@edwardneal

Copy link
Copy Markdown
ContributorAuthor

@carlossanlop I've seen several outerloop tests failing on main when dealing with Zip64 structures. I'm pretty sure they came from this PR and I'm going to trace them down this evening - sorry for missing these.

@carlossanlop

Copy link
Copy Markdown
Contributor

Okay, thank you. We might have to revert both to avoid risking the preview.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.IO.Compressioncommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@edwardneal@carlossanlop@bartonjs
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Remove BinaryReader and BinaryWriter references from ZipArchive - #103153

Merged
carlossanlop merged 14 commits into
dotnet:mainfrom
edwardneal:ziparchive-stream-read-write
Jan 23, 2025
Merged

Remove BinaryReader and BinaryWriter references from ZipArchive#103153
carlossanlop merged 14 commits into
dotnet:mainfrom
edwardneal:ziparchive-stream-read-write

Conversation

@edwardneal

@edwardnealedwardneal commented Jun 7, 2024

Copy link
Copy Markdown
Contributor

Relates to #83909, #31460.

This removes all references to BinaryReader and BinaryWriter from ZipArchive and ZipArchiveEntry. It also changes the way that the ZIP central directory header is read from the source stream and makes one tweak to the way that the EOCD header is detected.

I've removed BinaryReader and BinaryWriter for three reasons:

  • They were being used to read and write to the stream field-by-field - so every file could generate nearly two dozen individual writes to the backing stream.
  • There are no async implementations on these two objects, so these need to be implemented (or their usage removed) in order to add any future async support for ZipArchive.
  • Removes some allocations & GC pressure.

The second change is to adjust the way that the ZIP central directory header is read. Previously, this was read from the source stream file-by-file. This PR now reads from the source in 4KB blocks and tries to read the headers from there. This is much faster. I've chosen not to implement it when writing the CD headers because they contain dynamic data and I didn't want to copy buffers around; I'm open to doing so.

The detection of the end-of-central-directory header is very similar too. It was already doing something similar, but with only 16 bytes at a time. I've tweaked this to read 4KB block instead, and changed the way it searches for the EOCD signature to use an intrinsic rather than byte-by-byte bit shuffling.

In both cases, I've picked 4KB because it feels like a small enough buffer to not make a massive difference to wait times, and it aligns with the 4KB buffer on FileStream (which I imagine would be the most common use case.)

There are performance improvements across the board. To benchmark this, I used a test wrapping stream which simulates the worst case - an Xms Thread.Sleep on every Read and Write call. Results are below, but in short:

Reads

  • 12-13% reduction in execution time as a baseline, rising to 18-19% as the number of entries in the archive increases.
  • 33-36% reduction in memory usage
  • When latency is introduced, as the number of entries in the archive increases, the reduction in execution time becomes more pronounced - 99.6%

Creation

  • Execution time is almost identical assuming no latency. As latency increases, the execution time reduces by around 82%
  • 10% reduction in memory usage
Benchmark results - Reads
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevMedianRatioRatioSDGen0Gen1AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-11.280 μs0.0178 μs0.0338 μs1.269 μs1.000.000.4482-1.84 KB1.00
BenchmarkCoreRun.NET 9.02-11.131 μs0.0104 μs0.0081 μs1.133 μs0.870.030.2861-1.17 KB0.64
BenchmarkBaseline.NET 8.02021.412 μs0.5187 μs1.5211 μs21.083 μs1.000.000.4578-1.88 KB1.00
BenchmarkCoreRun.NET 9.0202.871 μs0.0318 μs0.0265 μs2.871 μs0.130.010.2937-1.21 KB0.65
BenchmarkBaseline.NET 8.021716,595.380 μs2,212.8744 μs2,069.9241 μs716,359.000 μs1.000.00--2.64 KB1.00
BenchmarkCoreRun.NET 9.02162,065.810 μs280.6085 μs248.7521 μs62,120.450 μs0.090.00--1.29 KB0.49
BenchmarkBaseline.NET 8.025716,582.567 μs2,484.5321 μs2,324.0329 μs716,860.600 μs1.000.00--2.64 KB1.00
BenchmarkCoreRun.NET 9.02562,190.336 μs212.1850 μs198.4780 μs62,244.822 μs0.090.00--1.29 KB0.49
BenchmarkBaseline.NET 8.010-13.925 μs0.0296 μs0.0247 μs3.922 μs1.000.001.7929-7.33 KB1.00
BenchmarkCoreRun.NET 9.010-13.452 μs0.0683 μs0.0786 μs3.424 μs0.880.031.1864-4.85 KB0.66
BenchmarkBaseline.NET 8.010098.469 μs5.5739 μs16.0820 μs92.865 μs1.000.001.7090-7.37 KB1.00
BenchmarkCoreRun.NET 9.01005.902 μs0.1166 μs0.2011 μs5.830 μs0.070.011.1902-4.89 KB0.66
BenchmarkBaseline.NET 8.01012,966,581.267 μs5,120.4172 μs4,789.6414 μs2,967,184.400 μs1.000.00--8.09 KB1.00
BenchmarkCoreRun.NET 9.010162,189.399 μs283.9015 μs265.5616 μs62,213.289 μs0.020.00--4.97 KB0.61
BenchmarkBaseline.NET 8.01052,965,424.293 μs5,345.5963 μs5,000.2741 μs2,965,234.300 μs1.000.00--8.09 KB1.00
BenchmarkCoreRun.NET 9.010562,184.896 μs320.7364 μs300.0170 μs62,224.356 μs0.020.00--4.97 KB0.61
BenchmarkBaseline.NET 8.025-19.367 μs0.1231 μs0.1028 μs9.356 μs1.000.004.2114-17.22 KB1.00
BenchmarkCoreRun.NET 9.025-17.661 μs0.0472 μs0.0394 μs7.654 μs0.820.012.7771-11.34 KB0.66
BenchmarkBaseline.NET 8.0250189.773 μs1.5383 μs1.2010 μs190.032 μs1.000.004.1504-17.26 KB1.00
BenchmarkCoreRun.NET 9.02509.558 μs0.1294 μs0.1210 μs9.529 μs0.050.002.7771-11.38 KB0.66
BenchmarkBaseline.NET 8.02517,184,576.887 μs6,870.6351 μs6,426.7964 μs7,183,295.200 μs1.0000.00--17.98 KB1.00
BenchmarkCoreRun.NET 9.025162,115.901 μs231.8962 μs216.9159 μs62,113.444 μs0.0090.00--11.46 KB0.64
BenchmarkBaseline.NET 8.02557,176,659.979 μs7,168.4172 μs6,354.6151 μs7,177,295.900 μs1.0000.00--17.98 KB1.00
BenchmarkCoreRun.NET 9.025562,154.444 μs243.2540 μs227.5399 μs62,233.989 μs0.0090.00--11.46 KB0.64
BenchmarkBaseline.NET 8.050-118.166 μs0.2379 μs0.2225 μs18.171 μs1.000.008.4229-34.48 KB1.00
BenchmarkCoreRun.NET 9.050-114.800 μs0.1155 μs0.1080 μs14.796 μs0.810.015.6152-22.95 KB0.67
BenchmarkBaseline.NET 8.0500368.723 μs3.1490 μs2.9455 μs367.986 μs1.000.008.3008-34.52 KB1.00
BenchmarkCoreRun.NET 9.050017.192 μs0.2263 μs0.2117 μs17.291 μs0.050.005.61520.030522.98 KB0.67
BenchmarkBaseline.NET 8.050114,205,316.860 μs11,442.8220 μs10,703.6228 μs14,203,486.100 μs1.0000.00--35.24 KB1.00
BenchmarkCoreRun.NET 9.050162,229.530 μs282.4781 μs264.2302 μs62,229.311 μs0.0040.00--23.06 KB0.65
BenchmarkBaseline.NET 8.050514,211,658.147 μs9,950.6110 μs9,307.8077 μs14,212,125.500 μs1.0000.00--35.24 KB1.00
BenchmarkCoreRun.NET 9.050562,237.983 μs341.3432 μs319.2927 μs62,250.889 μs0.0040.00--23.06 KB0.65
Benchmark results - Reads (scaling up by number of ZipArchiveEntry children)
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevRatioRatioSDGen0Gen1Gen2AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-11.276 μs0.0162 μs0.0152 μs1.000.000.4482--1.84 KB1.00
BenchmarkCoreRun.NET 9.02-11.120 μs0.0116 μs0.0129 μs0.880.010.2861--1.17 KB0.64
BenchmarkBaseline.NET 8.010-14.527 μs0.0259 μs0.0229 μs1.000.001.7929--7.33 KB1.00
BenchmarkCoreRun.NET 9.010-13.964 μs0.0315 μs0.0295 μs0.880.011.1826--4.85 KB0.66
BenchmarkBaseline.NET 8.025-110.897 μs0.0787 μs0.0698 μs1.000.004.2114--17.22 KB1.00
BenchmarkCoreRun.NET 9.025-18.824 μs0.0499 μs0.0466 μs0.810.012.7771--11.34 KB0.66
BenchmarkBaseline.NET 8.050-121.540 μs0.1920 μs0.1604 μs1.000.008.4229--34.48 KB1.00
BenchmarkCoreRun.NET 9.050-117.341 μs0.1089 μs0.1019 μs0.810.015.6152--22.95 KB0.67
BenchmarkBaseline.NET 8.0100-143.626 μs0.4626 μs0.6016 μs1.000.0016.96780.1221-69.46 KB1.00
BenchmarkCoreRun.NET 9.0100-133.674 μs0.1899 μs0.1683 μs0.770.0111.3525--46.59 KB0.67
BenchmarkBaseline.NET 8.0250-1109.428 μs1.3621 μs1.2074 μs1.000.0041.25980.1221-168.87 KB1.00
BenchmarkCoreRun.NET 9.0250-186.518 μs1.4735 μs1.2305 μs0.790.0127.34386.7139-112.02 KB0.66
BenchmarkBaseline.NET 8.0500-1222.703 μs1.4561 μs1.3621 μs1.000.0083.25200.2441-340.65 KB1.00
BenchmarkCoreRun.NET 9.0500-1171.378 μs1.2795 μs1.0684 μs0.770.0155.41990.2441-227.16 KB0.67
BenchmarkBaseline.NET 8.01000-1476.601 μs8.2338 μs6.8756 μs1.000.00133.789172.2656-686.68 KB1.00
BenchmarkCoreRun.NET 9.01000-1361.508 μs1.1992 μs1.0630 μs0.760.0190.820347.3633-459.91 KB0.67
BenchmarkBaseline.NET 8.010000-112,294.500 μs244.9925 μs547.9609 μs1.000.001171.8750687.5000265.62506879.98 KB1.00
BenchmarkCoreRun.NET 9.010000-110,722.270 μs212.4330 μs504.8696 μs0.870.06843.7500578.1250234.37504614.13 KB0.67
Benchmark results - Creation
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevMedianRatioRatioSDGen0Gen1AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-117.17 μs0.289 μs0.256 μs17.09 μs1.000.000.5798-2.44 KB1.00
BenchmarkCoreRun.NET 9.02-114.20 μs0.214 μs0.179 μs14.26 μs0.830.010.5188-2.15 KB0.88
BenchmarkBaseline.NET 8.02048.28 μs0.883 μs2.115 μs47.69 μs1.000.000.5493-2.48 KB1.00
BenchmarkCoreRun.NET 9.02020.52 μs0.390 μs0.848 μs20.22 μs0.420.030.5188-2.19 KB0.88
BenchmarkBaseline.NET 8.0211,153,370.77 μs3,033.808 μs2,837.826 μs1,153,076.60 μs1.000.00--3.24 KB1.00
BenchmarkCoreRun.NET 9.021202,163.82 μs810.531 μs758.172 μs202,433.70 μs0.180.00--2.43 KB0.75
BenchmarkBaseline.NET 8.0251,154,271.62 μs2,629.954 μs2,460.061 μs1,153,689.70 μs1.000.00--3.24 KB1.00
BenchmarkCoreRun.NET 9.025202,071.53 μs987.541 μs923.747 μs201,793.43 μs0.180.00--2.43 KB0.75
BenchmarkBaseline.NET 8.010-159.75 μs0.906 μs0.803 μs59.52 μs1.000.003.2959-13.5 KB1.00
BenchmarkCoreRun.NET 9.010-159.42 μs0.269 μs0.238 μs59.40 μs0.990.012.9297-12.21 KB0.90
BenchmarkBaseline.NET 8.0100226.82 μs5.396 μs15.395 μs227.63 μs1.000.003.1738-13.54 KB1.00
BenchmarkCoreRun.NET 9.010098.33 μs1.223 μs1.084 μs98.45 μs0.480.032.9297-12.25 KB0.90
BenchmarkBaseline.NET 8.01015,274,439.79 μs6,901.351 μs6,455.528 μs5,273,943.90 μs1.000.00--14.26 KB1.00
BenchmarkCoreRun.NET 9.0101950,228.86 μs2,438.419 μs2,161.595 μs950,750.30 μs0.180.00--12.97 KB0.91
BenchmarkBaseline.NET 8.01055,276,480.03 μs5,844.084 μs5,466.560 μs5,279,086.70 μs1.000.00--14.26 KB1.00
BenchmarkCoreRun.NET 9.0105949,421.14 μs2,346.035 μs2,194.483 μs948,990.50 μs0.180.00--12.97 KB0.91
BenchmarkBaseline.NET 8.025-1148.85 μs2.834 μs3.264 μs149.14 μs1.000.007.5684-31.16 KB1.00
BenchmarkCoreRun.NET 9.025-1147.24 μs1.313 μs1.229 μs146.78 μs0.990.036.8359-28 KB0.90
BenchmarkBaseline.NET 8.0250490.65 μs5.097 μs4.257 μs491.34 μs1.000.006.8359-31.2 KB1.00
BenchmarkCoreRun.NET 9.0250214.95 μs1.275 μs1.065 μs214.92 μs0.440.006.8359-28.04 KB0.90
BenchmarkBaseline.NET 8.025113,010,754.08 μs7,587.337 μs7,097.199 μs13,008,804.00 μs1.000.00--31.92 KB1.00
BenchmarkCoreRun.NET 9.02512,353,101.94 μs4,536.628 μs3,788.290 μs2,352,594.90 μs0.180.00--28.76 KB0.90
BenchmarkBaseline.NET 8.025513,008,020.51 μs8,158.693 μs7,631.646 μs13,008,868.30 μs1.000.00--31.92 KB1.00
BenchmarkCoreRun.NET 9.02552,356,024.47 μs4,256.167 μs3,981.221 μs2,356,131.80 μs0.180.00--28.76 KB0.90
BenchmarkBaseline.NET 8.050-1294.44 μs1.924 μs1.800 μs294.02 μs1.000.0015.1367-62.7 KB1.00
BenchmarkCoreRun.NET 9.050-1297.19 μs4.039 μs3.580 μs297.07 μs1.010.0113.67190.976656.41 KB0.90
BenchmarkBaseline.NET 8.0500981.38 μs4.847 μs4.534 μs980.08 μs1.000.0013.6719-62.74 KB1.00
BenchmarkCoreRun.NET 9.0500428.33 μs3.014 μs2.672 μs428.36 μs0.440.0013.67190.488356.45 KB0.90
BenchmarkBaseline.NET 8.050125,900,673.22 μs15,093.982 μs14,118.920 μs25,902,252.70 μs1.000.00--63.46 KB1.00
BenchmarkCoreRun.NET 9.05014,699,400.51 μs5,271.756 μs4,931.204 μs4,697,546.80 μs0.180.00--57.17 KB0.90
BenchmarkBaseline.NET 8.050525,891,853.07 μs12,417.690 μs11,615.515 μs25,891,875.90 μs1.000.00--63.46 KB1.00
BenchmarkCoreRun.NET 9.05054,698,669.15 μs7,373.175 μs6,896.872 μs4,699,045.70 μs0.180.00--57.17 KB0.90
Benchmark header
BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3593/23H2/2023Update/SunValley3)
Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Baseline : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
CoreRun : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2

NB: because this changes the number of reads/writes to a stream, it'll have an impact on the tests for #102704. I'll change these depending on the order the PRs are merged in.

We now read the data into a (sometimes stack-allocated) byte array and slice it up with BinaryPrimitives.
This reduces the number of reads and writes required to read and write a ZipArchive. It also makes future work to enable async APIs easier, since BinaryReader and BinaryWriter lack this support.
Also changed approach to reading central file directory headers. Rather than performing X reads per header, we read 4KB of data at a time and look for all applicable headers in that data. This should improve performance when dealing with many small files.
This allowed the removal of the ArchiveReader property from ZipArchive.
Now pooling the file IO buffers and the temporary buffers for extra fields of the CD file header (which would otherwise be allocated and deallocated in a loop.)
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Jun 7, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-io-compression
See info in area-owners.md if you want to be subscribed.

This handles 64x entries with 19-character filenames (and thus, 65-byte file headers.) As a result, it straddles two 4KB read buffers.
Also corrected the seek logic while reading the central directory header
@carlossanlop

Copy link
Copy Markdown
Contributor

@edwardneal the main branch is currently only taking changes for RC1 that will go into .NET 9. This change currently does not meet the bar for merging it now, but we would love to consider taking it for .NET 10. The RC1 branch will get snapped from main on August 14th, and we can consider merging this PR after that date, when main will start pointing to .NET 10.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick partial review. Also needs resolving the merge conflict so we get proper CI results.

Comment threadsrc/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs Outdated
Comment threadsrc/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs Outdated
@edwardneal

edwardneal commented Aug 8, 2024

Copy link
Copy Markdown
ContributorAuthor

Thanks @carlossanlop - I've addressed the merge conflict and made those test changes.

I've got a number of post-merge test failures to deal with - I'll look at those shortly, ready for review post-14th.
Edit: these should now be resolved, all tests are passing. It's ready for review.

The buffer returned from the ArrayPool contained older data (including a ZIP header.) When reading the last chunk of the file (i.e a chunk which was less than BackwardsSeekingBufferSize) the buffer's Span wasn't resized to account for this.
SeekBackwardsToSignature would thus find the older data, try to seek beyond the end of the stream and fail to read the file.
@carlossanlop

Copy link
Copy Markdown
Contributor

I fetched your branch and all the relevant tests passed locally: System.IO.Compression, System.IO.Compression.ZipFile and System.IO.Packaging.

I merged the latest bits in main into this branch, as the last CI results were already stale and the build info was gone. Let's see what comes out of this.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left more feedback for you to consider. Thank you so much for this change.

Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs Outdated
Reads and writes are now performed using a new set of field lengths and locations.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@edwardneal thanks for applying the suggestions, I'm happy with how this is looking! I left a (hopefully) final batch of comments for you to consider.

After addressing those comments, I'd like to run a CI run that doesn't get executed by default, which will test your code in mobile platforms. If we don't see anything concerning there, I think we can merge it.

If it's in your possibilities, we could try to get this merged before the Code Complete day for .NET 10 Preview 1, which is Monday January 27th. Let me know if this works for you, otherwise I can take over for the above two final steps.

Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.cs Outdated
Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.cs Outdated
Formatting change; added one comment to Zip64EndOfCentralDirectoryLocator.SignatureConstantBytes; clarified comment on ZipHelper.SeekBackwardsAndRead
Added comment to SeekBackwardsToStream.
Lingering references to SignatureConstantBytes.Length.
Added two asserts to CanReadLargeCentralDirectoryHeader test, verifying that the archive entry metadata is in a sensible state.
@carlossanlop

Copy link
Copy Markdown
Contributor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you @edwardneal for this PR.

The CI failures in the default legs are all marked as expected. The runtime-extra-platforms legs unfortunately are plagued with exit 137 for all assemblies, so we'll have to verify the results there at a later time after that issue gets resolved.

@carlossanlop
carlossanlop merged commit 77fc88a into dotnet:mainJan 23, 2025
@edwardneal
edwardneal deleted the ziparchive-stream-read-write branch January 23, 2025 04:17
@edwardneal

Copy link
Copy Markdown
ContributorAuthor

@carlossanlop I've seen several outerloop tests failing on main when dealing with Zip64 structures. I'm pretty sure they came from this PR and I'm going to trace them down this evening - sorry for missing these.

@carlossanlop

Copy link
Copy Markdown
Contributor

Okay, thank you. We might have to revert both to avoid risking the preview.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.IO.Compressioncommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@edwardneal@carlossanlop@bartonjs
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Remove BinaryReader and BinaryWriter references from ZipArchive - #103153

Merged
carlossanlop merged 14 commits into
dotnet:mainfrom
edwardneal:ziparchive-stream-read-write
Jan 23, 2025
Merged

Remove BinaryReader and BinaryWriter references from ZipArchive#103153
carlossanlop merged 14 commits into
dotnet:mainfrom
edwardneal:ziparchive-stream-read-write

Conversation

@edwardneal

@edwardnealedwardneal commented Jun 7, 2024

Copy link
Copy Markdown
Contributor

Relates to #83909, #31460.

This removes all references to BinaryReader and BinaryWriter from ZipArchive and ZipArchiveEntry. It also changes the way that the ZIP central directory header is read from the source stream and makes one tweak to the way that the EOCD header is detected.

I've removed BinaryReader and BinaryWriter for three reasons:

  • They were being used to read and write to the stream field-by-field - so every file could generate nearly two dozen individual writes to the backing stream.
  • There are no async implementations on these two objects, so these need to be implemented (or their usage removed) in order to add any future async support for ZipArchive.
  • Removes some allocations & GC pressure.

The second change is to adjust the way that the ZIP central directory header is read. Previously, this was read from the source stream file-by-file. This PR now reads from the source in 4KB blocks and tries to read the headers from there. This is much faster. I've chosen not to implement it when writing the CD headers because they contain dynamic data and I didn't want to copy buffers around; I'm open to doing so.

The detection of the end-of-central-directory header is very similar too. It was already doing something similar, but with only 16 bytes at a time. I've tweaked this to read 4KB block instead, and changed the way it searches for the EOCD signature to use an intrinsic rather than byte-by-byte bit shuffling.

In both cases, I've picked 4KB because it feels like a small enough buffer to not make a massive difference to wait times, and it aligns with the 4KB buffer on FileStream (which I imagine would be the most common use case.)

There are performance improvements across the board. To benchmark this, I used a test wrapping stream which simulates the worst case - an Xms Thread.Sleep on every Read and Write call. Results are below, but in short:

Reads

  • 12-13% reduction in execution time as a baseline, rising to 18-19% as the number of entries in the archive increases.
  • 33-36% reduction in memory usage
  • When latency is introduced, as the number of entries in the archive increases, the reduction in execution time becomes more pronounced - 99.6%

Creation

  • Execution time is almost identical assuming no latency. As latency increases, the execution time reduces by around 82%
  • 10% reduction in memory usage
Benchmark results - Reads
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevMedianRatioRatioSDGen0Gen1AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-11.280 μs0.0178 μs0.0338 μs1.269 μs1.000.000.4482-1.84 KB1.00
BenchmarkCoreRun.NET 9.02-11.131 μs0.0104 μs0.0081 μs1.133 μs0.870.030.2861-1.17 KB0.64
BenchmarkBaseline.NET 8.02021.412 μs0.5187 μs1.5211 μs21.083 μs1.000.000.4578-1.88 KB1.00
BenchmarkCoreRun.NET 9.0202.871 μs0.0318 μs0.0265 μs2.871 μs0.130.010.2937-1.21 KB0.65
BenchmarkBaseline.NET 8.021716,595.380 μs2,212.8744 μs2,069.9241 μs716,359.000 μs1.000.00--2.64 KB1.00
BenchmarkCoreRun.NET 9.02162,065.810 μs280.6085 μs248.7521 μs62,120.450 μs0.090.00--1.29 KB0.49
BenchmarkBaseline.NET 8.025716,582.567 μs2,484.5321 μs2,324.0329 μs716,860.600 μs1.000.00--2.64 KB1.00
BenchmarkCoreRun.NET 9.02562,190.336 μs212.1850 μs198.4780 μs62,244.822 μs0.090.00--1.29 KB0.49
BenchmarkBaseline.NET 8.010-13.925 μs0.0296 μs0.0247 μs3.922 μs1.000.001.7929-7.33 KB1.00
BenchmarkCoreRun.NET 9.010-13.452 μs0.0683 μs0.0786 μs3.424 μs0.880.031.1864-4.85 KB0.66
BenchmarkBaseline.NET 8.010098.469 μs5.5739 μs16.0820 μs92.865 μs1.000.001.7090-7.37 KB1.00
BenchmarkCoreRun.NET 9.01005.902 μs0.1166 μs0.2011 μs5.830 μs0.070.011.1902-4.89 KB0.66
BenchmarkBaseline.NET 8.01012,966,581.267 μs5,120.4172 μs4,789.6414 μs2,967,184.400 μs1.000.00--8.09 KB1.00
BenchmarkCoreRun.NET 9.010162,189.399 μs283.9015 μs265.5616 μs62,213.289 μs0.020.00--4.97 KB0.61
BenchmarkBaseline.NET 8.01052,965,424.293 μs5,345.5963 μs5,000.2741 μs2,965,234.300 μs1.000.00--8.09 KB1.00
BenchmarkCoreRun.NET 9.010562,184.896 μs320.7364 μs300.0170 μs62,224.356 μs0.020.00--4.97 KB0.61
BenchmarkBaseline.NET 8.025-19.367 μs0.1231 μs0.1028 μs9.356 μs1.000.004.2114-17.22 KB1.00
BenchmarkCoreRun.NET 9.025-17.661 μs0.0472 μs0.0394 μs7.654 μs0.820.012.7771-11.34 KB0.66
BenchmarkBaseline.NET 8.0250189.773 μs1.5383 μs1.2010 μs190.032 μs1.000.004.1504-17.26 KB1.00
BenchmarkCoreRun.NET 9.02509.558 μs0.1294 μs0.1210 μs9.529 μs0.050.002.7771-11.38 KB0.66
BenchmarkBaseline.NET 8.02517,184,576.887 μs6,870.6351 μs6,426.7964 μs7,183,295.200 μs1.0000.00--17.98 KB1.00
BenchmarkCoreRun.NET 9.025162,115.901 μs231.8962 μs216.9159 μs62,113.444 μs0.0090.00--11.46 KB0.64
BenchmarkBaseline.NET 8.02557,176,659.979 μs7,168.4172 μs6,354.6151 μs7,177,295.900 μs1.0000.00--17.98 KB1.00
BenchmarkCoreRun.NET 9.025562,154.444 μs243.2540 μs227.5399 μs62,233.989 μs0.0090.00--11.46 KB0.64
BenchmarkBaseline.NET 8.050-118.166 μs0.2379 μs0.2225 μs18.171 μs1.000.008.4229-34.48 KB1.00
BenchmarkCoreRun.NET 9.050-114.800 μs0.1155 μs0.1080 μs14.796 μs0.810.015.6152-22.95 KB0.67
BenchmarkBaseline.NET 8.0500368.723 μs3.1490 μs2.9455 μs367.986 μs1.000.008.3008-34.52 KB1.00
BenchmarkCoreRun.NET 9.050017.192 μs0.2263 μs0.2117 μs17.291 μs0.050.005.61520.030522.98 KB0.67
BenchmarkBaseline.NET 8.050114,205,316.860 μs11,442.8220 μs10,703.6228 μs14,203,486.100 μs1.0000.00--35.24 KB1.00
BenchmarkCoreRun.NET 9.050162,229.530 μs282.4781 μs264.2302 μs62,229.311 μs0.0040.00--23.06 KB0.65
BenchmarkBaseline.NET 8.050514,211,658.147 μs9,950.6110 μs9,307.8077 μs14,212,125.500 μs1.0000.00--35.24 KB1.00
BenchmarkCoreRun.NET 9.050562,237.983 μs341.3432 μs319.2927 μs62,250.889 μs0.0040.00--23.06 KB0.65
Benchmark results - Reads (scaling up by number of ZipArchiveEntry children)
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevRatioRatioSDGen0Gen1Gen2AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-11.276 μs0.0162 μs0.0152 μs1.000.000.4482--1.84 KB1.00
BenchmarkCoreRun.NET 9.02-11.120 μs0.0116 μs0.0129 μs0.880.010.2861--1.17 KB0.64
BenchmarkBaseline.NET 8.010-14.527 μs0.0259 μs0.0229 μs1.000.001.7929--7.33 KB1.00
BenchmarkCoreRun.NET 9.010-13.964 μs0.0315 μs0.0295 μs0.880.011.1826--4.85 KB0.66
BenchmarkBaseline.NET 8.025-110.897 μs0.0787 μs0.0698 μs1.000.004.2114--17.22 KB1.00
BenchmarkCoreRun.NET 9.025-18.824 μs0.0499 μs0.0466 μs0.810.012.7771--11.34 KB0.66
BenchmarkBaseline.NET 8.050-121.540 μs0.1920 μs0.1604 μs1.000.008.4229--34.48 KB1.00
BenchmarkCoreRun.NET 9.050-117.341 μs0.1089 μs0.1019 μs0.810.015.6152--22.95 KB0.67
BenchmarkBaseline.NET 8.0100-143.626 μs0.4626 μs0.6016 μs1.000.0016.96780.1221-69.46 KB1.00
BenchmarkCoreRun.NET 9.0100-133.674 μs0.1899 μs0.1683 μs0.770.0111.3525--46.59 KB0.67
BenchmarkBaseline.NET 8.0250-1109.428 μs1.3621 μs1.2074 μs1.000.0041.25980.1221-168.87 KB1.00
BenchmarkCoreRun.NET 9.0250-186.518 μs1.4735 μs1.2305 μs0.790.0127.34386.7139-112.02 KB0.66
BenchmarkBaseline.NET 8.0500-1222.703 μs1.4561 μs1.3621 μs1.000.0083.25200.2441-340.65 KB1.00
BenchmarkCoreRun.NET 9.0500-1171.378 μs1.2795 μs1.0684 μs0.770.0155.41990.2441-227.16 KB0.67
BenchmarkBaseline.NET 8.01000-1476.601 μs8.2338 μs6.8756 μs1.000.00133.789172.2656-686.68 KB1.00
BenchmarkCoreRun.NET 9.01000-1361.508 μs1.1992 μs1.0630 μs0.760.0190.820347.3633-459.91 KB0.67
BenchmarkBaseline.NET 8.010000-112,294.500 μs244.9925 μs547.9609 μs1.000.001171.8750687.5000265.62506879.98 KB1.00
BenchmarkCoreRun.NET 9.010000-110,722.270 μs212.4330 μs504.8696 μs0.870.06843.7500578.1250234.37504614.13 KB0.67
Benchmark results - Creation
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevMedianRatioRatioSDGen0Gen1AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-117.17 μs0.289 μs0.256 μs17.09 μs1.000.000.5798-2.44 KB1.00
BenchmarkCoreRun.NET 9.02-114.20 μs0.214 μs0.179 μs14.26 μs0.830.010.5188-2.15 KB0.88
BenchmarkBaseline.NET 8.02048.28 μs0.883 μs2.115 μs47.69 μs1.000.000.5493-2.48 KB1.00
BenchmarkCoreRun.NET 9.02020.52 μs0.390 μs0.848 μs20.22 μs0.420.030.5188-2.19 KB0.88
BenchmarkBaseline.NET 8.0211,153,370.77 μs3,033.808 μs2,837.826 μs1,153,076.60 μs1.000.00--3.24 KB1.00
BenchmarkCoreRun.NET 9.021202,163.82 μs810.531 μs758.172 μs202,433.70 μs0.180.00--2.43 KB0.75
BenchmarkBaseline.NET 8.0251,154,271.62 μs2,629.954 μs2,460.061 μs1,153,689.70 μs1.000.00--3.24 KB1.00
BenchmarkCoreRun.NET 9.025202,071.53 μs987.541 μs923.747 μs201,793.43 μs0.180.00--2.43 KB0.75
BenchmarkBaseline.NET 8.010-159.75 μs0.906 μs0.803 μs59.52 μs1.000.003.2959-13.5 KB1.00
BenchmarkCoreRun.NET 9.010-159.42 μs0.269 μs0.238 μs59.40 μs0.990.012.9297-12.21 KB0.90
BenchmarkBaseline.NET 8.0100226.82 μs5.396 μs15.395 μs227.63 μs1.000.003.1738-13.54 KB1.00
BenchmarkCoreRun.NET 9.010098.33 μs1.223 μs1.084 μs98.45 μs0.480.032.9297-12.25 KB0.90
BenchmarkBaseline.NET 8.01015,274,439.79 μs6,901.351 μs6,455.528 μs5,273,943.90 μs1.000.00--14.26 KB1.00
BenchmarkCoreRun.NET 9.0101950,228.86 μs2,438.419 μs2,161.595 μs950,750.30 μs0.180.00--12.97 KB0.91
BenchmarkBaseline.NET 8.01055,276,480.03 μs5,844.084 μs5,466.560 μs5,279,086.70 μs1.000.00--14.26 KB1.00
BenchmarkCoreRun.NET 9.0105949,421.14 μs2,346.035 μs2,194.483 μs948,990.50 μs0.180.00--12.97 KB0.91
BenchmarkBaseline.NET 8.025-1148.85 μs2.834 μs3.264 μs149.14 μs1.000.007.5684-31.16 KB1.00
BenchmarkCoreRun.NET 9.025-1147.24 μs1.313 μs1.229 μs146.78 μs0.990.036.8359-28 KB0.90
BenchmarkBaseline.NET 8.0250490.65 μs5.097 μs4.257 μs491.34 μs1.000.006.8359-31.2 KB1.00
BenchmarkCoreRun.NET 9.0250214.95 μs1.275 μs1.065 μs214.92 μs0.440.006.8359-28.04 KB0.90
BenchmarkBaseline.NET 8.025113,010,754.08 μs7,587.337 μs7,097.199 μs13,008,804.00 μs1.000.00--31.92 KB1.00
BenchmarkCoreRun.NET 9.02512,353,101.94 μs4,536.628 μs3,788.290 μs2,352,594.90 μs0.180.00--28.76 KB0.90
BenchmarkBaseline.NET 8.025513,008,020.51 μs8,158.693 μs7,631.646 μs13,008,868.30 μs1.000.00--31.92 KB1.00
BenchmarkCoreRun.NET 9.02552,356,024.47 μs4,256.167 μs3,981.221 μs2,356,131.80 μs0.180.00--28.76 KB0.90
BenchmarkBaseline.NET 8.050-1294.44 μs1.924 μs1.800 μs294.02 μs1.000.0015.1367-62.7 KB1.00
BenchmarkCoreRun.NET 9.050-1297.19 μs4.039 μs3.580 μs297.07 μs1.010.0113.67190.976656.41 KB0.90
BenchmarkBaseline.NET 8.0500981.38 μs4.847 μs4.534 μs980.08 μs1.000.0013.6719-62.74 KB1.00
BenchmarkCoreRun.NET 9.0500428.33 μs3.014 μs2.672 μs428.36 μs0.440.0013.67190.488356.45 KB0.90
BenchmarkBaseline.NET 8.050125,900,673.22 μs15,093.982 μs14,118.920 μs25,902,252.70 μs1.000.00--63.46 KB1.00
BenchmarkCoreRun.NET 9.05014,699,400.51 μs5,271.756 μs4,931.204 μs4,697,546.80 μs0.180.00--57.17 KB0.90
BenchmarkBaseline.NET 8.050525,891,853.07 μs12,417.690 μs11,615.515 μs25,891,875.90 μs1.000.00--63.46 KB1.00
BenchmarkCoreRun.NET 9.05054,698,669.15 μs7,373.175 μs6,896.872 μs4,699,045.70 μs0.180.00--57.17 KB0.90
Benchmark header
BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3593/23H2/2023Update/SunValley3)
Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Baseline : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
CoreRun : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2

NB: because this changes the number of reads/writes to a stream, it'll have an impact on the tests for #102704. I'll change these depending on the order the PRs are merged in.

We now read the data into a (sometimes stack-allocated) byte array and slice it up with BinaryPrimitives.
This reduces the number of reads and writes required to read and write a ZipArchive. It also makes future work to enable async APIs easier, since BinaryReader and BinaryWriter lack this support.
Also changed approach to reading central file directory headers. Rather than performing X reads per header, we read 4KB of data at a time and look for all applicable headers in that data. This should improve performance when dealing with many small files.
This allowed the removal of the ArchiveReader property from ZipArchive.
Now pooling the file IO buffers and the temporary buffers for extra fields of the CD file header (which would otherwise be allocated and deallocated in a loop.)
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Jun 7, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-io-compression
See info in area-owners.md if you want to be subscribed.

This handles 64x entries with 19-character filenames (and thus, 65-byte file headers.) As a result, it straddles two 4KB read buffers.
Also corrected the seek logic while reading the central directory header
@carlossanlop

Copy link
Copy Markdown
Contributor

@edwardneal the main branch is currently only taking changes for RC1 that will go into .NET 9. This change currently does not meet the bar for merging it now, but we would love to consider taking it for .NET 10. The RC1 branch will get snapped from main on August 14th, and we can consider merging this PR after that date, when main will start pointing to .NET 10.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick partial review. Also needs resolving the merge conflict so we get proper CI results.

Comment threadsrc/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs Outdated
Comment threadsrc/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs Outdated
@edwardneal

edwardneal commented Aug 8, 2024

Copy link
Copy Markdown
ContributorAuthor

Thanks @carlossanlop - I've addressed the merge conflict and made those test changes.

I've got a number of post-merge test failures to deal with - I'll look at those shortly, ready for review post-14th.
Edit: these should now be resolved, all tests are passing. It's ready for review.

The buffer returned from the ArrayPool contained older data (including a ZIP header.) When reading the last chunk of the file (i.e a chunk which was less than BackwardsSeekingBufferSize) the buffer's Span wasn't resized to account for this.
SeekBackwardsToSignature would thus find the older data, try to seek beyond the end of the stream and fail to read the file.
@carlossanlop

Copy link
Copy Markdown
Contributor

I fetched your branch and all the relevant tests passed locally: System.IO.Compression, System.IO.Compression.ZipFile and System.IO.Packaging.

I merged the latest bits in main into this branch, as the last CI results were already stale and the build info was gone. Let's see what comes out of this.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left more feedback for you to consider. Thank you so much for this change.

Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs Outdated
Reads and writes are now performed using a new set of field lengths and locations.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@edwardneal thanks for applying the suggestions, I'm happy with how this is looking! I left a (hopefully) final batch of comments for you to consider.

After addressing those comments, I'd like to run a CI run that doesn't get executed by default, which will test your code in mobile platforms. If we don't see anything concerning there, I think we can merge it.

If it's in your possibilities, we could try to get this merged before the Code Complete day for .NET 10 Preview 1, which is Monday January 27th. Let me know if this works for you, otherwise I can take over for the above two final steps.

Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.cs Outdated
Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.cs Outdated
Formatting change; added one comment to Zip64EndOfCentralDirectoryLocator.SignatureConstantBytes; clarified comment on ZipHelper.SeekBackwardsAndRead
Added comment to SeekBackwardsToStream.
Lingering references to SignatureConstantBytes.Length.
Added two asserts to CanReadLargeCentralDirectoryHeader test, verifying that the archive entry metadata is in a sensible state.
@carlossanlop

Copy link
Copy Markdown
Contributor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you @edwardneal for this PR.

The CI failures in the default legs are all marked as expected. The runtime-extra-platforms legs unfortunately are plagued with exit 137 for all assemblies, so we'll have to verify the results there at a later time after that issue gets resolved.

@carlossanlop
carlossanlop merged commit 77fc88a into dotnet:mainJan 23, 2025
@edwardneal
edwardneal deleted the ziparchive-stream-read-write branch January 23, 2025 04:17
@edwardneal

Copy link
Copy Markdown
ContributorAuthor

@carlossanlop I've seen several outerloop tests failing on main when dealing with Zip64 structures. I'm pretty sure they came from this PR and I'm going to trace them down this evening - sorry for missing these.

@carlossanlop

Copy link
Copy Markdown
Contributor

Okay, thank you. We might have to revert both to avoid risking the preview.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.IO.Compressioncommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@edwardneal@carlossanlop@bartonjs
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Remove BinaryReader and BinaryWriter references from ZipArchive - #103153

Merged
carlossanlop merged 14 commits into
dotnet:mainfrom
edwardneal:ziparchive-stream-read-write
Jan 23, 2025
Merged

Remove BinaryReader and BinaryWriter references from ZipArchive#103153
carlossanlop merged 14 commits into
dotnet:mainfrom
edwardneal:ziparchive-stream-read-write

Conversation

@edwardneal

@edwardnealedwardneal commented Jun 7, 2024

Copy link
Copy Markdown
Contributor

Relates to #83909, #31460.

This removes all references to BinaryReader and BinaryWriter from ZipArchive and ZipArchiveEntry. It also changes the way that the ZIP central directory header is read from the source stream and makes one tweak to the way that the EOCD header is detected.

I've removed BinaryReader and BinaryWriter for three reasons:

  • They were being used to read and write to the stream field-by-field - so every file could generate nearly two dozen individual writes to the backing stream.
  • There are no async implementations on these two objects, so these need to be implemented (or their usage removed) in order to add any future async support for ZipArchive.
  • Removes some allocations & GC pressure.

The second change is to adjust the way that the ZIP central directory header is read. Previously, this was read from the source stream file-by-file. This PR now reads from the source in 4KB blocks and tries to read the headers from there. This is much faster. I've chosen not to implement it when writing the CD headers because they contain dynamic data and I didn't want to copy buffers around; I'm open to doing so.

The detection of the end-of-central-directory header is very similar too. It was already doing something similar, but with only 16 bytes at a time. I've tweaked this to read 4KB block instead, and changed the way it searches for the EOCD signature to use an intrinsic rather than byte-by-byte bit shuffling.

In both cases, I've picked 4KB because it feels like a small enough buffer to not make a massive difference to wait times, and it aligns with the 4KB buffer on FileStream (which I imagine would be the most common use case.)

There are performance improvements across the board. To benchmark this, I used a test wrapping stream which simulates the worst case - an Xms Thread.Sleep on every Read and Write call. Results are below, but in short:

Reads

  • 12-13% reduction in execution time as a baseline, rising to 18-19% as the number of entries in the archive increases.
  • 33-36% reduction in memory usage
  • When latency is introduced, as the number of entries in the archive increases, the reduction in execution time becomes more pronounced - 99.6%

Creation

  • Execution time is almost identical assuming no latency. As latency increases, the execution time reduces by around 82%
  • 10% reduction in memory usage
Benchmark results - Reads
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevMedianRatioRatioSDGen0Gen1AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-11.280 μs0.0178 μs0.0338 μs1.269 μs1.000.000.4482-1.84 KB1.00
BenchmarkCoreRun.NET 9.02-11.131 μs0.0104 μs0.0081 μs1.133 μs0.870.030.2861-1.17 KB0.64
BenchmarkBaseline.NET 8.02021.412 μs0.5187 μs1.5211 μs21.083 μs1.000.000.4578-1.88 KB1.00
BenchmarkCoreRun.NET 9.0202.871 μs0.0318 μs0.0265 μs2.871 μs0.130.010.2937-1.21 KB0.65
BenchmarkBaseline.NET 8.021716,595.380 μs2,212.8744 μs2,069.9241 μs716,359.000 μs1.000.00--2.64 KB1.00
BenchmarkCoreRun.NET 9.02162,065.810 μs280.6085 μs248.7521 μs62,120.450 μs0.090.00--1.29 KB0.49
BenchmarkBaseline.NET 8.025716,582.567 μs2,484.5321 μs2,324.0329 μs716,860.600 μs1.000.00--2.64 KB1.00
BenchmarkCoreRun.NET 9.02562,190.336 μs212.1850 μs198.4780 μs62,244.822 μs0.090.00--1.29 KB0.49
BenchmarkBaseline.NET 8.010-13.925 μs0.0296 μs0.0247 μs3.922 μs1.000.001.7929-7.33 KB1.00
BenchmarkCoreRun.NET 9.010-13.452 μs0.0683 μs0.0786 μs3.424 μs0.880.031.1864-4.85 KB0.66
BenchmarkBaseline.NET 8.010098.469 μs5.5739 μs16.0820 μs92.865 μs1.000.001.7090-7.37 KB1.00
BenchmarkCoreRun.NET 9.01005.902 μs0.1166 μs0.2011 μs5.830 μs0.070.011.1902-4.89 KB0.66
BenchmarkBaseline.NET 8.01012,966,581.267 μs5,120.4172 μs4,789.6414 μs2,967,184.400 μs1.000.00--8.09 KB1.00
BenchmarkCoreRun.NET 9.010162,189.399 μs283.9015 μs265.5616 μs62,213.289 μs0.020.00--4.97 KB0.61
BenchmarkBaseline.NET 8.01052,965,424.293 μs5,345.5963 μs5,000.2741 μs2,965,234.300 μs1.000.00--8.09 KB1.00
BenchmarkCoreRun.NET 9.010562,184.896 μs320.7364 μs300.0170 μs62,224.356 μs0.020.00--4.97 KB0.61
BenchmarkBaseline.NET 8.025-19.367 μs0.1231 μs0.1028 μs9.356 μs1.000.004.2114-17.22 KB1.00
BenchmarkCoreRun.NET 9.025-17.661 μs0.0472 μs0.0394 μs7.654 μs0.820.012.7771-11.34 KB0.66
BenchmarkBaseline.NET 8.0250189.773 μs1.5383 μs1.2010 μs190.032 μs1.000.004.1504-17.26 KB1.00
BenchmarkCoreRun.NET 9.02509.558 μs0.1294 μs0.1210 μs9.529 μs0.050.002.7771-11.38 KB0.66
BenchmarkBaseline.NET 8.02517,184,576.887 μs6,870.6351 μs6,426.7964 μs7,183,295.200 μs1.0000.00--17.98 KB1.00
BenchmarkCoreRun.NET 9.025162,115.901 μs231.8962 μs216.9159 μs62,113.444 μs0.0090.00--11.46 KB0.64
BenchmarkBaseline.NET 8.02557,176,659.979 μs7,168.4172 μs6,354.6151 μs7,177,295.900 μs1.0000.00--17.98 KB1.00
BenchmarkCoreRun.NET 9.025562,154.444 μs243.2540 μs227.5399 μs62,233.989 μs0.0090.00--11.46 KB0.64
BenchmarkBaseline.NET 8.050-118.166 μs0.2379 μs0.2225 μs18.171 μs1.000.008.4229-34.48 KB1.00
BenchmarkCoreRun.NET 9.050-114.800 μs0.1155 μs0.1080 μs14.796 μs0.810.015.6152-22.95 KB0.67
BenchmarkBaseline.NET 8.0500368.723 μs3.1490 μs2.9455 μs367.986 μs1.000.008.3008-34.52 KB1.00
BenchmarkCoreRun.NET 9.050017.192 μs0.2263 μs0.2117 μs17.291 μs0.050.005.61520.030522.98 KB0.67
BenchmarkBaseline.NET 8.050114,205,316.860 μs11,442.8220 μs10,703.6228 μs14,203,486.100 μs1.0000.00--35.24 KB1.00
BenchmarkCoreRun.NET 9.050162,229.530 μs282.4781 μs264.2302 μs62,229.311 μs0.0040.00--23.06 KB0.65
BenchmarkBaseline.NET 8.050514,211,658.147 μs9,950.6110 μs9,307.8077 μs14,212,125.500 μs1.0000.00--35.24 KB1.00
BenchmarkCoreRun.NET 9.050562,237.983 μs341.3432 μs319.2927 μs62,250.889 μs0.0040.00--23.06 KB0.65
Benchmark results - Reads (scaling up by number of ZipArchiveEntry children)
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevRatioRatioSDGen0Gen1Gen2AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-11.276 μs0.0162 μs0.0152 μs1.000.000.4482--1.84 KB1.00
BenchmarkCoreRun.NET 9.02-11.120 μs0.0116 μs0.0129 μs0.880.010.2861--1.17 KB0.64
BenchmarkBaseline.NET 8.010-14.527 μs0.0259 μs0.0229 μs1.000.001.7929--7.33 KB1.00
BenchmarkCoreRun.NET 9.010-13.964 μs0.0315 μs0.0295 μs0.880.011.1826--4.85 KB0.66
BenchmarkBaseline.NET 8.025-110.897 μs0.0787 μs0.0698 μs1.000.004.2114--17.22 KB1.00
BenchmarkCoreRun.NET 9.025-18.824 μs0.0499 μs0.0466 μs0.810.012.7771--11.34 KB0.66
BenchmarkBaseline.NET 8.050-121.540 μs0.1920 μs0.1604 μs1.000.008.4229--34.48 KB1.00
BenchmarkCoreRun.NET 9.050-117.341 μs0.1089 μs0.1019 μs0.810.015.6152--22.95 KB0.67
BenchmarkBaseline.NET 8.0100-143.626 μs0.4626 μs0.6016 μs1.000.0016.96780.1221-69.46 KB1.00
BenchmarkCoreRun.NET 9.0100-133.674 μs0.1899 μs0.1683 μs0.770.0111.3525--46.59 KB0.67
BenchmarkBaseline.NET 8.0250-1109.428 μs1.3621 μs1.2074 μs1.000.0041.25980.1221-168.87 KB1.00
BenchmarkCoreRun.NET 9.0250-186.518 μs1.4735 μs1.2305 μs0.790.0127.34386.7139-112.02 KB0.66
BenchmarkBaseline.NET 8.0500-1222.703 μs1.4561 μs1.3621 μs1.000.0083.25200.2441-340.65 KB1.00
BenchmarkCoreRun.NET 9.0500-1171.378 μs1.2795 μs1.0684 μs0.770.0155.41990.2441-227.16 KB0.67
BenchmarkBaseline.NET 8.01000-1476.601 μs8.2338 μs6.8756 μs1.000.00133.789172.2656-686.68 KB1.00
BenchmarkCoreRun.NET 9.01000-1361.508 μs1.1992 μs1.0630 μs0.760.0190.820347.3633-459.91 KB0.67
BenchmarkBaseline.NET 8.010000-112,294.500 μs244.9925 μs547.9609 μs1.000.001171.8750687.5000265.62506879.98 KB1.00
BenchmarkCoreRun.NET 9.010000-110,722.270 μs212.4330 μs504.8696 μs0.870.06843.7500578.1250234.37504614.13 KB0.67
Benchmark results - Creation
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevMedianRatioRatioSDGen0Gen1AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-117.17 μs0.289 μs0.256 μs17.09 μs1.000.000.5798-2.44 KB1.00
BenchmarkCoreRun.NET 9.02-114.20 μs0.214 μs0.179 μs14.26 μs0.830.010.5188-2.15 KB0.88
BenchmarkBaseline.NET 8.02048.28 μs0.883 μs2.115 μs47.69 μs1.000.000.5493-2.48 KB1.00
BenchmarkCoreRun.NET 9.02020.52 μs0.390 μs0.848 μs20.22 μs0.420.030.5188-2.19 KB0.88
BenchmarkBaseline.NET 8.0211,153,370.77 μs3,033.808 μs2,837.826 μs1,153,076.60 μs1.000.00--3.24 KB1.00
BenchmarkCoreRun.NET 9.021202,163.82 μs810.531 μs758.172 μs202,433.70 μs0.180.00--2.43 KB0.75
BenchmarkBaseline.NET 8.0251,154,271.62 μs2,629.954 μs2,460.061 μs1,153,689.70 μs1.000.00--3.24 KB1.00
BenchmarkCoreRun.NET 9.025202,071.53 μs987.541 μs923.747 μs201,793.43 μs0.180.00--2.43 KB0.75
BenchmarkBaseline.NET 8.010-159.75 μs0.906 μs0.803 μs59.52 μs1.000.003.2959-13.5 KB1.00
BenchmarkCoreRun.NET 9.010-159.42 μs0.269 μs0.238 μs59.40 μs0.990.012.9297-12.21 KB0.90
BenchmarkBaseline.NET 8.0100226.82 μs5.396 μs15.395 μs227.63 μs1.000.003.1738-13.54 KB1.00
BenchmarkCoreRun.NET 9.010098.33 μs1.223 μs1.084 μs98.45 μs0.480.032.9297-12.25 KB0.90
BenchmarkBaseline.NET 8.01015,274,439.79 μs6,901.351 μs6,455.528 μs5,273,943.90 μs1.000.00--14.26 KB1.00
BenchmarkCoreRun.NET 9.0101950,228.86 μs2,438.419 μs2,161.595 μs950,750.30 μs0.180.00--12.97 KB0.91
BenchmarkBaseline.NET 8.01055,276,480.03 μs5,844.084 μs5,466.560 μs5,279,086.70 μs1.000.00--14.26 KB1.00
BenchmarkCoreRun.NET 9.0105949,421.14 μs2,346.035 μs2,194.483 μs948,990.50 μs0.180.00--12.97 KB0.91
BenchmarkBaseline.NET 8.025-1148.85 μs2.834 μs3.264 μs149.14 μs1.000.007.5684-31.16 KB1.00
BenchmarkCoreRun.NET 9.025-1147.24 μs1.313 μs1.229 μs146.78 μs0.990.036.8359-28 KB0.90
BenchmarkBaseline.NET 8.0250490.65 μs5.097 μs4.257 μs491.34 μs1.000.006.8359-31.2 KB1.00
BenchmarkCoreRun.NET 9.0250214.95 μs1.275 μs1.065 μs214.92 μs0.440.006.8359-28.04 KB0.90
BenchmarkBaseline.NET 8.025113,010,754.08 μs7,587.337 μs7,097.199 μs13,008,804.00 μs1.000.00--31.92 KB1.00
BenchmarkCoreRun.NET 9.02512,353,101.94 μs4,536.628 μs3,788.290 μs2,352,594.90 μs0.180.00--28.76 KB0.90
BenchmarkBaseline.NET 8.025513,008,020.51 μs8,158.693 μs7,631.646 μs13,008,868.30 μs1.000.00--31.92 KB1.00
BenchmarkCoreRun.NET 9.02552,356,024.47 μs4,256.167 μs3,981.221 μs2,356,131.80 μs0.180.00--28.76 KB0.90
BenchmarkBaseline.NET 8.050-1294.44 μs1.924 μs1.800 μs294.02 μs1.000.0015.1367-62.7 KB1.00
BenchmarkCoreRun.NET 9.050-1297.19 μs4.039 μs3.580 μs297.07 μs1.010.0113.67190.976656.41 KB0.90
BenchmarkBaseline.NET 8.0500981.38 μs4.847 μs4.534 μs980.08 μs1.000.0013.6719-62.74 KB1.00
BenchmarkCoreRun.NET 9.0500428.33 μs3.014 μs2.672 μs428.36 μs0.440.0013.67190.488356.45 KB0.90
BenchmarkBaseline.NET 8.050125,900,673.22 μs15,093.982 μs14,118.920 μs25,902,252.70 μs1.000.00--63.46 KB1.00
BenchmarkCoreRun.NET 9.05014,699,400.51 μs5,271.756 μs4,931.204 μs4,697,546.80 μs0.180.00--57.17 KB0.90
BenchmarkBaseline.NET 8.050525,891,853.07 μs12,417.690 μs11,615.515 μs25,891,875.90 μs1.000.00--63.46 KB1.00
BenchmarkCoreRun.NET 9.05054,698,669.15 μs7,373.175 μs6,896.872 μs4,699,045.70 μs0.180.00--57.17 KB0.90
Benchmark header
BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3593/23H2/2023Update/SunValley3)
Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Baseline : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
CoreRun : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2

NB: because this changes the number of reads/writes to a stream, it'll have an impact on the tests for #102704. I'll change these depending on the order the PRs are merged in.

We now read the data into a (sometimes stack-allocated) byte array and slice it up with BinaryPrimitives.
This reduces the number of reads and writes required to read and write a ZipArchive. It also makes future work to enable async APIs easier, since BinaryReader and BinaryWriter lack this support.
Also changed approach to reading central file directory headers. Rather than performing X reads per header, we read 4KB of data at a time and look for all applicable headers in that data. This should improve performance when dealing with many small files.
This allowed the removal of the ArchiveReader property from ZipArchive.
Now pooling the file IO buffers and the temporary buffers for extra fields of the CD file header (which would otherwise be allocated and deallocated in a loop.)
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Jun 7, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-io-compression
See info in area-owners.md if you want to be subscribed.

This handles 64x entries with 19-character filenames (and thus, 65-byte file headers.) As a result, it straddles two 4KB read buffers.
Also corrected the seek logic while reading the central directory header
@carlossanlop

Copy link
Copy Markdown
Contributor

@edwardneal the main branch is currently only taking changes for RC1 that will go into .NET 9. This change currently does not meet the bar for merging it now, but we would love to consider taking it for .NET 10. The RC1 branch will get snapped from main on August 14th, and we can consider merging this PR after that date, when main will start pointing to .NET 10.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick partial review. Also needs resolving the merge conflict so we get proper CI results.

Comment threadsrc/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs Outdated
Comment threadsrc/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs Outdated
@edwardneal

edwardneal commented Aug 8, 2024

Copy link
Copy Markdown
ContributorAuthor

Thanks @carlossanlop - I've addressed the merge conflict and made those test changes.

I've got a number of post-merge test failures to deal with - I'll look at those shortly, ready for review post-14th.
Edit: these should now be resolved, all tests are passing. It's ready for review.

The buffer returned from the ArrayPool contained older data (including a ZIP header.) When reading the last chunk of the file (i.e a chunk which was less than BackwardsSeekingBufferSize) the buffer's Span wasn't resized to account for this.
SeekBackwardsToSignature would thus find the older data, try to seek beyond the end of the stream and fail to read the file.
@carlossanlop

Copy link
Copy Markdown
Contributor

I fetched your branch and all the relevant tests passed locally: System.IO.Compression, System.IO.Compression.ZipFile and System.IO.Packaging.

I merged the latest bits in main into this branch, as the last CI results were already stale and the build info was gone. Let's see what comes out of this.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left more feedback for you to consider. Thank you so much for this change.

Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs Outdated
Reads and writes are now performed using a new set of field lengths and locations.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@edwardneal thanks for applying the suggestions, I'm happy with how this is looking! I left a (hopefully) final batch of comments for you to consider.

After addressing those comments, I'd like to run a CI run that doesn't get executed by default, which will test your code in mobile platforms. If we don't see anything concerning there, I think we can merge it.

If it's in your possibilities, we could try to get this merged before the Code Complete day for .NET 10 Preview 1, which is Monday January 27th. Let me know if this works for you, otherwise I can take over for the above two final steps.

Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.cs Outdated
Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.cs Outdated
Formatting change; added one comment to Zip64EndOfCentralDirectoryLocator.SignatureConstantBytes; clarified comment on ZipHelper.SeekBackwardsAndRead
Added comment to SeekBackwardsToStream.
Lingering references to SignatureConstantBytes.Length.
Added two asserts to CanReadLargeCentralDirectoryHeader test, verifying that the archive entry metadata is in a sensible state.
@carlossanlop

Copy link
Copy Markdown
Contributor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you @edwardneal for this PR.

The CI failures in the default legs are all marked as expected. The runtime-extra-platforms legs unfortunately are plagued with exit 137 for all assemblies, so we'll have to verify the results there at a later time after that issue gets resolved.

@carlossanlop
carlossanlop merged commit 77fc88a into dotnet:mainJan 23, 2025
@edwardneal
edwardneal deleted the ziparchive-stream-read-write branch January 23, 2025 04:17
@edwardneal

Copy link
Copy Markdown
ContributorAuthor

@carlossanlop I've seen several outerloop tests failing on main when dealing with Zip64 structures. I'm pretty sure they came from this PR and I'm going to trace them down this evening - sorry for missing these.

@carlossanlop

Copy link
Copy Markdown
Contributor

Okay, thank you. We might have to revert both to avoid risking the preview.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.IO.Compressioncommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@edwardneal@carlossanlop@bartonjs
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Remove BinaryReader and BinaryWriter references from ZipArchive - #103153

Merged
carlossanlop merged 14 commits into
dotnet:mainfrom
edwardneal:ziparchive-stream-read-write
Jan 23, 2025
Merged

Remove BinaryReader and BinaryWriter references from ZipArchive#103153
carlossanlop merged 14 commits into
dotnet:mainfrom
edwardneal:ziparchive-stream-read-write

Conversation

@edwardneal

@edwardnealedwardneal commented Jun 7, 2024

Copy link
Copy Markdown
Contributor

Relates to #83909, #31460.

This removes all references to BinaryReader and BinaryWriter from ZipArchive and ZipArchiveEntry. It also changes the way that the ZIP central directory header is read from the source stream and makes one tweak to the way that the EOCD header is detected.

I've removed BinaryReader and BinaryWriter for three reasons:

  • They were being used to read and write to the stream field-by-field - so every file could generate nearly two dozen individual writes to the backing stream.
  • There are no async implementations on these two objects, so these need to be implemented (or their usage removed) in order to add any future async support for ZipArchive.
  • Removes some allocations & GC pressure.

The second change is to adjust the way that the ZIP central directory header is read. Previously, this was read from the source stream file-by-file. This PR now reads from the source in 4KB blocks and tries to read the headers from there. This is much faster. I've chosen not to implement it when writing the CD headers because they contain dynamic data and I didn't want to copy buffers around; I'm open to doing so.

The detection of the end-of-central-directory header is very similar too. It was already doing something similar, but with only 16 bytes at a time. I've tweaked this to read 4KB block instead, and changed the way it searches for the EOCD signature to use an intrinsic rather than byte-by-byte bit shuffling.

In both cases, I've picked 4KB because it feels like a small enough buffer to not make a massive difference to wait times, and it aligns with the 4KB buffer on FileStream (which I imagine would be the most common use case.)

There are performance improvements across the board. To benchmark this, I used a test wrapping stream which simulates the worst case - an Xms Thread.Sleep on every Read and Write call. Results are below, but in short:

Reads

  • 12-13% reduction in execution time as a baseline, rising to 18-19% as the number of entries in the archive increases.
  • 33-36% reduction in memory usage
  • When latency is introduced, as the number of entries in the archive increases, the reduction in execution time becomes more pronounced - 99.6%

Creation

  • Execution time is almost identical assuming no latency. As latency increases, the execution time reduces by around 82%
  • 10% reduction in memory usage
Benchmark results - Reads
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevMedianRatioRatioSDGen0Gen1AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-11.280 μs0.0178 μs0.0338 μs1.269 μs1.000.000.4482-1.84 KB1.00
BenchmarkCoreRun.NET 9.02-11.131 μs0.0104 μs0.0081 μs1.133 μs0.870.030.2861-1.17 KB0.64
BenchmarkBaseline.NET 8.02021.412 μs0.5187 μs1.5211 μs21.083 μs1.000.000.4578-1.88 KB1.00
BenchmarkCoreRun.NET 9.0202.871 μs0.0318 μs0.0265 μs2.871 μs0.130.010.2937-1.21 KB0.65
BenchmarkBaseline.NET 8.021716,595.380 μs2,212.8744 μs2,069.9241 μs716,359.000 μs1.000.00--2.64 KB1.00
BenchmarkCoreRun.NET 9.02162,065.810 μs280.6085 μs248.7521 μs62,120.450 μs0.090.00--1.29 KB0.49
BenchmarkBaseline.NET 8.025716,582.567 μs2,484.5321 μs2,324.0329 μs716,860.600 μs1.000.00--2.64 KB1.00
BenchmarkCoreRun.NET 9.02562,190.336 μs212.1850 μs198.4780 μs62,244.822 μs0.090.00--1.29 KB0.49
BenchmarkBaseline.NET 8.010-13.925 μs0.0296 μs0.0247 μs3.922 μs1.000.001.7929-7.33 KB1.00
BenchmarkCoreRun.NET 9.010-13.452 μs0.0683 μs0.0786 μs3.424 μs0.880.031.1864-4.85 KB0.66
BenchmarkBaseline.NET 8.010098.469 μs5.5739 μs16.0820 μs92.865 μs1.000.001.7090-7.37 KB1.00
BenchmarkCoreRun.NET 9.01005.902 μs0.1166 μs0.2011 μs5.830 μs0.070.011.1902-4.89 KB0.66
BenchmarkBaseline.NET 8.01012,966,581.267 μs5,120.4172 μs4,789.6414 μs2,967,184.400 μs1.000.00--8.09 KB1.00
BenchmarkCoreRun.NET 9.010162,189.399 μs283.9015 μs265.5616 μs62,213.289 μs0.020.00--4.97 KB0.61
BenchmarkBaseline.NET 8.01052,965,424.293 μs5,345.5963 μs5,000.2741 μs2,965,234.300 μs1.000.00--8.09 KB1.00
BenchmarkCoreRun.NET 9.010562,184.896 μs320.7364 μs300.0170 μs62,224.356 μs0.020.00--4.97 KB0.61
BenchmarkBaseline.NET 8.025-19.367 μs0.1231 μs0.1028 μs9.356 μs1.000.004.2114-17.22 KB1.00
BenchmarkCoreRun.NET 9.025-17.661 μs0.0472 μs0.0394 μs7.654 μs0.820.012.7771-11.34 KB0.66
BenchmarkBaseline.NET 8.0250189.773 μs1.5383 μs1.2010 μs190.032 μs1.000.004.1504-17.26 KB1.00
BenchmarkCoreRun.NET 9.02509.558 μs0.1294 μs0.1210 μs9.529 μs0.050.002.7771-11.38 KB0.66
BenchmarkBaseline.NET 8.02517,184,576.887 μs6,870.6351 μs6,426.7964 μs7,183,295.200 μs1.0000.00--17.98 KB1.00
BenchmarkCoreRun.NET 9.025162,115.901 μs231.8962 μs216.9159 μs62,113.444 μs0.0090.00--11.46 KB0.64
BenchmarkBaseline.NET 8.02557,176,659.979 μs7,168.4172 μs6,354.6151 μs7,177,295.900 μs1.0000.00--17.98 KB1.00
BenchmarkCoreRun.NET 9.025562,154.444 μs243.2540 μs227.5399 μs62,233.989 μs0.0090.00--11.46 KB0.64
BenchmarkBaseline.NET 8.050-118.166 μs0.2379 μs0.2225 μs18.171 μs1.000.008.4229-34.48 KB1.00
BenchmarkCoreRun.NET 9.050-114.800 μs0.1155 μs0.1080 μs14.796 μs0.810.015.6152-22.95 KB0.67
BenchmarkBaseline.NET 8.0500368.723 μs3.1490 μs2.9455 μs367.986 μs1.000.008.3008-34.52 KB1.00
BenchmarkCoreRun.NET 9.050017.192 μs0.2263 μs0.2117 μs17.291 μs0.050.005.61520.030522.98 KB0.67
BenchmarkBaseline.NET 8.050114,205,316.860 μs11,442.8220 μs10,703.6228 μs14,203,486.100 μs1.0000.00--35.24 KB1.00
BenchmarkCoreRun.NET 9.050162,229.530 μs282.4781 μs264.2302 μs62,229.311 μs0.0040.00--23.06 KB0.65
BenchmarkBaseline.NET 8.050514,211,658.147 μs9,950.6110 μs9,307.8077 μs14,212,125.500 μs1.0000.00--35.24 KB1.00
BenchmarkCoreRun.NET 9.050562,237.983 μs341.3432 μs319.2927 μs62,250.889 μs0.0040.00--23.06 KB0.65
Benchmark results - Reads (scaling up by number of ZipArchiveEntry children)
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevRatioRatioSDGen0Gen1Gen2AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-11.276 μs0.0162 μs0.0152 μs1.000.000.4482--1.84 KB1.00
BenchmarkCoreRun.NET 9.02-11.120 μs0.0116 μs0.0129 μs0.880.010.2861--1.17 KB0.64
BenchmarkBaseline.NET 8.010-14.527 μs0.0259 μs0.0229 μs1.000.001.7929--7.33 KB1.00
BenchmarkCoreRun.NET 9.010-13.964 μs0.0315 μs0.0295 μs0.880.011.1826--4.85 KB0.66
BenchmarkBaseline.NET 8.025-110.897 μs0.0787 μs0.0698 μs1.000.004.2114--17.22 KB1.00
BenchmarkCoreRun.NET 9.025-18.824 μs0.0499 μs0.0466 μs0.810.012.7771--11.34 KB0.66
BenchmarkBaseline.NET 8.050-121.540 μs0.1920 μs0.1604 μs1.000.008.4229--34.48 KB1.00
BenchmarkCoreRun.NET 9.050-117.341 μs0.1089 μs0.1019 μs0.810.015.6152--22.95 KB0.67
BenchmarkBaseline.NET 8.0100-143.626 μs0.4626 μs0.6016 μs1.000.0016.96780.1221-69.46 KB1.00
BenchmarkCoreRun.NET 9.0100-133.674 μs0.1899 μs0.1683 μs0.770.0111.3525--46.59 KB0.67
BenchmarkBaseline.NET 8.0250-1109.428 μs1.3621 μs1.2074 μs1.000.0041.25980.1221-168.87 KB1.00
BenchmarkCoreRun.NET 9.0250-186.518 μs1.4735 μs1.2305 μs0.790.0127.34386.7139-112.02 KB0.66
BenchmarkBaseline.NET 8.0500-1222.703 μs1.4561 μs1.3621 μs1.000.0083.25200.2441-340.65 KB1.00
BenchmarkCoreRun.NET 9.0500-1171.378 μs1.2795 μs1.0684 μs0.770.0155.41990.2441-227.16 KB0.67
BenchmarkBaseline.NET 8.01000-1476.601 μs8.2338 μs6.8756 μs1.000.00133.789172.2656-686.68 KB1.00
BenchmarkCoreRun.NET 9.01000-1361.508 μs1.1992 μs1.0630 μs0.760.0190.820347.3633-459.91 KB0.67
BenchmarkBaseline.NET 8.010000-112,294.500 μs244.9925 μs547.9609 μs1.000.001171.8750687.5000265.62506879.98 KB1.00
BenchmarkCoreRun.NET 9.010000-110,722.270 μs212.4330 μs504.8696 μs0.870.06843.7500578.1250234.37504614.13 KB0.67
Benchmark results - Creation
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevMedianRatioRatioSDGen0Gen1AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-117.17 μs0.289 μs0.256 μs17.09 μs1.000.000.5798-2.44 KB1.00
BenchmarkCoreRun.NET 9.02-114.20 μs0.214 μs0.179 μs14.26 μs0.830.010.5188-2.15 KB0.88
BenchmarkBaseline.NET 8.02048.28 μs0.883 μs2.115 μs47.69 μs1.000.000.5493-2.48 KB1.00
BenchmarkCoreRun.NET 9.02020.52 μs0.390 μs0.848 μs20.22 μs0.420.030.5188-2.19 KB0.88
BenchmarkBaseline.NET 8.0211,153,370.77 μs3,033.808 μs2,837.826 μs1,153,076.60 μs1.000.00--3.24 KB1.00
BenchmarkCoreRun.NET 9.021202,163.82 μs810.531 μs758.172 μs202,433.70 μs0.180.00--2.43 KB0.75
BenchmarkBaseline.NET 8.0251,154,271.62 μs2,629.954 μs2,460.061 μs1,153,689.70 μs1.000.00--3.24 KB1.00
BenchmarkCoreRun.NET 9.025202,071.53 μs987.541 μs923.747 μs201,793.43 μs0.180.00--2.43 KB0.75
BenchmarkBaseline.NET 8.010-159.75 μs0.906 μs0.803 μs59.52 μs1.000.003.2959-13.5 KB1.00
BenchmarkCoreRun.NET 9.010-159.42 μs0.269 μs0.238 μs59.40 μs0.990.012.9297-12.21 KB0.90
BenchmarkBaseline.NET 8.0100226.82 μs5.396 μs15.395 μs227.63 μs1.000.003.1738-13.54 KB1.00
BenchmarkCoreRun.NET 9.010098.33 μs1.223 μs1.084 μs98.45 μs0.480.032.9297-12.25 KB0.90
BenchmarkBaseline.NET 8.01015,274,439.79 μs6,901.351 μs6,455.528 μs5,273,943.90 μs1.000.00--14.26 KB1.00
BenchmarkCoreRun.NET 9.0101950,228.86 μs2,438.419 μs2,161.595 μs950,750.30 μs0.180.00--12.97 KB0.91
BenchmarkBaseline.NET 8.01055,276,480.03 μs5,844.084 μs5,466.560 μs5,279,086.70 μs1.000.00--14.26 KB1.00
BenchmarkCoreRun.NET 9.0105949,421.14 μs2,346.035 μs2,194.483 μs948,990.50 μs0.180.00--12.97 KB0.91
BenchmarkBaseline.NET 8.025-1148.85 μs2.834 μs3.264 μs149.14 μs1.000.007.5684-31.16 KB1.00
BenchmarkCoreRun.NET 9.025-1147.24 μs1.313 μs1.229 μs146.78 μs0.990.036.8359-28 KB0.90
BenchmarkBaseline.NET 8.0250490.65 μs5.097 μs4.257 μs491.34 μs1.000.006.8359-31.2 KB1.00
BenchmarkCoreRun.NET 9.0250214.95 μs1.275 μs1.065 μs214.92 μs0.440.006.8359-28.04 KB0.90
BenchmarkBaseline.NET 8.025113,010,754.08 μs7,587.337 μs7,097.199 μs13,008,804.00 μs1.000.00--31.92 KB1.00
BenchmarkCoreRun.NET 9.02512,353,101.94 μs4,536.628 μs3,788.290 μs2,352,594.90 μs0.180.00--28.76 KB0.90
BenchmarkBaseline.NET 8.025513,008,020.51 μs8,158.693 μs7,631.646 μs13,008,868.30 μs1.000.00--31.92 KB1.00
BenchmarkCoreRun.NET 9.02552,356,024.47 μs4,256.167 μs3,981.221 μs2,356,131.80 μs0.180.00--28.76 KB0.90
BenchmarkBaseline.NET 8.050-1294.44 μs1.924 μs1.800 μs294.02 μs1.000.0015.1367-62.7 KB1.00
BenchmarkCoreRun.NET 9.050-1297.19 μs4.039 μs3.580 μs297.07 μs1.010.0113.67190.976656.41 KB0.90
BenchmarkBaseline.NET 8.0500981.38 μs4.847 μs4.534 μs980.08 μs1.000.0013.6719-62.74 KB1.00
BenchmarkCoreRun.NET 9.0500428.33 μs3.014 μs2.672 μs428.36 μs0.440.0013.67190.488356.45 KB0.90
BenchmarkBaseline.NET 8.050125,900,673.22 μs15,093.982 μs14,118.920 μs25,902,252.70 μs1.000.00--63.46 KB1.00
BenchmarkCoreRun.NET 9.05014,699,400.51 μs5,271.756 μs4,931.204 μs4,697,546.80 μs0.180.00--57.17 KB0.90
BenchmarkBaseline.NET 8.050525,891,853.07 μs12,417.690 μs11,615.515 μs25,891,875.90 μs1.000.00--63.46 KB1.00
BenchmarkCoreRun.NET 9.05054,698,669.15 μs7,373.175 μs6,896.872 μs4,699,045.70 μs0.180.00--57.17 KB0.90
Benchmark header
BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3593/23H2/2023Update/SunValley3)
Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Baseline : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
CoreRun : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2

NB: because this changes the number of reads/writes to a stream, it'll have an impact on the tests for #102704. I'll change these depending on the order the PRs are merged in.

We now read the data into a (sometimes stack-allocated) byte array and slice it up with BinaryPrimitives.
This reduces the number of reads and writes required to read and write a ZipArchive. It also makes future work to enable async APIs easier, since BinaryReader and BinaryWriter lack this support.
Also changed approach to reading central file directory headers. Rather than performing X reads per header, we read 4KB of data at a time and look for all applicable headers in that data. This should improve performance when dealing with many small files.
This allowed the removal of the ArchiveReader property from ZipArchive.
Now pooling the file IO buffers and the temporary buffers for extra fields of the CD file header (which would otherwise be allocated and deallocated in a loop.)
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Jun 7, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-io-compression
See info in area-owners.md if you want to be subscribed.

This handles 64x entries with 19-character filenames (and thus, 65-byte file headers.) As a result, it straddles two 4KB read buffers.
Also corrected the seek logic while reading the central directory header
@carlossanlop

Copy link
Copy Markdown
Contributor

@edwardneal the main branch is currently only taking changes for RC1 that will go into .NET 9. This change currently does not meet the bar for merging it now, but we would love to consider taking it for .NET 10. The RC1 branch will get snapped from main on August 14th, and we can consider merging this PR after that date, when main will start pointing to .NET 10.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick partial review. Also needs resolving the merge conflict so we get proper CI results.

Comment threadsrc/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs Outdated
Comment threadsrc/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs Outdated
@edwardneal

edwardneal commented Aug 8, 2024

Copy link
Copy Markdown
ContributorAuthor

Thanks @carlossanlop - I've addressed the merge conflict and made those test changes.

I've got a number of post-merge test failures to deal with - I'll look at those shortly, ready for review post-14th.
Edit: these should now be resolved, all tests are passing. It's ready for review.

The buffer returned from the ArrayPool contained older data (including a ZIP header.) When reading the last chunk of the file (i.e a chunk which was less than BackwardsSeekingBufferSize) the buffer's Span wasn't resized to account for this.
SeekBackwardsToSignature would thus find the older data, try to seek beyond the end of the stream and fail to read the file.
@carlossanlop

Copy link
Copy Markdown
Contributor

I fetched your branch and all the relevant tests passed locally: System.IO.Compression, System.IO.Compression.ZipFile and System.IO.Packaging.

I merged the latest bits in main into this branch, as the last CI results were already stale and the build info was gone. Let's see what comes out of this.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left more feedback for you to consider. Thank you so much for this change.

Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs Outdated
Reads and writes are now performed using a new set of field lengths and locations.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@edwardneal thanks for applying the suggestions, I'm happy with how this is looking! I left a (hopefully) final batch of comments for you to consider.

After addressing those comments, I'd like to run a CI run that doesn't get executed by default, which will test your code in mobile platforms. If we don't see anything concerning there, I think we can merge it.

If it's in your possibilities, we could try to get this merged before the Code Complete day for .NET 10 Preview 1, which is Monday January 27th. Let me know if this works for you, otherwise I can take over for the above two final steps.

Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.cs Outdated
Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.cs Outdated
Formatting change; added one comment to Zip64EndOfCentralDirectoryLocator.SignatureConstantBytes; clarified comment on ZipHelper.SeekBackwardsAndRead
Added comment to SeekBackwardsToStream.
Lingering references to SignatureConstantBytes.Length.
Added two asserts to CanReadLargeCentralDirectoryHeader test, verifying that the archive entry metadata is in a sensible state.
@carlossanlop

Copy link
Copy Markdown
Contributor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you @edwardneal for this PR.

The CI failures in the default legs are all marked as expected. The runtime-extra-platforms legs unfortunately are plagued with exit 137 for all assemblies, so we'll have to verify the results there at a later time after that issue gets resolved.

@carlossanlop
carlossanlop merged commit 77fc88a into dotnet:mainJan 23, 2025
@edwardneal
edwardneal deleted the ziparchive-stream-read-write branch January 23, 2025 04:17
@edwardneal

Copy link
Copy Markdown
ContributorAuthor

@carlossanlop I've seen several outerloop tests failing on main when dealing with Zip64 structures. I'm pretty sure they came from this PR and I'm going to trace them down this evening - sorry for missing these.

@carlossanlop

Copy link
Copy Markdown
Contributor

Okay, thank you. We might have to revert both to avoid risking the preview.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.IO.Compressioncommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@edwardneal@carlossanlop@bartonjs
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Remove BinaryReader and BinaryWriter references from ZipArchive - #103153

Merged
carlossanlop merged 14 commits into
dotnet:mainfrom
edwardneal:ziparchive-stream-read-write
Jan 23, 2025
Merged

Remove BinaryReader and BinaryWriter references from ZipArchive#103153
carlossanlop merged 14 commits into
dotnet:mainfrom
edwardneal:ziparchive-stream-read-write

Conversation

@edwardneal

@edwardnealedwardneal commented Jun 7, 2024

Copy link
Copy Markdown
Contributor

Relates to #83909, #31460.

This removes all references to BinaryReader and BinaryWriter from ZipArchive and ZipArchiveEntry. It also changes the way that the ZIP central directory header is read from the source stream and makes one tweak to the way that the EOCD header is detected.

I've removed BinaryReader and BinaryWriter for three reasons:

  • They were being used to read and write to the stream field-by-field - so every file could generate nearly two dozen individual writes to the backing stream.
  • There are no async implementations on these two objects, so these need to be implemented (or their usage removed) in order to add any future async support for ZipArchive.
  • Removes some allocations & GC pressure.

The second change is to adjust the way that the ZIP central directory header is read. Previously, this was read from the source stream file-by-file. This PR now reads from the source in 4KB blocks and tries to read the headers from there. This is much faster. I've chosen not to implement it when writing the CD headers because they contain dynamic data and I didn't want to copy buffers around; I'm open to doing so.

The detection of the end-of-central-directory header is very similar too. It was already doing something similar, but with only 16 bytes at a time. I've tweaked this to read 4KB block instead, and changed the way it searches for the EOCD signature to use an intrinsic rather than byte-by-byte bit shuffling.

In both cases, I've picked 4KB because it feels like a small enough buffer to not make a massive difference to wait times, and it aligns with the 4KB buffer on FileStream (which I imagine would be the most common use case.)

There are performance improvements across the board. To benchmark this, I used a test wrapping stream which simulates the worst case - an Xms Thread.Sleep on every Read and Write call. Results are below, but in short:

Reads

  • 12-13% reduction in execution time as a baseline, rising to 18-19% as the number of entries in the archive increases.
  • 33-36% reduction in memory usage
  • When latency is introduced, as the number of entries in the archive increases, the reduction in execution time becomes more pronounced - 99.6%

Creation

  • Execution time is almost identical assuming no latency. As latency increases, the execution time reduces by around 82%
  • 10% reduction in memory usage
Benchmark results - Reads
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevMedianRatioRatioSDGen0Gen1AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-11.280 μs0.0178 μs0.0338 μs1.269 μs1.000.000.4482-1.84 KB1.00
BenchmarkCoreRun.NET 9.02-11.131 μs0.0104 μs0.0081 μs1.133 μs0.870.030.2861-1.17 KB0.64
BenchmarkBaseline.NET 8.02021.412 μs0.5187 μs1.5211 μs21.083 μs1.000.000.4578-1.88 KB1.00
BenchmarkCoreRun.NET 9.0202.871 μs0.0318 μs0.0265 μs2.871 μs0.130.010.2937-1.21 KB0.65
BenchmarkBaseline.NET 8.021716,595.380 μs2,212.8744 μs2,069.9241 μs716,359.000 μs1.000.00--2.64 KB1.00
BenchmarkCoreRun.NET 9.02162,065.810 μs280.6085 μs248.7521 μs62,120.450 μs0.090.00--1.29 KB0.49
BenchmarkBaseline.NET 8.025716,582.567 μs2,484.5321 μs2,324.0329 μs716,860.600 μs1.000.00--2.64 KB1.00
BenchmarkCoreRun.NET 9.02562,190.336 μs212.1850 μs198.4780 μs62,244.822 μs0.090.00--1.29 KB0.49
BenchmarkBaseline.NET 8.010-13.925 μs0.0296 μs0.0247 μs3.922 μs1.000.001.7929-7.33 KB1.00
BenchmarkCoreRun.NET 9.010-13.452 μs0.0683 μs0.0786 μs3.424 μs0.880.031.1864-4.85 KB0.66
BenchmarkBaseline.NET 8.010098.469 μs5.5739 μs16.0820 μs92.865 μs1.000.001.7090-7.37 KB1.00
BenchmarkCoreRun.NET 9.01005.902 μs0.1166 μs0.2011 μs5.830 μs0.070.011.1902-4.89 KB0.66
BenchmarkBaseline.NET 8.01012,966,581.267 μs5,120.4172 μs4,789.6414 μs2,967,184.400 μs1.000.00--8.09 KB1.00
BenchmarkCoreRun.NET 9.010162,189.399 μs283.9015 μs265.5616 μs62,213.289 μs0.020.00--4.97 KB0.61
BenchmarkBaseline.NET 8.01052,965,424.293 μs5,345.5963 μs5,000.2741 μs2,965,234.300 μs1.000.00--8.09 KB1.00
BenchmarkCoreRun.NET 9.010562,184.896 μs320.7364 μs300.0170 μs62,224.356 μs0.020.00--4.97 KB0.61
BenchmarkBaseline.NET 8.025-19.367 μs0.1231 μs0.1028 μs9.356 μs1.000.004.2114-17.22 KB1.00
BenchmarkCoreRun.NET 9.025-17.661 μs0.0472 μs0.0394 μs7.654 μs0.820.012.7771-11.34 KB0.66
BenchmarkBaseline.NET 8.0250189.773 μs1.5383 μs1.2010 μs190.032 μs1.000.004.1504-17.26 KB1.00
BenchmarkCoreRun.NET 9.02509.558 μs0.1294 μs0.1210 μs9.529 μs0.050.002.7771-11.38 KB0.66
BenchmarkBaseline.NET 8.02517,184,576.887 μs6,870.6351 μs6,426.7964 μs7,183,295.200 μs1.0000.00--17.98 KB1.00
BenchmarkCoreRun.NET 9.025162,115.901 μs231.8962 μs216.9159 μs62,113.444 μs0.0090.00--11.46 KB0.64
BenchmarkBaseline.NET 8.02557,176,659.979 μs7,168.4172 μs6,354.6151 μs7,177,295.900 μs1.0000.00--17.98 KB1.00
BenchmarkCoreRun.NET 9.025562,154.444 μs243.2540 μs227.5399 μs62,233.989 μs0.0090.00--11.46 KB0.64
BenchmarkBaseline.NET 8.050-118.166 μs0.2379 μs0.2225 μs18.171 μs1.000.008.4229-34.48 KB1.00
BenchmarkCoreRun.NET 9.050-114.800 μs0.1155 μs0.1080 μs14.796 μs0.810.015.6152-22.95 KB0.67
BenchmarkBaseline.NET 8.0500368.723 μs3.1490 μs2.9455 μs367.986 μs1.000.008.3008-34.52 KB1.00
BenchmarkCoreRun.NET 9.050017.192 μs0.2263 μs0.2117 μs17.291 μs0.050.005.61520.030522.98 KB0.67
BenchmarkBaseline.NET 8.050114,205,316.860 μs11,442.8220 μs10,703.6228 μs14,203,486.100 μs1.0000.00--35.24 KB1.00
BenchmarkCoreRun.NET 9.050162,229.530 μs282.4781 μs264.2302 μs62,229.311 μs0.0040.00--23.06 KB0.65
BenchmarkBaseline.NET 8.050514,211,658.147 μs9,950.6110 μs9,307.8077 μs14,212,125.500 μs1.0000.00--35.24 KB1.00
BenchmarkCoreRun.NET 9.050562,237.983 μs341.3432 μs319.2927 μs62,250.889 μs0.0040.00--23.06 KB0.65
Benchmark results - Reads (scaling up by number of ZipArchiveEntry children)
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevRatioRatioSDGen0Gen1Gen2AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-11.276 μs0.0162 μs0.0152 μs1.000.000.4482--1.84 KB1.00
BenchmarkCoreRun.NET 9.02-11.120 μs0.0116 μs0.0129 μs0.880.010.2861--1.17 KB0.64
BenchmarkBaseline.NET 8.010-14.527 μs0.0259 μs0.0229 μs1.000.001.7929--7.33 KB1.00
BenchmarkCoreRun.NET 9.010-13.964 μs0.0315 μs0.0295 μs0.880.011.1826--4.85 KB0.66
BenchmarkBaseline.NET 8.025-110.897 μs0.0787 μs0.0698 μs1.000.004.2114--17.22 KB1.00
BenchmarkCoreRun.NET 9.025-18.824 μs0.0499 μs0.0466 μs0.810.012.7771--11.34 KB0.66
BenchmarkBaseline.NET 8.050-121.540 μs0.1920 μs0.1604 μs1.000.008.4229--34.48 KB1.00
BenchmarkCoreRun.NET 9.050-117.341 μs0.1089 μs0.1019 μs0.810.015.6152--22.95 KB0.67
BenchmarkBaseline.NET 8.0100-143.626 μs0.4626 μs0.6016 μs1.000.0016.96780.1221-69.46 KB1.00
BenchmarkCoreRun.NET 9.0100-133.674 μs0.1899 μs0.1683 μs0.770.0111.3525--46.59 KB0.67
BenchmarkBaseline.NET 8.0250-1109.428 μs1.3621 μs1.2074 μs1.000.0041.25980.1221-168.87 KB1.00
BenchmarkCoreRun.NET 9.0250-186.518 μs1.4735 μs1.2305 μs0.790.0127.34386.7139-112.02 KB0.66
BenchmarkBaseline.NET 8.0500-1222.703 μs1.4561 μs1.3621 μs1.000.0083.25200.2441-340.65 KB1.00
BenchmarkCoreRun.NET 9.0500-1171.378 μs1.2795 μs1.0684 μs0.770.0155.41990.2441-227.16 KB0.67
BenchmarkBaseline.NET 8.01000-1476.601 μs8.2338 μs6.8756 μs1.000.00133.789172.2656-686.68 KB1.00
BenchmarkCoreRun.NET 9.01000-1361.508 μs1.1992 μs1.0630 μs0.760.0190.820347.3633-459.91 KB0.67
BenchmarkBaseline.NET 8.010000-112,294.500 μs244.9925 μs547.9609 μs1.000.001171.8750687.5000265.62506879.98 KB1.00
BenchmarkCoreRun.NET 9.010000-110,722.270 μs212.4330 μs504.8696 μs0.870.06843.7500578.1250234.37504614.13 KB0.67
Benchmark results - Creation
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevMedianRatioRatioSDGen0Gen1AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-117.17 μs0.289 μs0.256 μs17.09 μs1.000.000.5798-2.44 KB1.00
BenchmarkCoreRun.NET 9.02-114.20 μs0.214 μs0.179 μs14.26 μs0.830.010.5188-2.15 KB0.88
BenchmarkBaseline.NET 8.02048.28 μs0.883 μs2.115 μs47.69 μs1.000.000.5493-2.48 KB1.00
BenchmarkCoreRun.NET 9.02020.52 μs0.390 μs0.848 μs20.22 μs0.420.030.5188-2.19 KB0.88
BenchmarkBaseline.NET 8.0211,153,370.77 μs3,033.808 μs2,837.826 μs1,153,076.60 μs1.000.00--3.24 KB1.00
BenchmarkCoreRun.NET 9.021202,163.82 μs810.531 μs758.172 μs202,433.70 μs0.180.00--2.43 KB0.75
BenchmarkBaseline.NET 8.0251,154,271.62 μs2,629.954 μs2,460.061 μs1,153,689.70 μs1.000.00--3.24 KB1.00
BenchmarkCoreRun.NET 9.025202,071.53 μs987.541 μs923.747 μs201,793.43 μs0.180.00--2.43 KB0.75
BenchmarkBaseline.NET 8.010-159.75 μs0.906 μs0.803 μs59.52 μs1.000.003.2959-13.5 KB1.00
BenchmarkCoreRun.NET 9.010-159.42 μs0.269 μs0.238 μs59.40 μs0.990.012.9297-12.21 KB0.90
BenchmarkBaseline.NET 8.0100226.82 μs5.396 μs15.395 μs227.63 μs1.000.003.1738-13.54 KB1.00
BenchmarkCoreRun.NET 9.010098.33 μs1.223 μs1.084 μs98.45 μs0.480.032.9297-12.25 KB0.90
BenchmarkBaseline.NET 8.01015,274,439.79 μs6,901.351 μs6,455.528 μs5,273,943.90 μs1.000.00--14.26 KB1.00
BenchmarkCoreRun.NET 9.0101950,228.86 μs2,438.419 μs2,161.595 μs950,750.30 μs0.180.00--12.97 KB0.91
BenchmarkBaseline.NET 8.01055,276,480.03 μs5,844.084 μs5,466.560 μs5,279,086.70 μs1.000.00--14.26 KB1.00
BenchmarkCoreRun.NET 9.0105949,421.14 μs2,346.035 μs2,194.483 μs948,990.50 μs0.180.00--12.97 KB0.91
BenchmarkBaseline.NET 8.025-1148.85 μs2.834 μs3.264 μs149.14 μs1.000.007.5684-31.16 KB1.00
BenchmarkCoreRun.NET 9.025-1147.24 μs1.313 μs1.229 μs146.78 μs0.990.036.8359-28 KB0.90
BenchmarkBaseline.NET 8.0250490.65 μs5.097 μs4.257 μs491.34 μs1.000.006.8359-31.2 KB1.00
BenchmarkCoreRun.NET 9.0250214.95 μs1.275 μs1.065 μs214.92 μs0.440.006.8359-28.04 KB0.90
BenchmarkBaseline.NET 8.025113,010,754.08 μs7,587.337 μs7,097.199 μs13,008,804.00 μs1.000.00--31.92 KB1.00
BenchmarkCoreRun.NET 9.02512,353,101.94 μs4,536.628 μs3,788.290 μs2,352,594.90 μs0.180.00--28.76 KB0.90
BenchmarkBaseline.NET 8.025513,008,020.51 μs8,158.693 μs7,631.646 μs13,008,868.30 μs1.000.00--31.92 KB1.00
BenchmarkCoreRun.NET 9.02552,356,024.47 μs4,256.167 μs3,981.221 μs2,356,131.80 μs0.180.00--28.76 KB0.90
BenchmarkBaseline.NET 8.050-1294.44 μs1.924 μs1.800 μs294.02 μs1.000.0015.1367-62.7 KB1.00
BenchmarkCoreRun.NET 9.050-1297.19 μs4.039 μs3.580 μs297.07 μs1.010.0113.67190.976656.41 KB0.90
BenchmarkBaseline.NET 8.0500981.38 μs4.847 μs4.534 μs980.08 μs1.000.0013.6719-62.74 KB1.00
BenchmarkCoreRun.NET 9.0500428.33 μs3.014 μs2.672 μs428.36 μs0.440.0013.67190.488356.45 KB0.90
BenchmarkBaseline.NET 8.050125,900,673.22 μs15,093.982 μs14,118.920 μs25,902,252.70 μs1.000.00--63.46 KB1.00
BenchmarkCoreRun.NET 9.05014,699,400.51 μs5,271.756 μs4,931.204 μs4,697,546.80 μs0.180.00--57.17 KB0.90
BenchmarkBaseline.NET 8.050525,891,853.07 μs12,417.690 μs11,615.515 μs25,891,875.90 μs1.000.00--63.46 KB1.00
BenchmarkCoreRun.NET 9.05054,698,669.15 μs7,373.175 μs6,896.872 μs4,699,045.70 μs0.180.00--57.17 KB0.90
Benchmark header
BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3593/23H2/2023Update/SunValley3)
Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Baseline : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
CoreRun : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2

NB: because this changes the number of reads/writes to a stream, it'll have an impact on the tests for #102704. I'll change these depending on the order the PRs are merged in.

We now read the data into a (sometimes stack-allocated) byte array and slice it up with BinaryPrimitives.
This reduces the number of reads and writes required to read and write a ZipArchive. It also makes future work to enable async APIs easier, since BinaryReader and BinaryWriter lack this support.
Also changed approach to reading central file directory headers. Rather than performing X reads per header, we read 4KB of data at a time and look for all applicable headers in that data. This should improve performance when dealing with many small files.
This allowed the removal of the ArchiveReader property from ZipArchive.
Now pooling the file IO buffers and the temporary buffers for extra fields of the CD file header (which would otherwise be allocated and deallocated in a loop.)
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Jun 7, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-io-compression
See info in area-owners.md if you want to be subscribed.

This handles 64x entries with 19-character filenames (and thus, 65-byte file headers.) As a result, it straddles two 4KB read buffers.
Also corrected the seek logic while reading the central directory header
@carlossanlop

Copy link
Copy Markdown
Contributor

@edwardneal the main branch is currently only taking changes for RC1 that will go into .NET 9. This change currently does not meet the bar for merging it now, but we would love to consider taking it for .NET 10. The RC1 branch will get snapped from main on August 14th, and we can consider merging this PR after that date, when main will start pointing to .NET 10.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick partial review. Also needs resolving the merge conflict so we get proper CI results.

Comment threadsrc/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs Outdated
Comment threadsrc/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs Outdated
@edwardneal

edwardneal commented Aug 8, 2024

Copy link
Copy Markdown
ContributorAuthor

Thanks @carlossanlop - I've addressed the merge conflict and made those test changes.

I've got a number of post-merge test failures to deal with - I'll look at those shortly, ready for review post-14th.
Edit: these should now be resolved, all tests are passing. It's ready for review.

The buffer returned from the ArrayPool contained older data (including a ZIP header.) When reading the last chunk of the file (i.e a chunk which was less than BackwardsSeekingBufferSize) the buffer's Span wasn't resized to account for this.
SeekBackwardsToSignature would thus find the older data, try to seek beyond the end of the stream and fail to read the file.
@carlossanlop

Copy link
Copy Markdown
Contributor

I fetched your branch and all the relevant tests passed locally: System.IO.Compression, System.IO.Compression.ZipFile and System.IO.Packaging.

I merged the latest bits in main into this branch, as the last CI results were already stale and the build info was gone. Let's see what comes out of this.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left more feedback for you to consider. Thank you so much for this change.

Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs Outdated
Reads and writes are now performed using a new set of field lengths and locations.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@edwardneal thanks for applying the suggestions, I'm happy with how this is looking! I left a (hopefully) final batch of comments for you to consider.

After addressing those comments, I'd like to run a CI run that doesn't get executed by default, which will test your code in mobile platforms. If we don't see anything concerning there, I think we can merge it.

If it's in your possibilities, we could try to get this merged before the Code Complete day for .NET 10 Preview 1, which is Monday January 27th. Let me know if this works for you, otherwise I can take over for the above two final steps.

Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.cs Outdated
Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.cs Outdated
Formatting change; added one comment to Zip64EndOfCentralDirectoryLocator.SignatureConstantBytes; clarified comment on ZipHelper.SeekBackwardsAndRead
Added comment to SeekBackwardsToStream.
Lingering references to SignatureConstantBytes.Length.
Added two asserts to CanReadLargeCentralDirectoryHeader test, verifying that the archive entry metadata is in a sensible state.
@carlossanlop

Copy link
Copy Markdown
Contributor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you @edwardneal for this PR.

The CI failures in the default legs are all marked as expected. The runtime-extra-platforms legs unfortunately are plagued with exit 137 for all assemblies, so we'll have to verify the results there at a later time after that issue gets resolved.

@carlossanlop
carlossanlop merged commit 77fc88a into dotnet:mainJan 23, 2025
@edwardneal
edwardneal deleted the ziparchive-stream-read-write branch January 23, 2025 04:17
@edwardneal

Copy link
Copy Markdown
ContributorAuthor

@carlossanlop I've seen several outerloop tests failing on main when dealing with Zip64 structures. I'm pretty sure they came from this PR and I'm going to trace them down this evening - sorry for missing these.

@carlossanlop

Copy link
Copy Markdown
Contributor

Okay, thank you. We might have to revert both to avoid risking the preview.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.IO.Compressioncommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@edwardneal@carlossanlop@bartonjs
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Remove BinaryReader and BinaryWriter references from ZipArchive - #103153

Merged
carlossanlop merged 14 commits into
dotnet:mainfrom
edwardneal:ziparchive-stream-read-write
Jan 23, 2025
Merged

Remove BinaryReader and BinaryWriter references from ZipArchive#103153
carlossanlop merged 14 commits into
dotnet:mainfrom
edwardneal:ziparchive-stream-read-write

Conversation

@edwardneal

@edwardnealedwardneal commented Jun 7, 2024

Copy link
Copy Markdown
Contributor

Relates to #83909, #31460.

This removes all references to BinaryReader and BinaryWriter from ZipArchive and ZipArchiveEntry. It also changes the way that the ZIP central directory header is read from the source stream and makes one tweak to the way that the EOCD header is detected.

I've removed BinaryReader and BinaryWriter for three reasons:

  • They were being used to read and write to the stream field-by-field - so every file could generate nearly two dozen individual writes to the backing stream.
  • There are no async implementations on these two objects, so these need to be implemented (or their usage removed) in order to add any future async support for ZipArchive.
  • Removes some allocations & GC pressure.

The second change is to adjust the way that the ZIP central directory header is read. Previously, this was read from the source stream file-by-file. This PR now reads from the source in 4KB blocks and tries to read the headers from there. This is much faster. I've chosen not to implement it when writing the CD headers because they contain dynamic data and I didn't want to copy buffers around; I'm open to doing so.

The detection of the end-of-central-directory header is very similar too. It was already doing something similar, but with only 16 bytes at a time. I've tweaked this to read 4KB block instead, and changed the way it searches for the EOCD signature to use an intrinsic rather than byte-by-byte bit shuffling.

In both cases, I've picked 4KB because it feels like a small enough buffer to not make a massive difference to wait times, and it aligns with the 4KB buffer on FileStream (which I imagine would be the most common use case.)

There are performance improvements across the board. To benchmark this, I used a test wrapping stream which simulates the worst case - an Xms Thread.Sleep on every Read and Write call. Results are below, but in short:

Reads

  • 12-13% reduction in execution time as a baseline, rising to 18-19% as the number of entries in the archive increases.
  • 33-36% reduction in memory usage
  • When latency is introduced, as the number of entries in the archive increases, the reduction in execution time becomes more pronounced - 99.6%

Creation

  • Execution time is almost identical assuming no latency. As latency increases, the execution time reduces by around 82%
  • 10% reduction in memory usage
Benchmark results - Reads
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevMedianRatioRatioSDGen0Gen1AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-11.280 μs0.0178 μs0.0338 μs1.269 μs1.000.000.4482-1.84 KB1.00
BenchmarkCoreRun.NET 9.02-11.131 μs0.0104 μs0.0081 μs1.133 μs0.870.030.2861-1.17 KB0.64
BenchmarkBaseline.NET 8.02021.412 μs0.5187 μs1.5211 μs21.083 μs1.000.000.4578-1.88 KB1.00
BenchmarkCoreRun.NET 9.0202.871 μs0.0318 μs0.0265 μs2.871 μs0.130.010.2937-1.21 KB0.65
BenchmarkBaseline.NET 8.021716,595.380 μs2,212.8744 μs2,069.9241 μs716,359.000 μs1.000.00--2.64 KB1.00
BenchmarkCoreRun.NET 9.02162,065.810 μs280.6085 μs248.7521 μs62,120.450 μs0.090.00--1.29 KB0.49
BenchmarkBaseline.NET 8.025716,582.567 μs2,484.5321 μs2,324.0329 μs716,860.600 μs1.000.00--2.64 KB1.00
BenchmarkCoreRun.NET 9.02562,190.336 μs212.1850 μs198.4780 μs62,244.822 μs0.090.00--1.29 KB0.49
BenchmarkBaseline.NET 8.010-13.925 μs0.0296 μs0.0247 μs3.922 μs1.000.001.7929-7.33 KB1.00
BenchmarkCoreRun.NET 9.010-13.452 μs0.0683 μs0.0786 μs3.424 μs0.880.031.1864-4.85 KB0.66
BenchmarkBaseline.NET 8.010098.469 μs5.5739 μs16.0820 μs92.865 μs1.000.001.7090-7.37 KB1.00
BenchmarkCoreRun.NET 9.01005.902 μs0.1166 μs0.2011 μs5.830 μs0.070.011.1902-4.89 KB0.66
BenchmarkBaseline.NET 8.01012,966,581.267 μs5,120.4172 μs4,789.6414 μs2,967,184.400 μs1.000.00--8.09 KB1.00
BenchmarkCoreRun.NET 9.010162,189.399 μs283.9015 μs265.5616 μs62,213.289 μs0.020.00--4.97 KB0.61
BenchmarkBaseline.NET 8.01052,965,424.293 μs5,345.5963 μs5,000.2741 μs2,965,234.300 μs1.000.00--8.09 KB1.00
BenchmarkCoreRun.NET 9.010562,184.896 μs320.7364 μs300.0170 μs62,224.356 μs0.020.00--4.97 KB0.61
BenchmarkBaseline.NET 8.025-19.367 μs0.1231 μs0.1028 μs9.356 μs1.000.004.2114-17.22 KB1.00
BenchmarkCoreRun.NET 9.025-17.661 μs0.0472 μs0.0394 μs7.654 μs0.820.012.7771-11.34 KB0.66
BenchmarkBaseline.NET 8.0250189.773 μs1.5383 μs1.2010 μs190.032 μs1.000.004.1504-17.26 KB1.00
BenchmarkCoreRun.NET 9.02509.558 μs0.1294 μs0.1210 μs9.529 μs0.050.002.7771-11.38 KB0.66
BenchmarkBaseline.NET 8.02517,184,576.887 μs6,870.6351 μs6,426.7964 μs7,183,295.200 μs1.0000.00--17.98 KB1.00
BenchmarkCoreRun.NET 9.025162,115.901 μs231.8962 μs216.9159 μs62,113.444 μs0.0090.00--11.46 KB0.64
BenchmarkBaseline.NET 8.02557,176,659.979 μs7,168.4172 μs6,354.6151 μs7,177,295.900 μs1.0000.00--17.98 KB1.00
BenchmarkCoreRun.NET 9.025562,154.444 μs243.2540 μs227.5399 μs62,233.989 μs0.0090.00--11.46 KB0.64
BenchmarkBaseline.NET 8.050-118.166 μs0.2379 μs0.2225 μs18.171 μs1.000.008.4229-34.48 KB1.00
BenchmarkCoreRun.NET 9.050-114.800 μs0.1155 μs0.1080 μs14.796 μs0.810.015.6152-22.95 KB0.67
BenchmarkBaseline.NET 8.0500368.723 μs3.1490 μs2.9455 μs367.986 μs1.000.008.3008-34.52 KB1.00
BenchmarkCoreRun.NET 9.050017.192 μs0.2263 μs0.2117 μs17.291 μs0.050.005.61520.030522.98 KB0.67
BenchmarkBaseline.NET 8.050114,205,316.860 μs11,442.8220 μs10,703.6228 μs14,203,486.100 μs1.0000.00--35.24 KB1.00
BenchmarkCoreRun.NET 9.050162,229.530 μs282.4781 μs264.2302 μs62,229.311 μs0.0040.00--23.06 KB0.65
BenchmarkBaseline.NET 8.050514,211,658.147 μs9,950.6110 μs9,307.8077 μs14,212,125.500 μs1.0000.00--35.24 KB1.00
BenchmarkCoreRun.NET 9.050562,237.983 μs341.3432 μs319.2927 μs62,250.889 μs0.0040.00--23.06 KB0.65
Benchmark results - Reads (scaling up by number of ZipArchiveEntry children)
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevRatioRatioSDGen0Gen1Gen2AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-11.276 μs0.0162 μs0.0152 μs1.000.000.4482--1.84 KB1.00
BenchmarkCoreRun.NET 9.02-11.120 μs0.0116 μs0.0129 μs0.880.010.2861--1.17 KB0.64
BenchmarkBaseline.NET 8.010-14.527 μs0.0259 μs0.0229 μs1.000.001.7929--7.33 KB1.00
BenchmarkCoreRun.NET 9.010-13.964 μs0.0315 μs0.0295 μs0.880.011.1826--4.85 KB0.66
BenchmarkBaseline.NET 8.025-110.897 μs0.0787 μs0.0698 μs1.000.004.2114--17.22 KB1.00
BenchmarkCoreRun.NET 9.025-18.824 μs0.0499 μs0.0466 μs0.810.012.7771--11.34 KB0.66
BenchmarkBaseline.NET 8.050-121.540 μs0.1920 μs0.1604 μs1.000.008.4229--34.48 KB1.00
BenchmarkCoreRun.NET 9.050-117.341 μs0.1089 μs0.1019 μs0.810.015.6152--22.95 KB0.67
BenchmarkBaseline.NET 8.0100-143.626 μs0.4626 μs0.6016 μs1.000.0016.96780.1221-69.46 KB1.00
BenchmarkCoreRun.NET 9.0100-133.674 μs0.1899 μs0.1683 μs0.770.0111.3525--46.59 KB0.67
BenchmarkBaseline.NET 8.0250-1109.428 μs1.3621 μs1.2074 μs1.000.0041.25980.1221-168.87 KB1.00
BenchmarkCoreRun.NET 9.0250-186.518 μs1.4735 μs1.2305 μs0.790.0127.34386.7139-112.02 KB0.66
BenchmarkBaseline.NET 8.0500-1222.703 μs1.4561 μs1.3621 μs1.000.0083.25200.2441-340.65 KB1.00
BenchmarkCoreRun.NET 9.0500-1171.378 μs1.2795 μs1.0684 μs0.770.0155.41990.2441-227.16 KB0.67
BenchmarkBaseline.NET 8.01000-1476.601 μs8.2338 μs6.8756 μs1.000.00133.789172.2656-686.68 KB1.00
BenchmarkCoreRun.NET 9.01000-1361.508 μs1.1992 μs1.0630 μs0.760.0190.820347.3633-459.91 KB0.67
BenchmarkBaseline.NET 8.010000-112,294.500 μs244.9925 μs547.9609 μs1.000.001171.8750687.5000265.62506879.98 KB1.00
BenchmarkCoreRun.NET 9.010000-110,722.270 μs212.4330 μs504.8696 μs0.870.06843.7500578.1250234.37504614.13 KB0.67
Benchmark results - Creation
MethodJobRuntimeNumberOfFilesLatencyMSMeanErrorStdDevMedianRatioRatioSDGen0Gen1AllocatedAlloc Ratio
BenchmarkBaseline.NET 8.02-117.17 μs0.289 μs0.256 μs17.09 μs1.000.000.5798-2.44 KB1.00
BenchmarkCoreRun.NET 9.02-114.20 μs0.214 μs0.179 μs14.26 μs0.830.010.5188-2.15 KB0.88
BenchmarkBaseline.NET 8.02048.28 μs0.883 μs2.115 μs47.69 μs1.000.000.5493-2.48 KB1.00
BenchmarkCoreRun.NET 9.02020.52 μs0.390 μs0.848 μs20.22 μs0.420.030.5188-2.19 KB0.88
BenchmarkBaseline.NET 8.0211,153,370.77 μs3,033.808 μs2,837.826 μs1,153,076.60 μs1.000.00--3.24 KB1.00
BenchmarkCoreRun.NET 9.021202,163.82 μs810.531 μs758.172 μs202,433.70 μs0.180.00--2.43 KB0.75
BenchmarkBaseline.NET 8.0251,154,271.62 μs2,629.954 μs2,460.061 μs1,153,689.70 μs1.000.00--3.24 KB1.00
BenchmarkCoreRun.NET 9.025202,071.53 μs987.541 μs923.747 μs201,793.43 μs0.180.00--2.43 KB0.75
BenchmarkBaseline.NET 8.010-159.75 μs0.906 μs0.803 μs59.52 μs1.000.003.2959-13.5 KB1.00
BenchmarkCoreRun.NET 9.010-159.42 μs0.269 μs0.238 μs59.40 μs0.990.012.9297-12.21 KB0.90
BenchmarkBaseline.NET 8.0100226.82 μs5.396 μs15.395 μs227.63 μs1.000.003.1738-13.54 KB1.00
BenchmarkCoreRun.NET 9.010098.33 μs1.223 μs1.084 μs98.45 μs0.480.032.9297-12.25 KB0.90
BenchmarkBaseline.NET 8.01015,274,439.79 μs6,901.351 μs6,455.528 μs5,273,943.90 μs1.000.00--14.26 KB1.00
BenchmarkCoreRun.NET 9.0101950,228.86 μs2,438.419 μs2,161.595 μs950,750.30 μs0.180.00--12.97 KB0.91
BenchmarkBaseline.NET 8.01055,276,480.03 μs5,844.084 μs5,466.560 μs5,279,086.70 μs1.000.00--14.26 KB1.00
BenchmarkCoreRun.NET 9.0105949,421.14 μs2,346.035 μs2,194.483 μs948,990.50 μs0.180.00--12.97 KB0.91
BenchmarkBaseline.NET 8.025-1148.85 μs2.834 μs3.264 μs149.14 μs1.000.007.5684-31.16 KB1.00
BenchmarkCoreRun.NET 9.025-1147.24 μs1.313 μs1.229 μs146.78 μs0.990.036.8359-28 KB0.90
BenchmarkBaseline.NET 8.0250490.65 μs5.097 μs4.257 μs491.34 μs1.000.006.8359-31.2 KB1.00
BenchmarkCoreRun.NET 9.0250214.95 μs1.275 μs1.065 μs214.92 μs0.440.006.8359-28.04 KB0.90
BenchmarkBaseline.NET 8.025113,010,754.08 μs7,587.337 μs7,097.199 μs13,008,804.00 μs1.000.00--31.92 KB1.00
BenchmarkCoreRun.NET 9.02512,353,101.94 μs4,536.628 μs3,788.290 μs2,352,594.90 μs0.180.00--28.76 KB0.90
BenchmarkBaseline.NET 8.025513,008,020.51 μs8,158.693 μs7,631.646 μs13,008,868.30 μs1.000.00--31.92 KB1.00
BenchmarkCoreRun.NET 9.02552,356,024.47 μs4,256.167 μs3,981.221 μs2,356,131.80 μs0.180.00--28.76 KB0.90
BenchmarkBaseline.NET 8.050-1294.44 μs1.924 μs1.800 μs294.02 μs1.000.0015.1367-62.7 KB1.00
BenchmarkCoreRun.NET 9.050-1297.19 μs4.039 μs3.580 μs297.07 μs1.010.0113.67190.976656.41 KB0.90
BenchmarkBaseline.NET 8.0500981.38 μs4.847 μs4.534 μs980.08 μs1.000.0013.6719-62.74 KB1.00
BenchmarkCoreRun.NET 9.0500428.33 μs3.014 μs2.672 μs428.36 μs0.440.0013.67190.488356.45 KB0.90
BenchmarkBaseline.NET 8.050125,900,673.22 μs15,093.982 μs14,118.920 μs25,902,252.70 μs1.000.00--63.46 KB1.00
BenchmarkCoreRun.NET 9.05014,699,400.51 μs5,271.756 μs4,931.204 μs4,697,546.80 μs0.180.00--57.17 KB0.90
BenchmarkBaseline.NET 8.050525,891,853.07 μs12,417.690 μs11,615.515 μs25,891,875.90 μs1.000.00--63.46 KB1.00
BenchmarkCoreRun.NET 9.05054,698,669.15 μs7,373.175 μs6,896.872 μs4,699,045.70 μs0.180.00--57.17 KB0.90
Benchmark header
BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3593/23H2/2023Update/SunValley3)
Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Baseline : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
CoreRun : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2

NB: because this changes the number of reads/writes to a stream, it'll have an impact on the tests for #102704. I'll change these depending on the order the PRs are merged in.

We now read the data into a (sometimes stack-allocated) byte array and slice it up with BinaryPrimitives.
This reduces the number of reads and writes required to read and write a ZipArchive. It also makes future work to enable async APIs easier, since BinaryReader and BinaryWriter lack this support.
Also changed approach to reading central file directory headers. Rather than performing X reads per header, we read 4KB of data at a time and look for all applicable headers in that data. This should improve performance when dealing with many small files.
This allowed the removal of the ArchiveReader property from ZipArchive.
Now pooling the file IO buffers and the temporary buffers for extra fields of the CD file header (which would otherwise be allocated and deallocated in a loop.)
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Jun 7, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-io-compression
See info in area-owners.md if you want to be subscribed.

This handles 64x entries with 19-character filenames (and thus, 65-byte file headers.) As a result, it straddles two 4KB read buffers.
Also corrected the seek logic while reading the central directory header
@carlossanlop

Copy link
Copy Markdown
Contributor

@edwardneal the main branch is currently only taking changes for RC1 that will go into .NET 9. This change currently does not meet the bar for merging it now, but we would love to consider taking it for .NET 10. The RC1 branch will get snapped from main on August 14th, and we can consider merging this PR after that date, when main will start pointing to .NET 10.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick partial review. Also needs resolving the merge conflict so we get proper CI results.

Comment threadsrc/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs Outdated
Comment threadsrc/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs Outdated
@edwardneal

edwardneal commented Aug 8, 2024

Copy link
Copy Markdown
ContributorAuthor

Thanks @carlossanlop - I've addressed the merge conflict and made those test changes.

I've got a number of post-merge test failures to deal with - I'll look at those shortly, ready for review post-14th.
Edit: these should now be resolved, all tests are passing. It's ready for review.

The buffer returned from the ArrayPool contained older data (including a ZIP header.) When reading the last chunk of the file (i.e a chunk which was less than BackwardsSeekingBufferSize) the buffer's Span wasn't resized to account for this.
SeekBackwardsToSignature would thus find the older data, try to seek beyond the end of the stream and fail to read the file.
@carlossanlop

Copy link
Copy Markdown
Contributor

I fetched your branch and all the relevant tests passed locally: System.IO.Compression, System.IO.Compression.ZipFile and System.IO.Packaging.

I merged the latest bits in main into this branch, as the last CI results were already stale and the build info was gone. Let's see what comes out of this.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left more feedback for you to consider. Thank you so much for this change.

Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs Outdated
Reads and writes are now performed using a new set of field lengths and locations.

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@edwardneal thanks for applying the suggestions, I'm happy with how this is looking! I left a (hopefully) final batch of comments for you to consider.

After addressing those comments, I'd like to run a CI run that doesn't get executed by default, which will test your code in mobile platforms. If we don't see anything concerning there, I think we can merge it.

If it's in your possibilities, we could try to get this merged before the Code Complete day for .NET 10 Preview 1, which is Monday January 27th. Let me know if this works for you, otherwise I can take over for the above two final steps.

Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.cs Outdated
Comment threadsrc/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.cs Outdated
Formatting change; added one comment to Zip64EndOfCentralDirectoryLocator.SignatureConstantBytes; clarified comment on ZipHelper.SeekBackwardsAndRead
Added comment to SeekBackwardsToStream.
Lingering references to SignatureConstantBytes.Length.
Added two asserts to CanReadLargeCentralDirectoryHeader test, verifying that the archive entry metadata is in a sensible state.
@carlossanlop

Copy link
Copy Markdown
Contributor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@carlossanlopcarlossanlop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you @edwardneal for this PR.

The CI failures in the default legs are all marked as expected. The runtime-extra-platforms legs unfortunately are plagued with exit 137 for all assemblies, so we'll have to verify the results there at a later time after that issue gets resolved.

@carlossanlop
carlossanlop merged commit 77fc88a into dotnet:mainJan 23, 2025
@edwardneal
edwardneal deleted the ziparchive-stream-read-write branch January 23, 2025 04:17
@edwardneal

Copy link
Copy Markdown
ContributorAuthor

@carlossanlop I've seen several outerloop tests failing on main when dealing with Zip64 structures. I'm pretty sure they came from this PR and I'm going to trace them down this evening - sorry for missing these.

@carlossanlop

Copy link
Copy Markdown
Contributor

Okay, thank you. We might have to revert both to avoid risking the preview.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.IO.Compressioncommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@edwardneal@carlossanlop@bartonjs