Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Improve default handling in ReadOnlySequence.Slice #38431

Merged
merged 10 commits into from
Jun 28, 2019
Merged

Improve default handling in ReadOnlySequence.Slice #38431

merged 10 commits into from
Jun 28, 2019

Conversation

pgovind
Copy link

@pgovind pgovind commented Jun 10, 2019

Fixes https://github.com/dotnet/corefx/issues/35254

The null dereference was because we now allow nullable objects in SequencePosition. Once I fixed that, there's still the issue of what default(SequencePosition)._object should be. If the expectation is that the default start position is 0, it makes sense that default(SequencePosition)._object = Start?

Also, this is my first patch in corefx, so I'd appreciate comments on the new test and its location. Also, this seems like a small change, but how do folks generally measure the performance impacts of new changes?

@@ -455,7 +455,7 @@ public ReadOnlySequence<T> Slice(SequencePosition start, SequencePosition end)
public ReadOnlySequence<T> Slice(SequencePosition start)
{
BoundsCheck(start);
return SliceImpl(start);
return SliceImpl(start.Equals(default) ? Start : start);
Copy link
Member

@ahsonkhan ahsonkhan Jun 11, 2019

Choose a reason for hiding this comment

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

As far as I recall, this code is quite perf-sensitive. Are we OK with introducing branches with checks like these?

Maybe start.GetObject() == null instead?

It's probably not worth it, but consider even calling a SliceImpl helper that takes object/int (and passing in _startObject, _startInteger rather than creating a SequencePosition to pass in which gets deconstructed anyway).

cc @davidfowl, @pakrym

@@ -355,7 +355,7 @@ private void BoundsCheck(in SequencePosition position)
// Storing this in a local since it is used twice within InRange()
ulong startRange = (ulong)(((ReadOnlySequenceSegment<T>)startObject!).RunningIndex + startIndex);
if (!InRange(
(ulong)(((ReadOnlySequenceSegment<T>)position.GetObject()!).RunningIndex + sliceStartIndex),
(ulong)((position.GetObject() != null ? ((ReadOnlySequenceSegment<T>)position.GetObject()!).RunningIndex : 0) + sliceStartIndex),
Copy link
Member

Choose a reason for hiding this comment

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

This seems counter to the annotated bang (!) on position.GetObject(). @safern - what should be the correct annotation here? Should we remove the !?

nit: We may want to extract this out in a local to help with readability.

Copy link
Member

Choose a reason for hiding this comment

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

The compiler will not do deep data flow analysis here to track that you already checked for null on the call of GetObject() and that you're calling it again when it is not null. So unfortunately the ! is still needed here. I just validated that: https://sharplab.io/#v2:EYLgtghgzgLgpgJwD4AEBMBGAsAKFwYgDsBXAG1ImFLgAI5DLrcUBmG9GgYRoG9caB7NgEtCMGgFkAFABEaABwD2UYTGGLCASl79BASBQB2BctXrCAOgDicGAHlgAKzgBjGFO0BCALw0S5GgB+GikpAC1NJRU1DWtbB2c3D00LAElCABM4AA8aEBoABgBuXQEAX1wKvBxWdjQaOT4cPVKaRSdXGGCAfXbE8V9/UhKcQRpW2r7O4Jt7DqTtbwA+Gl75mBGq5jYOMJ1m2tFxdKzcnhoAc1simihrmjKaXwxNoA

If perf is not affected and adding an extra local doesn't hurt, that would be the only way to remove the !, but if that hurts the produced IL we should leave as is.

cc: @cston I guess this behavior is by design and not something the compiler can infer?

Copy link
Member

Choose a reason for hiding this comment

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

@safern, yes, this is by design - the compiler tracks the nullability of locals, parameters, and fields and properties of those only.

@ahsonkhan
Copy link
Member

Also, this is my first patch in corefx, so I'd appreciate comments on the new test and its location. Also, this seems like a small change, but how do folks generally measure the performance impacts of new changes?

The test location seems fine to me.

For perf testing, I would start by looking at the existing perf tests which are measuring just the API call specifically (do a before/after comparison). If there are some tests missing, please add them:
https://github.com/dotnet/performance/blob/master/src/benchmarks/micro/corefx/System.Buffers/ReadOnlySequenceTests.cs

For more general, end-to-end testing and impact, you would have to take a look at where such APIs are used most heavily, which in this case is in Kestrel Web Server (this is probably not necessary for such a change). For instance:
https://github.com/aspnet/AspNetCore/blob/a3c8bd16f7c0eaa473364460e0354b2b0cc7fdad/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs#L54
https://github.com/aspnet/AspNetCore/blob/04bf1bf32e13d35d4d7f61f9cbeb9bfa22e0c617/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs#L47

cc @jkotalik, who might also be able to help with that (if needed)

@pgovind
Copy link
Author

pgovind commented Jun 12, 2019

Edit: Patch is complete now. Can be reviewed

Just adding a note here: the patch is not complete yet. There’s bugs in the other Slice APIs that I’m fixing. I’ll have a complete patch up later tonight or tomorrow morning

@pgovind pgovind force-pushed the ros_slice branch 2 times, most recently from ba77d2d to 0a9071b Compare June 14, 2019 19:48
Copy link
Member

@ahsonkhan ahsonkhan left a comment

Choose a reason for hiding this comment

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

Otherwise, LGTM (pending some perf numbers, even from micro-benchmarks)

@@ -455,7 +455,7 @@ public ReadOnlySequence<T> Slice(SequencePosition start, SequencePosition end)
public ReadOnlySequence<T> Slice(SequencePosition start)
{
BoundsCheck(start);
return SliceImpl(start);
return SliceImpl(start.GetObject() == null ? Start : start);
Copy link
Member

Choose a reason for hiding this comment

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

It looks like we are already checking whether start.GetObject() is null in BoundsCheck:

long runningIndex = ((ReadOnlySequenceSegment<T>?)position.GetObject())?.RunningIndex ?? 0;

But there isn't a great way to avoid that (and only occurs in one of the branches).

Maybe, store the result of start.Object == null in a bool and pass that in to BoundsCheck. Then runningIndex can be 0 unless that bool is true (in which case we can get rid of the null checks there). Something to consider, if it can reduce the instruction size of BoundsCheck and Slice.

long runningIndex = 0;
if (positionIsNotNull)
{
   Debug.Assert(position.GetObject() != null);
   runningIndex = ((ReadOnlySequenceSegment<Byte>)position.GetObject()).RunningIndex;
}

sharplab.io

Copy link
Member

Choose a reason for hiding this comment

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

Store start.GetObject() != null in a local and re-use in the SliceImpl call

Copy link
Author

Choose a reason for hiding this comment

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

Right. In the works :)

@@ -122,6 +122,39 @@ public void Default_SliceNegativeLength()
Assert.Throws<ArgumentOutOfRangeException>("length", () => buffer.Slice(buffer.Start, -1L));
}

[Fact]
Copy link
Member

Choose a reason for hiding this comment

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

Can you share the code coverage numbers. Let's make sure all the new branches are covered (and if not, add more tests).

Copy link
Author

@pgovind pgovind Jun 24, 2019

Choose a reason for hiding this comment

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

How do people generally share this? Can I just upload the result of the code coverage run here? In any case, this patch adds 1 new branch and code coverage is telling me that both branches are covered by the tests.

CodeCoverage

Copy link
Member

@ahsonkhan ahsonkhan Jun 25, 2019

Choose a reason for hiding this comment

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

Yep, uploading the zip containing the code coverage works.

Looks like we have other branches too. Like in Slice(long start, SequencePosition end) and also the new if (positionIsNotNull) branch in BoundsCheck

Copy link
Author

Choose a reason for hiding this comment

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

Welp, it looks like github doesn't like zips

Copy link
Author

Choose a reason for hiding this comment

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

"\scratch2\scratch\prgovi\ROS_Slice_CodeCoverage.zip"

@@ -451,34 +454,25 @@ private SequenceType GetSequenceType()
private static int GetIndex(int Integer) => Integer & ReadOnlySequence.IndexBitMask;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ReadOnlySequence<T> SliceImpl(in SequencePosition start, in SequencePosition end)
private ReadOnlySequence<T> SliceImpl(in object? startObject, in int startIndex, in object? endObject, in int endIndex)
Copy link
Member

Choose a reason for hiding this comment

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

From a quick view of the disassembly (in a similar sample), this pattern seems to help reduce instruction count. So looks good.

sharplab.io

Copy link
Member

Choose a reason for hiding this comment

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

From a quick view of the disassembly (in a similar sample), this pattern seems to help reduce instruction count.

On Linux, too?

Perf test?

Copy link
Member

Choose a reason for hiding this comment

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

@pgovind was going to do some perf validation.

Copy link
Author

@pgovind pgovind Jun 25, 2019

Choose a reason for hiding this comment

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

Right, I thought this would've performed better too, but it does not and I don't have an explanation for why yet. It seems that calling SliceImpl(new SequencePosition(someObject, someIndex), someLength) is somehow faster than calling SliceImpl(someObject, someIndex, endObject, endLength) 🤷‍♂️.

Edit: I modified the patch to include just the bug fix and no refactoring.

Copy link
Member

Choose a reason for hiding this comment

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

On Linux, too?

@stephentoub, why do you think the behavior would be different on Linux?

Copy link
Member

Choose a reason for hiding this comment

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

why do you think the behavior would be different on Linux?

Different calling convention?

Prashanth Govindarajan added 6 commits June 25, 2019 16:38
summary:
better: 1, geomean: 1.203
worse: 1, geomean: 1.121
total diff: 2

| Slower                                                                                                                               | diff/base | Base Median (ns) | Diff Median (ns) | Modality|
| ----------------------------------------------------------------------------- | ---------:| ----------------:| ----------------:| --------:|
| MicroBenchmarks.corefx.System.Memory.ReadOnlySequenceBenchmarks.StartPosition |      1.12 |             9.69 |            10.87 |         |

| Faster                                                                                                                                      | base/diff | Base Median (ns) | Diff Median (ns) | Modality|
| -------------------------------------------------------------------------------- | ---------:| ----------------:| ----------------:| --------:|
| MicroBenchmarks.corefx.System.Memory.ReadOnlySequenceBenchmarks.StartPosition_An |      1.20 |             5.74 |             4.77 |         |
@pgovind
Copy link
Author

pgovind commented Jun 26, 2019

Updated with the latest numbers:

I setup some benchmarks here:

dotnet/performance#589

The results are as follows:
C:\Users\prgovi\Desktop\Work\performance\src\tools\ResultsComparer>dotnet run --base "C:\Users\prgovi\Desktop\Work\Benchmarks\Before" --diff "C:\Users\prgovi\Desktop\Work\Benchmarks\After" --threshold 2%
summary:
better: 5, geomean: 1.057
worse: 3, geomean: 1.152
total diff: 8

Slower diff/base Base Median (ns) Diff Median (ns) Modality
MicroBenchmarks.corefx.ReadOnlySequenceBenchmarks.MS_StartPosition 1.24 8.44 10.47
MicroBenchmarks.corefx.ReadOnlySequenceBenchmarks.StartPosition 1.17 4.97 5.79
MicroBenchmarks.corefx.ReadOnlySequenceBenchmarks.MS_StartPosition_And_EndPositi 1.06 11.02 11.65
Faster base/diff Base Median (ns) Diff Median (ns) Modality
MicroBenchmarks.corefx.ReadOnlySequenceBenchmarks.RepeatSlice_StartPosition_And_ 1.08 24.39 22.56
MicroBenchmarks.corefx.ReadOnlySequenceBenchmarks.Start_And_EndPosition 1.07 6.55 6.10
MicroBenchmarks.corefx.ReadOnlySequenceBenchmarks.StartPosition_And_EndPosition 1.07 5.99 5.61
MicroBenchmarks.corefx.ReadOnlySequenceBenchmarks.RepeatSlice 1.03 66.89 64.63
MicroBenchmarks.corefx.ReadOnlySequenceBenchmarks.MS_RepeatSlice 1.03 81.18 79.00

@@ -329,7 +329,7 @@ public ReadOnlySequence<T> Slice(long start, SequencePosition end)
FoundInFirstSegment:
// startIndex + start <= int.MaxValue
Debug.Assert(start <= int.MaxValue - startIndex);
return SliceImpl(new SequencePosition(startObject, (int)startIndex + (int)start), end);
return SliceImpl(new SequencePosition(startObject, (int)startIndex + (int)start), new SequencePosition(sliceEndObject, (int)sliceEndIndex));
Copy link
Member

Choose a reason for hiding this comment

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

Aren't we potentially mismatching the object and the index here? sliceEndObject could be pointing to _startObject, but the sliceEndIndex is still the index from end

Copy link
Author

Choose a reason for hiding this comment

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

Nope. When sliceEndObject is null(happens only when default is passed in), sliceEndIndex will be 0.

Copy link
Member

Choose a reason for hiding this comment

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

As discussed offline, this issue needs to be resolved.

src/System.Memory/src/System/Buffers/ReadOnlySequence.cs Outdated Show resolved Hide resolved
@pgovind
Copy link
Author

pgovind commented Jun 26, 2019

Assuming the CI finishes before snapping, can I merge this patch? Are we ok with the perf numbers?

@ahsonkhan ahsonkhan added the auto-merge Automatically merge PR once CI passes. label Jun 26, 2019
@ghost
Copy link

ghost commented Jun 26, 2019

Hello @ahsonkhan!

Because this pull request has the auto-merge label, I will be glad to assist with helping to merge this pull request once all check-in policies pass.

p.s. you can customize the way I help with merging this pull request, such as holding this pull request until a specific person approves. Simply @mention me (@msftbot) and give me an instruction to get started! Learn more here.

@ahsonkhan ahsonkhan removed the auto-merge Automatically merge PR once CI passes. label Jun 26, 2019
Assert.Equal(0, slicedSequence.Length);

// Slice(x, default) returns empty if x = 0. Otherwise throws
sequence = sequence.Slice(2);
Copy link
Author

Choose a reason for hiding this comment

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

This should cover both the Slice(int, SequencePosition) and the Slice(SequencePosition, int) cases

Copy link
Member

Choose a reason for hiding this comment

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

How so? I would feel more confident if we had separate test cases for both overloads, explicitly.

Copy link
Member

Choose a reason for hiding this comment

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

We discussed this offline, and it makes sense to me now.

@pgovind pgovind merged commit 5f6cba8 into master Jun 28, 2019
@pgovind pgovind deleted the ros_slice branch June 28, 2019 16:42
@davidfowl
Copy link
Member

We'll look out for regressions.

picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
* Improve default handling in ReadOnlySequence.Slice


Commit migrated from dotnet/corefx@5f6cba8
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Passing default SequencePosition to ReadOnlySequence.Slice shouldn't throw NullRef
6 participants