Skip to content

Vectorize {Last}IndexOfAny{Except} for ASCII needles - #76740

Merged
MihaZupan merged 3 commits into
dotnet:mainfrom
MihaZupan:indexofanyascii
Nov 4, 2022
Merged

Vectorize {Last}IndexOfAny{Except} for ASCII needles#76740
MihaZupan merged 3 commits into
dotnet:mainfrom
MihaZupan:indexofanyascii

Conversation

@MihaZupan

@MihaZupanMihaZupan commented Oct 7, 2022

Copy link
Copy Markdown
Member

Contributes to #68328 (and contributes the workhorse implementation for an eventual dedicated API)
cc: @gfoidl

This PR adds a vectorized path for IndexOfAny-like methods if the input is at least 8 characters long and the needle is only ASCII.
It is used if Ssse3 or AdvSimd.Arm64 are supported.

I also unified all (Last)IndexOfAny(Except) methods to use the same approach of

if(HaystackIsShort)returnSimpleSearch();if(NeedleIsAscii)returnVectorized();returnProbabilisticMap();

leading to perf improvements for both short and long inputs.

For inputs with long runs of no matches, the vectorized path has 10 to 140+ times the throughput on realistic inputs.
As the -Except methods were previously O(n * m), you can of course see arbitrarily large improvements.

X86 numbers

These numbers were collected on a Windows Azure VM running on an Intel Xeon 8370C processor.

MethodToolchainLengthNeedleMeanErrorRatio
IndexOfAnymain1ABCDEF9.787 ns0.0087 ns1.00
IndexOfAnypr1ABCDEF5.852 ns0.0053 ns0.60
IndexOfAnyExceptmain1ABCDEF5.313 ns0.0077 ns1.00
IndexOfAnyExceptpr1ABCDEF6.676 ns0.0104 ns1.26
LastIndexOfAnymain1ABCDEF11.376 ns0.0065 ns1.00
LastIndexOfAnypr1ABCDEF5.783 ns0.0037 ns0.51
LastIndexOfAnyExceptmain1ABCDEF5.946 ns0.0123 ns1.00
LastIndexOfAnyExceptpr1ABCDEF6.030 ns0.0324 ns1.01
IndexOfAnymain1AlphaNumeric63.653 ns0.0265 ns1.00
IndexOfAnypr1AlphaNumeric5.855 ns0.0066 ns0.09
IndexOfAnyExceptmain1AlphaNumeric6.371 ns0.0128 ns1.00
IndexOfAnyExceptpr1AlphaNumeric7.619 ns0.0104 ns1.20
LastIndexOfAnymain1AlphaNumeric74.164 ns0.0712 ns1.00
LastIndexOfAnypr1AlphaNumeric5.558 ns0.0011 ns0.07
LastIndexOfAnyExceptmain1AlphaNumeric8.766 ns0.2798 ns1.00
LastIndexOfAnyExceptpr1AlphaNumeric6.859 ns0.0018 ns0.88
IndexOfAnymain8ABCDEF14.12 ns0.016 ns1.00
IndexOfAnypr8ABCDEF12.25 ns0.006 ns0.87
IndexOfAnyExceptmain8ABCDEF24.38 ns0.019 ns1.00
IndexOfAnyExceptpr8ABCDEF14.86 ns0.005 ns0.61
LastIndexOfAnymain8ABCDEF16.51 ns0.008 ns1.00
LastIndexOfAnypr8ABCDEF12.23 ns0.006 ns0.74
LastIndexOfAnyExceptmain8ABCDEF23.42 ns0.022 ns1.00
LastIndexOfAnyExceptpr8ABCDEF14.93 ns0.013 ns0.64
IndexOfAnymain8AlphaNumeric67.53 ns0.075 ns1.00
IndexOfAnypr8AlphaNumeric34.92 ns0.045 ns0.52
IndexOfAnyExceptmain8AlphaNumeric27.99 ns0.025 ns1.00
IndexOfAnyExceptpr8AlphaNumeric31.09 ns0.036 ns1.11
LastIndexOfAnymain8AlphaNumeric79.39 ns0.065 ns1.00
LastIndexOfAnypr8AlphaNumeric34.32 ns0.028 ns0.43
LastIndexOfAnyExceptmain8AlphaNumeric27.67 ns0.097 ns1.00
LastIndexOfAnyExceptpr8AlphaNumeric25.80 ns0.014 ns0.93
IndexOfAnymain16ABCDEF20.15 ns0.060 ns1.00
IndexOfAnypr16ABCDEF12.25 ns0.007 ns0.61
IndexOfAnyExceptmain16ABCDEF43.59 ns0.043 ns1.00
IndexOfAnyExceptpr16ABCDEF14.96 ns0.011 ns0.34
LastIndexOfAnymain16ABCDEF21.76 ns0.012 ns1.00
LastIndexOfAnypr16ABCDEF12.23 ns0.006 ns0.56
LastIndexOfAnyExceptmain16ABCDEF41.38 ns0.033 ns1.00
LastIndexOfAnyExceptpr16ABCDEF14.92 ns0.010 ns0.36
IndexOfAnymain16AlphaNumeric73.53 ns0.060 ns1.00
IndexOfAnypr16AlphaNumeric67.08 ns0.084 ns0.91
IndexOfAnyExceptmain16AlphaNumeric49.79 ns0.032 ns1.00
IndexOfAnyExceptpr16AlphaNumeric54.95 ns0.051 ns1.10
LastIndexOfAnymain16AlphaNumeric84.76 ns0.061 ns1.00
LastIndexOfAnypr16AlphaNumeric66.56 ns0.060 ns0.79
LastIndexOfAnyExceptmain16AlphaNumeric50.64 ns0.162 ns1.00
LastIndexOfAnyExceptpr16AlphaNumeric45.80 ns0.062 ns0.91
IndexOfAnymain32ABCDEF29.04 ns0.034 ns1.00
IndexOfAnypr32ABCDEF13.37 ns0.009 ns0.46
IndexOfAnyExceptmain32ABCDEF83.02 ns0.093 ns1.00
IndexOfAnyExceptpr32ABCDEF15.98 ns0.007 ns0.19
LastIndexOfAnymain32ABCDEF31.55 ns0.022 ns1.00
LastIndexOfAnypr32ABCDEF13.92 ns0.050 ns0.44
LastIndexOfAnyExceptmain32ABCDEF82.09 ns0.090 ns1.00
LastIndexOfAnyExceptpr32ABCDEF15.77 ns0.007 ns0.19
IndexOfAnymain32AlphaNumeric83.50 ns0.061 ns1.00
IndexOfAnypr32AlphaNumeric65.77 ns0.252 ns0.79
IndexOfAnyExceptmain32AlphaNumeric106.94 ns0.054 ns1.00
IndexOfAnyExceptpr32AlphaNumeric66.03 ns0.099 ns0.62
LastIndexOfAnymain32AlphaNumeric94.24 ns0.052 ns1.00
LastIndexOfAnypr32AlphaNumeric64.81 ns0.073 ns0.69
LastIndexOfAnyExceptmain32AlphaNumeric107.73 ns0.249 ns1.00
LastIndexOfAnyExceptpr32AlphaNumeric65.58 ns0.034 ns0.61
IndexOfAnymain64ABCDEF51.95 ns0.321 ns1.00
IndexOfAnypr64ABCDEF15.31 ns0.009 ns0.30
IndexOfAnyExceptmain64ABCDEF165.25 ns0.166 ns1.00
IndexOfAnyExceptpr64ABCDEF17.54 ns0.026 ns0.11
LastIndexOfAnymain64ABCDEF52.16 ns0.039 ns1.00
LastIndexOfAnypr64ABCDEF15.83 ns0.020 ns0.30
LastIndexOfAnyExceptmain64ABCDEF164.63 ns0.167 ns1.00
LastIndexOfAnyExceptpr64ABCDEF17.01 ns0.008 ns0.10
IndexOfAnymain64AlphaNumeric105.16 ns0.079 ns1.00
IndexOfAnypr64AlphaNumeric67.26 ns0.093 ns0.64
IndexOfAnyExceptmain64AlphaNumeric208.43 ns0.070 ns1.00
IndexOfAnyExceptpr64AlphaNumeric68.18 ns0.107 ns0.33
LastIndexOfAnymain64AlphaNumeric115.14 ns0.057 ns1.00
LastIndexOfAnypr64AlphaNumeric66.35 ns0.040 ns0.58
LastIndexOfAnyExceptmain64AlphaNumeric212.70 ns0.583 ns1.00
LastIndexOfAnyExceptpr64AlphaNumeric67.31 ns0.059 ns0.32
IndexOfAnymain128ABCDEF90.31 ns0.083 ns1.00
IndexOfAnypr128ABCDEF18.74 ns0.008 ns0.21
IndexOfAnyExceptmain128ABCDEF336.07 ns0.234 ns1.00
IndexOfAnyExceptpr128ABCDEF20.78 ns0.015 ns0.06
LastIndexOfAnymain128ABCDEF100.96 ns0.050 ns1.00
LastIndexOfAnypr128ABCDEF18.80 ns0.024 ns0.19
LastIndexOfAnyExceptmain128ABCDEF337.61 ns0.233 ns1.00
LastIndexOfAnyExceptpr128ABCDEF21.00 ns0.040 ns0.06
IndexOfAnymain128AlphaNumeric148.35 ns0.231 ns1.00
IndexOfAnypr128AlphaNumeric70.20 ns0.121 ns0.47
IndexOfAnyExceptmain128AlphaNumeric425.60 ns0.178 ns1.00
IndexOfAnyExceptpr128AlphaNumeric71.25 ns0.041 ns0.17
LastIndexOfAnymain128AlphaNumeric165.64 ns0.085 ns1.00
LastIndexOfAnypr128AlphaNumeric70.08 ns0.118 ns0.42
LastIndexOfAnyExceptmain128AlphaNumeric438.12 ns1.407 ns1.00
LastIndexOfAnyExceptpr128AlphaNumeric72.16 ns0.126 ns0.16
IndexOfAnymain256ABCDEF182.60 ns0.382 ns1.00
IndexOfAnypr256ABCDEF29.07 ns0.026 ns0.16
IndexOfAnyExceptmain256ABCDEF703.61 ns0.468 ns1.00
IndexOfAnyExceptpr256ABCDEF32.34 ns0.019 ns0.05
LastIndexOfAnymain256ABCDEF196.47 ns1.878 ns1.00
LastIndexOfAnypr256ABCDEF29.52 ns0.063 ns0.15
LastIndexOfAnyExceptmain256ABCDEF704.98 ns0.586 ns1.00
LastIndexOfAnyExceptpr256ABCDEF31.83 ns0.019 ns0.05
IndexOfAnymain256AlphaNumeric236.97 ns0.128 ns1.00
IndexOfAnypr256AlphaNumeric81.93 ns0.287 ns0.35
IndexOfAnyExceptmain256AlphaNumeric878.10 ns0.445 ns1.00
IndexOfAnyExceptpr256AlphaNumeric82.96 ns0.094 ns0.09
LastIndexOfAnymain256AlphaNumeric247.84 ns0.110 ns1.00
LastIndexOfAnypr256AlphaNumeric80.38 ns0.055 ns0.32
LastIndexOfAnyExceptmain256AlphaNumeric871.79 ns0.353 ns1.00
LastIndexOfAnyExceptpr256AlphaNumeric83.21 ns0.096 ns0.10
IndexOfAnymain10000ABCDEF6,409.6 ns4.42 ns1.00
IndexOfAnypr10000ABCDEF651.2 ns0.36 ns0.10
IndexOfAnyExceptmain10000ABCDEF71,960.2 ns31.30 ns1.00
IndexOfAnyExceptpr10000ABCDEF741.1 ns0.36 ns0.01
LastIndexOfAnymain10000ABCDEF6,396.7 ns2.27 ns1.00
LastIndexOfAnypr10000ABCDEF643.2 ns0.36 ns0.10
LastIndexOfAnyExceptmain10000ABCDEF81,927.2 ns23.11 ns1.000
LastIndexOfAnyExceptpr10000ABCDEF728.1 ns0.26 ns0.009
IndexOfAnymain10000AlphaNumeric6,450.1 ns2.54 ns1.00
IndexOfAnypr10000AlphaNumeric704.2 ns0.41 ns0.11
IndexOfAnyExceptmain10000AlphaNumeric114,854.9 ns91.93 ns1.000
IndexOfAnyExceptpr10000AlphaNumeric791.0 ns0.36 ns0.007
LastIndexOfAnymain10000AlphaNumeric6,613.7 ns21.32 ns1.00
LastIndexOfAnypr10000AlphaNumeric690.0 ns0.08 ns0.10
LastIndexOfAnyExceptmain10000AlphaNumeric110,475.7 ns74.60 ns1.000
LastIndexOfAnyExceptpr10000AlphaNumeric774.9 ns0.04 ns0.007

