Skip to content

Optimize BufferedSubStream.ReadByte #912

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 25, 2025

Conversation

jdpurcell
Copy link
Contributor

@jdpurcell jdpurcell commented Mar 25, 2025

In BufferedSubStream, by changing what BytesLeftToRead and _cacheLength track, we can simplify the work to be done when reading a byte. BytesLeftToRead now tracks the amount of completely unread/uncached data, and _cacheLength tracks the total cache length including the consumed portion. Then we only need to update the cache offset when reading a byte. It also moves the logic to refresh the cache into its own method. Interestingly, only about half of the performance increase comes from simplifying the variable usage. The other half comes from moving out the cache refresh logic, presumably because it makes the ReadByte method small enough for callers to inline.

Core i7-6700k (1.7% reduction):

Method Mean Error StdDev
Before 1.088 s 0.0031 s 0.0028 s
After 1.070 s 0.0011 s 0.0010 s

Apple M3 (2.8% reduction):

Method Mean Error StdDev
Before 785.0 ms 1.60 ms 1.42 ms
After 763.2 ms 2.84 ms 2.52 ms

Time to extract a 14MB 7z. The benchmarks are with .NET 8.0 but it also helped slightly with .NET Framework 4.8 (~1.1% reduction).

I wish I could keep going with these optimization PRs but I think I'm out of ideas after this one 😄.

@@ -20,29 +20,41 @@ internal class BufferedSubStream(Stream stream, long origin, long bytesToRead)

public override void Flush() { }

public override long Length => BytesLeftToRead;
public override long Length => BytesLeftToRead + _cacheLength - _cacheOffset;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It seems weird that Length in a stream refers only to the remaining portion, but since that's the way it already worked I assume it's for a reason.

Copy link
Owner

Choose a reason for hiding this comment

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

maybe that's not the contract but I "meant" to do it 😬


public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}

private void RefillCache()
{
var count = (int)Math.Min(BytesLeftToRead, _cache.Length);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a new bit of logic here; the old code would always request _cache.Length bytes when refilling. It probably wasn't hurting anything but I figure it's better to request only what we need.

Copy link
Owner

@adamhathcock adamhathcock left a comment

Choose a reason for hiding this comment

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

Smells good, thanks!

@@ -20,29 +20,41 @@ internal class BufferedSubStream(Stream stream, long origin, long bytesToRead)

public override void Flush() { }

public override long Length => BytesLeftToRead;
public override long Length => BytesLeftToRead + _cacheLength - _cacheOffset;
Copy link
Owner

Choose a reason for hiding this comment

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

maybe that's not the contract but I "meant" to do it 😬

@adamhathcock adamhathcock merged commit 6e48302 into adamhathcock:master Mar 25, 2025
2 checks passed
@jdpurcell jdpurcell deleted the pr-bss-optim branch March 25, 2025 13:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants