Skip to content
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

Improve ArraySegmentStream with ReadOnlyMemory/Span signatures, avoid extra alloc/copy #2556

Merged
merged 17 commits into from
Mar 29, 2024

Conversation

mregen
Copy link
Contributor

@mregen mregen commented Mar 15, 2024

Proposed changes

Many MemoryStream writer functions via BinaryWriter call the byte[] methods with an additional copy and allocation that can be avoided by providing the new methods with int Read(Span<byte> buffer) and void Write(ReadOnlySpan<byte> buffer).
ArraySegmentStream outperforms or is on parity with RecyclableMemoryStream for both speed and memory allocations due to the special buffer handling.

Current benchmark results:

  • binary encoding with ArraySegmentStream gets a speed up by a factor of two with the used test set and lesser memory allocations because the callers in the underlying memory stream do not have to do an extra Rent/copy for small items (in BinaryWriter) like an int when calling the new Write signature (compare BinaryEncoderArraySegmentStream vs BinaryEncoderArraySegmentStreamNoSpan)
  • binary decoding doesn't show a difference in memory use due to the nature of preallocated buffers. For .NET Framework 4.8 and .NET6 there is almost no penalty in not supporting the Read(Span). However, in .NET 8 there is all of a sudden a big difference when the Read(Span) is not supported. Still MemoryStream is the fastest, but without the Read(Span) signature the ArraySegmentStream were running slower than in .NET6, while the streams with span get a nice perf boost. RecycleMemoryStream has no implementation for decoding a ReadOnlySequence.
  • JSON encoding seems unaffected due to the underlying StreamWriter class. There is no obvious perf difference. But for the JSON case RecyclableMemoryStream outperforms all other MemoryStream implementations by some 10 or 20%.
  • It is worth to mention that RecyclableMemoryStream is for the binary encoder as slow as the NoSpan version of the ArraySegmentStream. So maybe the span support is missing? (Checked that it is implemented, needs investigation). For JSON encoding there is no difference between ArraySegmentStream and RecyclableMemoryStream.
| Method                                | Runtime            | PayLoadSize | Mean        | Error     | StdDev    | Median      | Ratio | RatioSD | Code Size | Gen0     | Gen1     | Gen2     | Allocated  | Alloc Ratio |
|-------------------------------------- |------------------- |------------ |------------:|----------:|----------:|------------:|------:|--------:|----------:|---------:|---------:|---------:|-----------:|------------:|
| BinaryEncoderMemoryStream             | .NET 6.0           | 64          |    313.8 us |  11.49 us |  33.70 us |    306.8 us |  0.68 |    0.11 |   1,266 B |  24.9023 |        - |        - |  205.02 KB |        1.00 |
| BinaryEncoderRecyclableMemoryStream   | .NET 6.0           | 64          |    623.6 us |  20.15 us |  58.47 us |    615.3 us |  1.36 |    0.21 |   3,170 B |   3.9063 |        - |        - |   31.93 KB |        0.16 |
| *BinaryEncoderArraySegmentStream      | .NET 6.0           | 64          |    313.1 us |   9.20 us |  26.40 us |    307.7 us |  0.68 |    0.09 |   2,337 B |   3.4180 |        - |        - |   30.65 KB |        0.15 |
| BinaryEncoderArraySegmentStreamNoSpan | .NET 6.0           | 64          |    719.8 us |  21.85 us |  63.72 us |    703.2 us |  1.57 |    0.21 |   2,337 B |   2.9297 |        - |        - |   30.65 KB |        0.15 |
| BinaryEncoderMemoryStream             | .NET 8.0           | 64          |    226.3 us |   8.67 us |  25.44 us |    223.5 us |  0.49 |    0.08 |   2,360 B |  24.9023 |        - |        - |  205.02 KB |        1.00 |
| BinaryEncoderRecyclableMemoryStream   | .NET 8.0           | 64          |    522.0 us |  14.70 us |  42.88 us |    507.1 us |  1.14 |    0.16 |   5,305 B |   3.9063 |        - |        - |   31.94 KB |        0.16 |
| *BinaryEncoderArraySegmentStream      | .NET 8.0           | 64          |    233.4 us |   9.16 us |  27.00 us |    222.8 us |  0.51 |    0.09 |   5,849 B |   3.6621 |        - |        - |   30.65 KB |        0.15 |
| BinaryEncoderArraySegmentStreamNoSpan | .NET 8.0           | 64          |    596.9 us |  17.92 us |  52.27 us |    584.6 us |  1.30 |    0.16 |   5,839 B |   2.9297 |        - |        - |   30.65 KB |        0.15 |
| BinaryEncoderMemoryStream             | .NET Framework 4.8 | 64          |    466.0 us |  20.71 us |  61.07 us |    446.2 us |  1.00 |    0.00 |     819 B |  33.2031 |   4.8828 |        - |  205.54 KB |        1.00 |
| BinaryEncoderRecyclableMemoryStream   | .NET Framework 4.8 | 64          |  1,065.6 us |  44.37 us | 130.13 us |  1,050.4 us |  2.33 |    0.41 |   1,511 B |   3.9063 |        - |        - |   32.31 KB |        0.16 |
| *BinaryEncoderArraySegmentStream      | .NET Framework 4.8 | 64          |    628.1 us |  21.74 us |  64.11 us |    631.7 us |  1.37 |    0.20 |   1,921 B |   4.8828 |        - |        - |   31.05 KB |        0.15 |
| BinaryEncoderArraySegmentStreamNoSpan | .NET Framework 4.8 | 64          |    670.5 us |  28.36 us |  82.74 us |    678.1 us |  1.48 |    0.31 |   1,921 B |   4.8828 |        - |        - |   31.05 KB |        0.15 |
|                                       |                    |             |             |           |           |             |       |         |           |          |          |          |            |             |
| BinaryEncoderMemoryStream             | .NET 6.0           | 1024        |  5,922.9 us | 206.52 us | 605.69 us |  5,873.8 us |  0.70 |    0.11 |   1,266 B | 710.9375 | 679.6875 | 671.8750 | 3354.38 KB |        1.00 |
| BinaryEncoderRecyclableMemoryStream   | .NET 6.0           | 1024        | 10,432.2 us | 250.46 us | 722.64 us | 10,313.6 us |  1.24 |    0.17 |   3,170 B |  78.1250 |        - |        - |  670.88 KB |        0.20 |
| *BinaryEncoderArraySegmentStream      | .NET 6.0           | 1024        |  5,354.3 us | 120.44 us | 345.56 us |  5,294.9 us |  0.64 |    0.08 |   2,337 B | 132.8125 |  31.2500 |        - | 1125.15 KB |        0.34 |
| BinaryEncoderArraySegmentStreamNoSpan | .NET 6.0           | 1024        | 12,628.9 us | 326.79 us | 932.35 us | 12,402.7 us |  1.50 |    0.23 |   2,337 B | 125.0000 |  31.2500 |        - | 1125.15 KB |        0.34 |
| BinaryEncoderMemoryStream             | .NET 8.0           | 1024        |  4,315.1 us |  88.94 us | 258.02 us |  4,268.5 us |  0.51 |    0.07 |   2,362 B | 718.7500 | 671.8750 | 671.8750 | 3354.44 KB |        1.00 |
| BinaryEncoderRecyclableMemoryStream   | .NET 8.0           | 1024        |  8,205.3 us | 163.70 us | 422.56 us |  8,198.4 us |  0.96 |    0.10 |   5,305 B |  78.1250 |        - |        - |  670.85 KB |        0.20 |
| *BinaryEncoderArraySegmentStream      | .NET 8.0           | 1024        |  3,836.7 us |  93.93 us | 273.99 us |  3,784.1 us |  0.46 |    0.06 |   5,894 B |  58.5938 |        - |        - |  491.29 KB |        0.15 |
| BinaryEncoderArraySegmentStreamNoSpan | .NET 8.0           | 1024        |  9,933.1 us | 313.87 us | 920.52 us |  9,717.3 us |  1.18 |    0.15 |   5,847 B |  46.8750 |        - |        - |   491.3 KB |        0.15 |
| BinaryEncoderMemoryStream             | .NET Framework 4.8 | 1024        |  8,525.3 us | 286.61 us | 840.58 us |  8,698.8 us |  1.00 |    0.00 |     819 B | 750.0000 | 671.8750 | 671.8750 | 3358.61 KB |        1.00 |
| BinaryEncoderRecyclableMemoryStream   | .NET Framework 4.8 | 1024        | 12,748.4 us | 251.76 us | 680.65 us | 12,593.7 us |  1.50 |    0.17 |   1,511 B | 109.3750 |        - |        - |  673.15 KB |        0.20 |
| *BinaryEncoderArraySegmentStream      | .NET Framework 4.8 | 1024        |  7,540.7 us | 150.69 us | 425.04 us |  7,529.2 us |  0.90 |    0.12 |   1,921 B | 218.7500 | 109.3750 |        - |  1362.6 KB |        0.41 |
| BinaryEncoderArraySegmentStreamNoSpan | .NET Framework 4.8 | 1024        |  7,709.3 us | 152.26 us | 324.47 us |  7,586.7 us |  0.93 |    0.11 |   1,921 B | 218.7500 | 109.3750 |        - | 1362.55 KB |        0.41 |

| BinaryDecoderMemoryStream             | .NET 6.0           | 64          |   202.7 us |   6.00 us |  17.40 us |   196.8 us |  0.63 |    0.10 |  21.4844 |     920 B |  176.28 KB |        0.98 |
| BinaryDecoderArraySegmentStream       | .NET 6.0           | 64          |   278.7 us |   7.51 us |  21.92 us |   271.7 us |  0.86 |    0.10 |  21.4844 |   1,230 B |  176.34 KB |        0.98 |
| BinaryDecoderArraySegmentStreamNoSpan | .NET 6.0           | 64          |   280.6 us |   7.51 us |  21.05 us |   275.6 us |  0.87 |    0.11 |  21.4844 |   1,230 B |  176.34 KB |        0.98 |
| BinaryDecoderMemoryStream             | .NET 8.0           | 64          |   113.7 us |   5.52 us |  15.93 us |   107.0 us |  0.35 |    0.06 |  21.4844 |     832 B |  176.28 KB |        0.98 |
| BinaryDecoderArraySegmentStream       | .NET 8.0           | 64          |   194.6 us |   3.89 us |  11.22 us |   193.3 us |  0.60 |    0.08 |  21.4844 |   1,829 B |  176.34 KB |        0.98 |
| BinaryDecoderArraySegmentStreamNoSpan | .NET 8.0           | 64          |   446.3 us |   8.85 us |  24.67 us |   442.1 us |  1.39 |    0.18 |  21.4844 |   1,848 B |  176.34 KB |        0.98 |
| BinaryDecoderMemoryStream             | .NET Framework 4.8 | 64          |   327.1 us |  13.11 us |  38.04 us |   320.3 us |  1.00 |    0.00 |  28.8086 |     346 B |  179.94 KB |        1.00 |
| BinaryDecoderArraySegmentStream       | .NET Framework 4.8 | 64          |   366.3 us |  16.57 us |  48.59 us |   346.8 us |  1.14 |    0.22 |  29.2969 |     550 B |  180.02 KB |        1.00 |
| BinaryDecoderArraySegmentStreamNoSpan | .NET Framework 4.8 | 64          |   329.7 us |  10.12 us |  28.55 us |   322.2 us |  1.02 |    0.12 |  29.2969 |     550 B |  180.02 KB |        1.00 |
|                                       |                    |             |            |           |           |            |       |         |          |           |            |             |
| BinaryDecoderMemoryStream             | .NET 6.0           | 1024        | 2,811.5 us |  91.72 us | 267.55 us | 2,739.1 us |  0.59 |    0.07 | 343.7500 |     920 B | 2816.29 KB |        0.98 |
| BinaryDecoderArraySegmentStream       | .NET 6.0           | 1024        | 4,173.1 us |  82.09 us | 167.69 us | 4,134.7 us |  0.87 |    0.07 | 343.7500 |   1,230 B | 2816.34 KB |        0.98 |
| BinaryDecoderArraySegmentStreamNoSpan | .NET 6.0           | 1024        | 4,289.2 us | 162.97 us | 454.30 us | 4,195.9 us |  0.90 |    0.11 | 343.7500 |   1,230 B | 2816.34 KB |        0.98 |
| BinaryDecoderMemoryStream             | .NET 8.0           | 1024        | 1,742.0 us |  41.25 us | 116.34 us | 1,715.3 us |  0.37 |    0.03 | 343.7500 |     832 B | 2816.28 KB |        0.98 |
| BinaryDecoderArraySegmentStream       | .NET 8.0           | 1024        | 3,128.8 us |  93.15 us | 268.76 us | 3,083.5 us |  0.66 |    0.07 | 343.7500 |   1,851 B | 2816.34 KB |        0.98 |
| BinaryDecoderArraySegmentStreamNoSpan | .NET 8.0           | 1024        | 6,996.5 us | 191.56 us | 552.69 us | 6,849.0 us |  1.46 |    0.14 | 343.7500 |   1,848 B | 2816.35 KB |        0.98 |
| BinaryDecoderMemoryStream             | .NET Framework 4.8 | 1024        | 4,781.3 us | 105.90 us | 298.69 us | 4,694.5 us |  1.00 |    0.00 | 460.9375 |     346 B | 2872.91 KB |        1.00 |
| BinaryDecoderArraySegmentStream       | .NET Framework 4.8 | 1024        | 5,476.0 us | 123.49 us | 350.32 us | 5,403.4 us |  1.15 |    0.11 | 460.9375 |     550 B | 2872.98 KB |        1.00 |
| BinaryDecoderArraySegmentStreamNoSpan | .NET Framework 4.8 | 1024        | 5,377.5 us | 104.08 us | 263.03 us | 5,341.9 us |  1.14 |    0.08 | 460.9375 |     550 B | 2872.98 KB |        1.00 |

Additional changes:

Related Issues

  • Fixes #

Types of changes

What types of changes does your code introduce?
Put an x in the boxes that apply. You can also fill these out after creating the PR.

  • Bugfix (non-breaking change which fixes an issue)
  • Enhancement (non-breaking change which adds functionality)
  • Test enhancement (non-breaking change to increase test coverage)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected, requires version increase of Nuget packages)
  • Documentation Update (if none of the other choices apply)

Checklist

Put an x in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code.

  • I have read the CONTRIBUTING doc.
  • I have signed the CLA.
  • I ran tests locally with my changes, all passed.
  • I fixed all failing tests in the CI pipelines.
  • I fixed all introduced issues with CodeQL and LGTM.
  • I have added tests that prove my fix is effective or that my feature works and increased code coverage.
  • I have added necessary documentation (if appropriate).
  • Any dependent changes have been merged and published in downstream modules.

Further comments

For references the JSON Encoder results for 1024 buffer size of the stream writer:

| JsonEncoderMemoryStream             | .NET 6.0           | 1024       | 1024        |  35.864 ms | 1.2359 ms |  3.6246 ms |  35.535 ms |  0.37 |    0.08 | 1500.0000 |   1,583 B |  937.5000 |  875.0000 | 18067.35 KB |        0.97 |
| JsonEncoderMemoryStream             | .NET 8.0           | 1024       | 1024        |  33.237 ms | 0.7876 ms |  2.2472 ms |  32.918 ms |  0.33 |    0.06 | 1687.5000 |   4,891 B |  968.7500 |  968.7500 | 18020.14 KB |        0.96 |
| JsonEncoderMemoryStream             | .NET Framework 4.8 | 1024       | 1024        | 101.484 ms | 6.8832 ms | 20.2951 ms |  96.902 ms |  1.00 |    0.00 | 1800.0000 |     877 B |  800.0000 |  800.0000 | 18683.35 KB |        1.00 |
|                                     |                    |            |             |            |           |            |            |       |         |           |           |           |           |             |             |
| JsonEncoderRecyclableMemoryStream   | .NET 6.0           | 1024       | 1024        |  29.173 ms | 1.1374 ms |  3.3359 ms |  28.254 ms |  0.34 |    0.05 | 1125.0000 |   3,487 B |   93.7500 |         - |  9430.07 KB |        0.94 |
| JsonEncoderRecyclableMemoryStream   | .NET 8.0           | 1024       | 1024        |  25.674 ms | 0.8479 ms |  2.4599 ms |  24.984 ms |  0.30 |    0.04 | 1125.0000 |   7,991 B |   93.7500 |         - |  9361.57 KB |        0.93 |
| JsonEncoderRecyclableMemoryStream   | .NET Framework 4.8 | 1024       | 1024        |  86.740 ms | 3.1096 ms |  9.1200 ms |  85.490 ms |  1.00 |    0.00 | 1500.0000 |   1,569 B |         - |         - | 10063.86 KB |        1.00 |
|                                     |                    |            |             |            |           |            |            |       |         |           |           |           |           |             |             |
| JsonEncoderArraySegmentStream       | .NET 6.0           | 1024       | 1024        |  30.560 ms | 1.1539 ms |  3.3107 ms |  29.916 ms |  0.34 |    0.06 | 1125.0000 |   2,671 B |  500.0000 |         - |  9440.85 KB |        0.92 |
| JsonEncoderArraySegmentStream       | .NET 8.0           | 1024       | 1024        |  28.017 ms | 1.4165 ms |  4.1766 ms |  26.816 ms |  0.32 |    0.06 |  937.5000 |   9,061 B |  125.0000 |         - |  7839.43 KB |        0.77 |
| JsonEncoderArraySegmentStream       | .NET Framework 4.8 | 1024       | 1024        |  89.666 ms | 3.7346 ms | 11.0116 ms |  88.298 ms |  1.00 |    0.00 | 1500.0000 |   1,742 B |  666.6667 |         - | 10208.53 KB |        1.00 |

| JsonEncoderArraySegmentStreamNoSpan | .NET 6.0           | 1024       | 1024        |  30.402 ms | 0.9505 ms |  2.7424 ms |  30.310 ms |  0.36 |    0.05 | 1125.0000 |   2,671 B |  500.0000 |         - |  9428.39 KB |        0.93 |
| JsonEncoderArraySegmentStreamNoSpan | .NET 8.0           | 1024       | 1024        |  33.638 ms | 2.5311 ms |  7.4631 ms |  33.054 ms |  0.40 |    0.10 |  500.0000 |   9,040 B |         - |         - |  7852.71 KB |        0.77 |
| JsonEncoderArraySegmentStreamNoSpan | .NET Framework 4.8 | 1024       | 1024        |  84.359 ms | 3.3380 ms |  9.6310 ms |  81.471 ms |  1.00 |    0.00 | 1571.4286 |   1,742 B |  714.2857 |         - | 10181.83 KB |        1.00 |

Copy link

codecov bot commented Mar 15, 2024

Codecov Report

Attention: Patch coverage is 85.40146% with 20 lines in your changes are missing coverage. Please review.

Project coverage is 54.80%. Comparing base (3f29a72) to head (45cebd6).
Report is 6 commits behind head on master.

Files Patch % Lines
...k/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs 85.18% 7 Missing and 5 partials ⚠️
Stack/Opc.Ua.Core/Stack/Bindings/BufferSegment.cs 50.00% 5 Missing ⚠️
Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs 85.00% 1 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2556      +/-   ##
==========================================
+ Coverage   54.55%   54.80%   +0.25%     
==========================================
  Files         335      341       +6     
  Lines       64749    65941    +1192     
  Branches    13292    13638     +346     
==========================================
+ Hits        35325    36142     +817     
- Misses      25594    25897     +303     
- Partials     3830     3902      +72     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@mregen mregen marked this pull request as ready for review March 15, 2024 19:18
@mregen
Copy link
Contributor Author

mregen commented Mar 18, 2024

will add a few benchmarks to understand the impact of the change

@mregen
Copy link
Contributor Author

mregen commented Mar 21, 2024

TODO: implement decoder benchmarks and GetSequence, to allow wider use of ArraySegmentStream class.

@mregen mregen marked this pull request as draft March 21, 2024 13:29
@mregen mregen added this to the March Update milestone Mar 25, 2024
Stack/Opc.Ua.Core/Stack/Bindings/BufferSegment.cs Outdated Show resolved Hide resolved
/// <summary>
/// A class to hold a sequence of buffers until disposed.
/// </summary>
public class BufferSequence : IDisposable
Copy link
Contributor

Choose a reason for hiding this comment

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

Make final classes sealed for extra perf opportunity for compiler.

}
m_sequence = ReadOnlySequence<byte>.Empty;
m_firstSegment = null;
GC.SuppressFinalize(this);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

you mean the whole dispose or just GC.Suppressfinalize? I want to have consistent behavior across the various MemoryStream implementations, so this returns the buffers if a user may have just called ToArray without transferring the buffers or if an exception occurs during the encoding, dispose will return the buffers.

Copy link
Contributor

Choose a reason for hiding this comment

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

I was just thinking the GC.SuppressFinalize.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

memory can be reclaimed earlier by the GC, thats my understanding.

@mregen mregen merged commit ebb55ad into master Mar 29, 2024
97 of 99 checks passed
@mregen mregen deleted the arraysegment branch March 29, 2024 10:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants