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

Optimize index bounds check for immutable sorted set and list #53266

Merged
merged 4 commits into from
Jul 9, 2021

Conversation

L2
Copy link
Contributor

@L2 L2 commented May 26, 2021

  • The bounds check to determine if a given index is >= 0 and < this.Count
    is only necessary on the first call to ItemRef.

  • The recursive steps within ItemRef do not need to continuously
    do this bounds check on these immutable data structures.

  • Proof:
    Elimination of index >= 0 bounds check:
    The first call to ItemRef checks if index >= 0.
    If we recurse on the left node, the index value does not change.
    If we recurse on the right node, index > _left._count. Then
    index - _left._count - 1 >= 0.

    Elimination of index < this.Count:
    The first call to ItemRef checks if index < this.Count. Then
    the given index must lie somewhere in this tree and
    (**) index < this.Count == left.Count + right.Count + 1.
    If we recurse on the left node, the index value does not change
    and a check is already made to determine that index < _left.Count.
    If we recurse on the right node, then we need to be sure that
    index - _left.count - 1 < _right.Count. But this is just a
    rearrangement of (**).

- The bounds check to determine if a given index is >= 0 and < this.Count
  is only necessary on the first call to ItemRef.
- The recursive steps within ItemRef do not need to continuously
  do this bounds check on these immutable data structures.
- Proof:
  Elimination of index >= 0 bounds check:
    The first call to ItemRef checks if index >= 0.
    If we recurse on the left node, the index value does not change.
    If we recurse on the right node, index > _left._count. Then
    index - _left._count - 1 >= 0.

  Elimination of index < this.Count:
    The first call to ItemRef checks if index < this.Count. Then
    the given index must lie somewhere in this tree and
    (**) index < this.Count == left.Count + right.Count + 1.
    If we recurse on the left node, the index value does not change
    and a check is already made to determine that index < _left.Count.
    If we recurse on the right node, then we need to be sure that
    index - _left.count - 1 < _right.Count. But this is just a
    rearrangement of (**).
@ghost
Copy link

ghost commented May 26, 2021

Tagging subscribers to this area: @eiriktsarpalis
See info in area-owners.md if you want to be subscribed.

Issue Details
  • The bounds check to determine if a given index is >= 0 and < this.Count
    is only necessary on the first call to ItemRef.

  • The recursive steps within ItemRef do not need to continuously
    do this bounds check on these immutable data structures.

  • Proof:
    Elimination of index >= 0 bounds check:
    The first call to ItemRef checks if index >= 0.
    If we recurse on the left node, the index value does not change.
    If we recurse on the right node, index > _left._count. Then
    index - _left._count - 1 >= 0.

    Elimination of index < this.Count:
    The first call to ItemRef checks if index < this.Count. Then
    the given index must lie somewhere in this tree and
    (**) index < this.Count == left.Count + right.Count + 1.
    If we recurse on the left node, the index value does not change
    and a check is already made to determine that index < _left.Count.
    If we recurse on the right node, then we need to be sure that
    index - _left.count - 1 < _right.Count. But this is just a
    rearrangement of (**).

Author: L2
Assignees: -
Labels:

area-System.Collections

Milestone: -

@stephentoub
Copy link
Member

Thanks. Can you share benchmark results showing that the extra code required here to avoid that range check actually makes a meaningful difference? (Note that range check itself could be optimized slightly as well, to Requires.Range((uint)index < (uint)this.Count, nameof(index));, which would further reduce the impact of avoiding it.)

@L2
Copy link
Contributor Author

L2 commented May 26, 2021

Sure @stephentoub

dotnet/performance diff with/without this change using --filter "*ImmutableSortedSet*" "*ImmutableList*"

summary:
better: 6, geomean: 1.202
total diff: 6

No Slower results for the provided threshold = 2% and noise filter = 25ns.

Faster base/diff Base Median (ns) Diff Median (ns) Modality
System.Collections.IterateFor<String>.ImmutableSortedSet(Size: 512) 1.40 6618.82 4735.50
System.Collections.IterateFor<Int32>.ImmutableSortedSet(Size: 512) 1.33 6613.09 4984.01
System.Collections.IterateFor<Int32>.ImmutableList(Size: 512) 1.27 6485.47 5117.11
System.Collections.IterateFor<String>.ImmutableList(Size: 512) 1.09 5579.58 5116.40
System.Collections.IterateForEach<Int32>.ImmutableSortedSet(Size: 512) 1.09 12384.04 11380.41
System.Collections.ContainsTrue<Int32>.ImmutableSortedSet(Size: 512) 1.08 15290.25 14142.94

@L2
Copy link
Contributor Author

L2 commented May 26, 2021

@stephentoub thanks, I did encounter some places with these Requires.Range checks that could benefit from the optimization you listed. Should I place those changes in another PR or keep it in this one?

@stephentoub
Copy link
Member

dotnet/performance diff with/without this change

Thanks for sharing numbers. I was surprised by the magnitude of the differences you're seeing, so I tried locally, and I'm not seeing results anything like that, e.g.

dotnet run -c Release -f net6.0 --filter "*ImmutableSortedSet*" "*ImmutableList*" --corerun d:\coreclrtest\main\corerun.exe d:\coreclrtest\pr\corerun.exe --join
Type Method Job Toolchain Size Mean Error StdDev Median Min Max Ratio RatioSD Gen 0 Gen 1 Gen 2 Allocated
ContainsTrueComparer ImmutableSortedSet Job-TGGIVO \main\corerun.exe 512 25.751 us 0.0948 us 0.0886 us 25.745 us 25.624 us 25.917 us 1.00 0.00 - - - -
ContainsTrueComparer ImmutableSortedSet Job-EZHMIY \pr\corerun.exe 512 25.579 us 0.0777 us 0.0727 us 25.586 us 25.435 us 25.706 us 0.99 0.00 - - - -
ContainsTrueComparer ImmutableSortedSet Job-TGGIVO \main\corerun.exe 512 249.051 us 3.7624 us 3.3353 us 247.656 us 246.321 us 255.871 us 1.00 0.00 - - - -
ContainsTrueComparer ImmutableSortedSet Job-EZHMIY \pr\corerun.exe 512 253.804 us 0.5012 us 0.4443 us 253.849 us 252.679 us 254.556 us 1.02 0.01 - - - -
IterateFor ImmutableList Job-TGGIVO \main\corerun.exe 512 12.231 us 0.0654 us 0.0612 us 12.229 us 12.134 us 12.352 us 1.00 0.00 - - - -
IterateFor ImmutableList Job-EZHMIY \pr\corerun.exe 512 10.500 us 0.0343 us 0.0286 us 10.499 us 10.434 us 10.552 us 0.86 0.00 - - - -
IterateFor ImmutableList Job-TGGIVO \main\corerun.exe 512 11.932 us 0.0684 us 0.0640 us 11.929 us 11.826 us 12.060 us 1.00 0.00 - - - -
IterateFor ImmutableList Job-EZHMIY \pr\corerun.exe 512 10.661 us 0.0537 us 0.0476 us 10.668 us 10.559 us 10.753 us 0.89 0.01 - - - -
IterateFor ImmutableSortedSet Job-TGGIVO \main\corerun.exe 512 12.075 us 0.0432 us 0.0404 us 12.067 us 12.009 us 12.159 us 1.00 0.00 - - - -
IterateFor ImmutableSortedSet Job-EZHMIY \pr\corerun.exe 512 10.912 us 0.0579 us 0.0541 us 10.909 us 10.831 us 11.017 us 0.90 0.00 - - - -
IterateFor ImmutableSortedSet Job-TGGIVO \main\corerun.exe 512 12.346 us 0.0657 us 0.0614 us 12.355 us 12.243 us 12.440 us 1.00 0.00 - - - -
IterateFor ImmutableSortedSet Job-EZHMIY \pr\corerun.exe 512 12.396 us 0.0393 us 0.0368 us 12.394 us 12.333 us 12.448 us 1.00 0.00 - - - -
ContainsFalse ImmutableList Job-TGGIVO \main\corerun.exe 512 1,138.721 us 10.7122 us 9.4961 us 1,140.382 us 1,126.251 us 1,155.342 us 1.00 0.00 - - - 1 B
ContainsFalse ImmutableList Job-EZHMIY \pr\corerun.exe 512 1,119.929 us 13.2908 us 12.4322 us 1,119.789 us 1,101.062 us 1,145.184 us 0.98 0.01 - - - 1 B
ContainsFalse ImmutableList Job-TGGIVO \main\corerun.exe 512 2,465.770 us 17.6445 us 15.6414 us 2,466.149 us 2,445.180 us 2,494.746 us 1.00 0.00 - - - 1 B
ContainsFalse ImmutableList Job-EZHMIY \pr\corerun.exe 512 2,563.138 us 13.3149 us 12.4548 us 2,564.662 us 2,543.398 us 2,588.892 us 1.04 0.01 - - - 1 B
ContainsTrue ImmutableList Job-TGGIVO \main\corerun.exe 512 578.459 us 11.2031 us 11.5048 us 572.648 us 568.086 us 606.604 us 1.00 0.00 - - - -
ContainsTrue ImmutableList Job-EZHMIY \pr\corerun.exe 512 537.503 us 10.3608 us 8.6518 us 534.372 us 529.038 us 559.094 us 0.93 0.02 - - - -
ContainsTrue ImmutableList Job-TGGIVO \main\corerun.exe 512 1,241.312 us 6.5191 us 5.4437 us 1,241.169 us 1,233.714 us 1,251.366 us 1.00 0.00 - - - 1 B
ContainsTrue ImmutableList Job-EZHMIY \pr\corerun.exe 512 1,246.525 us 12.9054 us 12.0717 us 1,243.906 us 1,233.100 us 1,269.617 us 1.00 0.01 - - - 1 B
ContainsFalse ImmutableSortedSet Job-TGGIVO \main\corerun.exe 512 30.266 us 0.0997 us 0.0884 us 30.243 us 30.170 us 30.499 us 1.00 0.00 - - - -
ContainsFalse ImmutableSortedSet Job-EZHMIY \pr\corerun.exe 512 30.336 us 0.0898 us 0.0749 us 30.328 us 30.198 us 30.494 us 1.00 0.00 - - - -
ContainsFalse ImmutableSortedSet Job-TGGIVO \main\corerun.exe 512 255.948 us 0.7200 us 0.6383 us 255.751 us 255.221 us 257.445 us 1.00 0.00 - - - -
ContainsFalse ImmutableSortedSet Job-EZHMIY \pr\corerun.exe 512 265.701 us 0.9717 us 0.7586 us 265.424 us 264.676 us 267.011 us 1.04 0.00 - - - -
ContainsTrue ImmutableSortedSet Job-TGGIVO \main\corerun.exe 512 27.005 us 0.2554 us 0.2389 us 26.969 us 26.734 us 27.488 us 1.00 0.00 - - - -
ContainsTrue ImmutableSortedSet Job-EZHMIY \pr\corerun.exe 512 27.067 us 0.0620 us 0.0580 us 27.051 us 26.998 us 27.177 us 1.00 0.01 - - - -
ContainsTrue ImmutableSortedSet Job-TGGIVO \main\corerun.exe 512 194.003 us 0.6218 us 0.5193 us 193.893 us 193.240 us 195.092 us 1.00 0.00 - - - -
ContainsTrue ImmutableSortedSet Job-EZHMIY \pr\corerun.exe 512 204.322 us 2.8966 us 2.7095 us 204.841 us 200.911 us 208.696 us 1.05 0.01 - - - -
CtorFromCollection ImmutableList Job-TGGIVO \main\corerun.exe 512 8.542 us 0.0679 us 0.0602 us 8.524 us 8.464 us 8.685 us 1.00 0.00 3.8999 0.4409 - 24,624 B
CtorFromCollection ImmutableList Job-EZHMIY \pr\corerun.exe 512 8.793 us 0.1887 us 0.2173 us 8.809 us 8.478 us 9.129 us 1.04 0.03 3.8913 0.4437 - 24,624 B
CtorFromCollection ImmutableList Job-TGGIVO \main\corerun.exe 512 11.678 us 0.0643 us 0.0601 us 11.667 us 11.562 us 11.788 us 1.00 0.00 3.8860 0.4164 - 24,624 B
CtorFromCollection ImmutableList Job-EZHMIY \pr\corerun.exe 512 11.707 us 0.1028 us 0.0961 us 11.673 us 11.590 us 11.930 us 1.00 0.01 3.8803 0.4157 - 24,624 B
CtorFromCollection ImmutableSortedSet Job-TGGIVO \main\corerun.exe 512 15.941 us 0.1666 us 0.1558 us 15.903 us 15.678 us 16.239 us 1.00 0.00 4.2170 0.5035 - 26,736 B
CtorFromCollection ImmutableSortedSet Job-EZHMIY \pr\corerun.exe 512 16.239 us 0.2711 us 0.2536 us 16.313 us 15.779 us 16.568 us 1.02 0.02 4.2142 0.5187 - 26,736 B
CtorFromCollection ImmutableSortedSet Job-TGGIVO \main\corerun.exe 512 282.278 us 0.7908 us 0.7010 us 282.259 us 281.298 us 283.706 us 1.00 0.00 4.4643 - - 28,784 B
CtorFromCollection ImmutableSortedSet Job-EZHMIY \pr\corerun.exe 512 286.381 us 0.7881 us 0.6987 us 286.041 us 285.556 us 287.849 us 1.01 0.00 4.5455 - - 28,784 B
IterateForEach ImmutableList Job-TGGIVO \main\corerun.exe 512 11.316 us 0.0252 us 0.0236 us 11.325 us 11.277 us 11.348 us 1.00 0.00 - - - -
IterateForEach ImmutableList Job-EZHMIY \pr\corerun.exe 512 12.338 us 0.0422 us 0.0374 us 12.330 us 12.262 us 12.412 us 1.09 0.00 - - - -
IterateForEach ImmutableList Job-TGGIVO \main\corerun.exe 512 24.239 us 0.2083 us 0.1846 us 24.242 us 24.000 us 24.628 us 1.00 0.00 - - - -
IterateForEach ImmutableList Job-EZHMIY \pr\corerun.exe 512 23.989 us 0.1145 us 0.0956 us 24.007 us 23.698 us 24.078 us 0.99 0.01 - - - -
IterateForEach ImmutableSortedSet Job-TGGIVO \main\corerun.exe 512 12.120 us 0.0687 us 0.0609 us 12.115 us 12.028 us 12.257 us 1.00 0.00 - - - -
IterateForEach ImmutableSortedSet Job-EZHMIY \pr\corerun.exe 512 11.638 us 0.0576 us 0.0511 us 11.641 us 11.533 us 11.718 us 0.96 0.01 - - - -
IterateForEach ImmutableSortedSet Job-TGGIVO \main\corerun.exe 512 23.186 us 0.0741 us 0.0693 us 23.194 us 23.048 us 23.308 us 1.00 0.00 - - - -
IterateForEach ImmutableSortedSet Job-EZHMIY \pr\corerun.exe 512 23.982 us 0.1367 us 0.1212 us 23.969 us 23.718 us 24.193 us 1.03 0.01 - - - -

Can you share more about how you're getting your numbers?

@stephentoub
Copy link
Member

The code changes look fine, but for the most part this doesn't appear to consistently move the needle, so I'm not sure it's worth the churn.

@L2
Copy link
Contributor Author

L2 commented May 28, 2021

Thanks @stephentoub , it looks like some recent commits have changed the perf numbers. Here's a breakdown of my investigation (Windows x64):

without my change + base commit = b443b8c (May 24, 2021) (The numbers I provided earlier were based on this)
with my change + base commit = b443b8c (May 24, 2021) (The numbers I provided earlier were based on this)
without my change + base commit = d43e886 (May 28, 2021)
with my change + base commit = d43e886 (May 28, 2021)

Today I cloned dotnet/runtime and created the four coreruns above and reran the microbenchmarks:

Here's an example of the commands I use:

py .\scripts\benchmarks_ci.py -c Release -f net6.0 --bdn-artifacts "C:\W052821\results\with_change" --corerun "C:\W052821\with_change\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe" --filter "*ImmutableSortedSet*" "*ImmutableList*"
py .\scripts\benchmarks_ci.py -c Release -f net6.0 --bdn-artifacts "C:\W052821\results\without_change" --corerun "C:\W052821\without_change\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\6.0.0\corerun.exe" --filter "*ImmutableSortedSet*" "*ImmutableList*"

then I diff the results using dotnet/performance's resultsComparer:

dotnet run --base "C:\W052821\results\without_change\" --diff "C:\W052821\results\with_change\"--threshold 2% --noise 25ns --full-id

Results:

without my change + base commit = b443b8c (May 24, 2021)
with my change + base commit = b443b8c (May 24, 2021) [DIFF]

summary:
better: 7, geomean: 1.191
worse: 1, geomean: 1.031
total diff: 8

Slower diff/base Base Median (ns) Diff Median (ns) Modality
System.Collections.IterateForEach.ImmutableSortedSet(Size: 512) 1.03 22179.70 22860.75
Faster base/diff Base Median (ns) Diff Median (ns) Modality
System.Collections.IterateFor.ImmutableSortedSet(Size: 512) 1.41 6615.04 4681.89
System.Collections.IterateFor.ImmutableList(Size: 512) 1.34 6480.18 4850.76
System.Collections.IterateFor.ImmutableSortedSet(Size: 512) 1.23 6629.84 5384.70
System.Collections.IterateFor.ImmutableList(Size: 512) 1.17 5595.22 4800.09
System.Collections.IterateForEach.ImmutableSortedSet(Size: 512) 1.09 12401.40 11399.09
System.Collections.ContainsTrue.ImmutableSortedSet(Size: 512) 1.09 15318.19 14107.30
System.Collections.ContainsTrueComparer.ImmutableSortedSet(Size: 512) 1.06 212987.67 200160.62

without my change + base commit = d43e886 (May 28, 2021)
with my change + base commit = d43e886 (May 28, 2021) [DIFF]

summary:
better: 6, geomean: 1.101
worse: 1, geomean: 1.083
total diff: 7

Slower diff/base Base Median (ns) Diff Median (ns) Modality
System.Collections.ContainsTrue.ImmutableSortedSet(Size: 512) 1.08 14120.79 15288.74 several?
Faster base/diff Base Median (ns) Diff Median (ns) Modality
System.Collections.IterateFor.ImmutableList(Size: 512) 1.25 6497.43 5215.18
System.Collections.IterateFor.ImmutableSortedSet(Size: 512) 1.15 5587.32 4839.56
System.Collections.IterateForEach.ImmutableSortedSet(Size: 512) 1.08 12242.48 11376.06
System.Collections.IterateFor.ImmutableList(Size: 512) 1.07 5592.33 5229.02 several?
System.Collections.CtorFromCollection.ImmutableSortedSet(Size: 512) 1.05 273723.27 261787.71
System.Collections.IterateForEach.ImmutableList(Size: 512) 1.03 22647.78 21959.21

Notes:
Rerunning on a fresh clone and build with base = b443b8c (May 24, 2021) with/without my change show the numbers being consistent on my machine with the numbers I provided earlier.

Building with the new base = d43e886 (May 28, 2021) seems to have changed some of the perf numbers around. Some faster and some slower than before:

without my change + base commit = b443b8c (May 24, 2021)
without my change + base commit = d43e886 (May 28, 2021) [DIFF]

summary:
better: 5, geomean: 1.138
worse: 3, geomean: 1.100
total diff: 8

Slower diff/base Base Median (ns) Diff Median (ns) Modality
System.Collections.IterateFor.ImmutableList(Size: 512) 1.16 5595.22 6497.43
System.Collections.CtorFromCollection.ImmutableSortedSet(Size: 512) 1.08 252765.37 273723.27
System.Collections.IterateForEach.ImmutableList(Size: 512) 1.06 21371.06 22647.78
Faster base/diff Base Median (ns) Diff Median (ns) Modality
System.Collections.IterateFor.ImmutableSortedSet(Size: 512) 1.21 6615.04 5447.31
System.Collections.IterateFor.ImmutableSortedSet(Size: 512) 1.19 6629.84 5587.32
System.Collections.IterateFor.ImmutableList(Size: 512) 1.16 6480.18 5592.33 several?
System.Collections.ContainsTrue.ImmutableSortedSet(Size: 512) 1.08 15318.19 14120.79 several?
System.Collections.ContainsTrueComparer.ImmutableSortedSet(Size: 512) 1.06 212987.67 201713.26

So this change still shows some decent results with the latest base commit = d43e886 (May 28, 2021), but you're right not nearly as good as the numbers on base commit base commit = b443b8c (May 24, 2021). I'll follow your recommendation.

Change from internal to private for unchecked methods.

Co-authored-by: Stephen Toub <stoub@microsoft.com>
@L2
Copy link
Contributor Author

L2 commented Jun 9, 2021

Latest .NET6 (commit ac87f00) before/after this change:

summary:
better: 5, geomean: 1.160
worse: 2, geomean: 1.053
total diff: 7

Slower diff/base Base Median (ns) Diff Median (ns) Modality
System.Collections.ContainsTrue.ImmutableSortedSet(Size: 512) 1.08 14111.28 15277.27
System.Collections.IterateForEach.ImmutableSortedSet(Size: 512) 1.02 22045.85 22582.00
Faster base/diff Base Median (ns) Diff Median (ns) Modality
System.Collections.IterateFor.ImmutableList(Size: 512) 1.24 6476.43 5206.49
System.Collections.IterateFor.ImmutableSortedSet(Size: 512) 1.23 6476.55 5246.25
System.Collections.IterateFor.ImmutableSortedSet(Size: 512) 1.16 5593.19 4840.51 several?
System.Collections.IterateFor.ImmutableList(Size: 512) 1.16 5595.04 4842.67
System.Collections.IterateForEach.ImmutableSortedSet(Size: 512) 1.02 11791.16 11525.10

Copy link
Member

@stephentoub stephentoub left a comment

Choose a reason for hiding this comment

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

Thanks!

@stephentoub stephentoub merged commit 8b42b7f into dotnet:main Jul 9, 2021
@L2 L2 deleted the immutableBounds branch July 14, 2021 20:24
@ghost ghost locked as resolved and limited conversation to collaborators Aug 13, 2021
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.

2 participants