Skip to content

Double IndexOf throughput for chars - #78861

Merged
MihaZupan merged 4 commits into
dotnet:mainfrom
MihaZupan:packed-indexof-char
Jan 3, 2023
Merged

Double IndexOf throughput for chars#78861
MihaZupan merged 4 commits into
dotnet:mainfrom
MihaZupan:packed-indexof-char

Conversation

@MihaZupan

@MihaZupanMihaZupan commented Nov 26, 2022

Copy link
Copy Markdown
Member

When searching through strings, it's very common to have single-byte values (think ASCII).
As long as the value falls within an appropriate range ([1, 254] on X86 or [0, 254] on ARM), we can speed up the search by packing two input vectors together before comparing the value.

The IndexOfAnyAsciiSearcher implementation I added in #78093 is already using this trick, but it applies to regular IndexOf as well.

In this PR, I added implementations that do such packing for Contains(char), IndexOf(char), IndexOfAny(char, char), IndexOfAny(char, char, char), and IndexOfAnyInRange(char, char), roughly doubling the throughput for long inputs.

Do we want to do the same for the Last- variants as well?
I don't think specialized IndexOf(4/5 values) would be useful. For ASCII values, using IndexOfAnyValues is already very close in throughput (and things like Regex will use that).

Benchmark numbers
MethodToolchainLengthMeanRatio
IndexOfmain11.973 ns1.00
IndexOfpr11.751 ns0.89
IndexOfAny2Valuesmain12.757 ns1.00
IndexOfAny2Valuespr12.593 ns0.94
IndexOfAnyInRangemain12.092 ns1.00
IndexOfAnyInRangepr11.818 ns0.87
IndexOfmain73.622 ns1.00
IndexOfpr73.741 ns1.03
IndexOfAny2Valuesmain75.889 ns1.00
IndexOfAny2Valuespr75.804 ns0.99
IndexOfAnyInRangemain75.102 ns1.00
IndexOfAnyInRangepr73.638 ns0.71
IndexOfmain82.604 ns1.00
IndexOfpr82.295 ns0.88
IndexOfAny2Valuesmain82.588 ns1.00
IndexOfAny2Valuespr82.790 ns1.08
IndexOfAnyInRangemain82.929 ns1.00
IndexOfAnyInRangepr82.369 ns0.81
IndexOfmain92.855 ns1.00
IndexOfpr92.279 ns0.80
IndexOfAny2Valuesmain92.849 ns1.00
IndexOfAny2Valuespr92.774 ns0.97
IndexOfAnyInRangemain92.914 ns1.00
IndexOfAnyInRangepr92.373 ns0.81
IndexOfmain152.836 ns1.00
IndexOfpr152.315 ns0.82
IndexOfAny2Valuesmain152.788 ns1.00
IndexOfAny2Valuespr152.799 ns1.00
IndexOfAnyInRangemain152.928 ns1.00
IndexOfAnyInRangepr152.361 ns0.81
IndexOfmain162.402 ns1.00
IndexOfpr162.273 ns0.95
IndexOfAny2Valuesmain162.830 ns1.00
IndexOfAny2Valuespr162.791 ns0.99
IndexOfAnyInRangemain162.871 ns1.00
IndexOfAnyInRangepr162.378 ns0.83
IndexOfmain172.740 ns1.00
IndexOfpr172.574 ns0.94
IndexOfAny2Valuesmain173.376 ns1.00
IndexOfAny2Valuespr172.967 ns0.88
IndexOfAnyInRangemain173.040 ns1.00
IndexOfAnyInRangepr172.312 ns0.76
IndexOfmain322.826 ns1.00
IndexOfpr322.558 ns0.91
IndexOfAny2Valuesmain323.293 ns1.00
IndexOfAny2Valuespr322.971 ns0.90
IndexOfAnyInRangemain322.903 ns1.00
IndexOfAnyInRangepr322.312 ns0.80
IndexOfmain100039.015 ns1.00
IndexOfpr100023.991 ns0.61
IndexOfAny2Valuesmain100045.199 ns1.00
IndexOfAny2Valuespr100025.563 ns0.57
IndexOfAnyInRangemain100055.958 ns1.00
IndexOfAnyInRangepr100023.020 ns0.41
IndexOfmain1000004,875.359 ns1.00
IndexOfpr1000002,190.711 ns0.45
IndexOfAny2Valuesmain1000005,709.625 ns1.00
IndexOfAny2Valuespr1000002,980.139 ns0.52
IndexOfAnyInRangemain1000005,133.861 ns1.00
IndexOfAnyInRangepr1000002,987.320 ns0.58
MethodToolchainLengthMeanErrorRatio
IndexOfIgnoreCasemain15.807 ns0.0755 ns1.00
IndexOfIgnoreCasepr15.766 ns0.0167 ns0.99
IndexOfIgnoreCasemain328.565 ns0.0465 ns1.00
IndexOfIgnoreCasepr328.471 ns0.0267 ns0.99
IndexOfIgnoreCasemain100051.199 ns0.1238 ns1.00
IndexOfIgnoreCasepr100034.857 ns0.3394 ns0.68
IndexOfIgnoreCasemain1000005,734.450 ns22.3808 ns1.00
IndexOfIgnoreCasepr1000002,979.708 ns12.2477 ns0.52

This is generally a slight regression if a match is found at the start

If the first character matches
MethodToolchainLengthMeanErrorRatio
IndexOfmain11.745 ns0.0005 ns1.00
IndexOfpr11.985 ns0.0026 ns1.14
IndexOfAny2Valuesmain12.245 ns0.0015 ns1.00
IndexOfAny2Valuespr12.182 ns0.0021 ns0.97
IndexOfAnyInRangemain12.079 ns0.0010 ns1.00
IndexOfAnyInRangepr11.720 ns0.0005 ns0.83
IndexOfmain71.526 ns0.0005 ns1.00
IndexOfpr71.510 ns0.0006 ns0.99
IndexOfAny2Valuesmain71.743 ns0.0009 ns1.00
IndexOfAny2Valuespr71.695 ns0.0023 ns0.97
IndexOfAnyInRangemain72.100 ns0.0010 ns1.00
IndexOfAnyInRangepr71.751 ns0.0043 ns0.83
IndexOfmain82.540 ns0.0291 ns1.00
IndexOfpr82.978 ns0.0365 ns1.18
IndexOfAny2Valuesmain83.021 ns0.0344 ns1.00
IndexOfAny2Valuespr83.319 ns0.0072 ns1.10
IndexOfAnyInRangemain82.943 ns0.0375 ns1.00
IndexOfAnyInRangepr82.901 ns0.0335 ns0.99
IndexOfmain162.934 ns0.0021 ns1.00
IndexOfpr162.791 ns0.0023 ns0.95
IndexOfAny2Valuesmain162.881 ns0.0028 ns1.00
IndexOfAny2Valuespr163.246 ns0.0037 ns1.13
IndexOfAnyInRangemain162.657 ns0.0029 ns1.00
IndexOfAnyInRangepr162.725 ns0.0023 ns1.03
IndexOfmain172.946 ns0.0016 ns1.00
IndexOfpr173.307 ns0.0018 ns1.12
IndexOfAny2Valuesmain172.877 ns0.0016 ns1.00
IndexOfAny2Valuespr173.496 ns0.0064 ns1.22
IndexOfAnyInRangemain172.654 ns0.0017 ns1.00
IndexOfAnyInRangepr173.057 ns0.0018 ns1.15
IndexOfmain322.949 ns0.0027 ns1.00
IndexOfpr323.360 ns0.0136 ns1.14
IndexOfAny2Valuesmain322.914 ns0.0057 ns1.00
IndexOfAny2Valuespr323.502 ns0.0073 ns1.20
IndexOfAnyInRangemain322.658 ns0.0021 ns1.00
IndexOfAnyInRangepr323.042 ns0.0012 ns1.14
IndexOfmain332.968 ns0.0033 ns1.00
IndexOfpr332.745 ns0.0059 ns0.92
IndexOfAny2Valuesmain332.900 ns0.0042 ns1.00
IndexOfAny2Valuespr333.260 ns0.0079 ns1.12
IndexOfAnyInRangemain332.684 ns0.0036 ns1.00
IndexOfAnyInRangepr332.871 ns0.0106 ns1.07
IndexOfmain10003.068 ns0.0239 ns1.00
IndexOfpr10002.903 ns0.0343 ns0.95
IndexOfAny2Valuesmain10003.021 ns0.0258 ns1.00
IndexOfAny2Valuespr10003.382 ns0.0335 ns1.12
IndexOfAnyInRangemain10002.784 ns0.0259 ns1.00
IndexOfAnyInRangepr10002.981 ns0.0312 ns1.07

@MihaZupanMihaZupan added this to the 8.0.0 milestone Nov 26, 2022
@MihaZupanMihaZupan self-assigned this Nov 26, 2022
@ghost

Copy link
Copy Markdown

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

Issue Details

When searching through strings, it's very common to have single-byte values (think ASCII).
As long as the value falls within an appropriate range ([1, 254] on X86 or [0, 254] on ARM), we can speed up the search by packing two input vectors together before comparing the value.

The IndexOfAnyAsciiSearcher implementation I added in #78093 is already using this trick, but it applies to regular IndexOf as well.

In this POC PR, I added implementations that do such packing for IndexOf(char), IndexOfAny(char, char), and IndexOfAnyInRange(char, char), roughly doubling the throughput for long inputs.

If we're happy with the direction, I can add implementations for Contains(char), IndexOfAny(3/5 values), and their Last- counterparts as well if we feel they are useful.

Benchmark numbers
MethodToolchainLengthMeanRatio
IndexOfmain11.973 ns1.00
IndexOfpr11.751 ns0.89
IndexOfAny2Valuesmain12.757 ns1.00
IndexOfAny2Valuespr12.593 ns0.94
IndexOfAnyInRangemain12.092 ns1.00
IndexOfAnyInRangepr11.818 ns0.87
IndexOfmain73.622 ns1.00
IndexOfpr73.741 ns1.03
IndexOfAny2Valuesmain75.889 ns1.00
IndexOfAny2Valuespr75.804 ns0.99
IndexOfAnyInRangemain75.102 ns1.00
IndexOfAnyInRangepr73.638 ns0.71
IndexOfmain82.604 ns1.00
IndexOfpr82.295 ns0.88
IndexOfAny2Valuesmain82.588 ns1.00
IndexOfAny2Valuespr82.790 ns1.08
IndexOfAnyInRangemain82.929 ns1.00
IndexOfAnyInRangepr82.369 ns0.81
IndexOfmain92.855 ns1.00
IndexOfpr92.279 ns0.80
IndexOfAny2Valuesmain92.849 ns1.00
IndexOfAny2Valuespr92.774 ns0.97
IndexOfAnyInRangemain92.914 ns1.00
IndexOfAnyInRangepr92.373 ns0.81
IndexOfmain152.836 ns1.00
IndexOfpr152.315 ns0.82
IndexOfAny2Valuesmain152.788 ns1.00
IndexOfAny2Valuespr152.799 ns1.00
IndexOfAnyInRangemain152.928 ns1.00
IndexOfAnyInRangepr152.361 ns0.81
IndexOfmain162.402 ns1.00
IndexOfpr162.273 ns0.95
IndexOfAny2Valuesmain162.830 ns1.00
IndexOfAny2Valuespr162.791 ns0.99
IndexOfAnyInRangemain162.871 ns1.00
IndexOfAnyInRangepr162.378 ns0.83
IndexOfmain172.740 ns1.00
IndexOfpr172.574 ns0.94
IndexOfAny2Valuesmain173.376 ns1.00
IndexOfAny2Valuespr172.967 ns0.88
IndexOfAnyInRangemain173.040 ns1.00
IndexOfAnyInRangepr172.312 ns0.76
IndexOfmain322.826 ns1.00
IndexOfpr322.558 ns0.91
IndexOfAny2Valuesmain323.293 ns1.00
IndexOfAny2Valuespr322.971 ns0.90
IndexOfAnyInRangemain322.903 ns1.00
IndexOfAnyInRangepr322.312 ns0.80
IndexOfmain100039.015 ns1.00
IndexOfpr100023.991 ns0.61
IndexOfAny2Valuesmain100045.199 ns1.00
IndexOfAny2Valuespr100025.563 ns0.57
IndexOfAnyInRangemain100055.958 ns1.00
IndexOfAnyInRangepr100023.020 ns0.41
IndexOfmain1000004,875.359 ns1.00
IndexOfpr1000002,190.711 ns0.45
IndexOfAny2Valuesmain1000005,709.625 ns1.00
IndexOfAny2Valuespr1000002,980.139 ns0.52
IndexOfAnyInRangemain1000005,133.861 ns1.00
IndexOfAnyInRangepr1000002,987.320 ns0.58
MethodToolchainLengthMeanErrorRatio
IndexOfIgnoreCasemain15.807 ns0.0755 ns1.00
IndexOfIgnoreCasepr15.766 ns0.0167 ns0.99
IndexOfIgnoreCasemain328.565 ns0.0465 ns1.00
IndexOfIgnoreCasepr328.471 ns0.0267 ns0.99
IndexOfIgnoreCasemain100051.199 ns0.1238 ns1.00
IndexOfIgnoreCasepr100034.857 ns0.3394 ns0.68
IndexOfIgnoreCasemain1000005,734.450 ns22.3808 ns1.00
IndexOfIgnoreCasepr1000002,979.708 ns12.2477 ns0.52
Author:MihaZupan
Assignees:MihaZupan
Labels:

area-System.Memory, tenet-performance

Milestone:8.0.0

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

cc: @EgorBo@stephentoub

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

Any thoughts on this approach @dotnet/area-system-memory?

@dakersnar

Copy link
Copy Markdown
Contributor

@MihaZupan Sorry for the delay, I'll take a look at this early next week.

@dakersnar

Copy link
Copy Markdown
Contributor

@MihaZupan I'm new to this area and I think I'm missing some context to properly review this.

Can you give me a high-level summary of the intentions behind the changes in each file?

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

Sure. The main idea behind this change is the observation that the char values we commonly search for are ASCII, in which case half of their UTF16 representation will always be 0. When doing vectorized searches, this means we're mostly ignoring half of the input and half of the result of each comparison.
If we instead pack the input (narrow with saturation) before the comparison, we can process twice as many characters in each loop iteration. Such optimization is only possible for values that aren't ambiguous after saturation ([0, 254]).

  • The core change is the introduction of the new SpanHelpers.Char.Packed.cs file that contains the PackedIndexOf workhorse implementation which mimics the existing IndexOf helpers, but uses the approach of packing the input. The file contains:
    • An internal CanUsePackedIndexOf helper method that determines whether the algorithm can be used for a given value.
    • The search methods themselves - PackedIndexOf, PackedIndexOfAny, etc.
  • The changes in SpanHelpers.T.cs are hooking into the existing IndexOf codepaths to delegate to the PackedIndexOf implementation if it's supported for the given value. If not, they fallback to the existing ("NonPacked") implementation.
  • Changes to Globalization/Ordinal.cs and String.Searching.cs are updating the callers where we know the value to be ASCII to take advantage of the new packed implementation directly, without incurring the cost of checking whether the value is ASCII again.
  • The CanUsePackedIndexOf helper I mentioned is intended to be "free" if the value is constant (common case). If the value isn't constant, the span.IndexOf path now incurs an additional check before calling into the appropriate implementation. Updates to files in /System/IndexOfAnyValues/* are avoiding this overhead for IndexOfAnyValues<char> implementations. They're not really the interesting part of the change.

@dakersnar

Copy link
Copy Markdown
Contributor

If we instead pack the input (narrow with saturation) before the comparison, we can process twice as many characters in each loop iteration. Such optimization is only possible for values that aren't ambiguous after saturation ([0, 254]).

To confirm, this is because any value that needs the full char to be represented will saturate to 255 when narrowed to a byte, correct?

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

To confirm, this is because any value that needs the full char to be represented will saturate to 255 when narrowed to a byte, correct?

That's right.
In reality, the ranges are [0, 254] for ARM and [1, 254] for X86 because X86 only has signed pack instructions, and values will saturate to both 0 and 255.

@dakersnardakersnar 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. Left a few clarifying questions.

For testing, I assume all these paths already had sufficient coverage, right?

public static unsafe int IndexOfAnyExcept<T>(this ReadOnlySpan<T> span, T value) where T : IEquatable<T>?
{
if (SpanHelpers.CanVectorizeAndBenefit<T>(span.Length))
if (RuntimeHelpers.IsBitwiseEquatable<T>())

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.

Can you explain this update?

@MihaZupanMihaZupanDec 16, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

@adamsitnik I see you added this in #73768, can you please clarify what the intent was?
As far as I can tell, this is just adding a redundant length check given that all the SpanHelpers implementations we're calling into also do the length check and have code for handling short inputs. Am I missing something (I didn't see any discussion about this on your PR)?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@MihaZupan, @adamsitnik, was this ever answered?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I don't believe so. Is the changed version causing issues somewhere?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No, but it sounded like there might be extra work happening unnecessarily.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I think that was the case before this change (we would inline a length check and 2 calls to the worker methods), now it should just be a call to the 1 worker method.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I see, I misunderstood the comment

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

For testing, I assume all these paths already had sufficient coverage, right?

Yes. I'll double-check that we don't accidentally end up losing substantial coverage of the existing (NonPacked) code paths if we're mostly testing with ASCII values.

In this POC PR, I added implementations that do such packing for IndexOf(char), IndexOfAny(char, char), and IndexOfAnyInRange(char, char)
If we're happy with the direction, I can add implementations for Contains(char), IndexOfAny(char, char, char), and their Last- counterparts as well if we feel they are useful.

I'll add the Contains(char) and IndexOfAny(char, char, char) to this PR (it's just more of the same idea).
Not sure how much we care about the Last- variants.

@MihaZupan

MihaZupan commented Dec 16, 2022

Copy link
Copy Markdown
MemberAuthor

Added Contains(char) and IndexOfAny(char, char, char) now.
I checked and we still have full test coverage of the existing and new methods.

Updated perf numbers
MethodToolchainLengthMeanErrorRatio
Containsmain12.228 ns0.0040 ns1.00
Containspr12.216 ns0.0127 ns0.99
IndexOfmain12.065 ns0.0041 ns1.00
IndexOfpr11.753 ns0.0043 ns0.85
IndexOfAny2Valuesmain12.902 ns0.0073 ns1.00
IndexOfAny2Valuespr12.537 ns0.0030 ns0.87
IndexOfAny3Valuesmain12.975 ns0.0015 ns1.00
IndexOfAny3Valuespr13.094 ns0.0041 ns1.04
IndexOfAnyInRangemain12.196 ns0.0027 ns1.00
IndexOfAnyInRangepr11.996 ns0.0039 ns0.91
Containsmain73.435 ns0.0033 ns1.00
Containspr73.465 ns0.0020 ns1.01
IndexOfmain73.726 ns0.0034 ns1.00
IndexOfpr73.755 ns0.0017 ns1.01
IndexOfAny2Valuesmain75.915 ns0.0023 ns1.00
IndexOfAny2Valuespr75.960 ns0.0092 ns1.01
IndexOfAny3Valuesmain78.116 ns0.0047 ns1.00
IndexOfAny3Valuespr77.975 ns0.0036 ns0.98
IndexOfAnyInRangemain75.145 ns0.0038 ns1.00
IndexOfAnyInRangepr73.678 ns0.0031 ns0.71
Containsmain82.089 ns0.0067 ns1.00
Containspr82.293 ns0.0019 ns1.10
IndexOfmain82.693 ns0.0040 ns1.00
IndexOfpr82.312 ns0.0019 ns0.86
IndexOfAny2Valuesmain82.956 ns0.0012 ns1.00
IndexOfAny2Valuespr82.886 ns0.0042 ns0.98
IndexOfAny3Valuesmain83.239 ns0.0022 ns1.00
IndexOfAny3Valuespr83.436 ns0.0022 ns1.06
IndexOfAnyInRangemain82.887 ns0.0021 ns1.00
IndexOfAnyInRangepr82.379 ns0.0115 ns0.82
Containsmain92.193 ns0.0058 ns1.00
Containspr92.296 ns0.0015 ns1.05
IndexOfmain92.914 ns0.0038 ns1.00
IndexOfpr92.301 ns0.0011 ns0.79
IndexOfAny2Valuesmain93.438 ns0.0038 ns1.00
IndexOfAny2Valuespr92.882 ns0.0045 ns0.84
IndexOfAny3Valuesmain93.727 ns0.0032 ns1.00
IndexOfAny3Valuespr93.431 ns0.0014 ns0.92
IndexOfAnyInRangemain92.891 ns0.0019 ns1.00
IndexOfAnyInRangepr92.395 ns0.0112 ns0.83
Containsmain152.194 ns0.0055 ns1.00
Containspr152.286 ns0.0016 ns1.04
IndexOfmain152.907 ns0.0042 ns1.00
IndexOfpr152.313 ns0.0037 ns0.80
IndexOfAny2Valuesmain153.450 ns0.0025 ns1.00
IndexOfAny2Valuespr152.895 ns0.0065 ns0.84
IndexOfAny3Valuesmain153.723 ns0.0015 ns1.00
IndexOfAny3Valuespr153.428 ns0.0021 ns0.92
IndexOfAnyInRangemain152.886 ns0.0024 ns1.00
IndexOfAnyInRangepr152.334 ns0.0054 ns0.81
Containsmain162.194 ns0.0063 ns1.00
Containspr162.298 ns0.0020 ns1.05
IndexOfmain162.497 ns0.0103 ns1.00
IndexOfpr162.336 ns0.0058 ns0.94
IndexOfAny2Valuesmain163.010 ns0.0021 ns1.00
IndexOfAny2Valuespr162.862 ns0.0016 ns0.95
IndexOfAny3Valuesmain163.333 ns0.0024 ns1.00
IndexOfAny3Valuespr163.428 ns0.0024 ns1.03
IndexOfAnyInRangemain162.863 ns0.0035 ns1.00
IndexOfAnyInRangepr162.376 ns0.0111 ns0.83
Containsmain172.391 ns0.0018 ns1.00
Containspr172.422 ns0.0024 ns1.01
IndexOfmain172.688 ns0.0030 ns1.00
IndexOfpr172.564 ns0.0032 ns0.95
IndexOfAny2Valuesmain173.439 ns0.0019 ns1.00
IndexOfAny2Valuespr172.943 ns0.0023 ns0.86
IndexOfAny3Valuesmain173.840 ns0.0071 ns1.00
IndexOfAny3Valuespr173.521 ns0.0017 ns0.92
IndexOfAnyInRangemain172.875 ns0.0037 ns1.00
IndexOfAnyInRangepr172.547 ns0.0028 ns0.89
Containsmain322.834 ns0.0238 ns1.00
Containspr322.419 ns0.0011 ns0.85
IndexOfmain322.890 ns0.0035 ns1.00
IndexOfpr322.575 ns0.0040 ns0.89
IndexOfAny2Valuesmain323.435 ns0.0020 ns1.00
IndexOfAny2Valuespr322.943 ns0.0023 ns0.86
IndexOfAny3Valuesmain324.146 ns0.0029 ns1.00
IndexOfAny3Valuespr323.488 ns0.0021 ns0.84
IndexOfAnyInRangemain322.865 ns0.0046 ns1.00
IndexOfAnyInRangepr322.548 ns0.0027 ns0.89
Containsmain332.903 ns0.0102 ns1.00
Containspr332.813 ns0.0022 ns0.97
IndexOfmain333.103 ns0.0035 ns1.00
IndexOfpr332.747 ns0.0030 ns0.89
IndexOfAny2Valuesmain333.871 ns0.0022 ns1.00
IndexOfAny2Valuespr333.283 ns0.0029 ns0.85
IndexOfAny3Valuesmain334.738 ns0.0033 ns1.00
IndexOfAny3Valuespr334.349 ns0.0020 ns0.92
IndexOfAnyInRangemain333.777 ns0.0169 ns1.00
IndexOfAnyInRangepr333.018 ns0.0029 ns0.80
Containsmain100032.797 ns0.0229 ns1.00
Containspr100017.534 ns0.0275 ns0.53
IndexOfmain100039.024 ns0.0396 ns1.00
IndexOfpr100023.966 ns0.0109 ns0.61
IndexOfAny2Valuesmain100044.677 ns0.0203 ns1.00
IndexOfAny2Valuespr100023.087 ns0.2666 ns0.52
IndexOfAny3Valuesmain100057.412 ns0.1221 ns1.00
IndexOfAny3Valuespr100031.902 ns0.2688 ns0.56
IndexOfAnyInRangemain100056.802 ns0.0273 ns1.00
IndexOfAnyInRangepr100026.263 ns0.1187 ns0.46
Containsmain1000003,686.442 ns3.9490 ns1.00
Containspr1000001,923.226 ns3.0266 ns0.52
IndexOfmain1000003,827.157 ns1.4482 ns1.00
IndexOfpr1000001,918.985 ns1.6545 ns0.50
IndexOfAny2Valuesmain1000004,436.271 ns2.3896 ns1.00
IndexOfAny2Valuespr1000002,559.311 ns2.1281 ns0.58
IndexOfAny3Valuesmain1000005,416.439 ns2.5865 ns1.00
IndexOfAny3Valuespr1000003,144.892 ns2.2510 ns0.58
IndexOfAnyInRangemain1000005,415.066 ns2.0259 ns1.00
IndexOfAnyInRangepr1000002,570.033 ns2.2194 ns0.47

@dakersnardakersnar 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

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-libraries-coreclr outerloop

@azure-pipelines

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

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

It appears that the packing is noticeably more expensive on ARM in comparison.
While the approach can improve throughput somewhat, the regression for cases where matches are close to the start seems unacceptable. I will update the logic to only apply when running on X86.

ARM64 benchmarks (no match)
MethodLengthMeanError
IndexOf83.173 ns0.0074 ns
IndexOfAny2Values84.455 ns0.0013 ns
IndexOfAny3Values83.814 ns0.0048 ns
PackedIndexOf83.343 ns0.0237 ns
PackedIndexOfAny2Values83.808 ns0.0104 ns
PackedIndexOfAny3Values84.283 ns0.0029 ns
IndexOf93.741 ns0.0144 ns
IndexOfAny2Values95.672 ns0.0098 ns
IndexOfAny3Values95.095 ns0.0245 ns
PackedIndexOf93.622 ns0.0072 ns
PackedIndexOfAny2Values94.226 ns0.0087 ns
PackedIndexOfAny3Values94.871 ns0.0148 ns
IndexOf164.141 ns0.0007 ns
IndexOfAny2Values165.607 ns0.0002 ns
IndexOfAny3Values165.344 ns0.0071 ns
PackedIndexOf163.366 ns0.0263 ns
PackedIndexOfAny2Values163.810 ns0.0142 ns
PackedIndexOfAny3Values164.285 ns0.0016 ns
IndexOf325.474 ns0.0002 ns
IndexOfAny2Values326.940 ns0.0044 ns
IndexOfAny3Values327.912 ns0.0124 ns
PackedIndexOf325.002 ns0.0205 ns
PackedIndexOfAny2Values325.233 ns0.0050 ns
PackedIndexOfAny3Values326.005 ns0.0073 ns
IndexOf12815.234 ns0.0050 ns
IndexOfAny2Values12818.834 ns0.0183 ns
IndexOfAny3Values12826.844 ns0.1520 ns
PackedIndexOf12814.835 ns0.0172 ns
PackedIndexOfAny2Values12817.159 ns0.2145 ns
PackedIndexOfAny3Values12818.359 ns0.0738 ns
IndexOf51255.640 ns0.0081 ns
IndexOfAny2Values51274.750 ns0.0188 ns
IndexOfAny3Values51293.405 ns0.0337 ns
PackedIndexOf51247.795 ns0.0702 ns
PackedIndexOfAny2Values51265.908 ns0.2881 ns
PackedIndexOfAny3Values51279.024 ns0.0501 ns
ARM64 benchmarks (the first character matches)
MethodLengthMeanError
IndexOf83.952 ns0.0183 ns
IndexOfAny2Values84.110 ns0.0237 ns
IndexOfAny3Values84.545 ns0.0016 ns
PackedIndexOf85.972 ns0.0140 ns
PackedIndexOfAny2Values86.750 ns0.0031 ns
PackedIndexOfAny3Values87.356 ns0.0046 ns
IndexOf93.638 ns0.0167 ns
IndexOfAny2Values94.476 ns0.0256 ns
IndexOfAny3Values94.549 ns0.0020 ns
PackedIndexOf96.668 ns0.0079 ns
PackedIndexOfAny2Values97.457 ns0.0016 ns
PackedIndexOfAny3Values98.082 ns0.0031 ns
IndexOf163.613 ns0.0304 ns
IndexOfAny2Values164.134 ns0.0233 ns
IndexOfAny3Values164.546 ns0.0018 ns
PackedIndexOf165.981 ns0.0103 ns
PackedIndexOfAny2Values166.758 ns0.0055 ns
PackedIndexOfAny3Values167.364 ns0.0026 ns
IndexOf323.646 ns0.0420 ns
IndexOfAny2Values324.110 ns0.0236 ns
IndexOfAny3Values324.546 ns0.0018 ns
PackedIndexOf325.233 ns0.0221 ns
PackedIndexOfAny2Values325.988 ns0.0044 ns
PackedIndexOfAny3Values326.673 ns0.0025 ns
IndexOf1283.638 ns0.0275 ns
IndexOfAny2Values1284.110 ns0.0284 ns
IndexOfAny3Values1285.053 ns0.0077 ns
PackedIndexOf1285.215 ns0.0140 ns
PackedIndexOfAny2Values1285.981 ns0.0080 ns
PackedIndexOfAny3Values1286.652 ns0.0018 ns

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@tannergooding any concerns about using this sort of approach only on X86, given that it's not profitable on ARM?

Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Packed.cs Outdated
@build-analysisbuild-analysisBot mentioned this pull request Dec 30, 2022
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Packed.cs Outdated
@MihaZupan