On X86 we have to do a bit more work if the needle contains a zero:

MethodLengthNeedleMeanError
IndexOfAny10000ABCDEF536.8 ns0.45 ns
IndexOfAny10000NeedleWithZero820.7 ns0.74 ns

Approximate ARM64 numbers:

MethodLengthNeedleMeanError
IndexOfAny10000ABCDEF1.783 µs0.0051 µs
CurrentIndexOfAny10000ABCDEF10.355 µs0.0010 µs

@MihaZupanMihaZupan added this to the 8.0.0 milestone Oct 7, 2022
@ghost

ghost commented Oct 7, 2022

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

Fixes #68328 (and contributes the workhorse implementation for an eventual dedicated API)
cc: @gfoidl

This PR adds a vectorized path for IndexOfAny-like methods if the input is at least 8 characters long and the needle is only ASCII.
It is used if Ssse3 or AdvSimd.Arm64 are supported.

Similarly to the IndexOfAnyProbabilistic code paths, the cost of computing the bitmap used in the vectorized path is quite high compared to the searching cost (+1 for a dedicated API from #68328).
The (Last)IndexOfAnyExcept methods that were previously using an O(n * m) loop regress with this change if the needle is long, while the haystack is small or matches occur close to the start. We could do a few length checks to mitigate that, but this is also already the case with existing (Last)IndexOfAny methods.

For inputs with long runs of no matches, the vectorized path has 10-30x the throughput.

X86 numbers

These numbers were collected on a Windows Azure VM running on an Intel Xeon 8370C processor.

MethodToolchainLengthNeedleMeanErrorRatio
IndexOfAnymain1ABCDEF10.018 ns0.0316 ns1.00
IndexOfAnypr1ABCDEF11.134 ns0.0114 ns1.11
IndexOfAnyExceptmain1ABCDEF4.768 ns0.0057 ns1.00
IndexOfAnyExceptpr1ABCDEF5.380 ns0.0053 ns1.13
LastIndexOfAnymain1ABCDEF11.007 ns0.0322 ns1.00
LastIndexOfAnypr1ABCDEF11.288 ns0.0097 ns1.03
LastIndexOfAnyExceptmain1ABCDEF4.911 ns0.0045 ns1.00
LastIndexOfAnyExceptpr1ABCDEF5.260 ns0.0076 ns1.07
IndexOfAnymain1AlphaNumeric63.540 ns0.0710 ns1.00
IndexOfAnypr1AlphaNumeric86.789 ns0.2262 ns1.37
IndexOfAnyExceptmain1AlphaNumeric5.407 ns0.0617 ns1.00
IndexOfAnyExceptpr1AlphaNumeric5.578 ns0.0013 ns1.05
LastIndexOfAnymain1AlphaNumeric67.869 ns0.3421 ns1.00
LastIndexOfAnypr1AlphaNumeric89.007 ns0.1024 ns1.32
LastIndexOfAnyExceptmain1AlphaNumeric5.485 ns0.0064 ns1.00
LastIndexOfAnyExceptpr1AlphaNumeric5.898 ns0.0021 ns1.08
IndexOfAnymain8ABCDEF14.55 ns0.063 ns1.00
IndexOfAnypr8ABCDEF12.86 ns0.009 ns0.89
IndexOfAnyExceptmain8ABCDEF18.43 ns0.018 ns1.00
IndexOfAnyExceptpr8ABCDEF14.61 ns0.012 ns0.79
LastIndexOfAnymain8ABCDEF16.69 ns0.023 ns1.00
LastIndexOfAnypr8ABCDEF12.89 ns0.016 ns0.77
LastIndexOfAnyExceptmain8ABCDEF19.02 ns0.062 ns1.00
LastIndexOfAnyExceptpr8ABCDEF14.73 ns0.011 ns0.78
IndexOfAnymain8AlphaNumeric67.92 ns0.043 ns1.00
IndexOfAnypr8AlphaNumeric67.60 ns0.075 ns1.00
IndexOfAnyExceptmain8AlphaNumeric20.91 ns0.022 ns1.00
IndexOfAnyExceptpr8AlphaNumeric65.43 ns0.048 ns3.13
LastIndexOfAnymain8AlphaNumeric79.32 ns0.082 ns1.00
LastIndexOfAnypr8AlphaNumeric70.53 ns0.137 ns0.89
LastIndexOfAnyExceptmain8AlphaNumeric21.74 ns0.015 ns1.00
LastIndexOfAnyExceptpr8AlphaNumeric71.20 ns0.023 ns3.28
IndexOfAnymain16ABCDEF19.80 ns0.022 ns1.00
IndexOfAnypr16ABCDEF12.79 ns0.007 ns0.65
IndexOfAnyExceptmain16ABCDEF34.45 ns0.031 ns1.00
IndexOfAnyExceptpr16ABCDEF14.72 ns0.021 ns0.43
LastIndexOfAnymain16ABCDEF21.83 ns0.017 ns1.00
LastIndexOfAnypr16ABCDEF12.99 ns0.015 ns0.60
LastIndexOfAnyExceptmain16ABCDEF35.17 ns0.100 ns1.00
LastIndexOfAnyExceptpr16ABCDEF15.01 ns0.032 ns0.43
IndexOfAnymain16AlphaNumeric73.40 ns0.056 ns1.00
IndexOfAnypr16AlphaNumeric67.11 ns0.058 ns0.91
IndexOfAnyExceptmain16AlphaNumeric39.20 ns0.041 ns1.00
IndexOfAnyExceptpr16AlphaNumeric65.49 ns0.054 ns1.67
LastIndexOfAnymain16AlphaNumeric85.42 ns0.074 ns1.00
LastIndexOfAnypr16AlphaNumeric71.06 ns0.239 ns0.83
LastIndexOfAnyExceptmain16AlphaNumeric39.99 ns0.054 ns1.00
LastIndexOfAnyExceptpr16AlphaNumeric70.94 ns0.042 ns1.77
IndexOfAnymain32ABCDEF29.25 ns0.040 ns1.00
IndexOfAnypr32ABCDEF14.21 ns0.020 ns0.49
IndexOfAnyExceptmain32ABCDEF66.76 ns0.054 ns1.00
IndexOfAnyExceptpr32ABCDEF16.14 ns0.035 ns0.24
LastIndexOfAnymain32ABCDEF31.49 ns0.049 ns1.00
LastIndexOfAnypr32ABCDEF14.28 ns0.019 ns0.45
LastIndexOfAnyExceptmain32ABCDEF68.10 ns0.255 ns1.00
LastIndexOfAnyExceptpr32ABCDEF16.29 ns0.013 ns0.24
IndexOfAnymain32AlphaNumeric83.77 ns0.061 ns1.00
IndexOfAnypr32AlphaNumeric67.79 ns0.035 ns0.81
IndexOfAnyExceptmain32AlphaNumeric76.04 ns0.062 ns1.00
IndexOfAnyExceptpr32AlphaNumeric67.08 ns0.063 ns0.88
LastIndexOfAnymain32AlphaNumeric95.07 ns0.082 ns1.00
LastIndexOfAnypr32AlphaNumeric68.43 ns0.065 ns0.72
LastIndexOfAnyExceptmain32AlphaNumeric77.10 ns0.089 ns1.00
LastIndexOfAnyExceptpr32AlphaNumeric75.36 ns0.223 ns0.98
IndexOfAnymain64ABCDEF49.95 ns0.065 ns1.00
IndexOfAnypr64ABCDEF15.56 ns0.008 ns0.31
IndexOfAnyExceptmain64ABCDEF141.76 ns0.164 ns1.00
IndexOfAnyExceptpr64ABCDEF17.25 ns0.012 ns0.12
LastIndexOfAnymain64ABCDEF52.32 ns0.054 ns1.00
LastIndexOfAnypr64ABCDEF16.22 ns0.015 ns0.31
LastIndexOfAnyExceptmain64ABCDEF140.36 ns0.092 ns1.00
LastIndexOfAnyExceptpr64ABCDEF18.84 ns0.107 ns0.13
IndexOfAnymain64AlphaNumeric104.80 ns0.069 ns1.00
IndexOfAnypr64AlphaNumeric70.33 ns0.062 ns0.67
IndexOfAnyExceptmain64AlphaNumeric159.90 ns0.214 ns1.00
IndexOfAnyExceptpr64AlphaNumeric100.41 ns4.322 ns0.63
LastIndexOfAnymain64AlphaNumeric115.49 ns0.065 ns1.00
LastIndexOfAnypr64AlphaNumeric70.45 ns0.026 ns0.61
LastIndexOfAnyExceptmain64AlphaNumeric159.35 ns0.152 ns1.00
LastIndexOfAnyExceptpr64AlphaNumeric74.42 ns0.015 ns0.47
IndexOfAnymain128ABCDEF90.87 ns0.078 ns1.00
IndexOfAnypr128ABCDEF19.31 ns0.019 ns0.21
IndexOfAnyExceptmain128ABCDEF271.81 ns0.272 ns1.00
IndexOfAnyExceptpr128ABCDEF20.93 ns0.011 ns0.08
LastIndexOfAnymain128ABCDEF101.48 ns0.053 ns1.00
LastIndexOfAnypr128ABCDEF19.41 ns0.012 ns0.19
LastIndexOfAnyExceptmain128ABCDEF270.32 ns0.180 ns1.00
LastIndexOfAnyExceptpr128ABCDEF21.64 ns0.049 ns0.08
IndexOfAnymain128AlphaNumeric146.21 ns0.100 ns1.00
IndexOfAnypr128AlphaNumeric73.25 ns0.049 ns0.50
IndexOfAnyExceptmain128AlphaNumeric309.32 ns0.400 ns1.00
IndexOfAnyExceptpr128AlphaNumeric72.91 ns0.052 ns0.24
LastIndexOfAnymain128AlphaNumeric170.34 ns0.509 ns1.00
LastIndexOfAnypr128AlphaNumeric74.98 ns0.194 ns0.44
LastIndexOfAnyExceptmain128AlphaNumeric308.21 ns0.256 ns1.00
LastIndexOfAnyExceptpr128AlphaNumeric78.56 ns0.019 ns0.25
IndexOfAnymain256ABCDEF181.28 ns0.119 ns1.00
IndexOfAnypr256ABCDEF29.36 ns0.030 ns0.16
IndexOfAnyExceptmain256ABCDEF530.41 ns0.329 ns1.00
IndexOfAnyExceptpr256ABCDEF32.47 ns0.074 ns0.06
LastIndexOfAnymain256ABCDEF198.31 ns1.919 ns1.00
LastIndexOfAnypr256ABCDEF29.15 ns0.023 ns0.15
LastIndexOfAnyExceptmain256ABCDEF535.68 ns1.678 ns1.00
LastIndexOfAnyExceptpr256ABCDEF32.78 ns0.079 ns0.06
IndexOfAnymain256AlphaNumeric236.67 ns0.174 ns1.00
IndexOfAnypr256AlphaNumeric83.34 ns0.048 ns0.35
IndexOfAnyExceptmain256AlphaNumeric604.52 ns0.440 ns1.00
IndexOfAnyExceptpr256AlphaNumeric84.98 ns0.088 ns0.14
LastIndexOfAnymain256AlphaNumeric248.39 ns0.126 ns1.00
LastIndexOfAnypr256AlphaNumeric84.54 ns0.055 ns0.34
LastIndexOfAnyExceptmain256AlphaNumeric606.97 ns0.625 ns1.00
LastIndexOfAnyExceptpr256AlphaNumeric90.93 ns0.164 ns0.15
IndexOfAnymain10000ABCDEF8,030.2 ns70.01 ns1.00
IndexOfAnypr10000ABCDEF653.0 ns0.38 ns0.08
IndexOfAnyExceptmain10000ABCDEF20,356.5 ns17.62 ns1.00
IndexOfAnyExceptpr10000ABCDEF726.7 ns0.37 ns0.04
LastIndexOfAnymain10000ABCDEF6,463.5 ns14.40 ns1.00
LastIndexOfAnypr10000ABCDEF645.3 ns0.33 ns0.10
LastIndexOfAnyExceptmain10000ABCDEF20,271.5 ns11.62 ns1.00
LastIndexOfAnyExceptpr10000ABCDEF730.8 ns0.34 ns0.04
IndexOfAnymain10000AlphaNumeric7,106.4 ns23.74 ns1.00
IndexOfAnypr10000AlphaNumeric712.5 ns1.73 ns0.10
IndexOfAnyExceptmain10000AlphaNumeric23,274.9 ns22.32 ns1.00
IndexOfAnyExceptpr10000AlphaNumeric776.4 ns0.30 ns0.03
LastIndexOfAnymain10000AlphaNumeric6,495.1 ns5.23 ns1.00
LastIndexOfAnypr10000AlphaNumeric694.5 ns0.03 ns0.11
LastIndexOfAnyExceptmain10000AlphaNumeric23,190.5 ns12.96 ns1.00
LastIndexOfAnyExceptpr10000AlphaNumeric783.1 ns0.11 ns0.03

On X86 we have to do a bit more work if the needle contains a zero:

MethodLengthNeedleMeanErrorStdDev
IndexOfAny10000ABCDEF536.8 ns0.45 ns2.22 ns
IndexOfAny10000NeedleWithZero820.7 ns0.74 ns3.69 ns

Approximate ARM64 numbers:

MethodLengthNeedleMeanError
IndexOfAny10000ABCDEF1.783 µs0.0051 µs
CurrentIndexOfAny10000ABCDEF10.355 µs0.0010 µs
Author:MihaZupan
Assignees:-
Labels:

area-System.Memory

Milestone:8.0.0

@ghostghost assigned MihaZupanOct 7, 2022
@stephentoub

stephentoub commented Oct 7, 2022

Copy link
Copy Markdown
Member

Excellent, thanks for working on this.

The (Last)IndexOfAnyExcept methods that were previously using an O(n * m) loop regress with this change if the needle is long, while the haystack is small

Have we investigated doing something different for short haystacks? e.g. something super simple like:

if(haystack.Length<Vector128<short>.Count){for(inti=0;i<haystack.Length;i++)if(needle.Contains(haystack[i]))returni;return-1;}
...

?

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

That is what we do for the -Except overloads with this PR.

haystack.Length>=8?Vectorized():SimpleLoop()

whereas (Last)IndexOfAny does

haystack.Length>=8?Vectorized():ProbabilisticMap()

In places where we were already using the ProbabilisticMap, the init overhead seems to be very similar.

It would likely be beneficial to tweak the exact cutoff and add the SimpleLoop variant where we currently only use the ProbabilisticMap.
E.g. (made up constants)

intIndexOfAny(){if(haystack.Length<Max(8,needle.Length/2))returnSimpleLoop();if(IsAscii(needle))returnVectorized();returnProbabilisticMap();}intIndexOfAnyExcept(){if(haystack.Length>=Max(8,needle.Length/2)&&IsAscii(needle))returnVectorized();returnSimpleLoop();}

I can look into what sort of numbers we'd see with something like that, though I would hope that in general, a dedicated API to hide the init cost completely would be the preferred approach.

@stephentoub

Copy link
Copy Markdown
Member

It would likely be beneficial to tweak the exact cutoff and add the SimpleLoop variant where we currently only use the ProbabilisticMap.

Right, this is the main thing I was asking about. The probabilistic map path today needs to loop through each char individually, and do additional work for each. My gut would be that, other than for obscenely long needles, you could have a reasonably-sized haystack and still win with the simple loop doing a vectorized contains on the needles.

@stephentoub

Copy link
Copy Markdown
Member

though I would hope that in general, a dedicated API to hide the init cost completely would be the preferred approach

Certainly for cases where you're going to be invoking something repeatedly and have the foresight to create and cache the preprocessed vector information.

@gfoidlgfoidl left a comment

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.

👍🏻
Left some notes.

I expect these APIs should work for byte-inputs too, so with another static abstract interface (probably my favorite C# 11 feature) this can be done without duplication. I'm re-writing the base64 code in my repo using Char- and Byte-Operations for this* -- i.e. reading two char/short-vectors and combining them into one byte vector for further processing. Something similar could be done here too.

* it's still WIP, so pack signed vs. unsigned isn't done there

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IndexOfAnyAsciiSearcher.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IndexOfAnyAsciiSearcher.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IndexOfAnyAsciiSearcher.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IndexOfAnyAsciiSearcher.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IndexOfAnyAsciiSearcher.cs Outdated
This was referenced Oct 8, 2022
@MihaZupan

MihaZupan commented Oct 9, 2022

Copy link
Copy Markdown
MemberAuthor

I added a fast path for short haystacks to the probabilistic code path, leading to nice improvements there (I updated the numbers in the top post). E.g.

MethodToolchainLengthNeedleMeanErrorRatio
LastIndexOfAnymain1AlphaNumeric74.164 ns0.0712 ns1.00
LastIndexOfAnypr1AlphaNumeric5.558 ns0.0011 ns0.07

My benchmark for -Except methods was also measuring the best-case performance for the previous simple loop. I updated it to measure the average case (match is found randomly in the needle instead of being the first element). The corresponding potential throughput change is therefore also much higher (145x instead of 30x).

The only real regression is the (Last)IndexOfAnyExcept for long needles and short haystacks as mentioned before. I'll keep playing around with cutoffs to try and improve that case too.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

The cutoff for the simple loop is now:

searchSpaceLength<Vector128<short>.Count||(searchSpaceLength<20&&searchSpaceLength<(valuesLength>>1))

Updated the benchmarks above, this is now a win pretty much across the board.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

/azp run runtime

@azure-pipelines

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

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IndexOfAnyAsciiSearcher.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IndexOfAnyAsciiSearcher.cs Outdated
@danmoseley

Copy link
Copy Markdown
Contributor

@stephentoub

stephentoub commented Oct 10, 2022

Copy link
Copy Markdown
Member

Curious to see what this does to regex redux

It won't do anything. We don't use this API from RegexCompiler / source generator (or, rather, we rely on the API immediately delegating to the 4/5-char overloads), nor does regex redux have any sets that would trigger it. We will update RegexCompiler / source generator to use whatever new API we create that plugs into the same implementation this is adding but that let's us precompute the vector/bit set rather than having to do it on each call.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

/azp run runtime

@azure-pipelines

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

Comment threadsrc/libraries/System.Private.CoreLib/src/System/ProbabilisticMap.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/ProbabilisticMap.cs Outdated

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.

To make this reproducable, we should include a seed here.

@MihaZupanMihaZupanNov 2, 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.

If I remember correctly, I removed it because the RNG part represents a significant portion of the test execution time, with the non-explicit-seed ctor using the faster impl AFAIK.

For reproducibility, I made sure to emit the exact inputs that failed as part of the error message. Does that address your concerns here?

@stephentoubstephentoubNov 3, 2022

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.

Does that address your concerns here?

Only partially. There are two concerns:

  1. That a test could fail and you not know why or be able to trigger it again.
  2. That two runs of the same test suite might be non-deterministic.

Outputting the exact inputs addresses (1) but not (2). And (2) in general contributes to test flakiness.

If this test is about randomly stressing the implementation, then I don't believe it belongs in the functional test suite. If it's about using pseudo-randomness to try out thousands of inputs without having to manually code them all and be creative enough to ensure a reasonable spread of inputs, then it should be done in a way that runs the same tests every time, which means using a seed.

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 changed it to waste less time in Random and use a constant seed.

It was extremely useful when working on the initial implementation to get rid of all the edge-case bugs. Some were so odd that I don't believe we would have come up with test cases for otherwise.

I increased the number of iterations so it now takes about a second of CPU time to run through them, so I moved these to outerloop.
Outerloop already takes ~4 minutes on my machine so I'm gonna assume that's acceptable.

Comment threadsrc/libraries/System.Memory/tests/Span/IndexOfAny.char.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IndexOfAnyAsciiSearcher.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IndexOfAnyAsciiSearcher.cs Outdated

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.

This was necessary? Or just added for consistency with something?

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.

This was removed by mistake in #75754 (comment)

I added it back to at least make it consistent with the rest of {Last}IndexOfAny overloads.

@stephentoub

Copy link
Copy Markdown
Member

Updated the benchmarks above, this is now a win pretty much across the board.

Meaning with this PR, for long or short inputs, vectorized or not, ASCII or not, everything you've tried is as good or better than it was before? If so, yay!

@MihaZupan

MihaZupan commented Nov 2, 2022

Copy link
Copy Markdown
MemberAuthor

Meaning with this PR, for long or short inputs, vectorized or not, ASCII or not, everything you've tried is as good or better than it was before? If so, yay!

Yes*.

The main scenario I can think of that would regress is if the needle contains non-ASCII, but starts with ASCII (e.g. alphabet + 'ü') as you now have to pay for an extra scan to build the ASCII bitmap that you don't actually need. The throughput would be the same as before, but the init cost would be higher.


For short inputs that still fall back to a simple for loop (short input + large needle), there is a slightly higher per-call overhead to do the extra calls/checks. E.g.

MethodToolchainLengthNeedleMeanErrorRatio
IndexOfAnyExceptmain8AlphaNumeric27.99 ns0.025 ns1.00
IndexOfAnyExceptpr8AlphaNumeric31.09 ns0.036 ns1.11

There can also be slight differences due to a change of "naive for loop" Contains vs span.Contains for the probabilistic path, where we may be slightly slower for short needles, but faster for longer ones.

@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

Test failures look like #76755, #77726

@MihaZupanMihaZupan changed the title Vectorize (Last)IndexOfAny(Except) for ASCII needlesVectorize {Last}IndexOfAny{Except} for ASCII needlesNov 4, 2022
@MihaZupan
MihaZupan merged commit 6dfd63c into dotnet:mainNov 4, 2022
@MihaZupanMihaZupan mentioned this pull request Nov 20, 2022
@ghostghost locked as resolved and limited conversation to collaborators Dec 5, 2022
Sign up for freeto 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.

4 participants

@MihaZupan@stephentoub@danmoseley@gfoidl