Copy link
Copy Markdown
MemberAuthor

All failures are known according to build-analysis

@MihaZupan
MihaZupan merged commit ac2ffdf into dotnet:mainJan 3, 2023
@EgorBo

EgorBo commented Jan 5, 2023

Copy link
Copy Markdown
Member

radekdoulik added a commit to radekdoulik/runtime that referenced this pull request Jan 5, 2023
This should avoids the size regression on WebAssembly and possibly other
platforms without Sse2.
The regression is side effect of dotnet#78861
which uses `PackedSpanHelpers.CanUsePackedIndexOf (!!T)` and TShouldUsePacked.Value
to guard the usage of PackedSpanHelpers.
Because these involve generics, illinker is unable to link
the PackedSpanHelpers type away and that pulls other parts in, like
System.Runtime.Intrinsics.X86.* types. See https://gist.github.com/radekdoulik/c0b52247d472f69bcf983ade78a924ea
for more complete list.
This change gets us back 9,216 bytes in the case of app used to repro
the regression.
...
- Type System.PackedSpanHelpers
- Type System.Runtime.Intrinsics.X86.X86Base
- Type System.Runtime.Intrinsics.X86.Sse
- Type System.Runtime.Intrinsics.X86.Sse2
Summary:
- 9,216 File size -0.76% (of 1,215,488)
- 2,744 Metadata size -0.43% (of 636,264)
- 4 Types count
radekdoulik added a commit that referenced this pull request Jan 9, 2023
* Use PackedIndexOfIsSupported checks in more places
This should avoids the size regression on WebAssembly and possibly other
platforms without Sse2.
The regression is side effect of #78861
which uses `PackedSpanHelpers.CanUsePackedIndexOf (!!T)` and TShouldUsePacked.Value
to guard the usage of PackedSpanHelpers.
Because these involve generics, illinker is unable to link
the PackedSpanHelpers type away and that pulls other parts in, like
System.Runtime.Intrinsics.X86.* types. See https://gist.github.com/radekdoulik/c0b52247d472f69bcf983ade78a924ea
for more complete list.
This change gets us back 9,216 bytes in the case of app used to repro
the regression.
...
- Type System.PackedSpanHelpers
- Type System.Runtime.Intrinsics.X86.X86Base
- Type System.Runtime.Intrinsics.X86.Sse
- Type System.Runtime.Intrinsics.X86.Sse2
Summary:
- 9,216 File size -0.76% (of 1,215,488)
- 2,744 Metadata size -0.43% (of 636,264)
- 4 Types count
* Update src/libraries/System.Private.CoreLib/src/System/IndexOfAnyValues/IndexOfAnyValues.cs
Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
* Update src/libraries/System.Private.CoreLib/src/System/IndexOfAnyValues/IndexOfAnyValues.cs
Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
* Feedback
Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
@ghostghost locked as resolved and limited conversation to collaborators Feb 9, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Memorytenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@MihaZupan@dakersnar@EgorBo@stephentoub@danmoseley@jkotas