Add SearchValues<char> implementation for two sets of 128 chars - #103216

Merged
stephentoub merged 4 commits into
dotnet:mainfrom
MihaZupan:searchvalues-asciiWithSecondSet
Jul 22, 2024
Merged

Add SearchValues<char> implementation for two sets of 128 chars#103216
stephentoub merged 4 commits into
dotnet:mainfrom
MihaZupan:searchvalues-asciiWithSecondSet

Conversation

@MihaZupan

@MihaZupanMihaZupan commented Jun 10, 2024

Copy link
Copy Markdown
Member

#101001 significantly improved the performance of the non-vectorized -Except paths of non-ASCII SearchValues<char>.
However, they are still not vectorized, and this PR changes that for values where the non-ASCII part can fit into a 128-bit bitmap.

This adds an implementation that's almost the same as the AsciiCharSearchValues, but where the core lookup checks against two 128-bit bitmaps, with the second one at a variable offset:

-Vector128<byte> source = TOptimizations.PackSources(source0.AsUInt16(), source1.AsUInt16());-Vector128<byte> result = IndexOfAnyLookupCore(source, bitmapLookup);-return TNegator.NegateIfNeeded(result);+Vector128<byte> packed0 = TOptimizations.PackSources(source0.AsUInt16(), source1.AsUInt16());+Vector128<byte> packed1 = Default.PackSources(source0.AsUInt16() - offset, source1.AsUInt16() - offset);+Vector128<byte> result0 = IndexOfAnyLookupCore(packed0, bitmapLookup0);+Vector128<byte> result1 = IndexOfAnyLookupCore(packed1, bitmapLookup1);+return TNegator.NegateIfNeeded(result0 | result1);

(All the numbers below are using the Avx2 path of the new implementation -- measured before #103710)

Avx2 where the text is mostly non-ASCII (previous 'Mixed' would use the prob map / scalar fallback)
MethodToolchainLengthMeanErrorRatio
Asciimain100038.13 ns0.129 ns1.00
Asciipr100037.32 ns0.160 ns0.98
Mixedmain1000127.47 ns0.429 ns1.00
Mixedpr100051.20 ns0.095 ns0.40
AsciiLastmain100035.82 ns0.264 ns1.00
AsciiLastpr100035.81 ns0.079 ns1.00
MixedLastmain1000138.47 ns0.335 ns1.00
MixedLastpr100052.36 ns0.138 ns0.38
AsciiExceptmain100035.40 ns0.138 ns1.00
AsciiExceptpr100035.47 ns0.078 ns1.00
MixedExceptmain1000592.08 ns1.005 ns1.00
MixedExceptpr100051.00 ns0.133 ns0.09
AsciiLastExceptmain100036.77 ns0.535 ns1.00
AsciiLastExceptpr100036.34 ns0.086 ns0.99
MixedLastExceptmain1000682.31 ns8.073 ns1.00
MixedLastExceptpr100053.97 ns1.372 ns0.08
Avx512 machine (the probabilistic map is a lot faster than on Avx2)
MethodToolchainLengthMeanErrorRatio
Asciimain100037.44 ns0.006 ns1.00
Asciipr100037.73 ns0.005 ns1.01
Mixedmain100086.86 ns0.111 ns1.00
Mixedpr100060.36 ns0.010 ns0.69
AsciiLastmain100038.02 ns0.013 ns1.00
AsciiLastpr100038.28 ns0.003 ns1.01
MixedLastmain100095.96 ns0.023 ns1.00
MixedLastpr100061.16 ns0.009 ns0.64
AsciiExceptmain100040.17 ns0.005 ns1.00
AsciiExceptpr100039.81 ns0.005 ns0.99
MixedExceptmain1000585.99 ns0.183 ns1.00
MixedExceptpr100063.22 ns0.010 ns0.11
AsciiLastExceptmain100041.54 ns0.003 ns1.00
AsciiLastExceptpr100041.54 ns0.005 ns1.00
MixedLastExceptmain1000855.18 ns0.059 ns1.00
MixedLastExceptpr100064.65 ns0.010 ns0.08
Early matches
MethodToolchainInputContainsNonAsciiMeanErrorRatio
MixedmainFalse5.130 ns0.0374 ns1.00
MixedprFalse3.425 ns0.0303 ns0.67
MixedLastmainFalse4.830 ns0.0182 ns1.00
MixedLastprFalse4.179 ns0.0750 ns0.87
MixedmainTrue8.440 ns0.0780 ns1.00
MixedprTrue3.383 ns0.0163 ns0.40
MixedLastmainTrue9.011 ns0.2175 ns1.00
MixedLastprTrue4.073 ns0.0087 ns0.45

From that, we can see that the Ascii-only search is at ~1.5x the throughput of the two bitmaps impl.
The two bitmaps have ~1.5x the throughput of the probabilistic map on Avx512 and ~2.5x on Avx2.
In other words, this change is a throughput regression for ProbabilisticWithAsciiCharSearchValues if the text is all ASCII, and an improvement otherwise. It's always chepaer for early matches though.
For the -Except paths where ProbabilisticWithAsciiCharSearchValues uses a scalar fallback, the two bitmaps approach is obviously a lot (10x+) faster.

The change doesn't seem to impact existing Regex benchmarks much, likely because they're very focused on ASCII.


I also tried different implementation approaches to try and reduce code duplication between the existing Ascii and "ascii with second set" implementations:

Details
  • c6f1495, combining the second set into the existing AsciiState, which does save some duplication, but increases the memory consumption of all existing Ascii-only SearchValues.
  • 32f9cf1, that goes all-in with generics, but the JIT can't quite deal with having the vector state be completely generic.
    publicstaticintIndexOfAny<TNegator,TOptimizations>(refshortsearchSpace,intsearchSpaceLength,refAsciiWithSecondSetStatestate)whereTNegator:struct,INegatorwhereTOptimizations:struct,IOptimizations=>IndexOfAnyCore<int,TNegator,IndexOfAnyResultMapper<short>,AsciiWithSecondSetLookup<TOptimizations>,AsciiWithSecondSetState,(Vector128<byte>AsciiBitmap,Vector128<byte>SecondBitmap,Vector128<ushort>Offset),(Vector256<byte>AsciiBitmap,Vector256<byte>SecondBitmap,Vector256<ushort>Offset)>(refsearchSpace,searchSpaceLength,refstate);privatestaticTResultIndexOfAnyCore<TResult,TNegator,TResultMapper,TLookup,TState,TVector128State,TVector256State>(refshortsearchSpace,intsearchSpaceLength,refTStatestate)whereTResult:structwhereTNegator:struct,INegatorwhereTResultMapper:struct,IResultMapper<short,TResult>whereTLookup:struct,ILookup<TState,TVector128State,TVector256State>whereTState:structwhereTVector128State:structwhereTVector256State:struct

Looking at patterns from Regex_RealWorldPatterns.json, ~75% of non-ASCII sets would use the new implementation over the Ascii+ProbMap, most of which because of the kelvin sign.

Number of sources: 18886
numberOfSearchValues
5771
numberOfSearchValuesWithNonAscii
1238
numberOfSearchValuesWithTwoSets
960
numberOfSearchValuesWhereNonAsciiIsKelvin
684
numberOfPatternsWithSearchValues
5093
numberOfPatternsWithSearchValuesWithNonAscii
1038
numberOfPatternsWithSearchValuesWithTwoSets
765

Parsed data for the above:
Regex_RealWorldPatterns.SearchValues.json

@MihaZupanMihaZupan added this to the 9.0.0 milestone Jun 10, 2024
@MihaZupanMihaZupan self-assigned this Jun 10, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

@danmoseley

Copy link
Copy Markdown
Contributor

The change doesn't seem to impact existing Regex benchmarks much, likely because they're very focused on ASCII.

is it worth adding one or two more non ASCII?

@MihaZupan

This comment was marked as outdated.

@MihuBot

This comment was marked as outdated.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@MihuBot benchmark RustLang_Sherlock https://github.com/MihaZupan/performance/tree/compiled-regex-only -medium

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@MihuBot fuzz SearchValues

@MihaZupan

This comment was marked as outdated.

@EgorBo

Copy link
Copy Markdown
Member

@MihaZupan you need to either omit Run<> or pass args to Run, so in your case:

BenchmarkRunner.Run<Bench>(args);

Otherwise --corerun /base/corerun /diff/corerun args will be ignored 🙂

@MihaZupan

MihaZupan commented Jun 22, 2024

Copy link
Copy Markdown
MemberAuthor

Aaah right, thanks. I also forgot to remove the ShortRunJob.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@EgorBot -intel

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Buffers;
#nullable disable
publicclassBench{privatestaticreadonlySearchValues<char>_allowedAscii=SearchValues.Create("1234567890abcdefghijklmnopqrstuvwxyz");privatestaticreadonlySearchValues<char>_allowedMixed=SearchValues.Create("äöü1234567890abcdefghijklmnopqrstuvw");privatestring_asciiInput;privatestring_mixedInput;[Params(16,100,10_000)]publicintLength;[GlobalSetup]publicvoidSetup(){_asciiInput=newstring('a',Length);_mixedInput='ä'+newstring('a',Length-1);}[Benchmark]publicboolContainsOnlyAscii()=>!_asciiInput.AsSpan().ContainsAnyExcept(_allowedAscii);[Benchmark]publicboolContainsOnlyMixed()=>!_mixedInput.AsSpan().ContainsAnyExcept(_allowedMixed);}

@EgorBot

This comment was marked as outdated.

@EgorBot

Copy link
Copy Markdown
Benchmark results on Intel
BenchmarkDotNet v0.13.12, Ubuntu 22.04.4 LTS (Jammy Jellyfish)
Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 16 logical and 8 physical cores
Job-SZXNIX : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Job-DHGYYU : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
MethodToolchainLengthMeanErrorRatio
ContainsOnlyAsciiMain162.434 ns0.0115 ns1.00
ContainsOnlyAsciiPR162.434 ns0.0063 ns1.00
ContainsOnlyMixedMain1612.207 ns0.0035 ns1.00
ContainsOnlyMixedPR163.184 ns0.0004 ns0.26
ContainsOnlyAsciiMain1004.251 ns0.0005 ns1.00
ContainsOnlyAsciiPR1004.251 ns0.0004 ns1.00
ContainsOnlyMixedMain10062.381 ns0.0320 ns1.00
ContainsOnlyMixedPR1005.499 ns0.0020 ns0.09
ContainsOnlyAsciiMain10000208.463 ns0.0200 ns1.00
ContainsOnlyAsciiPR10000208.571 ns0.0259 ns1.00
ContainsOnlyMixedMain100005,783.502 ns1.4382 ns1.00
ContainsOnlyMixedPR10000391.050 ns0.0232 ns0.07

BDN_Artifacts.zip

@MihuBot

Copy link
Copy Markdown
System.Text.RegularExpressions.Tests.Perf_Regex_Industry_RustLang_Sherlock
BenchmarkDotNet v0.13.13-nightly.20240311.145, Ubuntu 22.04.4 LTS (Jammy Jellyfish)
AMD EPYC 9V74, 1 CPU, 8 logical and 4 physical cores
MediumRun : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Job=MediumRun OutlierMode=DontRemove IterationCount=15
LaunchCount=2 MemoryRandomization=True WarmupCount=10
MethodToolchainPatternMeanErrorRatioAllocatedAlloc Ratio
CountMain.*577,117.72 ns1,898.483 ns1.002 B1.00
CountPR.*609,940.38 ns5,541.168 ns1.062 B1.00
CountMain(?i)Holmes53,669.80 ns163.206 ns1.00-NA
CountPR(?i)Holmes53,751.77 ns95.378 ns1.00-NA
CountMain(?i)Sher[a-z]+|Hol[a-z]+96,797.28 ns7,445.773 ns1.01-NA
CountPR(?i)Sher[a-z]+|Hol[a-z]+101,675.49 ns7,778.427 ns1.06-NA
CountMain(?i)Sherlock45,540.09 ns158.021 ns1.00-NA
CountPR(?i)Sherlock45,699.65 ns198.254 ns1.00-NA
CountMain(?i)Sherlock Holmes45,299.62 ns140.891 ns1.00-NA
CountPR(?i)Sherlock Holmes45,322.99 ns60.249 ns1.00-NA
CountMain(?i)Sherlock|Holmes|Watson98,222.98 ns9,160.202 ns1.02-NA
CountPR(?i)Sherlock|Holmes|Watson98,387.61 ns9,077.154 ns1.02-NA
CountMain(?i)Sherlock|(...)er|John|Baker [49]210,972.18 ns25,319.086 ns1.031 B1.00
CountPR(?i)Sherlock|(...)er|John|Baker [49]216,838.46 ns27,873.222 ns1.061 B1.00
CountMain(?i)the249,601.71 ns10,074.303 ns1.001 B1.00
CountPR(?i)the245,957.03 ns10,337.071 ns0.991 B1.00
CountMain(?m)^Sherlock(...)rlock Holmes$ [37]58,240.97 ns2,237.819 ns1.00-NA
CountPR(?m)^Sherlock(...)rlock Holmes$ [37]59,147.93 ns2,749.661 ns1.02-NA
CountMain(?s).*39.26 ns0.088 ns1.00-NA
CountPR(?s).*41.27 ns1.728 ns1.05-NA
CountMain[^\\n]*576,131.95 ns2,377.308 ns1.002 B1.00
CountPR[^\\n]*577,114.20 ns4,299.926 ns1.002 B1.00
CountMain[a-q][^u-z]{13}x23,158.57 ns113.019 ns1.00-NA
CountPR[a-q][^u-z]{13}x23,147.72 ns90.268 ns1.00-NA
CountMain[a-zA-Z]+ing4,112,765.60 ns6,996.218 ns1.0019 B1.00
CountPR[a-zA-Z]+ing4,195,122.65 ns50,241.289 ns1.0221 B1.11
CountMain\b\w+n\b8,324,707.71 ns18,654.897 ns1.0044 B1.00
CountPR\b\w+n\b8,419,875.57 ns67,893.821 ns1.0144 B1.00
CountMain\p{L}10,252,616.17 ns258,692.197 ns1.0035 B1.00
CountPR\p{L}10,178,460.27 ns120,522.988 ns0.9935 B1.00
CountMain\p{Ll}10,218,675.98 ns78,494.108 ns1.0035 B1.00
CountPR\p{Ll}11,001,606.18 ns501,912.621 ns1.0835 B1.00
CountMain\p{Lu}355,667.38 ns7,908.522 ns1.001 B1.00
CountPR\p{Lu}348,108.98 ns3,458.353 ns0.981 B1.00
CountMain\s[a-zA-Z]{0,12}ing\s4,387,091.94 ns11,384.876 ns1.0024 B1.00
CountPR\s[a-zA-Z]{0,12}ing\s4,404,995.88 ns9,088.263 ns1.0024 B1.00
CountMain\w+4,712,257.79 ns27,825.642 ns1.0018 B1.00
CountPR\w+4,671,552.17 ns7,259.567 ns0.9921 B1.17
CountMain\w+\s+Holmes3,340,654.11 ns10,688.067 ns1.0011 B1.00
CountPR\w+\s+Holmes3,355,149.72 ns15,672.577 ns1.0010 B0.91
CountMain\w+\s+Holmes\s+\w+3,609,649.22 ns65,235.529 ns1.0010 B1.00
CountPR\w+\s+Holmes\s+\w+3,502,413.34 ns60,104.536 ns0.9712 B1.20
CountMainaei38,764.39 ns528.311 ns1.00-NA
CountPRaei38,671.05 ns532.363 ns1.00-NA
CountMainaqj38,552.12 ns579.654 ns1.00-NA
CountPRaqj38,708.25 ns516.758 ns1.00-NA
CountMainHolmes50,202.09 ns78.982 ns1.00-NA
CountPRHolmes50,229.34 ns115.917 ns1.00-NA
CountMainHolmes.{0,25}(...).{0,25}Holmes [39]44,351.33 ns90.615 ns1.00-NA
CountPRHolmes.{0,25}(...).{0,25}Holmes [39]44,416.66 ns119.101 ns1.00-NA
CountMainSher[a-z]+|Hol[a-z]+48,711.02 ns113.935 ns1.00-NA
CountPRSher[a-z]+|Hol[a-z]+48,852.79 ns227.029 ns1.00-NA
CountMainSherlock58,662.58 ns2,239.132 ns1.00-NA
CountPRSherlock59,492.49 ns2,839.858 ns1.02-NA
CountMainSherlock Holmes59,536.37 ns2,887.051 ns1.01-NA
CountPRSherlock Holmes59,734.87 ns2,760.671 ns1.01-NA
CountMainSherlock\s+Holmes59,934.57 ns2,381.314 ns1.00-NA
CountPRSherlock\s+Holmes60,715.04 ns3,024.286 ns1.02-NA
CountMainSherlock|Holmes44,782.94 ns106.657 ns1.00-NA
CountPRSherlock|Holmes44,866.33 ns114.894 ns1.00-NA
CountMainSherlock|Holmes|Watson58,630.95 ns77.246 ns1.00-NA
CountPRSherlock|Holmes|Watson59,046.70 ns110.269 ns1.01-NA
CountMainSherlock|Holm(...)er|John|Baker [45]109,855.32 ns133.155 ns1.00-NA
CountPRSherlock|Holm(...)er|John|Baker [45]109,887.13 ns89.847 ns1.00-NA
CountMainSherlock|Street25,047.25 ns62.195 ns1.00-NA
CountPRSherlock|Street25,004.97 ns85.354 ns1.00-NA
CountMainthe179,288.74 ns632.033 ns1.001 B1.00
CountPRthe179,042.42 ns643.054 ns1.001 B1.00
CountMainThe54,675.24 ns84.398 ns1.00-NA
CountPRThe54,504.36 ns156.081 ns1.00-NA
CountMainthe\s+\w+282,854.54 ns12,723.264 ns1.001 B1.00
CountPRthe\s+\w+288,927.85 ns11,924.621 ns1.031 B1.00
CountMainzqj38,776.08 ns523.611 ns1.00-NA
CountPRzqj38,694.38 ns541.346 ns1.00-NA

Vector512<byte> secondBitmap512 = state.SecondBitmap512;
Vector512<ushort> offset512 = Vector512.Create(state.Offset);

if (searchSpaceLength > 2 * Vector512<short>.Count)

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.

@GrabYourPitchforks, in the UTF8 experiment you shared with me, you had code that, after having validated that Vector256 was hardware accelerated, aligned an address and then read a full vector, without concern for whether that vector read under or overread the target region. Is that safe to do on all platforms? It seems we could avoid some branching with similar techniques in many of our implementations that use vectorization.

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.

Is that safe to do on all platforms?

As long as the data is pinned, otherwise GC can interrupt at any point and break the alignment assumption

@MihaZupan
MihaZupanforce-pushed the searchvalues-asciiWithSecondSet branch 2 times, most recently from f378aca to e747fd0CompareJuly 10, 2024 22:48
@stephentoub

Copy link
Copy Markdown
Member

the Ascii-only search is at ~1.5x the throughput of the two bitmaps impl.

Does this mean someone searching mostly ASCII text with IgnoreCase for [a-z] will see a 50% regression?

@MihaZupan

MihaZupan commented Jul 11, 2024

Copy link
Copy Markdown
MemberAuthor

Does this mean someone searching mostly ASCII text with IgnoreCase for [a-z] will see a 50% regression?

If you had a long run of text that was just ASCII and didn't match, yes, it would be slower.
But if you were stopping on matches along the way (even if they were ASCII), the two sets can be cheaper due to the lower overhead on matches (mainly since there's an extra method involved in the ascii+probmap implementation).

I reran Sherlock, and the throughput difference doesn't seem to be affecting it: MihuBot/runtime-utils#505 (comment)

We could also choose to use the two bitmaps approach only on -Except paths to avoid the scalar fallbacks.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

Any concerns with merging this one as-is and seeing if benchmarks complain?

@stephentoub

Copy link
Copy Markdown
Member

Ok, let's give it a try but be ready to back it out if any meaningful regressions pop up.

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.

6 participants

@MihaZupan@danmoseley@MihuBot@EgorBo@EgorBot@stephentoub
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add SearchValues<char> implementation for two sets of 128 chars - #103216

Merged
stephentoub merged 4 commits into
dotnet:mainfrom
MihaZupan:searchvalues-asciiWithSecondSet
Jul 22, 2024
Merged

Add SearchValues<char> implementation for two sets of 128 chars#103216
stephentoub merged 4 commits into
dotnet:mainfrom
MihaZupan:searchvalues-asciiWithSecondSet

Conversation

@MihaZupan

@MihaZupanMihaZupan commented Jun 10, 2024

Copy link
Copy Markdown
Member

#101001 significantly improved the performance of the non-vectorized -Except paths of non-ASCII SearchValues<char>.
However, they are still not vectorized, and this PR changes that for values where the non-ASCII part can fit into a 128-bit bitmap.

This adds an implementation that's almost the same as the AsciiCharSearchValues, but where the core lookup checks against two 128-bit bitmaps, with the second one at a variable offset:

-Vector128<byte> source = TOptimizations.PackSources(source0.AsUInt16(), source1.AsUInt16());-Vector128<byte> result = IndexOfAnyLookupCore(source, bitmapLookup);-return TNegator.NegateIfNeeded(result);+Vector128<byte> packed0 = TOptimizations.PackSources(source0.AsUInt16(), source1.AsUInt16());+Vector128<byte> packed1 = Default.PackSources(source0.AsUInt16() - offset, source1.AsUInt16() - offset);+Vector128<byte> result0 = IndexOfAnyLookupCore(packed0, bitmapLookup0);+Vector128<byte> result1 = IndexOfAnyLookupCore(packed1, bitmapLookup1);+return TNegator.NegateIfNeeded(result0 | result1);

(All the numbers below are using the Avx2 path of the new implementation -- measured before #103710)

Avx2 where the text is mostly non-ASCII (previous 'Mixed' would use the prob map / scalar fallback)
MethodToolchainLengthMeanErrorRatio
Asciimain100038.13 ns0.129 ns1.00
Asciipr100037.32 ns0.160 ns0.98
Mixedmain1000127.47 ns0.429 ns1.00
Mixedpr100051.20 ns0.095 ns0.40
AsciiLastmain100035.82 ns0.264 ns1.00
AsciiLastpr100035.81 ns0.079 ns1.00
MixedLastmain1000138.47 ns0.335 ns1.00
MixedLastpr100052.36 ns0.138 ns0.38
AsciiExceptmain100035.40 ns0.138 ns1.00
AsciiExceptpr100035.47 ns0.078 ns1.00
MixedExceptmain1000592.08 ns1.005 ns1.00
MixedExceptpr100051.00 ns0.133 ns0.09
AsciiLastExceptmain100036.77 ns0.535 ns1.00
AsciiLastExceptpr100036.34 ns0.086 ns0.99
MixedLastExceptmain1000682.31 ns8.073 ns1.00
MixedLastExceptpr100053.97 ns1.372 ns0.08
Avx512 machine (the probabilistic map is a lot faster than on Avx2)
MethodToolchainLengthMeanErrorRatio
Asciimain100037.44 ns0.006 ns1.00
Asciipr100037.73 ns0.005 ns1.01
Mixedmain100086.86 ns0.111 ns1.00
Mixedpr100060.36 ns0.010 ns0.69
AsciiLastmain100038.02 ns0.013 ns1.00
AsciiLastpr100038.28 ns0.003 ns1.01
MixedLastmain100095.96 ns0.023 ns1.00
MixedLastpr100061.16 ns0.009 ns0.64
AsciiExceptmain100040.17 ns0.005 ns1.00
AsciiExceptpr100039.81 ns0.005 ns0.99
MixedExceptmain1000585.99 ns0.183 ns1.00
MixedExceptpr100063.22 ns0.010 ns0.11
AsciiLastExceptmain100041.54 ns0.003 ns1.00
AsciiLastExceptpr100041.54 ns0.005 ns1.00
MixedLastExceptmain1000855.18 ns0.059 ns1.00
MixedLastExceptpr100064.65 ns0.010 ns0.08
Early matches
MethodToolchainInputContainsNonAsciiMeanErrorRatio
MixedmainFalse5.130 ns0.0374 ns1.00
MixedprFalse3.425 ns0.0303 ns0.67
MixedLastmainFalse4.830 ns0.0182 ns1.00
MixedLastprFalse4.179 ns0.0750 ns0.87
MixedmainTrue8.440 ns0.0780 ns1.00
MixedprTrue3.383 ns0.0163 ns0.40
MixedLastmainTrue9.011 ns0.2175 ns1.00
MixedLastprTrue4.073 ns0.0087 ns0.45

From that, we can see that the Ascii-only search is at ~1.5x the throughput of the two bitmaps impl.
The two bitmaps have ~1.5x the throughput of the probabilistic map on Avx512 and ~2.5x on Avx2.
In other words, this change is a throughput regression for ProbabilisticWithAsciiCharSearchValues if the text is all ASCII, and an improvement otherwise. It's always chepaer for early matches though.
For the -Except paths where ProbabilisticWithAsciiCharSearchValues uses a scalar fallback, the two bitmaps approach is obviously a lot (10x+) faster.

The change doesn't seem to impact existing Regex benchmarks much, likely because they're very focused on ASCII.


I also tried different implementation approaches to try and reduce code duplication between the existing Ascii and "ascii with second set" implementations:

Details
  • c6f1495, combining the second set into the existing AsciiState, which does save some duplication, but increases the memory consumption of all existing Ascii-only SearchValues.
  • 32f9cf1, that goes all-in with generics, but the JIT can't quite deal with having the vector state be completely generic.
    publicstaticintIndexOfAny<TNegator,TOptimizations>(refshortsearchSpace,intsearchSpaceLength,refAsciiWithSecondSetStatestate)whereTNegator:struct,INegatorwhereTOptimizations:struct,IOptimizations=>IndexOfAnyCore<int,TNegator,IndexOfAnyResultMapper<short>,AsciiWithSecondSetLookup<TOptimizations>,AsciiWithSecondSetState,(Vector128<byte>AsciiBitmap,Vector128<byte>SecondBitmap,Vector128<ushort>Offset),(Vector256<byte>AsciiBitmap,Vector256<byte>SecondBitmap,Vector256<ushort>Offset)>(refsearchSpace,searchSpaceLength,refstate);privatestaticTResultIndexOfAnyCore<TResult,TNegator,TResultMapper,TLookup,TState,TVector128State,TVector256State>(refshortsearchSpace,intsearchSpaceLength,refTStatestate)whereTResult:structwhereTNegator:struct,INegatorwhereTResultMapper:struct,IResultMapper<short,TResult>whereTLookup:struct,ILookup<TState,TVector128State,TVector256State>whereTState:structwhereTVector128State:structwhereTVector256State:struct

Looking at patterns from Regex_RealWorldPatterns.json, ~75% of non-ASCII sets would use the new implementation over the Ascii+ProbMap, most of which because of the kelvin sign.

Number of sources: 18886
numberOfSearchValues
5771
numberOfSearchValuesWithNonAscii
1238
numberOfSearchValuesWithTwoSets
960
numberOfSearchValuesWhereNonAsciiIsKelvin
684
numberOfPatternsWithSearchValues
5093
numberOfPatternsWithSearchValuesWithNonAscii
1038
numberOfPatternsWithSearchValuesWithTwoSets
765

Parsed data for the above:
Regex_RealWorldPatterns.SearchValues.json

@MihaZupanMihaZupan added this to the 9.0.0 milestone Jun 10, 2024
@MihaZupanMihaZupan self-assigned this Jun 10, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

@danmoseley

Copy link
Copy Markdown
Contributor

The change doesn't seem to impact existing Regex benchmarks much, likely because they're very focused on ASCII.

is it worth adding one or two more non ASCII?

@MihaZupan

This comment was marked as outdated.

@MihuBot

This comment was marked as outdated.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@MihuBot benchmark RustLang_Sherlock https://github.com/MihaZupan/performance/tree/compiled-regex-only -medium

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@MihuBot fuzz SearchValues

@MihaZupan

This comment was marked as outdated.

@EgorBo

Copy link
Copy Markdown
Member

@MihaZupan you need to either omit Run<> or pass args to Run, so in your case:

BenchmarkRunner.Run<Bench>(args);

Otherwise --corerun /base/corerun /diff/corerun args will be ignored 🙂

@MihaZupan

MihaZupan commented Jun 22, 2024

Copy link
Copy Markdown
MemberAuthor

Aaah right, thanks. I also forgot to remove the ShortRunJob.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@EgorBot -intel

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Buffers;
#nullable disable
publicclassBench{privatestaticreadonlySearchValues<char>_allowedAscii=SearchValues.Create("1234567890abcdefghijklmnopqrstuvwxyz");privatestaticreadonlySearchValues<char>_allowedMixed=SearchValues.Create("äöü1234567890abcdefghijklmnopqrstuvw");privatestring_asciiInput;privatestring_mixedInput;[Params(16,100,10_000)]publicintLength;[GlobalSetup]publicvoidSetup(){_asciiInput=newstring('a',Length);_mixedInput='ä'+newstring('a',Length-1);}[Benchmark]publicboolContainsOnlyAscii()=>!_asciiInput.AsSpan().ContainsAnyExcept(_allowedAscii);[Benchmark]publicboolContainsOnlyMixed()=>!_mixedInput.AsSpan().ContainsAnyExcept(_allowedMixed);}

@EgorBot

This comment was marked as outdated.

@EgorBot

Copy link
Copy Markdown
Benchmark results on Intel
BenchmarkDotNet v0.13.12, Ubuntu 22.04.4 LTS (Jammy Jellyfish)
Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 16 logical and 8 physical cores
Job-SZXNIX : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Job-DHGYYU : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
MethodToolchainLengthMeanErrorRatio
ContainsOnlyAsciiMain162.434 ns0.0115 ns1.00
ContainsOnlyAsciiPR162.434 ns0.0063 ns1.00
ContainsOnlyMixedMain1612.207 ns0.0035 ns1.00
ContainsOnlyMixedPR163.184 ns0.0004 ns0.26
ContainsOnlyAsciiMain1004.251 ns0.0005 ns1.00
ContainsOnlyAsciiPR1004.251 ns0.0004 ns1.00
ContainsOnlyMixedMain10062.381 ns0.0320 ns1.00
ContainsOnlyMixedPR1005.499 ns0.0020 ns0.09
ContainsOnlyAsciiMain10000208.463 ns0.0200 ns1.00
ContainsOnlyAsciiPR10000208.571 ns0.0259 ns1.00
ContainsOnlyMixedMain100005,783.502 ns1.4382 ns1.00
ContainsOnlyMixedPR10000391.050 ns0.0232 ns0.07

BDN_Artifacts.zip

@MihuBot

Copy link
Copy Markdown
System.Text.RegularExpressions.Tests.Perf_Regex_Industry_RustLang_Sherlock
BenchmarkDotNet v0.13.13-nightly.20240311.145, Ubuntu 22.04.4 LTS (Jammy Jellyfish)
AMD EPYC 9V74, 1 CPU, 8 logical and 4 physical cores
MediumRun : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Job=MediumRun OutlierMode=DontRemove IterationCount=15
LaunchCount=2 MemoryRandomization=True WarmupCount=10
MethodToolchainPatternMeanErrorRatioAllocatedAlloc Ratio
CountMain.*577,117.72 ns1,898.483 ns1.002 B1.00
CountPR.*609,940.38 ns5,541.168 ns1.062 B1.00
CountMain(?i)Holmes53,669.80 ns163.206 ns1.00-NA
CountPR(?i)Holmes53,751.77 ns95.378 ns1.00-NA
CountMain(?i)Sher[a-z]+|Hol[a-z]+96,797.28 ns7,445.773 ns1.01-NA
CountPR(?i)Sher[a-z]+|Hol[a-z]+101,675.49 ns7,778.427 ns1.06-NA
CountMain(?i)Sherlock45,540.09 ns158.021 ns1.00-NA
CountPR(?i)Sherlock45,699.65 ns198.254 ns1.00-NA
CountMain(?i)Sherlock Holmes45,299.62 ns140.891 ns1.00-NA
CountPR(?i)Sherlock Holmes45,322.99 ns60.249 ns1.00-NA
CountMain(?i)Sherlock|Holmes|Watson98,222.98 ns9,160.202 ns1.02-NA
CountPR(?i)Sherlock|Holmes|Watson98,387.61 ns9,077.154 ns1.02-NA
CountMain(?i)Sherlock|(...)er|John|Baker [49]210,972.18 ns25,319.086 ns1.031 B1.00
CountPR(?i)Sherlock|(...)er|John|Baker [49]216,838.46 ns27,873.222 ns1.061 B1.00
CountMain(?i)the249,601.71 ns10,074.303 ns1.001 B1.00
CountPR(?i)the245,957.03 ns10,337.071 ns0.991 B1.00
CountMain(?m)^Sherlock(...)rlock Holmes$ [37]58,240.97 ns2,237.819 ns1.00-NA
CountPR(?m)^Sherlock(...)rlock Holmes$ [37]59,147.93 ns2,749.661 ns1.02-NA
CountMain(?s).*39.26 ns0.088 ns1.00-NA
CountPR(?s).*41.27 ns1.728 ns1.05-NA
CountMain[^\\n]*576,131.95 ns2,377.308 ns1.002 B1.00
CountPR[^\\n]*577,114.20 ns4,299.926 ns1.002 B1.00
CountMain[a-q][^u-z]{13}x23,158.57 ns113.019 ns1.00-NA
CountPR[a-q][^u-z]{13}x23,147.72 ns90.268 ns1.00-NA
CountMain[a-zA-Z]+ing4,112,765.60 ns6,996.218 ns1.0019 B1.00
CountPR[a-zA-Z]+ing4,195,122.65 ns50,241.289 ns1.0221 B1.11
CountMain\b\w+n\b8,324,707.71 ns18,654.897 ns1.0044 B1.00
CountPR\b\w+n\b8,419,875.57 ns67,893.821 ns1.0144 B1.00
CountMain\p{L}10,252,616.17 ns258,692.197 ns1.0035 B1.00
CountPR\p{L}10,178,460.27 ns120,522.988 ns0.9935 B1.00
CountMain\p{Ll}10,218,675.98 ns78,494.108 ns1.0035 B1.00
CountPR\p{Ll}11,001,606.18 ns501,912.621 ns1.0835 B1.00
CountMain\p{Lu}355,667.38 ns7,908.522 ns1.001 B1.00
CountPR\p{Lu}348,108.98 ns3,458.353 ns0.981 B1.00
CountMain\s[a-zA-Z]{0,12}ing\s4,387,091.94 ns11,384.876 ns1.0024 B1.00
CountPR\s[a-zA-Z]{0,12}ing\s4,404,995.88 ns9,088.263 ns1.0024 B1.00
CountMain\w+4,712,257.79 ns27,825.642 ns1.0018 B1.00
CountPR\w+4,671,552.17 ns7,259.567 ns0.9921 B1.17
CountMain\w+\s+Holmes3,340,654.11 ns10,688.067 ns1.0011 B1.00
CountPR\w+\s+Holmes3,355,149.72 ns15,672.577 ns1.0010 B0.91
CountMain\w+\s+Holmes\s+\w+3,609,649.22 ns65,235.529 ns1.0010 B1.00
CountPR\w+\s+Holmes\s+\w+3,502,413.34 ns60,104.536 ns0.9712 B1.20
CountMainaei38,764.39 ns528.311 ns1.00-NA
CountPRaei38,671.05 ns532.363 ns1.00-NA
CountMainaqj38,552.12 ns579.654 ns1.00-NA
CountPRaqj38,708.25 ns516.758 ns1.00-NA
CountMainHolmes50,202.09 ns78.982 ns1.00-NA
CountPRHolmes50,229.34 ns115.917 ns1.00-NA
CountMainHolmes.{0,25}(...).{0,25}Holmes [39]44,351.33 ns90.615 ns1.00-NA
CountPRHolmes.{0,25}(...).{0,25}Holmes [39]44,416.66 ns119.101 ns1.00-NA
CountMainSher[a-z]+|Hol[a-z]+48,711.02 ns113.935 ns1.00-NA
CountPRSher[a-z]+|Hol[a-z]+48,852.79 ns227.029 ns1.00-NA
CountMainSherlock58,662.58 ns2,239.132 ns1.00-NA
CountPRSherlock59,492.49 ns2,839.858 ns1.02-NA
CountMainSherlock Holmes59,536.37 ns2,887.051 ns1.01-NA
CountPRSherlock Holmes59,734.87 ns2,760.671 ns1.01-NA
CountMainSherlock\s+Holmes59,934.57 ns2,381.314 ns1.00-NA
CountPRSherlock\s+Holmes60,715.04 ns3,024.286 ns1.02-NA
CountMainSherlock|Holmes44,782.94 ns106.657 ns1.00-NA
CountPRSherlock|Holmes44,866.33 ns114.894 ns1.00-NA
CountMainSherlock|Holmes|Watson58,630.95 ns77.246 ns1.00-NA
CountPRSherlock|Holmes|Watson59,046.70 ns110.269 ns1.01-NA
CountMainSherlock|Holm(...)er|John|Baker [45]109,855.32 ns133.155 ns1.00-NA
CountPRSherlock|Holm(...)er|John|Baker [45]109,887.13 ns89.847 ns1.00-NA
CountMainSherlock|Street25,047.25 ns62.195 ns1.00-NA
CountPRSherlock|Street25,004.97 ns85.354 ns1.00-NA
CountMainthe179,288.74 ns632.033 ns1.001 B1.00
CountPRthe179,042.42 ns643.054 ns1.001 B1.00
CountMainThe54,675.24 ns84.398 ns1.00-NA
CountPRThe54,504.36 ns156.081 ns1.00-NA
CountMainthe\s+\w+282,854.54 ns12,723.264 ns1.001 B1.00
CountPRthe\s+\w+288,927.85 ns11,924.621 ns1.031 B1.00
CountMainzqj38,776.08 ns523.611 ns1.00-NA
CountPRzqj38,694.38 ns541.346 ns1.00-NA

Vector512<byte> secondBitmap512 = state.SecondBitmap512;
Vector512<ushort> offset512 = Vector512.Create(state.Offset);

if (searchSpaceLength > 2 * Vector512<short>.Count)

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.

@GrabYourPitchforks, in the UTF8 experiment you shared with me, you had code that, after having validated that Vector256 was hardware accelerated, aligned an address and then read a full vector, without concern for whether that vector read under or overread the target region. Is that safe to do on all platforms? It seems we could avoid some branching with similar techniques in many of our implementations that use vectorization.

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.

Is that safe to do on all platforms?

As long as the data is pinned, otherwise GC can interrupt at any point and break the alignment assumption

@MihaZupan
MihaZupanforce-pushed the searchvalues-asciiWithSecondSet branch 2 times, most recently from f378aca to e747fd0CompareJuly 10, 2024 22:48
@stephentoub

Copy link
Copy Markdown
Member

the Ascii-only search is at ~1.5x the throughput of the two bitmaps impl.

Does this mean someone searching mostly ASCII text with IgnoreCase for [a-z] will see a 50% regression?

@MihaZupan

MihaZupan commented Jul 11, 2024

Copy link
Copy Markdown
MemberAuthor

Does this mean someone searching mostly ASCII text with IgnoreCase for [a-z] will see a 50% regression?

If you had a long run of text that was just ASCII and didn't match, yes, it would be slower.
But if you were stopping on matches along the way (even if they were ASCII), the two sets can be cheaper due to the lower overhead on matches (mainly since there's an extra method involved in the ascii+probmap implementation).

I reran Sherlock, and the throughput difference doesn't seem to be affecting it: MihuBot/runtime-utils#505 (comment)

We could also choose to use the two bitmaps approach only on -Except paths to avoid the scalar fallbacks.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

Any concerns with merging this one as-is and seeing if benchmarks complain?

@stephentoub

Copy link
Copy Markdown
Member

Ok, let's give it a try but be ready to back it out if any meaningful regressions pop up.

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.

6 participants

@MihaZupan@danmoseley@MihuBot@EgorBo@EgorBot@stephentoub
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add SearchValues<char> implementation for two sets of 128 chars - #103216

Merged
stephentoub merged 4 commits into
dotnet:mainfrom
MihaZupan:searchvalues-asciiWithSecondSet
Jul 22, 2024
Merged

Add SearchValues<char> implementation for two sets of 128 chars#103216
stephentoub merged 4 commits into
dotnet:mainfrom
MihaZupan:searchvalues-asciiWithSecondSet

Conversation

@MihaZupan

@MihaZupanMihaZupan commented Jun 10, 2024

Copy link
Copy Markdown
Member

#101001 significantly improved the performance of the non-vectorized -Except paths of non-ASCII SearchValues<char>.
However, they are still not vectorized, and this PR changes that for values where the non-ASCII part can fit into a 128-bit bitmap.

This adds an implementation that's almost the same as the AsciiCharSearchValues, but where the core lookup checks against two 128-bit bitmaps, with the second one at a variable offset:

-Vector128<byte> source = TOptimizations.PackSources(source0.AsUInt16(), source1.AsUInt16());-Vector128<byte> result = IndexOfAnyLookupCore(source, bitmapLookup);-return TNegator.NegateIfNeeded(result);+Vector128<byte> packed0 = TOptimizations.PackSources(source0.AsUInt16(), source1.AsUInt16());+Vector128<byte> packed1 = Default.PackSources(source0.AsUInt16() - offset, source1.AsUInt16() - offset);+Vector128<byte> result0 = IndexOfAnyLookupCore(packed0, bitmapLookup0);+Vector128<byte> result1 = IndexOfAnyLookupCore(packed1, bitmapLookup1);+return TNegator.NegateIfNeeded(result0 | result1);

(All the numbers below are using the Avx2 path of the new implementation -- measured before #103710)

Avx2 where the text is mostly non-ASCII (previous 'Mixed' would use the prob map / scalar fallback)
MethodToolchainLengthMeanErrorRatio
Asciimain100038.13 ns0.129 ns1.00
Asciipr100037.32 ns0.160 ns0.98
Mixedmain1000127.47 ns0.429 ns1.00
Mixedpr100051.20 ns0.095 ns0.40
AsciiLastmain100035.82 ns0.264 ns1.00
AsciiLastpr100035.81 ns0.079 ns1.00
MixedLastmain1000138.47 ns0.335 ns1.00
MixedLastpr100052.36 ns0.138 ns0.38
AsciiExceptmain100035.40 ns0.138 ns1.00
AsciiExceptpr100035.47 ns0.078 ns1.00
MixedExceptmain1000592.08 ns1.005 ns1.00
MixedExceptpr100051.00 ns0.133 ns0.09
AsciiLastExceptmain100036.77 ns0.535 ns1.00
AsciiLastExceptpr100036.34 ns0.086 ns0.99
MixedLastExceptmain1000682.31 ns8.073 ns1.00
MixedLastExceptpr100053.97 ns1.372 ns0.08
Avx512 machine (the probabilistic map is a lot faster than on Avx2)
MethodToolchainLengthMeanErrorRatio
Asciimain100037.44 ns0.006 ns1.00
Asciipr100037.73 ns0.005 ns1.01
Mixedmain100086.86 ns0.111 ns1.00
Mixedpr100060.36 ns0.010 ns0.69
AsciiLastmain100038.02 ns0.013 ns1.00
AsciiLastpr100038.28 ns0.003 ns1.01
MixedLastmain100095.96 ns0.023 ns1.00
MixedLastpr100061.16 ns0.009 ns0.64
AsciiExceptmain100040.17 ns0.005 ns1.00
AsciiExceptpr100039.81 ns0.005 ns0.99
MixedExceptmain1000585.99 ns0.183 ns1.00
MixedExceptpr100063.22 ns0.010 ns0.11
AsciiLastExceptmain100041.54 ns0.003 ns1.00
AsciiLastExceptpr100041.54 ns0.005 ns1.00
MixedLastExceptmain1000855.18 ns0.059 ns1.00
MixedLastExceptpr100064.65 ns0.010 ns0.08
Early matches
MethodToolchainInputContainsNonAsciiMeanErrorRatio
MixedmainFalse5.130 ns0.0374 ns1.00
MixedprFalse3.425 ns0.0303 ns0.67
MixedLastmainFalse4.830 ns0.0182 ns1.00
MixedLastprFalse4.179 ns0.0750 ns0.87
MixedmainTrue8.440 ns0.0780 ns1.00
MixedprTrue3.383 ns0.0163 ns0.40
MixedLastmainTrue9.011 ns0.2175 ns1.00
MixedLastprTrue4.073 ns0.0087 ns0.45

From that, we can see that the Ascii-only search is at ~1.5x the throughput of the two bitmaps impl.
The two bitmaps have ~1.5x the throughput of the probabilistic map on Avx512 and ~2.5x on Avx2.
In other words, this change is a throughput regression for ProbabilisticWithAsciiCharSearchValues if the text is all ASCII, and an improvement otherwise. It's always chepaer for early matches though.
For the -Except paths where ProbabilisticWithAsciiCharSearchValues uses a scalar fallback, the two bitmaps approach is obviously a lot (10x+) faster.

The change doesn't seem to impact existing Regex benchmarks much, likely because they're very focused on ASCII.


I also tried different implementation approaches to try and reduce code duplication between the existing Ascii and "ascii with second set" implementations:

Details
  • c6f1495, combining the second set into the existing AsciiState, which does save some duplication, but increases the memory consumption of all existing Ascii-only SearchValues.
  • 32f9cf1, that goes all-in with generics, but the JIT can't quite deal with having the vector state be completely generic.
    publicstaticintIndexOfAny<TNegator,TOptimizations>(refshortsearchSpace,intsearchSpaceLength,refAsciiWithSecondSetStatestate)whereTNegator:struct,INegatorwhereTOptimizations:struct,IOptimizations=>IndexOfAnyCore<int,TNegator,IndexOfAnyResultMapper<short>,AsciiWithSecondSetLookup<TOptimizations>,AsciiWithSecondSetState,(Vector128<byte>AsciiBitmap,Vector128<byte>SecondBitmap,Vector128<ushort>Offset),(Vector256<byte>AsciiBitmap,Vector256<byte>SecondBitmap,Vector256<ushort>Offset)>(refsearchSpace,searchSpaceLength,refstate);privatestaticTResultIndexOfAnyCore<TResult,TNegator,TResultMapper,TLookup,TState,TVector128State,TVector256State>(refshortsearchSpace,intsearchSpaceLength,refTStatestate)whereTResult:structwhereTNegator:struct,INegatorwhereTResultMapper:struct,IResultMapper<short,TResult>whereTLookup:struct,ILookup<TState,TVector128State,TVector256State>whereTState:structwhereTVector128State:structwhereTVector256State:struct

Looking at patterns from Regex_RealWorldPatterns.json, ~75% of non-ASCII sets would use the new implementation over the Ascii+ProbMap, most of which because of the kelvin sign.

Number of sources: 18886
numberOfSearchValues
5771
numberOfSearchValuesWithNonAscii
1238
numberOfSearchValuesWithTwoSets
960
numberOfSearchValuesWhereNonAsciiIsKelvin
684
numberOfPatternsWithSearchValues
5093
numberOfPatternsWithSearchValuesWithNonAscii
1038
numberOfPatternsWithSearchValuesWithTwoSets
765

Parsed data for the above:
Regex_RealWorldPatterns.SearchValues.json

@MihaZupanMihaZupan added this to the 9.0.0 milestone Jun 10, 2024
@MihaZupanMihaZupan self-assigned this Jun 10, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

@danmoseley

Copy link
Copy Markdown
Contributor

The change doesn't seem to impact existing Regex benchmarks much, likely because they're very focused on ASCII.

is it worth adding one or two more non ASCII?

@MihaZupan

This comment was marked as outdated.

@MihuBot

This comment was marked as outdated.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@MihuBot benchmark RustLang_Sherlock https://github.com/MihaZupan/performance/tree/compiled-regex-only -medium

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@MihuBot fuzz SearchValues

@MihaZupan

This comment was marked as outdated.

@EgorBo

Copy link
Copy Markdown
Member

@MihaZupan you need to either omit Run<> or pass args to Run, so in your case:

BenchmarkRunner.Run<Bench>(args);

Otherwise --corerun /base/corerun /diff/corerun args will be ignored 🙂

@MihaZupan

MihaZupan commented Jun 22, 2024

Copy link
Copy Markdown
MemberAuthor

Aaah right, thanks. I also forgot to remove the ShortRunJob.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@EgorBot -intel

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Buffers;
#nullable disable
publicclassBench{privatestaticreadonlySearchValues<char>_allowedAscii=SearchValues.Create("1234567890abcdefghijklmnopqrstuvwxyz");privatestaticreadonlySearchValues<char>_allowedMixed=SearchValues.Create("äöü1234567890abcdefghijklmnopqrstuvw");privatestring_asciiInput;privatestring_mixedInput;[Params(16,100,10_000)]publicintLength;[GlobalSetup]publicvoidSetup(){_asciiInput=newstring('a',Length);_mixedInput='ä'+newstring('a',Length-1);}[Benchmark]publicboolContainsOnlyAscii()=>!_asciiInput.AsSpan().ContainsAnyExcept(_allowedAscii);[Benchmark]publicboolContainsOnlyMixed()=>!_mixedInput.AsSpan().ContainsAnyExcept(_allowedMixed);}

@EgorBot

This comment was marked as outdated.

@EgorBot

Copy link
Copy Markdown
Benchmark results on Intel
BenchmarkDotNet v0.13.12, Ubuntu 22.04.4 LTS (Jammy Jellyfish)
Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 16 logical and 8 physical cores
Job-SZXNIX : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Job-DHGYYU : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
MethodToolchainLengthMeanErrorRatio
ContainsOnlyAsciiMain162.434 ns0.0115 ns1.00
ContainsOnlyAsciiPR162.434 ns0.0063 ns1.00
ContainsOnlyMixedMain1612.207 ns0.0035 ns1.00
ContainsOnlyMixedPR163.184 ns0.0004 ns0.26
ContainsOnlyAsciiMain1004.251 ns0.0005 ns1.00
ContainsOnlyAsciiPR1004.251 ns0.0004 ns1.00
ContainsOnlyMixedMain10062.381 ns0.0320 ns1.00
ContainsOnlyMixedPR1005.499 ns0.0020 ns0.09
ContainsOnlyAsciiMain10000208.463 ns0.0200 ns1.00
ContainsOnlyAsciiPR10000208.571 ns0.0259 ns1.00
ContainsOnlyMixedMain100005,783.502 ns1.4382 ns1.00
ContainsOnlyMixedPR10000391.050 ns0.0232 ns0.07

BDN_Artifacts.zip

@MihuBot

Copy link
Copy Markdown
System.Text.RegularExpressions.Tests.Perf_Regex_Industry_RustLang_Sherlock
BenchmarkDotNet v0.13.13-nightly.20240311.145, Ubuntu 22.04.4 LTS (Jammy Jellyfish)
AMD EPYC 9V74, 1 CPU, 8 logical and 4 physical cores
MediumRun : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Job=MediumRun OutlierMode=DontRemove IterationCount=15
LaunchCount=2 MemoryRandomization=True WarmupCount=10
MethodToolchainPatternMeanErrorRatioAllocatedAlloc Ratio
CountMain.*577,117.72 ns1,898.483 ns1.002 B1.00
CountPR.*609,940.38 ns5,541.168 ns1.062 B1.00
CountMain(?i)Holmes53,669.80 ns163.206 ns1.00-NA
CountPR(?i)Holmes53,751.77 ns95.378 ns1.00-NA
CountMain(?i)Sher[a-z]+|Hol[a-z]+96,797.28 ns7,445.773 ns1.01-NA
CountPR(?i)Sher[a-z]+|Hol[a-z]+101,675.49 ns7,778.427 ns1.06-NA
CountMain(?i)Sherlock45,540.09 ns158.021 ns1.00-NA
CountPR(?i)Sherlock45,699.65 ns198.254 ns1.00-NA
CountMain(?i)Sherlock Holmes45,299.62 ns140.891 ns1.00-NA
CountPR(?i)Sherlock Holmes45,322.99 ns60.249 ns1.00-NA
CountMain(?i)Sherlock|Holmes|Watson98,222.98 ns9,160.202 ns1.02-NA
CountPR(?i)Sherlock|Holmes|Watson98,387.61 ns9,077.154 ns1.02-NA
CountMain(?i)Sherlock|(...)er|John|Baker [49]210,972.18 ns25,319.086 ns1.031 B1.00
CountPR(?i)Sherlock|(...)er|John|Baker [49]216,838.46 ns27,873.222 ns1.061 B1.00
CountMain(?i)the249,601.71 ns10,074.303 ns1.001 B1.00
CountPR(?i)the245,957.03 ns10,337.071 ns0.991 B1.00
CountMain(?m)^Sherlock(...)rlock Holmes$ [37]58,240.97 ns2,237.819 ns1.00-NA
CountPR(?m)^Sherlock(...)rlock Holmes$ [37]59,147.93 ns2,749.661 ns1.02-NA
CountMain(?s).*39.26 ns0.088 ns1.00-NA
CountPR(?s).*41.27 ns1.728 ns1.05-NA
CountMain[^\\n]*576,131.95 ns2,377.308 ns1.002 B1.00
CountPR[^\\n]*577,114.20 ns4,299.926 ns1.002 B1.00
CountMain[a-q][^u-z]{13}x23,158.57 ns113.019 ns1.00-NA
CountPR[a-q][^u-z]{13}x23,147.72 ns90.268 ns1.00-NA
CountMain[a-zA-Z]+ing4,112,765.60 ns6,996.218 ns1.0019 B1.00
CountPR[a-zA-Z]+ing4,195,122.65 ns50,241.289 ns1.0221 B1.11
CountMain\b\w+n\b8,324,707.71 ns18,654.897 ns1.0044 B1.00
CountPR\b\w+n\b8,419,875.57 ns67,893.821 ns1.0144 B1.00
CountMain\p{L}10,252,616.17 ns258,692.197 ns1.0035 B1.00
CountPR\p{L}10,178,460.27 ns120,522.988 ns0.9935 B1.00
CountMain\p{Ll}10,218,675.98 ns78,494.108 ns1.0035 B1.00
CountPR\p{Ll}11,001,606.18 ns501,912.621 ns1.0835 B1.00
CountMain\p{Lu}355,667.38 ns7,908.522 ns1.001 B1.00
CountPR\p{Lu}348,108.98 ns3,458.353 ns0.981 B1.00
CountMain\s[a-zA-Z]{0,12}ing\s4,387,091.94 ns11,384.876 ns1.0024 B1.00
CountPR\s[a-zA-Z]{0,12}ing\s4,404,995.88 ns9,088.263 ns1.0024 B1.00
CountMain\w+4,712,257.79 ns27,825.642 ns1.0018 B1.00
CountPR\w+4,671,552.17 ns7,259.567 ns0.9921 B1.17
CountMain\w+\s+Holmes3,340,654.11 ns10,688.067 ns1.0011 B1.00
CountPR\w+\s+Holmes3,355,149.72 ns15,672.577 ns1.0010 B0.91
CountMain\w+\s+Holmes\s+\w+3,609,649.22 ns65,235.529 ns1.0010 B1.00
CountPR\w+\s+Holmes\s+\w+3,502,413.34 ns60,104.536 ns0.9712 B1.20
CountMainaei38,764.39 ns528.311 ns1.00-NA
CountPRaei38,671.05 ns532.363 ns1.00-NA
CountMainaqj38,552.12 ns579.654 ns1.00-NA
CountPRaqj38,708.25 ns516.758 ns1.00-NA
CountMainHolmes50,202.09 ns78.982 ns1.00-NA
CountPRHolmes50,229.34 ns115.917 ns1.00-NA
CountMainHolmes.{0,25}(...).{0,25}Holmes [39]44,351.33 ns90.615 ns1.00-NA
CountPRHolmes.{0,25}(...).{0,25}Holmes [39]44,416.66 ns119.101 ns1.00-NA
CountMainSher[a-z]+|Hol[a-z]+48,711.02 ns113.935 ns1.00-NA
CountPRSher[a-z]+|Hol[a-z]+48,852.79 ns227.029 ns1.00-NA
CountMainSherlock58,662.58 ns2,239.132 ns1.00-NA
CountPRSherlock59,492.49 ns2,839.858 ns1.02-NA
CountMainSherlock Holmes59,536.37 ns2,887.051 ns1.01-NA
CountPRSherlock Holmes59,734.87 ns2,760.671 ns1.01-NA
CountMainSherlock\s+Holmes59,934.57 ns2,381.314 ns1.00-NA
CountPRSherlock\s+Holmes60,715.04 ns3,024.286 ns1.02-NA
CountMainSherlock|Holmes44,782.94 ns106.657 ns1.00-NA
CountPRSherlock|Holmes44,866.33 ns114.894 ns1.00-NA
CountMainSherlock|Holmes|Watson58,630.95 ns77.246 ns1.00-NA
CountPRSherlock|Holmes|Watson59,046.70 ns110.269 ns1.01-NA
CountMainSherlock|Holm(...)er|John|Baker [45]109,855.32 ns133.155 ns1.00-NA
CountPRSherlock|Holm(...)er|John|Baker [45]109,887.13 ns89.847 ns1.00-NA
CountMainSherlock|Street25,047.25 ns62.195 ns1.00-NA
CountPRSherlock|Street25,004.97 ns85.354 ns1.00-NA
CountMainthe179,288.74 ns632.033 ns1.001 B1.00
CountPRthe179,042.42 ns643.054 ns1.001 B1.00
CountMainThe54,675.24 ns84.398 ns1.00-NA
CountPRThe54,504.36 ns156.081 ns1.00-NA
CountMainthe\s+\w+282,854.54 ns12,723.264 ns1.001 B1.00
CountPRthe\s+\w+288,927.85 ns11,924.621 ns1.031 B1.00
CountMainzqj38,776.08 ns523.611 ns1.00-NA
CountPRzqj38,694.38 ns541.346 ns1.00-NA

Vector512<byte> secondBitmap512 = state.SecondBitmap512;
Vector512<ushort> offset512 = Vector512.Create(state.Offset);

if (searchSpaceLength > 2 * Vector512<short>.Count)

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.

@GrabYourPitchforks, in the UTF8 experiment you shared with me, you had code that, after having validated that Vector256 was hardware accelerated, aligned an address and then read a full vector, without concern for whether that vector read under or overread the target region. Is that safe to do on all platforms? It seems we could avoid some branching with similar techniques in many of our implementations that use vectorization.

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.

Is that safe to do on all platforms?

As long as the data is pinned, otherwise GC can interrupt at any point and break the alignment assumption

@MihaZupan
MihaZupanforce-pushed the searchvalues-asciiWithSecondSet branch 2 times, most recently from f378aca to e747fd0CompareJuly 10, 2024 22:48
@stephentoub

Copy link
Copy Markdown
Member

the Ascii-only search is at ~1.5x the throughput of the two bitmaps impl.

Does this mean someone searching mostly ASCII text with IgnoreCase for [a-z] will see a 50% regression?

@MihaZupan

MihaZupan commented Jul 11, 2024

Copy link
Copy Markdown
MemberAuthor

Does this mean someone searching mostly ASCII text with IgnoreCase for [a-z] will see a 50% regression?

If you had a long run of text that was just ASCII and didn't match, yes, it would be slower.
But if you were stopping on matches along the way (even if they were ASCII), the two sets can be cheaper due to the lower overhead on matches (mainly since there's an extra method involved in the ascii+probmap implementation).

I reran Sherlock, and the throughput difference doesn't seem to be affecting it: MihuBot/runtime-utils#505 (comment)

We could also choose to use the two bitmaps approach only on -Except paths to avoid the scalar fallbacks.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

Any concerns with merging this one as-is and seeing if benchmarks complain?

@stephentoub

Copy link
Copy Markdown
Member

Ok, let's give it a try but be ready to back it out if any meaningful regressions pop up.

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.

6 participants

@MihaZupan@danmoseley@MihuBot@EgorBo@EgorBot@stephentoub
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add SearchValues<char> implementation for two sets of 128 chars - #103216

Merged
stephentoub merged 4 commits into
dotnet:mainfrom
MihaZupan:searchvalues-asciiWithSecondSet
Jul 22, 2024
Merged

Add SearchValues<char> implementation for two sets of 128 chars#103216
stephentoub merged 4 commits into
dotnet:mainfrom
MihaZupan:searchvalues-asciiWithSecondSet

Conversation

@MihaZupan

@MihaZupanMihaZupan commented Jun 10, 2024

Copy link
Copy Markdown
Member

#101001 significantly improved the performance of the non-vectorized -Except paths of non-ASCII SearchValues<char>.
However, they are still not vectorized, and this PR changes that for values where the non-ASCII part can fit into a 128-bit bitmap.

This adds an implementation that's almost the same as the AsciiCharSearchValues, but where the core lookup checks against two 128-bit bitmaps, with the second one at a variable offset:

-Vector128<byte> source = TOptimizations.PackSources(source0.AsUInt16(), source1.AsUInt16());-Vector128<byte> result = IndexOfAnyLookupCore(source, bitmapLookup);-return TNegator.NegateIfNeeded(result);+Vector128<byte> packed0 = TOptimizations.PackSources(source0.AsUInt16(), source1.AsUInt16());+Vector128<byte> packed1 = Default.PackSources(source0.AsUInt16() - offset, source1.AsUInt16() - offset);+Vector128<byte> result0 = IndexOfAnyLookupCore(packed0, bitmapLookup0);+Vector128<byte> result1 = IndexOfAnyLookupCore(packed1, bitmapLookup1);+return TNegator.NegateIfNeeded(result0 | result1);

(All the numbers below are using the Avx2 path of the new implementation -- measured before #103710)

Avx2 where the text is mostly non-ASCII (previous 'Mixed' would use the prob map / scalar fallback)
MethodToolchainLengthMeanErrorRatio
Asciimain100038.13 ns0.129 ns1.00
Asciipr100037.32 ns0.160 ns0.98
Mixedmain1000127.47 ns0.429 ns1.00
Mixedpr100051.20 ns0.095 ns0.40
AsciiLastmain100035.82 ns0.264 ns1.00
AsciiLastpr100035.81 ns0.079 ns1.00
MixedLastmain1000138.47 ns0.335 ns1.00
MixedLastpr100052.36 ns0.138 ns0.38
AsciiExceptmain100035.40 ns0.138 ns1.00
AsciiExceptpr100035.47 ns0.078 ns1.00
MixedExceptmain1000592.08 ns1.005 ns1.00
MixedExceptpr100051.00 ns0.133 ns0.09
AsciiLastExceptmain100036.77 ns0.535 ns1.00
AsciiLastExceptpr100036.34 ns0.086 ns0.99
MixedLastExceptmain1000682.31 ns8.073 ns1.00
MixedLastExceptpr100053.97 ns1.372 ns0.08
Avx512 machine (the probabilistic map is a lot faster than on Avx2)
MethodToolchainLengthMeanErrorRatio
Asciimain100037.44 ns0.006 ns1.00
Asciipr100037.73 ns0.005 ns1.01
Mixedmain100086.86 ns0.111 ns1.00
Mixedpr100060.36 ns0.010 ns0.69
AsciiLastmain100038.02 ns0.013 ns1.00
AsciiLastpr100038.28 ns0.003 ns1.01
MixedLastmain100095.96 ns0.023 ns1.00
MixedLastpr100061.16 ns0.009 ns0.64
AsciiExceptmain100040.17 ns0.005 ns1.00
AsciiExceptpr100039.81 ns0.005 ns0.99
MixedExceptmain1000585.99 ns0.183 ns1.00
MixedExceptpr100063.22 ns0.010 ns0.11
AsciiLastExceptmain100041.54 ns0.003 ns1.00
AsciiLastExceptpr100041.54 ns0.005 ns1.00
MixedLastExceptmain1000855.18 ns0.059 ns1.00
MixedLastExceptpr100064.65 ns0.010 ns0.08
Early matches
MethodToolchainInputContainsNonAsciiMeanErrorRatio
MixedmainFalse5.130 ns0.0374 ns1.00
MixedprFalse3.425 ns0.0303 ns0.67
MixedLastmainFalse4.830 ns0.0182 ns1.00
MixedLastprFalse4.179 ns0.0750 ns0.87
MixedmainTrue8.440 ns0.0780 ns1.00
MixedprTrue3.383 ns0.0163 ns0.40
MixedLastmainTrue9.011 ns0.2175 ns1.00
MixedLastprTrue4.073 ns0.0087 ns0.45

From that, we can see that the Ascii-only search is at ~1.5x the throughput of the two bitmaps impl.
The two bitmaps have ~1.5x the throughput of the probabilistic map on Avx512 and ~2.5x on Avx2.
In other words, this change is a throughput regression for ProbabilisticWithAsciiCharSearchValues if the text is all ASCII, and an improvement otherwise. It's always chepaer for early matches though.
For the -Except paths where ProbabilisticWithAsciiCharSearchValues uses a scalar fallback, the two bitmaps approach is obviously a lot (10x+) faster.

The change doesn't seem to impact existing Regex benchmarks much, likely because they're very focused on ASCII.


I also tried different implementation approaches to try and reduce code duplication between the existing Ascii and "ascii with second set" implementations:

Details
  • c6f1495, combining the second set into the existing AsciiState, which does save some duplication, but increases the memory consumption of all existing Ascii-only SearchValues.
  • 32f9cf1, that goes all-in with generics, but the JIT can't quite deal with having the vector state be completely generic.
    publicstaticintIndexOfAny<TNegator,TOptimizations>(refshortsearchSpace,intsearchSpaceLength,refAsciiWithSecondSetStatestate)whereTNegator:struct,INegatorwhereTOptimizations:struct,IOptimizations=>IndexOfAnyCore<int,TNegator,IndexOfAnyResultMapper<short>,AsciiWithSecondSetLookup<TOptimizations>,AsciiWithSecondSetState,(Vector128<byte>AsciiBitmap,Vector128<byte>SecondBitmap,Vector128<ushort>Offset),(Vector256<byte>AsciiBitmap,Vector256<byte>SecondBitmap,Vector256<ushort>Offset)>(refsearchSpace,searchSpaceLength,refstate);privatestaticTResultIndexOfAnyCore<TResult,TNegator,TResultMapper,TLookup,TState,TVector128State,TVector256State>(refshortsearchSpace,intsearchSpaceLength,refTStatestate)whereTResult:structwhereTNegator:struct,INegatorwhereTResultMapper:struct,IResultMapper<short,TResult>whereTLookup:struct,ILookup<TState,TVector128State,TVector256State>whereTState:structwhereTVector128State:structwhereTVector256State:struct

Looking at patterns from Regex_RealWorldPatterns.json, ~75% of non-ASCII sets would use the new implementation over the Ascii+ProbMap, most of which because of the kelvin sign.

Number of sources: 18886
numberOfSearchValues
5771
numberOfSearchValuesWithNonAscii
1238
numberOfSearchValuesWithTwoSets
960
numberOfSearchValuesWhereNonAsciiIsKelvin
684
numberOfPatternsWithSearchValues
5093
numberOfPatternsWithSearchValuesWithNonAscii
1038
numberOfPatternsWithSearchValuesWithTwoSets
765

Parsed data for the above:
Regex_RealWorldPatterns.SearchValues.json

@MihaZupanMihaZupan added this to the 9.0.0 milestone Jun 10, 2024
@MihaZupanMihaZupan self-assigned this Jun 10, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

@danmoseley

Copy link
Copy Markdown
Contributor

The change doesn't seem to impact existing Regex benchmarks much, likely because they're very focused on ASCII.

is it worth adding one or two more non ASCII?

@MihaZupan

This comment was marked as outdated.

@MihuBot

This comment was marked as outdated.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@MihuBot benchmark RustLang_Sherlock https://github.com/MihaZupan/performance/tree/compiled-regex-only -medium

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@MihuBot fuzz SearchValues

@MihaZupan

This comment was marked as outdated.

@EgorBo

Copy link
Copy Markdown
Member

@MihaZupan you need to either omit Run<> or pass args to Run, so in your case:

BenchmarkRunner.Run<Bench>(args);

Otherwise --corerun /base/corerun /diff/corerun args will be ignored 🙂

@MihaZupan

MihaZupan commented Jun 22, 2024

Copy link
Copy Markdown
MemberAuthor

Aaah right, thanks. I also forgot to remove the ShortRunJob.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@EgorBot -intel

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Buffers;
#nullable disable
publicclassBench{privatestaticreadonlySearchValues<char>_allowedAscii=SearchValues.Create("1234567890abcdefghijklmnopqrstuvwxyz");privatestaticreadonlySearchValues<char>_allowedMixed=SearchValues.Create("äöü1234567890abcdefghijklmnopqrstuvw");privatestring_asciiInput;privatestring_mixedInput;[Params(16,100,10_000)]publicintLength;[GlobalSetup]publicvoidSetup(){_asciiInput=newstring('a',Length);_mixedInput='ä'+newstring('a',Length-1);}[Benchmark]publicboolContainsOnlyAscii()=>!_asciiInput.AsSpan().ContainsAnyExcept(_allowedAscii);[Benchmark]publicboolContainsOnlyMixed()=>!_mixedInput.AsSpan().ContainsAnyExcept(_allowedMixed);}

@EgorBot

This comment was marked as outdated.

@EgorBot

Copy link
Copy Markdown
Benchmark results on Intel
BenchmarkDotNet v0.13.12, Ubuntu 22.04.4 LTS (Jammy Jellyfish)
Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 16 logical and 8 physical cores
Job-SZXNIX : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Job-DHGYYU : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
MethodToolchainLengthMeanErrorRatio
ContainsOnlyAsciiMain162.434 ns0.0115 ns1.00
ContainsOnlyAsciiPR162.434 ns0.0063 ns1.00
ContainsOnlyMixedMain1612.207 ns0.0035 ns1.00
ContainsOnlyMixedPR163.184 ns0.0004 ns0.26
ContainsOnlyAsciiMain1004.251 ns0.0005 ns1.00
ContainsOnlyAsciiPR1004.251 ns0.0004 ns1.00
ContainsOnlyMixedMain10062.381 ns0.0320 ns1.00
ContainsOnlyMixedPR1005.499 ns0.0020 ns0.09
ContainsOnlyAsciiMain10000208.463 ns0.0200 ns1.00
ContainsOnlyAsciiPR10000208.571 ns0.0259 ns1.00
ContainsOnlyMixedMain100005,783.502 ns1.4382 ns1.00
ContainsOnlyMixedPR10000391.050 ns0.0232 ns0.07

BDN_Artifacts.zip

@MihuBot

Copy link
Copy Markdown
System.Text.RegularExpressions.Tests.Perf_Regex_Industry_RustLang_Sherlock
BenchmarkDotNet v0.13.13-nightly.20240311.145, Ubuntu 22.04.4 LTS (Jammy Jellyfish)
AMD EPYC 9V74, 1 CPU, 8 logical and 4 physical cores
MediumRun : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Job=MediumRun OutlierMode=DontRemove IterationCount=15
LaunchCount=2 MemoryRandomization=True WarmupCount=10
MethodToolchainPatternMeanErrorRatioAllocatedAlloc Ratio
CountMain.*577,117.72 ns1,898.483 ns1.002 B1.00
CountPR.*609,940.38 ns5,541.168 ns1.062 B1.00
CountMain(?i)Holmes53,669.80 ns163.206 ns1.00-NA
CountPR(?i)Holmes53,751.77 ns95.378 ns1.00-NA
CountMain(?i)Sher[a-z]+|Hol[a-z]+96,797.28 ns7,445.773 ns1.01-NA
CountPR(?i)Sher[a-z]+|Hol[a-z]+101,675.49 ns7,778.427 ns1.06-NA
CountMain(?i)Sherlock45,540.09 ns158.021 ns1.00-NA
CountPR(?i)Sherlock45,699.65 ns198.254 ns1.00-NA
CountMain(?i)Sherlock Holmes45,299.62 ns140.891 ns1.00-NA
CountPR(?i)Sherlock Holmes45,322.99 ns60.249 ns1.00-NA
CountMain(?i)Sherlock|Holmes|Watson98,222.98 ns9,160.202 ns1.02-NA
CountPR(?i)Sherlock|Holmes|Watson98,387.61 ns9,077.154 ns1.02-NA
CountMain(?i)Sherlock|(...)er|John|Baker [49]210,972.18 ns25,319.086 ns1.031 B1.00
CountPR(?i)Sherlock|(...)er|John|Baker [49]216,838.46 ns27,873.222 ns1.061 B1.00
CountMain(?i)the249,601.71 ns10,074.303 ns1.001 B1.00
CountPR(?i)the245,957.03 ns10,337.071 ns0.991 B1.00
CountMain(?m)^Sherlock(...)rlock Holmes$ [37]58,240.97 ns2,237.819 ns1.00-NA
CountPR(?m)^Sherlock(...)rlock Holmes$ [37]59,147.93 ns2,749.661 ns1.02-NA
CountMain(?s).*39.26 ns0.088 ns1.00-NA
CountPR(?s).*41.27 ns1.728 ns1.05-NA
CountMain[^\\n]*576,131.95 ns2,377.308 ns1.002 B1.00
CountPR[^\\n]*577,114.20 ns4,299.926 ns1.002 B1.00
CountMain[a-q][^u-z]{13}x23,158.57 ns113.019 ns1.00-NA
CountPR[a-q][^u-z]{13}x23,147.72 ns90.268 ns1.00-NA
CountMain[a-zA-Z]+ing4,112,765.60 ns6,996.218 ns1.0019 B1.00
CountPR[a-zA-Z]+ing4,195,122.65 ns50,241.289 ns1.0221 B1.11
CountMain\b\w+n\b8,324,707.71 ns18,654.897 ns1.0044 B1.00
CountPR\b\w+n\b8,419,875.57 ns67,893.821 ns1.0144 B1.00
CountMain\p{L}10,252,616.17 ns258,692.197 ns1.0035 B1.00
CountPR\p{L}10,178,460.27 ns120,522.988 ns0.9935 B1.00
CountMain\p{Ll}10,218,675.98 ns78,494.108 ns1.0035 B1.00
CountPR\p{Ll}11,001,606.18 ns501,912.621 ns1.0835 B1.00
CountMain\p{Lu}355,667.38 ns7,908.522 ns1.001 B1.00
CountPR\p{Lu}348,108.98 ns3,458.353 ns0.981 B1.00
CountMain\s[a-zA-Z]{0,12}ing\s4,387,091.94 ns11,384.876 ns1.0024 B1.00
CountPR\s[a-zA-Z]{0,12}ing\s4,404,995.88 ns9,088.263 ns1.0024 B1.00
CountMain\w+4,712,257.79 ns27,825.642 ns1.0018 B1.00
CountPR\w+4,671,552.17 ns7,259.567 ns0.9921 B1.17
CountMain\w+\s+Holmes3,340,654.11 ns10,688.067 ns1.0011 B1.00
CountPR\w+\s+Holmes3,355,149.72 ns15,672.577 ns1.0010 B0.91
CountMain\w+\s+Holmes\s+\w+3,609,649.22 ns65,235.529 ns1.0010 B1.00
CountPR\w+\s+Holmes\s+\w+3,502,413.34 ns60,104.536 ns0.9712 B1.20
CountMainaei38,764.39 ns528.311 ns1.00-NA
CountPRaei38,671.05 ns532.363 ns1.00-NA
CountMainaqj38,552.12 ns579.654 ns1.00-NA
CountPRaqj38,708.25 ns516.758 ns1.00-NA
CountMainHolmes50,202.09 ns78.982 ns1.00-NA
CountPRHolmes50,229.34 ns115.917 ns1.00-NA
CountMainHolmes.{0,25}(...).{0,25}Holmes [39]44,351.33 ns90.615 ns1.00-NA
CountPRHolmes.{0,25}(...).{0,25}Holmes [39]44,416.66 ns119.101 ns1.00-NA
CountMainSher[a-z]+|Hol[a-z]+48,711.02 ns113.935 ns1.00-NA
CountPRSher[a-z]+|Hol[a-z]+48,852.79 ns227.029 ns1.00-NA
CountMainSherlock58,662.58 ns2,239.132 ns1.00-NA
CountPRSherlock59,492.49 ns2,839.858 ns1.02-NA
CountMainSherlock Holmes59,536.37 ns2,887.051 ns1.01-NA
CountPRSherlock Holmes59,734.87 ns2,760.671 ns1.01-NA
CountMainSherlock\s+Holmes59,934.57 ns2,381.314 ns1.00-NA
CountPRSherlock\s+Holmes60,715.04 ns3,024.286 ns1.02-NA
CountMainSherlock|Holmes44,782.94 ns106.657 ns1.00-NA
CountPRSherlock|Holmes44,866.33 ns114.894 ns1.00-NA
CountMainSherlock|Holmes|Watson58,630.95 ns77.246 ns1.00-NA
CountPRSherlock|Holmes|Watson59,046.70 ns110.269 ns1.01-NA
CountMainSherlock|Holm(...)er|John|Baker [45]109,855.32 ns133.155 ns1.00-NA
CountPRSherlock|Holm(...)er|John|Baker [45]109,887.13 ns89.847 ns1.00-NA
CountMainSherlock|Street25,047.25 ns62.195 ns1.00-NA
CountPRSherlock|Street25,004.97 ns85.354 ns1.00-NA
CountMainthe179,288.74 ns632.033 ns1.001 B1.00
CountPRthe179,042.42 ns643.054 ns1.001 B1.00
CountMainThe54,675.24 ns84.398 ns1.00-NA
CountPRThe54,504.36 ns156.081 ns1.00-NA
CountMainthe\s+\w+282,854.54 ns12,723.264 ns1.001 B1.00
CountPRthe\s+\w+288,927.85 ns11,924.621 ns1.031 B1.00
CountMainzqj38,776.08 ns523.611 ns1.00-NA
CountPRzqj38,694.38 ns541.346 ns1.00-NA

Vector512<byte> secondBitmap512 = state.SecondBitmap512;
Vector512<ushort> offset512 = Vector512.Create(state.Offset);

if (searchSpaceLength > 2 * Vector512<short>.Count)

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.

@GrabYourPitchforks, in the UTF8 experiment you shared with me, you had code that, after having validated that Vector256 was hardware accelerated, aligned an address and then read a full vector, without concern for whether that vector read under or overread the target region. Is that safe to do on all platforms? It seems we could avoid some branching with similar techniques in many of our implementations that use vectorization.

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.

Is that safe to do on all platforms?

As long as the data is pinned, otherwise GC can interrupt at any point and break the alignment assumption

@MihaZupan
MihaZupanforce-pushed the searchvalues-asciiWithSecondSet branch 2 times, most recently from f378aca to e747fd0CompareJuly 10, 2024 22:48
@stephentoub

Copy link
Copy Markdown
Member

the Ascii-only search is at ~1.5x the throughput of the two bitmaps impl.

Does this mean someone searching mostly ASCII text with IgnoreCase for [a-z] will see a 50% regression?

@MihaZupan

MihaZupan commented Jul 11, 2024

Copy link
Copy Markdown
MemberAuthor

Does this mean someone searching mostly ASCII text with IgnoreCase for [a-z] will see a 50% regression?

If you had a long run of text that was just ASCII and didn't match, yes, it would be slower.
But if you were stopping on matches along the way (even if they were ASCII), the two sets can be cheaper due to the lower overhead on matches (mainly since there's an extra method involved in the ascii+probmap implementation).

I reran Sherlock, and the throughput difference doesn't seem to be affecting it: MihuBot/runtime-utils#505 (comment)

We could also choose to use the two bitmaps approach only on -Except paths to avoid the scalar fallbacks.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

Any concerns with merging this one as-is and seeing if benchmarks complain?

@stephentoub

Copy link
Copy Markdown
Member

Ok, let's give it a try but be ready to back it out if any meaningful regressions pop up.

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.

6 participants

@MihaZupan@danmoseley@MihuBot@EgorBo@EgorBot@stephentoub
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add SearchValues<char> implementation for two sets of 128 chars - #103216

Merged
stephentoub merged 4 commits into
dotnet:mainfrom
MihaZupan:searchvalues-asciiWithSecondSet
Jul 22, 2024
Merged

Add SearchValues<char> implementation for two sets of 128 chars#103216
stephentoub merged 4 commits into
dotnet:mainfrom
MihaZupan:searchvalues-asciiWithSecondSet

Conversation

@MihaZupan

@MihaZupanMihaZupan commented Jun 10, 2024

Copy link
Copy Markdown
Member

#101001 significantly improved the performance of the non-vectorized -Except paths of non-ASCII SearchValues<char>.
However, they are still not vectorized, and this PR changes that for values where the non-ASCII part can fit into a 128-bit bitmap.

This adds an implementation that's almost the same as the AsciiCharSearchValues, but where the core lookup checks against two 128-bit bitmaps, with the second one at a variable offset:

-Vector128<byte> source = TOptimizations.PackSources(source0.AsUInt16(), source1.AsUInt16());-Vector128<byte> result = IndexOfAnyLookupCore(source, bitmapLookup);-return TNegator.NegateIfNeeded(result);+Vector128<byte> packed0 = TOptimizations.PackSources(source0.AsUInt16(), source1.AsUInt16());+Vector128<byte> packed1 = Default.PackSources(source0.AsUInt16() - offset, source1.AsUInt16() - offset);+Vector128<byte> result0 = IndexOfAnyLookupCore(packed0, bitmapLookup0);+Vector128<byte> result1 = IndexOfAnyLookupCore(packed1, bitmapLookup1);+return TNegator.NegateIfNeeded(result0 | result1);

(All the numbers below are using the Avx2 path of the new implementation -- measured before #103710)

Avx2 where the text is mostly non-ASCII (previous 'Mixed' would use the prob map / scalar fallback)
MethodToolchainLengthMeanErrorRatio
Asciimain100038.13 ns0.129 ns1.00
Asciipr100037.32 ns0.160 ns0.98
Mixedmain1000127.47 ns0.429 ns1.00
Mixedpr100051.20 ns0.095 ns0.40
AsciiLastmain100035.82 ns0.264 ns1.00
AsciiLastpr100035.81 ns0.079 ns1.00
MixedLastmain1000138.47 ns0.335 ns1.00
MixedLastpr100052.36 ns0.138 ns0.38
AsciiExceptmain100035.40 ns0.138 ns1.00
AsciiExceptpr100035.47 ns0.078 ns1.00
MixedExceptmain1000592.08 ns1.005 ns1.00
MixedExceptpr100051.00 ns0.133 ns0.09
AsciiLastExceptmain100036.77 ns0.535 ns1.00
AsciiLastExceptpr100036.34 ns0.086 ns0.99
MixedLastExceptmain1000682.31 ns8.073 ns1.00
MixedLastExceptpr100053.97 ns1.372 ns0.08
Avx512 machine (the probabilistic map is a lot faster than on Avx2)
MethodToolchainLengthMeanErrorRatio
Asciimain100037.44 ns0.006 ns1.00
Asciipr100037.73 ns0.005 ns1.01
Mixedmain100086.86 ns0.111 ns1.00
Mixedpr100060.36 ns0.010 ns0.69
AsciiLastmain100038.02 ns0.013 ns1.00
AsciiLastpr100038.28 ns0.003 ns1.01
MixedLastmain100095.96 ns0.023 ns1.00
MixedLastpr100061.16 ns0.009 ns0.64
AsciiExceptmain100040.17 ns0.005 ns1.00
AsciiExceptpr100039.81 ns0.005 ns0.99
MixedExceptmain1000585.99 ns0.183 ns1.00
MixedExceptpr100063.22 ns0.010 ns0.11
AsciiLastExceptmain100041.54 ns0.003 ns1.00
AsciiLastExceptpr100041.54 ns0.005 ns1.00
MixedLastExceptmain1000855.18 ns0.059 ns1.00
MixedLastExceptpr100064.65 ns0.010 ns0.08
Early matches
MethodToolchainInputContainsNonAsciiMeanErrorRatio
MixedmainFalse5.130 ns0.0374 ns1.00
MixedprFalse3.425 ns0.0303 ns0.67
MixedLastmainFalse4.830 ns0.0182 ns1.00
MixedLastprFalse4.179 ns0.0750 ns0.87
MixedmainTrue8.440 ns0.0780 ns1.00
MixedprTrue3.383 ns0.0163 ns0.40
MixedLastmainTrue9.011 ns0.2175 ns1.00
MixedLastprTrue4.073 ns0.0087 ns0.45

From that, we can see that the Ascii-only search is at ~1.5x the throughput of the two bitmaps impl.
The two bitmaps have ~1.5x the throughput of the probabilistic map on Avx512 and ~2.5x on Avx2.
In other words, this change is a throughput regression for ProbabilisticWithAsciiCharSearchValues if the text is all ASCII, and an improvement otherwise. It's always chepaer for early matches though.
For the -Except paths where ProbabilisticWithAsciiCharSearchValues uses a scalar fallback, the two bitmaps approach is obviously a lot (10x+) faster.

The change doesn't seem to impact existing Regex benchmarks much, likely because they're very focused on ASCII.


I also tried different implementation approaches to try and reduce code duplication between the existing Ascii and "ascii with second set" implementations:

Details
  • c6f1495, combining the second set into the existing AsciiState, which does save some duplication, but increases the memory consumption of all existing Ascii-only SearchValues.
  • 32f9cf1, that goes all-in with generics, but the JIT can't quite deal with having the vector state be completely generic.
    publicstaticintIndexOfAny<TNegator,TOptimizations>(refshortsearchSpace,intsearchSpaceLength,refAsciiWithSecondSetStatestate)whereTNegator:struct,INegatorwhereTOptimizations:struct,IOptimizations=>IndexOfAnyCore<int,TNegator,IndexOfAnyResultMapper<short>,AsciiWithSecondSetLookup<TOptimizations>,AsciiWithSecondSetState,(Vector128<byte>AsciiBitmap,Vector128<byte>SecondBitmap,Vector128<ushort>Offset),(Vector256<byte>AsciiBitmap,Vector256<byte>SecondBitmap,Vector256<ushort>Offset)>(refsearchSpace,searchSpaceLength,refstate);privatestaticTResultIndexOfAnyCore<TResult,TNegator,TResultMapper,TLookup,TState,TVector128State,TVector256State>(refshortsearchSpace,intsearchSpaceLength,refTStatestate)whereTResult:structwhereTNegator:struct,INegatorwhereTResultMapper:struct,IResultMapper<short,TResult>whereTLookup:struct,ILookup<TState,TVector128State,TVector256State>whereTState:structwhereTVector128State:structwhereTVector256State:struct

Looking at patterns from Regex_RealWorldPatterns.json, ~75% of non-ASCII sets would use the new implementation over the Ascii+ProbMap, most of which because of the kelvin sign.

Number of sources: 18886
numberOfSearchValues
5771
numberOfSearchValuesWithNonAscii
1238
numberOfSearchValuesWithTwoSets
960
numberOfSearchValuesWhereNonAsciiIsKelvin
684
numberOfPatternsWithSearchValues
5093
numberOfPatternsWithSearchValuesWithNonAscii
1038
numberOfPatternsWithSearchValuesWithTwoSets
765

Parsed data for the above:
Regex_RealWorldPatterns.SearchValues.json

@MihaZupanMihaZupan added this to the 9.0.0 milestone Jun 10, 2024
@MihaZupanMihaZupan self-assigned this Jun 10, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

@danmoseley

Copy link
Copy Markdown
Contributor

The change doesn't seem to impact existing Regex benchmarks much, likely because they're very focused on ASCII.

is it worth adding one or two more non ASCII?

@MihaZupan

This comment was marked as outdated.

@MihuBot

This comment was marked as outdated.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@MihuBot benchmark RustLang_Sherlock https://github.com/MihaZupan/performance/tree/compiled-regex-only -medium

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@MihuBot fuzz SearchValues

@MihaZupan

This comment was marked as outdated.

@EgorBo

Copy link
Copy Markdown
Member

@MihaZupan you need to either omit Run<> or pass args to Run, so in your case:

BenchmarkRunner.Run<Bench>(args);

Otherwise --corerun /base/corerun /diff/corerun args will be ignored 🙂

@MihaZupan

MihaZupan commented Jun 22, 2024

Copy link
Copy Markdown
MemberAuthor

Aaah right, thanks. I also forgot to remove the ShortRunJob.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@EgorBot -intel

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Buffers;
#nullable disable
publicclassBench{privatestaticreadonlySearchValues<char>_allowedAscii=SearchValues.Create("1234567890abcdefghijklmnopqrstuvwxyz");privatestaticreadonlySearchValues<char>_allowedMixed=SearchValues.Create("äöü1234567890abcdefghijklmnopqrstuvw");privatestring_asciiInput;privatestring_mixedInput;[Params(16,100,10_000)]publicintLength;[GlobalSetup]publicvoidSetup(){_asciiInput=newstring('a',Length);_mixedInput='ä'+newstring('a',Length-1);}[Benchmark]publicboolContainsOnlyAscii()=>!_asciiInput.AsSpan().ContainsAnyExcept(_allowedAscii);[Benchmark]publicboolContainsOnlyMixed()=>!_mixedInput.AsSpan().ContainsAnyExcept(_allowedMixed);}

@EgorBot

This comment was marked as outdated.

@EgorBot

Copy link
Copy Markdown
Benchmark results on Intel
BenchmarkDotNet v0.13.12, Ubuntu 22.04.4 LTS (Jammy Jellyfish)
Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 16 logical and 8 physical cores
Job-SZXNIX : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Job-DHGYYU : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
MethodToolchainLengthMeanErrorRatio
ContainsOnlyAsciiMain162.434 ns0.0115 ns1.00
ContainsOnlyAsciiPR162.434 ns0.0063 ns1.00
ContainsOnlyMixedMain1612.207 ns0.0035 ns1.00
ContainsOnlyMixedPR163.184 ns0.0004 ns0.26
ContainsOnlyAsciiMain1004.251 ns0.0005 ns1.00
ContainsOnlyAsciiPR1004.251 ns0.0004 ns1.00
ContainsOnlyMixedMain10062.381 ns0.0320 ns1.00
ContainsOnlyMixedPR1005.499 ns0.0020 ns0.09
ContainsOnlyAsciiMain10000208.463 ns0.0200 ns1.00
ContainsOnlyAsciiPR10000208.571 ns0.0259 ns1.00
ContainsOnlyMixedMain100005,783.502 ns1.4382 ns1.00
ContainsOnlyMixedPR10000391.050 ns0.0232 ns0.07

BDN_Artifacts.zip

@MihuBot

Copy link
Copy Markdown
System.Text.RegularExpressions.Tests.Perf_Regex_Industry_RustLang_Sherlock
BenchmarkDotNet v0.13.13-nightly.20240311.145, Ubuntu 22.04.4 LTS (Jammy Jellyfish)
AMD EPYC 9V74, 1 CPU, 8 logical and 4 physical cores
MediumRun : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Job=MediumRun OutlierMode=DontRemove IterationCount=15
LaunchCount=2 MemoryRandomization=True WarmupCount=10
MethodToolchainPatternMeanErrorRatioAllocatedAlloc Ratio
CountMain.*577,117.72 ns1,898.483 ns1.002 B1.00
CountPR.*609,940.38 ns5,541.168 ns1.062 B1.00
CountMain(?i)Holmes53,669.80 ns163.206 ns1.00-NA
CountPR(?i)Holmes53,751.77 ns95.378 ns1.00-NA
CountMain(?i)Sher[a-z]+|Hol[a-z]+96,797.28 ns7,445.773 ns1.01-NA
CountPR(?i)Sher[a-z]+|Hol[a-z]+101,675.49 ns7,778.427 ns1.06-NA
CountMain(?i)Sherlock45,540.09 ns158.021 ns1.00-NA
CountPR(?i)Sherlock45,699.65 ns198.254 ns1.00-NA
CountMain(?i)Sherlock Holmes45,299.62 ns140.891 ns1.00-NA
CountPR(?i)Sherlock Holmes45,322.99 ns60.249 ns1.00-NA
CountMain(?i)Sherlock|Holmes|Watson98,222.98 ns9,160.202 ns1.02-NA
CountPR(?i)Sherlock|Holmes|Watson98,387.61 ns9,077.154 ns1.02-NA
CountMain(?i)Sherlock|(...)er|John|Baker [49]210,972.18 ns25,319.086 ns1.031 B1.00
CountPR(?i)Sherlock|(...)er|John|Baker [49]216,838.46 ns27,873.222 ns1.061 B1.00
CountMain(?i)the249,601.71 ns10,074.303 ns1.001 B1.00
CountPR(?i)the245,957.03 ns10,337.071 ns0.991 B1.00
CountMain(?m)^Sherlock(...)rlock Holmes$ [37]58,240.97 ns2,237.819 ns1.00-NA
CountPR(?m)^Sherlock(...)rlock Holmes$ [37]59,147.93 ns2,749.661 ns1.02-NA
CountMain(?s).*39.26 ns0.088 ns1.00-NA
CountPR(?s).*41.27 ns1.728 ns1.05-NA
CountMain[^\\n]*576,131.95 ns2,377.308 ns1.002 B1.00
CountPR[^\\n]*577,114.20 ns4,299.926 ns1.002 B1.00
CountMain[a-q][^u-z]{13}x23,158.57 ns113.019 ns1.00-NA
CountPR[a-q][^u-z]{13}x23,147.72 ns90.268 ns1.00-NA
CountMain[a-zA-Z]+ing4,112,765.60 ns6,996.218 ns1.0019 B1.00
CountPR[a-zA-Z]+ing4,195,122.65 ns50,241.289 ns1.0221 B1.11
CountMain\b\w+n\b8,324,707.71 ns18,654.897 ns1.0044 B1.00
CountPR\b\w+n\b8,419,875.57 ns67,893.821 ns1.0144 B1.00
CountMain\p{L}10,252,616.17 ns258,692.197 ns1.0035 B1.00
CountPR\p{L}10,178,460.27 ns120,522.988 ns0.9935 B1.00
CountMain\p{Ll}10,218,675.98 ns78,494.108 ns1.0035 B1.00
CountPR\p{Ll}11,001,606.18 ns501,912.621 ns1.0835 B1.00
CountMain\p{Lu}355,667.38 ns7,908.522 ns1.001 B1.00
CountPR\p{Lu}348,108.98 ns3,458.353 ns0.981 B1.00
CountMain\s[a-zA-Z]{0,12}ing\s4,387,091.94 ns11,384.876 ns1.0024 B1.00
CountPR\s[a-zA-Z]{0,12}ing\s4,404,995.88 ns9,088.263 ns1.0024 B1.00
CountMain\w+4,712,257.79 ns27,825.642 ns1.0018 B1.00
CountPR\w+4,671,552.17 ns7,259.567 ns0.9921 B1.17
CountMain\w+\s+Holmes3,340,654.11 ns10,688.067 ns1.0011 B1.00
CountPR\w+\s+Holmes3,355,149.72 ns15,672.577 ns1.0010 B0.91
CountMain\w+\s+Holmes\s+\w+3,609,649.22 ns65,235.529 ns1.0010 B1.00
CountPR\w+\s+Holmes\s+\w+3,502,413.34 ns60,104.536 ns0.9712 B1.20
CountMainaei38,764.39 ns528.311 ns1.00-NA
CountPRaei38,671.05 ns532.363 ns1.00-NA
CountMainaqj38,552.12 ns579.654 ns1.00-NA
CountPRaqj38,708.25 ns516.758 ns1.00-NA
CountMainHolmes50,202.09 ns78.982 ns1.00-NA
CountPRHolmes50,229.34 ns115.917 ns1.00-NA
CountMainHolmes.{0,25}(...).{0,25}Holmes [39]44,351.33 ns90.615 ns1.00-NA
CountPRHolmes.{0,25}(...).{0,25}Holmes [39]44,416.66 ns119.101 ns1.00-NA
CountMainSher[a-z]+|Hol[a-z]+48,711.02 ns113.935 ns1.00-NA
CountPRSher[a-z]+|Hol[a-z]+48,852.79 ns227.029 ns1.00-NA
CountMainSherlock58,662.58 ns2,239.132 ns1.00-NA
CountPRSherlock59,492.49 ns2,839.858 ns1.02-NA
CountMainSherlock Holmes59,536.37 ns2,887.051 ns1.01-NA
CountPRSherlock Holmes59,734.87 ns2,760.671 ns1.01-NA
CountMainSherlock\s+Holmes59,934.57 ns2,381.314 ns1.00-NA
CountPRSherlock\s+Holmes60,715.04 ns3,024.286 ns1.02-NA
CountMainSherlock|Holmes44,782.94 ns106.657 ns1.00-NA
CountPRSherlock|Holmes44,866.33 ns114.894 ns1.00-NA
CountMainSherlock|Holmes|Watson58,630.95 ns77.246 ns1.00-NA
CountPRSherlock|Holmes|Watson59,046.70 ns110.269 ns1.01-NA
CountMainSherlock|Holm(...)er|John|Baker [45]109,855.32 ns133.155 ns1.00-NA
CountPRSherlock|Holm(...)er|John|Baker [45]109,887.13 ns89.847 ns1.00-NA
CountMainSherlock|Street25,047.25 ns62.195 ns1.00-NA
CountPRSherlock|Street25,004.97 ns85.354 ns1.00-NA
CountMainthe179,288.74 ns632.033 ns1.001 B1.00
CountPRthe179,042.42 ns643.054 ns1.001 B1.00
CountMainThe54,675.24 ns84.398 ns1.00-NA
CountPRThe54,504.36 ns156.081 ns1.00-NA
CountMainthe\s+\w+282,854.54 ns12,723.264 ns1.001 B1.00
CountPRthe\s+\w+288,927.85 ns11,924.621 ns1.031 B1.00
CountMainzqj38,776.08 ns523.611 ns1.00-NA
CountPRzqj38,694.38 ns541.346 ns1.00-NA

Vector512<byte> secondBitmap512 = state.SecondBitmap512;
Vector512<ushort> offset512 = Vector512.Create(state.Offset);

if (searchSpaceLength > 2 * Vector512<short>.Count)

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.

@GrabYourPitchforks, in the UTF8 experiment you shared with me, you had code that, after having validated that Vector256 was hardware accelerated, aligned an address and then read a full vector, without concern for whether that vector read under or overread the target region. Is that safe to do on all platforms? It seems we could avoid some branching with similar techniques in many of our implementations that use vectorization.

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.

Is that safe to do on all platforms?

As long as the data is pinned, otherwise GC can interrupt at any point and break the alignment assumption

@MihaZupan
MihaZupanforce-pushed the searchvalues-asciiWithSecondSet branch 2 times, most recently from f378aca to e747fd0CompareJuly 10, 2024 22:48
@stephentoub

Copy link
Copy Markdown
Member

the Ascii-only search is at ~1.5x the throughput of the two bitmaps impl.

Does this mean someone searching mostly ASCII text with IgnoreCase for [a-z] will see a 50% regression?

@MihaZupan

MihaZupan commented Jul 11, 2024

Copy link
Copy Markdown
MemberAuthor

Does this mean someone searching mostly ASCII text with IgnoreCase for [a-z] will see a 50% regression?

If you had a long run of text that was just ASCII and didn't match, yes, it would be slower.
But if you were stopping on matches along the way (even if they were ASCII), the two sets can be cheaper due to the lower overhead on matches (mainly since there's an extra method involved in the ascii+probmap implementation).

I reran Sherlock, and the throughput difference doesn't seem to be affecting it: MihuBot/runtime-utils#505 (comment)

We could also choose to use the two bitmaps approach only on -Except paths to avoid the scalar fallbacks.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

Any concerns with merging this one as-is and seeing if benchmarks complain?

@stephentoub

Copy link
Copy Markdown
Member

Ok, let's give it a try but be ready to back it out if any meaningful regressions pop up.

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.

6 participants

@MihaZupan@danmoseley@MihuBot@EgorBo@EgorBot@stephentoub
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add SearchValues<char> implementation for two sets of 128 chars - #103216

Merged
stephentoub merged 4 commits into
dotnet:mainfrom
MihaZupan:searchvalues-asciiWithSecondSet
Jul 22, 2024
Merged

Add SearchValues<char> implementation for two sets of 128 chars#103216
stephentoub merged 4 commits into
dotnet:mainfrom
MihaZupan:searchvalues-asciiWithSecondSet

Conversation

@MihaZupan

@MihaZupanMihaZupan commented Jun 10, 2024

Copy link
Copy Markdown
Member

#101001 significantly improved the performance of the non-vectorized -Except paths of non-ASCII SearchValues<char>.
However, they are still not vectorized, and this PR changes that for values where the non-ASCII part can fit into a 128-bit bitmap.

This adds an implementation that's almost the same as the AsciiCharSearchValues, but where the core lookup checks against two 128-bit bitmaps, with the second one at a variable offset:

-Vector128<byte> source = TOptimizations.PackSources(source0.AsUInt16(), source1.AsUInt16());-Vector128<byte> result = IndexOfAnyLookupCore(source, bitmapLookup);-return TNegator.NegateIfNeeded(result);+Vector128<byte> packed0 = TOptimizations.PackSources(source0.AsUInt16(), source1.AsUInt16());+Vector128<byte> packed1 = Default.PackSources(source0.AsUInt16() - offset, source1.AsUInt16() - offset);+Vector128<byte> result0 = IndexOfAnyLookupCore(packed0, bitmapLookup0);+Vector128<byte> result1 = IndexOfAnyLookupCore(packed1, bitmapLookup1);+return TNegator.NegateIfNeeded(result0 | result1);

(All the numbers below are using the Avx2 path of the new implementation -- measured before #103710)

Avx2 where the text is mostly non-ASCII (previous 'Mixed' would use the prob map / scalar fallback)
MethodToolchainLengthMeanErrorRatio
Asciimain100038.13 ns0.129 ns1.00
Asciipr100037.32 ns0.160 ns0.98
Mixedmain1000127.47 ns0.429 ns1.00
Mixedpr100051.20 ns0.095 ns0.40
AsciiLastmain100035.82 ns0.264 ns1.00
AsciiLastpr100035.81 ns0.079 ns1.00
MixedLastmain1000138.47 ns0.335 ns1.00
MixedLastpr100052.36 ns0.138 ns0.38
AsciiExceptmain100035.40 ns0.138 ns1.00
AsciiExceptpr100035.47 ns0.078 ns1.00
MixedExceptmain1000592.08 ns1.005 ns1.00
MixedExceptpr100051.00 ns0.133 ns0.09
AsciiLastExceptmain100036.77 ns0.535 ns1.00
AsciiLastExceptpr100036.34 ns0.086 ns0.99
MixedLastExceptmain1000682.31 ns8.073 ns1.00
MixedLastExceptpr100053.97 ns1.372 ns0.08
Avx512 machine (the probabilistic map is a lot faster than on Avx2)
MethodToolchainLengthMeanErrorRatio
Asciimain100037.44 ns0.006 ns1.00
Asciipr100037.73 ns0.005 ns1.01
Mixedmain100086.86 ns0.111 ns1.00
Mixedpr100060.36 ns0.010 ns0.69
AsciiLastmain100038.02 ns0.013 ns1.00
AsciiLastpr100038.28 ns0.003 ns1.01
MixedLastmain100095.96 ns0.023 ns1.00
MixedLastpr100061.16 ns0.009 ns0.64
AsciiExceptmain100040.17 ns0.005 ns1.00
AsciiExceptpr100039.81 ns0.005 ns0.99
MixedExceptmain1000585.99 ns0.183 ns1.00
MixedExceptpr100063.22 ns0.010 ns0.11
AsciiLastExceptmain100041.54 ns0.003 ns1.00
AsciiLastExceptpr100041.54 ns0.005 ns1.00
MixedLastExceptmain1000855.18 ns0.059 ns1.00
MixedLastExceptpr100064.65 ns0.010 ns0.08
Early matches
MethodToolchainInputContainsNonAsciiMeanErrorRatio
MixedmainFalse5.130 ns0.0374 ns1.00
MixedprFalse3.425 ns0.0303 ns0.67
MixedLastmainFalse4.830 ns0.0182 ns1.00
MixedLastprFalse4.179 ns0.0750 ns0.87
MixedmainTrue8.440 ns0.0780 ns1.00
MixedprTrue3.383 ns0.0163 ns0.40
MixedLastmainTrue9.011 ns0.2175 ns1.00
MixedLastprTrue4.073 ns0.0087 ns0.45

From that, we can see that the Ascii-only search is at ~1.5x the throughput of the two bitmaps impl.
The two bitmaps have ~1.5x the throughput of the probabilistic map on Avx512 and ~2.5x on Avx2.
In other words, this change is a throughput regression for ProbabilisticWithAsciiCharSearchValues if the text is all ASCII, and an improvement otherwise. It's always chepaer for early matches though.
For the -Except paths where ProbabilisticWithAsciiCharSearchValues uses a scalar fallback, the two bitmaps approach is obviously a lot (10x+) faster.

The change doesn't seem to impact existing Regex benchmarks much, likely because they're very focused on ASCII.


I also tried different implementation approaches to try and reduce code duplication between the existing Ascii and "ascii with second set" implementations:

Details
  • c6f1495, combining the second set into the existing AsciiState, which does save some duplication, but increases the memory consumption of all existing Ascii-only SearchValues.
  • 32f9cf1, that goes all-in with generics, but the JIT can't quite deal with having the vector state be completely generic.
    publicstaticintIndexOfAny<TNegator,TOptimizations>(refshortsearchSpace,intsearchSpaceLength,refAsciiWithSecondSetStatestate)whereTNegator:struct,INegatorwhereTOptimizations:struct,IOptimizations=>IndexOfAnyCore<int,TNegator,IndexOfAnyResultMapper<short>,AsciiWithSecondSetLookup<TOptimizations>,AsciiWithSecondSetState,(Vector128<byte>AsciiBitmap,Vector128<byte>SecondBitmap,Vector128<ushort>Offset),(Vector256<byte>AsciiBitmap,Vector256<byte>SecondBitmap,Vector256<ushort>Offset)>(refsearchSpace,searchSpaceLength,refstate);privatestaticTResultIndexOfAnyCore<TResult,TNegator,TResultMapper,TLookup,TState,TVector128State,TVector256State>(refshortsearchSpace,intsearchSpaceLength,refTStatestate)whereTResult:structwhereTNegator:struct,INegatorwhereTResultMapper:struct,IResultMapper<short,TResult>whereTLookup:struct,ILookup<TState,TVector128State,TVector256State>whereTState:structwhereTVector128State:structwhereTVector256State:struct

Looking at patterns from Regex_RealWorldPatterns.json, ~75% of non-ASCII sets would use the new implementation over the Ascii+ProbMap, most of which because of the kelvin sign.

Number of sources: 18886
numberOfSearchValues
5771
numberOfSearchValuesWithNonAscii
1238
numberOfSearchValuesWithTwoSets
960
numberOfSearchValuesWhereNonAsciiIsKelvin
684
numberOfPatternsWithSearchValues
5093
numberOfPatternsWithSearchValuesWithNonAscii
1038
numberOfPatternsWithSearchValuesWithTwoSets
765

Parsed data for the above:
Regex_RealWorldPatterns.SearchValues.json

@MihaZupanMihaZupan added this to the 9.0.0 milestone Jun 10, 2024
@MihaZupanMihaZupan self-assigned this Jun 10, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

@danmoseley

Copy link
Copy Markdown
Contributor

The change doesn't seem to impact existing Regex benchmarks much, likely because they're very focused on ASCII.

is it worth adding one or two more non ASCII?

@MihaZupan

This comment was marked as outdated.

@MihuBot

This comment was marked as outdated.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@MihuBot benchmark RustLang_Sherlock https://github.com/MihaZupan/performance/tree/compiled-regex-only -medium

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@MihuBot fuzz SearchValues

@MihaZupan

This comment was marked as outdated.

@EgorBo

Copy link
Copy Markdown
Member

@MihaZupan you need to either omit Run<> or pass args to Run, so in your case:

BenchmarkRunner.Run<Bench>(args);

Otherwise --corerun /base/corerun /diff/corerun args will be ignored 🙂

@MihaZupan

MihaZupan commented Jun 22, 2024

Copy link
Copy Markdown
MemberAuthor

Aaah right, thanks. I also forgot to remove the ShortRunJob.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@EgorBot -intel

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Buffers;
#nullable disable
publicclassBench{privatestaticreadonlySearchValues<char>_allowedAscii=SearchValues.Create("1234567890abcdefghijklmnopqrstuvwxyz");privatestaticreadonlySearchValues<char>_allowedMixed=SearchValues.Create("äöü1234567890abcdefghijklmnopqrstuvw");privatestring_asciiInput;privatestring_mixedInput;[Params(16,100,10_000)]publicintLength;[GlobalSetup]publicvoidSetup(){_asciiInput=newstring('a',Length);_mixedInput='ä'+newstring('a',Length-1);}[Benchmark]publicboolContainsOnlyAscii()=>!_asciiInput.AsSpan().ContainsAnyExcept(_allowedAscii);[Benchmark]publicboolContainsOnlyMixed()=>!_mixedInput.AsSpan().ContainsAnyExcept(_allowedMixed);}

@EgorBot

This comment was marked as outdated.

@EgorBot

Copy link
Copy Markdown
Benchmark results on Intel
BenchmarkDotNet v0.13.12, Ubuntu 22.04.4 LTS (Jammy Jellyfish)
Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 16 logical and 8 physical cores
Job-SZXNIX : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Job-DHGYYU : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
MethodToolchainLengthMeanErrorRatio
ContainsOnlyAsciiMain162.434 ns0.0115 ns1.00
ContainsOnlyAsciiPR162.434 ns0.0063 ns1.00
ContainsOnlyMixedMain1612.207 ns0.0035 ns1.00
ContainsOnlyMixedPR163.184 ns0.0004 ns0.26
ContainsOnlyAsciiMain1004.251 ns0.0005 ns1.00
ContainsOnlyAsciiPR1004.251 ns0.0004 ns1.00
ContainsOnlyMixedMain10062.381 ns0.0320 ns1.00
ContainsOnlyMixedPR1005.499 ns0.0020 ns0.09
ContainsOnlyAsciiMain10000208.463 ns0.0200 ns1.00
ContainsOnlyAsciiPR10000208.571 ns0.0259 ns1.00
ContainsOnlyMixedMain100005,783.502 ns1.4382 ns1.00
ContainsOnlyMixedPR10000391.050 ns0.0232 ns0.07

BDN_Artifacts.zip

@MihuBot

Copy link
Copy Markdown
System.Text.RegularExpressions.Tests.Perf_Regex_Industry_RustLang_Sherlock
BenchmarkDotNet v0.13.13-nightly.20240311.145, Ubuntu 22.04.4 LTS (Jammy Jellyfish)
AMD EPYC 9V74, 1 CPU, 8 logical and 4 physical cores
MediumRun : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Job=MediumRun OutlierMode=DontRemove IterationCount=15
LaunchCount=2 MemoryRandomization=True WarmupCount=10
MethodToolchainPatternMeanErrorRatioAllocatedAlloc Ratio
CountMain.*577,117.72 ns1,898.483 ns1.002 B1.00
CountPR.*609,940.38 ns5,541.168 ns1.062 B1.00
CountMain(?i)Holmes53,669.80 ns163.206 ns1.00-NA
CountPR(?i)Holmes53,751.77 ns95.378 ns1.00-NA
CountMain(?i)Sher[a-z]+|Hol[a-z]+96,797.28 ns7,445.773 ns1.01-NA
CountPR(?i)Sher[a-z]+|Hol[a-z]+101,675.49 ns7,778.427 ns1.06-NA
CountMain(?i)Sherlock45,540.09 ns158.021 ns1.00-NA
CountPR(?i)Sherlock45,699.65 ns198.254 ns1.00-NA
CountMain(?i)Sherlock Holmes45,299.62 ns140.891 ns1.00-NA
CountPR(?i)Sherlock Holmes45,322.99 ns60.249 ns1.00-NA
CountMain(?i)Sherlock|Holmes|Watson98,222.98 ns9,160.202 ns1.02-NA
CountPR(?i)Sherlock|Holmes|Watson98,387.61 ns9,077.154 ns1.02-NA
CountMain(?i)Sherlock|(...)er|John|Baker [49]210,972.18 ns25,319.086 ns1.031 B1.00
CountPR(?i)Sherlock|(...)er|John|Baker [49]216,838.46 ns27,873.222 ns1.061 B1.00
CountMain(?i)the249,601.71 ns10,074.303 ns1.001 B1.00
CountPR(?i)the245,957.03 ns10,337.071 ns0.991 B1.00
CountMain(?m)^Sherlock(...)rlock Holmes$ [37]58,240.97 ns2,237.819 ns1.00-NA
CountPR(?m)^Sherlock(...)rlock Holmes$ [37]59,147.93 ns2,749.661 ns1.02-NA
CountMain(?s).*39.26 ns0.088 ns1.00-NA
CountPR(?s).*41.27 ns1.728 ns1.05-NA
CountMain[^\\n]*576,131.95 ns2,377.308 ns1.002 B1.00
CountPR[^\\n]*577,114.20 ns4,299.926 ns1.002 B1.00
CountMain[a-q][^u-z]{13}x23,158.57 ns113.019 ns1.00-NA
CountPR[a-q][^u-z]{13}x23,147.72 ns90.268 ns1.00-NA
CountMain[a-zA-Z]+ing4,112,765.60 ns6,996.218 ns1.0019 B1.00
CountPR[a-zA-Z]+ing4,195,122.65 ns50,241.289 ns1.0221 B1.11
CountMain\b\w+n\b8,324,707.71 ns18,654.897 ns1.0044 B1.00
CountPR\b\w+n\b8,419,875.57 ns67,893.821 ns1.0144 B1.00
CountMain\p{L}10,252,616.17 ns258,692.197 ns1.0035 B1.00
CountPR\p{L}10,178,460.27 ns120,522.988 ns0.9935 B1.00
CountMain\p{Ll}10,218,675.98 ns78,494.108 ns1.0035 B1.00
CountPR\p{Ll}11,001,606.18 ns501,912.621 ns1.0835 B1.00
CountMain\p{Lu}355,667.38 ns7,908.522 ns1.001 B1.00
CountPR\p{Lu}348,108.98 ns3,458.353 ns0.981 B1.00
CountMain\s[a-zA-Z]{0,12}ing\s4,387,091.94 ns11,384.876 ns1.0024 B1.00
CountPR\s[a-zA-Z]{0,12}ing\s4,404,995.88 ns9,088.263 ns1.0024 B1.00
CountMain\w+4,712,257.79 ns27,825.642 ns1.0018 B1.00
CountPR\w+4,671,552.17 ns7,259.567 ns0.9921 B1.17
CountMain\w+\s+Holmes3,340,654.11 ns10,688.067 ns1.0011 B1.00
CountPR\w+\s+Holmes3,355,149.72 ns15,672.577 ns1.0010 B0.91
CountMain\w+\s+Holmes\s+\w+3,609,649.22 ns65,235.529 ns1.0010 B1.00
CountPR\w+\s+Holmes\s+\w+3,502,413.34 ns60,104.536 ns0.9712 B1.20
CountMainaei38,764.39 ns528.311 ns1.00-NA
CountPRaei38,671.05 ns532.363 ns1.00-NA
CountMainaqj38,552.12 ns579.654 ns1.00-NA
CountPRaqj38,708.25 ns516.758 ns1.00-NA
CountMainHolmes50,202.09 ns78.982 ns1.00-NA
CountPRHolmes50,229.34 ns115.917 ns1.00-NA
CountMainHolmes.{0,25}(...).{0,25}Holmes [39]44,351.33 ns90.615 ns1.00-NA
CountPRHolmes.{0,25}(...).{0,25}Holmes [39]44,416.66 ns119.101 ns1.00-NA
CountMainSher[a-z]+|Hol[a-z]+48,711.02 ns113.935 ns1.00-NA
CountPRSher[a-z]+|Hol[a-z]+48,852.79 ns227.029 ns1.00-NA
CountMainSherlock58,662.58 ns2,239.132 ns1.00-NA
CountPRSherlock59,492.49 ns2,839.858 ns1.02-NA
CountMainSherlock Holmes59,536.37 ns2,887.051 ns1.01-NA
CountPRSherlock Holmes59,734.87 ns2,760.671 ns1.01-NA
CountMainSherlock\s+Holmes59,934.57 ns2,381.314 ns1.00-NA
CountPRSherlock\s+Holmes60,715.04 ns3,024.286 ns1.02-NA
CountMainSherlock|Holmes44,782.94 ns106.657 ns1.00-NA
CountPRSherlock|Holmes44,866.33 ns114.894 ns1.00-NA
CountMainSherlock|Holmes|Watson58,630.95 ns77.246 ns1.00-NA
CountPRSherlock|Holmes|Watson59,046.70 ns110.269 ns1.01-NA
CountMainSherlock|Holm(...)er|John|Baker [45]109,855.32 ns133.155 ns1.00-NA
CountPRSherlock|Holm(...)er|John|Baker [45]109,887.13 ns89.847 ns1.00-NA
CountMainSherlock|Street25,047.25 ns62.195 ns1.00-NA
CountPRSherlock|Street25,004.97 ns85.354 ns1.00-NA
CountMainthe179,288.74 ns632.033 ns1.001 B1.00
CountPRthe179,042.42 ns643.054 ns1.001 B1.00
CountMainThe54,675.24 ns84.398 ns1.00-NA
CountPRThe54,504.36 ns156.081 ns1.00-NA
CountMainthe\s+\w+282,854.54 ns12,723.264 ns1.001 B1.00
CountPRthe\s+\w+288,927.85 ns11,924.621 ns1.031 B1.00
CountMainzqj38,776.08 ns523.611 ns1.00-NA
CountPRzqj38,694.38 ns541.346 ns1.00-NA

Vector512<byte> secondBitmap512 = state.SecondBitmap512;
Vector512<ushort> offset512 = Vector512.Create(state.Offset);

if (searchSpaceLength > 2 * Vector512<short>.Count)

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.

@GrabYourPitchforks, in the UTF8 experiment you shared with me, you had code that, after having validated that Vector256 was hardware accelerated, aligned an address and then read a full vector, without concern for whether that vector read under or overread the target region. Is that safe to do on all platforms? It seems we could avoid some branching with similar techniques in many of our implementations that use vectorization.

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.

Is that safe to do on all platforms?

As long as the data is pinned, otherwise GC can interrupt at any point and break the alignment assumption

@MihaZupan
MihaZupanforce-pushed the searchvalues-asciiWithSecondSet branch 2 times, most recently from f378aca to e747fd0CompareJuly 10, 2024 22:48
@stephentoub

Copy link
Copy Markdown
Member

the Ascii-only search is at ~1.5x the throughput of the two bitmaps impl.

Does this mean someone searching mostly ASCII text with IgnoreCase for [a-z] will see a 50% regression?

@MihaZupan

MihaZupan commented Jul 11, 2024

Copy link
Copy Markdown
MemberAuthor

Does this mean someone searching mostly ASCII text with IgnoreCase for [a-z] will see a 50% regression?

If you had a long run of text that was just ASCII and didn't match, yes, it would be slower.
But if you were stopping on matches along the way (even if they were ASCII), the two sets can be cheaper due to the lower overhead on matches (mainly since there's an extra method involved in the ascii+probmap implementation).

I reran Sherlock, and the throughput difference doesn't seem to be affecting it: MihuBot/runtime-utils#505 (comment)

We could also choose to use the two bitmaps approach only on -Except paths to avoid the scalar fallbacks.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

Any concerns with merging this one as-is and seeing if benchmarks complain?

@stephentoub

Copy link
Copy Markdown
Member

Ok, let's give it a try but be ready to back it out if any meaningful regressions pop up.

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.

6 participants

@MihaZupan@danmoseley@MihuBot@EgorBo@EgorBot@stephentoub
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add SearchValues<char> implementation for two sets of 128 chars - #103216

Merged
stephentoub merged 4 commits into
dotnet:mainfrom
MihaZupan:searchvalues-asciiWithSecondSet
Jul 22, 2024
Merged

Add SearchValues<char> implementation for two sets of 128 chars#103216
stephentoub merged 4 commits into
dotnet:mainfrom
MihaZupan:searchvalues-asciiWithSecondSet

Conversation

@MihaZupan

@MihaZupanMihaZupan commented Jun 10, 2024

Copy link
Copy Markdown
Member

#101001 significantly improved the performance of the non-vectorized -Except paths of non-ASCII SearchValues<char>.
However, they are still not vectorized, and this PR changes that for values where the non-ASCII part can fit into a 128-bit bitmap.

This adds an implementation that's almost the same as the AsciiCharSearchValues, but where the core lookup checks against two 128-bit bitmaps, with the second one at a variable offset:

-Vector128<byte> source = TOptimizations.PackSources(source0.AsUInt16(), source1.AsUInt16());-Vector128<byte> result = IndexOfAnyLookupCore(source, bitmapLookup);-return TNegator.NegateIfNeeded(result);+Vector128<byte> packed0 = TOptimizations.PackSources(source0.AsUInt16(), source1.AsUInt16());+Vector128<byte> packed1 = Default.PackSources(source0.AsUInt16() - offset, source1.AsUInt16() - offset);+Vector128<byte> result0 = IndexOfAnyLookupCore(packed0, bitmapLookup0);+Vector128<byte> result1 = IndexOfAnyLookupCore(packed1, bitmapLookup1);+return TNegator.NegateIfNeeded(result0 | result1);

(All the numbers below are using the Avx2 path of the new implementation -- measured before #103710)

Avx2 where the text is mostly non-ASCII (previous 'Mixed' would use the prob map / scalar fallback)
MethodToolchainLengthMeanErrorRatio
Asciimain100038.13 ns0.129 ns1.00
Asciipr100037.32 ns0.160 ns0.98
Mixedmain1000127.47 ns0.429 ns1.00
Mixedpr100051.20 ns0.095 ns0.40
AsciiLastmain100035.82 ns0.264 ns1.00
AsciiLastpr100035.81 ns0.079 ns1.00
MixedLastmain1000138.47 ns0.335 ns1.00
MixedLastpr100052.36 ns0.138 ns0.38
AsciiExceptmain100035.40 ns0.138 ns1.00
AsciiExceptpr100035.47 ns0.078 ns1.00
MixedExceptmain1000592.08 ns1.005 ns1.00
MixedExceptpr100051.00 ns0.133 ns0.09
AsciiLastExceptmain100036.77 ns0.535 ns1.00
AsciiLastExceptpr100036.34 ns0.086 ns0.99
MixedLastExceptmain1000682.31 ns8.073 ns1.00
MixedLastExceptpr100053.97 ns1.372 ns0.08
Avx512 machine (the probabilistic map is a lot faster than on Avx2)
MethodToolchainLengthMeanErrorRatio
Asciimain100037.44 ns0.006 ns1.00
Asciipr100037.73 ns0.005 ns1.01
Mixedmain100086.86 ns0.111 ns1.00
Mixedpr100060.36 ns0.010 ns0.69
AsciiLastmain100038.02 ns0.013 ns1.00
AsciiLastpr100038.28 ns0.003 ns1.01
MixedLastmain100095.96 ns0.023 ns1.00
MixedLastpr100061.16 ns0.009 ns0.64
AsciiExceptmain100040.17 ns0.005 ns1.00
AsciiExceptpr100039.81 ns0.005 ns0.99
MixedExceptmain1000585.99 ns0.183 ns1.00
MixedExceptpr100063.22 ns0.010 ns0.11
AsciiLastExceptmain100041.54 ns0.003 ns1.00
AsciiLastExceptpr100041.54 ns0.005 ns1.00
MixedLastExceptmain1000855.18 ns0.059 ns1.00
MixedLastExceptpr100064.65 ns0.010 ns0.08
Early matches
MethodToolchainInputContainsNonAsciiMeanErrorRatio
MixedmainFalse5.130 ns0.0374 ns1.00
MixedprFalse3.425 ns0.0303 ns0.67
MixedLastmainFalse4.830 ns0.0182 ns1.00
MixedLastprFalse4.179 ns0.0750 ns0.87
MixedmainTrue8.440 ns0.0780 ns1.00
MixedprTrue3.383 ns0.0163 ns0.40
MixedLastmainTrue9.011 ns0.2175 ns1.00
MixedLastprTrue4.073 ns0.0087 ns0.45

From that, we can see that the Ascii-only search is at ~1.5x the throughput of the two bitmaps impl.
The two bitmaps have ~1.5x the throughput of the probabilistic map on Avx512 and ~2.5x on Avx2.
In other words, this change is a throughput regression for ProbabilisticWithAsciiCharSearchValues if the text is all ASCII, and an improvement otherwise. It's always chepaer for early matches though.
For the -Except paths where ProbabilisticWithAsciiCharSearchValues uses a scalar fallback, the two bitmaps approach is obviously a lot (10x+) faster.

The change doesn't seem to impact existing Regex benchmarks much, likely because they're very focused on ASCII.


I also tried different implementation approaches to try and reduce code duplication between the existing Ascii and "ascii with second set" implementations:

Details
  • c6f1495, combining the second set into the existing AsciiState, which does save some duplication, but increases the memory consumption of all existing Ascii-only SearchValues.
  • 32f9cf1, that goes all-in with generics, but the JIT can't quite deal with having the vector state be completely generic.
    publicstaticintIndexOfAny<TNegator,TOptimizations>(refshortsearchSpace,intsearchSpaceLength,refAsciiWithSecondSetStatestate)whereTNegator:struct,INegatorwhereTOptimizations:struct,IOptimizations=>IndexOfAnyCore<int,TNegator,IndexOfAnyResultMapper<short>,AsciiWithSecondSetLookup<TOptimizations>,AsciiWithSecondSetState,(Vector128<byte>AsciiBitmap,Vector128<byte>SecondBitmap,Vector128<ushort>Offset),(Vector256<byte>AsciiBitmap,Vector256<byte>SecondBitmap,Vector256<ushort>Offset)>(refsearchSpace,searchSpaceLength,refstate);privatestaticTResultIndexOfAnyCore<TResult,TNegator,TResultMapper,TLookup,TState,TVector128State,TVector256State>(refshortsearchSpace,intsearchSpaceLength,refTStatestate)whereTResult:structwhereTNegator:struct,INegatorwhereTResultMapper:struct,IResultMapper<short,TResult>whereTLookup:struct,ILookup<TState,TVector128State,TVector256State>whereTState:structwhereTVector128State:structwhereTVector256State:struct

Looking at patterns from Regex_RealWorldPatterns.json, ~75% of non-ASCII sets would use the new implementation over the Ascii+ProbMap, most of which because of the kelvin sign.

Number of sources: 18886
numberOfSearchValues
5771
numberOfSearchValuesWithNonAscii
1238
numberOfSearchValuesWithTwoSets
960
numberOfSearchValuesWhereNonAsciiIsKelvin
684
numberOfPatternsWithSearchValues
5093
numberOfPatternsWithSearchValuesWithNonAscii
1038
numberOfPatternsWithSearchValuesWithTwoSets
765

Parsed data for the above:
Regex_RealWorldPatterns.SearchValues.json

@MihaZupanMihaZupan added this to the 9.0.0 milestone Jun 10, 2024
@MihaZupanMihaZupan self-assigned this Jun 10, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

@danmoseley

Copy link
Copy Markdown
Contributor

The change doesn't seem to impact existing Regex benchmarks much, likely because they're very focused on ASCII.

is it worth adding one or two more non ASCII?

@MihaZupan

This comment was marked as outdated.

@MihuBot

This comment was marked as outdated.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@MihuBot benchmark RustLang_Sherlock https://github.com/MihaZupan/performance/tree/compiled-regex-only -medium

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@MihuBot fuzz SearchValues

@MihaZupan

This comment was marked as outdated.

@EgorBo

Copy link
Copy Markdown
Member

@MihaZupan you need to either omit Run<> or pass args to Run, so in your case:

BenchmarkRunner.Run<Bench>(args);

Otherwise --corerun /base/corerun /diff/corerun args will be ignored 🙂

@MihaZupan

MihaZupan commented Jun 22, 2024

Copy link
Copy Markdown
MemberAuthor

Aaah right, thanks. I also forgot to remove the ShortRunJob.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@EgorBot -intel

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Buffers;
#nullable disable
publicclassBench{privatestaticreadonlySearchValues<char>_allowedAscii=SearchValues.Create("1234567890abcdefghijklmnopqrstuvwxyz");privatestaticreadonlySearchValues<char>_allowedMixed=SearchValues.Create("äöü1234567890abcdefghijklmnopqrstuvw");privatestring_asciiInput;privatestring_mixedInput;[Params(16,100,10_000)]publicintLength;[GlobalSetup]publicvoidSetup(){_asciiInput=newstring('a',Length);_mixedInput='ä'+newstring('a',Length-1);}[Benchmark]publicboolContainsOnlyAscii()=>!_asciiInput.AsSpan().ContainsAnyExcept(_allowedAscii);[Benchmark]publicboolContainsOnlyMixed()=>!_mixedInput.AsSpan().ContainsAnyExcept(_allowedMixed);}

@EgorBot

This comment was marked as outdated.

@EgorBot

Copy link
Copy Markdown
Benchmark results on Intel
BenchmarkDotNet v0.13.12, Ubuntu 22.04.4 LTS (Jammy Jellyfish)
Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 16 logical and 8 physical cores
Job-SZXNIX : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Job-DHGYYU : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
MethodToolchainLengthMeanErrorRatio
ContainsOnlyAsciiMain162.434 ns0.0115 ns1.00
ContainsOnlyAsciiPR162.434 ns0.0063 ns1.00
ContainsOnlyMixedMain1612.207 ns0.0035 ns1.00
ContainsOnlyMixedPR163.184 ns0.0004 ns0.26
ContainsOnlyAsciiMain1004.251 ns0.0005 ns1.00
ContainsOnlyAsciiPR1004.251 ns0.0004 ns1.00
ContainsOnlyMixedMain10062.381 ns0.0320 ns1.00
ContainsOnlyMixedPR1005.499 ns0.0020 ns0.09
ContainsOnlyAsciiMain10000208.463 ns0.0200 ns1.00
ContainsOnlyAsciiPR10000208.571 ns0.0259 ns1.00
ContainsOnlyMixedMain100005,783.502 ns1.4382 ns1.00
ContainsOnlyMixedPR10000391.050 ns0.0232 ns0.07

BDN_Artifacts.zip

@MihuBot

Copy link
Copy Markdown
System.Text.RegularExpressions.Tests.Perf_Regex_Industry_RustLang_Sherlock
BenchmarkDotNet v0.13.13-nightly.20240311.145, Ubuntu 22.04.4 LTS (Jammy Jellyfish)
AMD EPYC 9V74, 1 CPU, 8 logical and 4 physical cores
MediumRun : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Job=MediumRun OutlierMode=DontRemove IterationCount=15
LaunchCount=2 MemoryRandomization=True WarmupCount=10
MethodToolchainPatternMeanErrorRatioAllocatedAlloc Ratio
CountMain.*577,117.72 ns1,898.483 ns1.002 B1.00
CountPR.*609,940.38 ns5,541.168 ns1.062 B1.00
CountMain(?i)Holmes53,669.80 ns163.206 ns1.00-NA
CountPR(?i)Holmes53,751.77 ns95.378 ns1.00-NA
CountMain(?i)Sher[a-z]+|Hol[a-z]+96,797.28 ns7,445.773 ns1.01-NA
CountPR(?i)Sher[a-z]+|Hol[a-z]+101,675.49 ns7,778.427 ns1.06-NA
CountMain(?i)Sherlock45,540.09 ns158.021 ns1.00-NA
CountPR(?i)Sherlock45,699.65 ns198.254 ns1.00-NA
CountMain(?i)Sherlock Holmes45,299.62 ns140.891 ns1.00-NA
CountPR(?i)Sherlock Holmes45,322.99 ns60.249 ns1.00-NA
CountMain(?i)Sherlock|Holmes|Watson98,222.98 ns9,160.202 ns1.02-NA
CountPR(?i)Sherlock|Holmes|Watson98,387.61 ns9,077.154 ns1.02-NA
CountMain(?i)Sherlock|(...)er|John|Baker [49]210,972.18 ns25,319.086 ns1.031 B1.00
CountPR(?i)Sherlock|(...)er|John|Baker [49]216,838.46 ns27,873.222 ns1.061 B1.00
CountMain(?i)the249,601.71 ns10,074.303 ns1.001 B1.00
CountPR(?i)the245,957.03 ns10,337.071 ns0.991 B1.00
CountMain(?m)^Sherlock(...)rlock Holmes$ [37]58,240.97 ns2,237.819 ns1.00-NA
CountPR(?m)^Sherlock(...)rlock Holmes$ [37]59,147.93 ns2,749.661 ns1.02-NA
CountMain(?s).*39.26 ns0.088 ns1.00-NA
CountPR(?s).*41.27 ns1.728 ns1.05-NA
CountMain[^\\n]*576,131.95 ns2,377.308 ns1.002 B1.00
CountPR[^\\n]*577,114.20 ns4,299.926 ns1.002 B1.00
CountMain[a-q][^u-z]{13}x23,158.57 ns113.019 ns1.00-NA
CountPR[a-q][^u-z]{13}x23,147.72 ns90.268 ns1.00-NA
CountMain[a-zA-Z]+ing4,112,765.60 ns6,996.218 ns1.0019 B1.00
CountPR[a-zA-Z]+ing4,195,122.65 ns50,241.289 ns1.0221 B1.11
CountMain\b\w+n\b8,324,707.71 ns18,654.897 ns1.0044 B1.00
CountPR\b\w+n\b8,419,875.57 ns67,893.821 ns1.0144 B1.00
CountMain\p{L}10,252,616.17 ns258,692.197 ns1.0035 B1.00
CountPR\p{L}10,178,460.27 ns120,522.988 ns0.9935 B1.00
CountMain\p{Ll}10,218,675.98 ns78,494.108 ns1.0035 B1.00
CountPR\p{Ll}11,001,606.18 ns501,912.621 ns1.0835 B1.00
CountMain\p{Lu}355,667.38 ns7,908.522 ns1.001 B1.00
CountPR\p{Lu}348,108.98 ns3,458.353 ns0.981 B1.00
CountMain\s[a-zA-Z]{0,12}ing\s4,387,091.94 ns11,384.876 ns1.0024 B1.00
CountPR\s[a-zA-Z]{0,12}ing\s4,404,995.88 ns9,088.263 ns1.0024 B1.00
CountMain\w+4,712,257.79 ns27,825.642 ns1.0018 B1.00
CountPR\w+4,671,552.17 ns7,259.567 ns0.9921 B1.17
CountMain\w+\s+Holmes3,340,654.11 ns10,688.067 ns1.0011 B1.00
CountPR\w+\s+Holmes3,355,149.72 ns15,672.577 ns1.0010 B0.91
CountMain\w+\s+Holmes\s+\w+3,609,649.22 ns65,235.529 ns1.0010 B1.00
CountPR\w+\s+Holmes\s+\w+3,502,413.34 ns60,104.536 ns0.9712 B1.20
CountMainaei38,764.39 ns528.311 ns1.00-NA
CountPRaei38,671.05 ns532.363 ns1.00-NA
CountMainaqj38,552.12 ns579.654 ns1.00-NA
CountPRaqj38,708.25 ns516.758 ns1.00-NA
CountMainHolmes50,202.09 ns78.982 ns1.00-NA
CountPRHolmes50,229.34 ns115.917 ns1.00-NA
CountMainHolmes.{0,25}(...).{0,25}Holmes [39]44,351.33 ns90.615 ns1.00-NA
CountPRHolmes.{0,25}(...).{0,25}Holmes [39]44,416.66 ns119.101 ns1.00-NA
CountMainSher[a-z]+|Hol[a-z]+48,711.02 ns113.935 ns1.00-NA
CountPRSher[a-z]+|Hol[a-z]+48,852.79 ns227.029 ns1.00-NA
CountMainSherlock58,662.58 ns2,239.132 ns1.00-NA
CountPRSherlock59,492.49 ns2,839.858 ns1.02-NA
CountMainSherlock Holmes59,536.37 ns2,887.051 ns1.01-NA
CountPRSherlock Holmes59,734.87 ns2,760.671 ns1.01-NA
CountMainSherlock\s+Holmes59,934.57 ns2,381.314 ns1.00-NA
CountPRSherlock\s+Holmes60,715.04 ns3,024.286 ns1.02-NA
CountMainSherlock|Holmes44,782.94 ns106.657 ns1.00-NA
CountPRSherlock|Holmes44,866.33 ns114.894 ns1.00-NA
CountMainSherlock|Holmes|Watson58,630.95 ns77.246 ns1.00-NA
CountPRSherlock|Holmes|Watson59,046.70 ns110.269 ns1.01-NA
CountMainSherlock|Holm(...)er|John|Baker [45]109,855.32 ns133.155 ns1.00-NA
CountPRSherlock|Holm(...)er|John|Baker [45]109,887.13 ns89.847 ns1.00-NA
CountMainSherlock|Street25,047.25 ns62.195 ns1.00-NA
CountPRSherlock|Street25,004.97 ns85.354 ns1.00-NA
CountMainthe179,288.74 ns632.033 ns1.001 B1.00
CountPRthe179,042.42 ns643.054 ns1.001 B1.00
CountMainThe54,675.24 ns84.398 ns1.00-NA
CountPRThe54,504.36 ns156.081 ns1.00-NA
CountMainthe\s+\w+282,854.54 ns12,723.264 ns1.001 B1.00
CountPRthe\s+\w+288,927.85 ns11,924.621 ns1.031 B1.00
CountMainzqj38,776.08 ns523.611 ns1.00-NA
CountPRzqj38,694.38 ns541.346 ns1.00-NA

Vector512<byte> secondBitmap512 = state.SecondBitmap512;
Vector512<ushort> offset512 = Vector512.Create(state.Offset);

if (searchSpaceLength > 2 * Vector512<short>.Count)

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.

@GrabYourPitchforks, in the UTF8 experiment you shared with me, you had code that, after having validated that Vector256 was hardware accelerated, aligned an address and then read a full vector, without concern for whether that vector read under or overread the target region. Is that safe to do on all platforms? It seems we could avoid some branching with similar techniques in many of our implementations that use vectorization.

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.

Is that safe to do on all platforms?

As long as the data is pinned, otherwise GC can interrupt at any point and break the alignment assumption

@MihaZupan
MihaZupanforce-pushed the searchvalues-asciiWithSecondSet branch 2 times, most recently from f378aca to e747fd0CompareJuly 10, 2024 22:48
@stephentoub

Copy link
Copy Markdown
Member

the Ascii-only search is at ~1.5x the throughput of the two bitmaps impl.

Does this mean someone searching mostly ASCII text with IgnoreCase for [a-z] will see a 50% regression?

@MihaZupan

MihaZupan commented Jul 11, 2024

Copy link
Copy Markdown
MemberAuthor

Does this mean someone searching mostly ASCII text with IgnoreCase for [a-z] will see a 50% regression?

If you had a long run of text that was just ASCII and didn't match, yes, it would be slower.
But if you were stopping on matches along the way (even if they were ASCII), the two sets can be cheaper due to the lower overhead on matches (mainly since there's an extra method involved in the ascii+probmap implementation).

I reran Sherlock, and the throughput difference doesn't seem to be affecting it: MihuBot/runtime-utils#505 (comment)

We could also choose to use the two bitmaps approach only on -Except paths to avoid the scalar fallbacks.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

Any concerns with merging this one as-is and seeing if benchmarks complain?

@stephentoub

Copy link
Copy Markdown
Member

Ok, let's give it a try but be ready to back it out if any meaningful regressions pop up.

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.

6 participants

@MihaZupan@danmoseley@MihuBot@EgorBo@EgorBot@stephentoub
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add SearchValues<char> implementation for two sets of 128 chars - #103216

Merged
stephentoub merged 4 commits into
dotnet:mainfrom
MihaZupan:searchvalues-asciiWithSecondSet
Jul 22, 2024
Merged

Add SearchValues<char> implementation for two sets of 128 chars#103216
stephentoub merged 4 commits into
dotnet:mainfrom
MihaZupan:searchvalues-asciiWithSecondSet

Conversation

@MihaZupan

@MihaZupanMihaZupan commented Jun 10, 2024

Copy link
Copy Markdown
Member

#101001 significantly improved the performance of the non-vectorized -Except paths of non-ASCII SearchValues<char>.
However, they are still not vectorized, and this PR changes that for values where the non-ASCII part can fit into a 128-bit bitmap.

This adds an implementation that's almost the same as the AsciiCharSearchValues, but where the core lookup checks against two 128-bit bitmaps, with the second one at a variable offset:

-Vector128<byte> source = TOptimizations.PackSources(source0.AsUInt16(), source1.AsUInt16());-Vector128<byte> result = IndexOfAnyLookupCore(source, bitmapLookup);-return TNegator.NegateIfNeeded(result);+Vector128<byte> packed0 = TOptimizations.PackSources(source0.AsUInt16(), source1.AsUInt16());+Vector128<byte> packed1 = Default.PackSources(source0.AsUInt16() - offset, source1.AsUInt16() - offset);+Vector128<byte> result0 = IndexOfAnyLookupCore(packed0, bitmapLookup0);+Vector128<byte> result1 = IndexOfAnyLookupCore(packed1, bitmapLookup1);+return TNegator.NegateIfNeeded(result0 | result1);

(All the numbers below are using the Avx2 path of the new implementation -- measured before #103710)

Avx2 where the text is mostly non-ASCII (previous 'Mixed' would use the prob map / scalar fallback)
MethodToolchainLengthMeanErrorRatio
Asciimain100038.13 ns0.129 ns1.00
Asciipr100037.32 ns0.160 ns0.98
Mixedmain1000127.47 ns0.429 ns1.00
Mixedpr100051.20 ns0.095 ns0.40
AsciiLastmain100035.82 ns0.264 ns1.00
AsciiLastpr100035.81 ns0.079 ns1.00
MixedLastmain1000138.47 ns0.335 ns1.00
MixedLastpr100052.36 ns0.138 ns0.38
AsciiExceptmain100035.40 ns0.138 ns1.00
AsciiExceptpr100035.47 ns0.078 ns1.00
MixedExceptmain1000592.08 ns1.005 ns1.00
MixedExceptpr100051.00 ns0.133 ns0.09
AsciiLastExceptmain100036.77 ns0.535 ns1.00
AsciiLastExceptpr100036.34 ns0.086 ns0.99
MixedLastExceptmain1000682.31 ns8.073 ns1.00
MixedLastExceptpr100053.97 ns1.372 ns0.08
Avx512 machine (the probabilistic map is a lot faster than on Avx2)
MethodToolchainLengthMeanErrorRatio
Asciimain100037.44 ns0.006 ns1.00
Asciipr100037.73 ns0.005 ns1.01
Mixedmain100086.86 ns0.111 ns1.00
Mixedpr100060.36 ns0.010 ns0.69
AsciiLastmain100038.02 ns0.013 ns1.00
AsciiLastpr100038.28 ns0.003 ns1.01
MixedLastmain100095.96 ns0.023 ns1.00
MixedLastpr100061.16 ns0.009 ns0.64
AsciiExceptmain100040.17 ns0.005 ns1.00
AsciiExceptpr100039.81 ns0.005 ns0.99
MixedExceptmain1000585.99 ns0.183 ns1.00
MixedExceptpr100063.22 ns0.010 ns0.11
AsciiLastExceptmain100041.54 ns0.003 ns1.00
AsciiLastExceptpr100041.54 ns0.005 ns1.00
MixedLastExceptmain1000855.18 ns0.059 ns1.00
MixedLastExceptpr100064.65 ns0.010 ns0.08
Early matches
MethodToolchainInputContainsNonAsciiMeanErrorRatio
MixedmainFalse5.130 ns0.0374 ns1.00
MixedprFalse3.425 ns0.0303 ns0.67
MixedLastmainFalse4.830 ns0.0182 ns1.00
MixedLastprFalse4.179 ns0.0750 ns0.87
MixedmainTrue8.440 ns0.0780 ns1.00
MixedprTrue3.383 ns0.0163 ns0.40
MixedLastmainTrue9.011 ns0.2175 ns1.00
MixedLastprTrue4.073 ns0.0087 ns0.45

From that, we can see that the Ascii-only search is at ~1.5x the throughput of the two bitmaps impl.
The two bitmaps have ~1.5x the throughput of the probabilistic map on Avx512 and ~2.5x on Avx2.
In other words, this change is a throughput regression for ProbabilisticWithAsciiCharSearchValues if the text is all ASCII, and an improvement otherwise. It's always chepaer for early matches though.
For the -Except paths where ProbabilisticWithAsciiCharSearchValues uses a scalar fallback, the two bitmaps approach is obviously a lot (10x+) faster.

The change doesn't seem to impact existing Regex benchmarks much, likely because they're very focused on ASCII.


I also tried different implementation approaches to try and reduce code duplication between the existing Ascii and "ascii with second set" implementations:

Details
  • c6f1495, combining the second set into the existing AsciiState, which does save some duplication, but increases the memory consumption of all existing Ascii-only SearchValues.
  • 32f9cf1, that goes all-in with generics, but the JIT can't quite deal with having the vector state be completely generic.
    publicstaticintIndexOfAny<TNegator,TOptimizations>(refshortsearchSpace,intsearchSpaceLength,refAsciiWithSecondSetStatestate)whereTNegator:struct,INegatorwhereTOptimizations:struct,IOptimizations=>IndexOfAnyCore<int,TNegator,IndexOfAnyResultMapper<short>,AsciiWithSecondSetLookup<TOptimizations>,AsciiWithSecondSetState,(Vector128<byte>AsciiBitmap,Vector128<byte>SecondBitmap,Vector128<ushort>Offset),(Vector256<byte>AsciiBitmap,Vector256<byte>SecondBitmap,Vector256<ushort>Offset)>(refsearchSpace,searchSpaceLength,refstate);privatestaticTResultIndexOfAnyCore<TResult,TNegator,TResultMapper,TLookup,TState,TVector128State,TVector256State>(refshortsearchSpace,intsearchSpaceLength,refTStatestate)whereTResult:structwhereTNegator:struct,INegatorwhereTResultMapper:struct,IResultMapper<short,TResult>whereTLookup:struct,ILookup<TState,TVector128State,TVector256State>whereTState:structwhereTVector128State:structwhereTVector256State:struct

Looking at patterns from Regex_RealWorldPatterns.json, ~75% of non-ASCII sets would use the new implementation over the Ascii+ProbMap, most of which because of the kelvin sign.

Number of sources: 18886
numberOfSearchValues
5771
numberOfSearchValuesWithNonAscii
1238
numberOfSearchValuesWithTwoSets
960
numberOfSearchValuesWhereNonAsciiIsKelvin
684
numberOfPatternsWithSearchValues
5093
numberOfPatternsWithSearchValuesWithNonAscii
1038
numberOfPatternsWithSearchValuesWithTwoSets
765

Parsed data for the above:
Regex_RealWorldPatterns.SearchValues.json

@MihaZupanMihaZupan added this to the 9.0.0 milestone Jun 10, 2024
@MihaZupanMihaZupan self-assigned this Jun 10, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

@danmoseley

Copy link
Copy Markdown
Contributor

The change doesn't seem to impact existing Regex benchmarks much, likely because they're very focused on ASCII.

is it worth adding one or two more non ASCII?

@MihaZupan

This comment was marked as outdated.

@MihuBot

This comment was marked as outdated.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@MihuBot benchmark RustLang_Sherlock https://github.com/MihaZupan/performance/tree/compiled-regex-only -medium

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@MihuBot fuzz SearchValues

@MihaZupan

This comment was marked as outdated.

@EgorBo

Copy link
Copy Markdown
Member

@MihaZupan you need to either omit Run<> or pass args to Run, so in your case:

BenchmarkRunner.Run<Bench>(args);

Otherwise --corerun /base/corerun /diff/corerun args will be ignored 🙂

@MihaZupan

MihaZupan commented Jun 22, 2024

Copy link
Copy Markdown
MemberAuthor

Aaah right, thanks. I also forgot to remove the ShortRunJob.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

@EgorBot -intel

usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Buffers;
#nullable disable
publicclassBench{privatestaticreadonlySearchValues<char>_allowedAscii=SearchValues.Create("1234567890abcdefghijklmnopqrstuvwxyz");privatestaticreadonlySearchValues<char>_allowedMixed=SearchValues.Create("äöü1234567890abcdefghijklmnopqrstuvw");privatestring_asciiInput;privatestring_mixedInput;[Params(16,100,10_000)]publicintLength;[GlobalSetup]publicvoidSetup(){_asciiInput=newstring('a',Length);_mixedInput='ä'+newstring('a',Length-1);}[Benchmark]publicboolContainsOnlyAscii()=>!_asciiInput.AsSpan().ContainsAnyExcept(_allowedAscii);[Benchmark]publicboolContainsOnlyMixed()=>!_mixedInput.AsSpan().ContainsAnyExcept(_allowedMixed);}

@EgorBot

This comment was marked as outdated.

@EgorBot

Copy link
Copy Markdown
Benchmark results on Intel
BenchmarkDotNet v0.13.12, Ubuntu 22.04.4 LTS (Jammy Jellyfish)
Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 16 logical and 8 physical cores
Job-SZXNIX : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Job-DHGYYU : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
MethodToolchainLengthMeanErrorRatio
ContainsOnlyAsciiMain162.434 ns0.0115 ns1.00
ContainsOnlyAsciiPR162.434 ns0.0063 ns1.00
ContainsOnlyMixedMain1612.207 ns0.0035 ns1.00
ContainsOnlyMixedPR163.184 ns0.0004 ns0.26
ContainsOnlyAsciiMain1004.251 ns0.0005 ns1.00
ContainsOnlyAsciiPR1004.251 ns0.0004 ns1.00
ContainsOnlyMixedMain10062.381 ns0.0320 ns1.00
ContainsOnlyMixedPR1005.499 ns0.0020 ns0.09
ContainsOnlyAsciiMain10000208.463 ns0.0200 ns1.00
ContainsOnlyAsciiPR10000208.571 ns0.0259 ns1.00
ContainsOnlyMixedMain100005,783.502 ns1.4382 ns1.00
ContainsOnlyMixedPR10000391.050 ns0.0232 ns0.07

BDN_Artifacts.zip

@MihuBot

Copy link
Copy Markdown
System.Text.RegularExpressions.Tests.Perf_Regex_Industry_RustLang_Sherlock
BenchmarkDotNet v0.13.13-nightly.20240311.145, Ubuntu 22.04.4 LTS (Jammy Jellyfish)
AMD EPYC 9V74, 1 CPU, 8 logical and 4 physical cores
MediumRun : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Job=MediumRun OutlierMode=DontRemove IterationCount=15
LaunchCount=2 MemoryRandomization=True WarmupCount=10
MethodToolchainPatternMeanErrorRatioAllocatedAlloc Ratio
CountMain.*577,117.72 ns1,898.483 ns1.002 B1.00
CountPR.*609,940.38 ns5,541.168 ns1.062 B1.00
CountMain(?i)Holmes53,669.80 ns163.206 ns1.00-NA
CountPR(?i)Holmes53,751.77 ns95.378 ns1.00-NA
CountMain(?i)Sher[a-z]+|Hol[a-z]+96,797.28 ns7,445.773 ns1.01-NA
CountPR(?i)Sher[a-z]+|Hol[a-z]+101,675.49 ns7,778.427 ns1.06-NA
CountMain(?i)Sherlock45,540.09 ns158.021 ns1.00-NA
CountPR(?i)Sherlock45,699.65 ns198.254 ns1.00-NA
CountMain(?i)Sherlock Holmes45,299.62 ns140.891 ns1.00-NA
CountPR(?i)Sherlock Holmes45,322.99 ns60.249 ns1.00-NA
CountMain(?i)Sherlock|Holmes|Watson98,222.98 ns9,160.202 ns1.02-NA
CountPR(?i)Sherlock|Holmes|Watson98,387.61 ns9,077.154 ns1.02-NA
CountMain(?i)Sherlock|(...)er|John|Baker [49]210,972.18 ns25,319.086 ns1.031 B1.00
CountPR(?i)Sherlock|(...)er|John|Baker [49]216,838.46 ns27,873.222 ns1.061 B1.00
CountMain(?i)the249,601.71 ns10,074.303 ns1.001 B1.00
CountPR(?i)the245,957.03 ns10,337.071 ns0.991 B1.00
CountMain(?m)^Sherlock(...)rlock Holmes$ [37]58,240.97 ns2,237.819 ns1.00-NA
CountPR(?m)^Sherlock(...)rlock Holmes$ [37]59,147.93 ns2,749.661 ns1.02-NA
CountMain(?s).*39.26 ns0.088 ns1.00-NA
CountPR(?s).*41.27 ns1.728 ns1.05-NA
CountMain[^\\n]*576,131.95 ns2,377.308 ns1.002 B1.00
CountPR[^\\n]*577,114.20 ns4,299.926 ns1.002 B1.00
CountMain[a-q][^u-z]{13}x23,158.57 ns113.019 ns1.00-NA
CountPR[a-q][^u-z]{13}x23,147.72 ns90.268 ns1.00-NA
CountMain[a-zA-Z]+ing4,112,765.60 ns6,996.218 ns1.0019 B1.00
CountPR[a-zA-Z]+ing4,195,122.65 ns50,241.289 ns1.0221 B1.11
CountMain\b\w+n\b8,324,707.71 ns18,654.897 ns1.0044 B1.00
CountPR\b\w+n\b8,419,875.57 ns67,893.821 ns1.0144 B1.00
CountMain\p{L}10,252,616.17 ns258,692.197 ns1.0035 B1.00
CountPR\p{L}10,178,460.27 ns120,522.988 ns0.9935 B1.00
CountMain\p{Ll}10,218,675.98 ns78,494.108 ns1.0035 B1.00
CountPR\p{Ll}11,001,606.18 ns501,912.621 ns1.0835 B1.00
CountMain\p{Lu}355,667.38 ns7,908.522 ns1.001 B1.00
CountPR\p{Lu}348,108.98 ns3,458.353 ns0.981 B1.00
CountMain\s[a-zA-Z]{0,12}ing\s4,387,091.94 ns11,384.876 ns1.0024 B1.00
CountPR\s[a-zA-Z]{0,12}ing\s4,404,995.88 ns9,088.263 ns1.0024 B1.00
CountMain\w+4,712,257.79 ns27,825.642 ns1.0018 B1.00
CountPR\w+4,671,552.17 ns7,259.567 ns0.9921 B1.17
CountMain\w+\s+Holmes3,340,654.11 ns10,688.067 ns1.0011 B1.00
CountPR\w+\s+Holmes3,355,149.72 ns15,672.577 ns1.0010 B0.91
CountMain\w+\s+Holmes\s+\w+3,609,649.22 ns65,235.529 ns1.0010 B1.00
CountPR\w+\s+Holmes\s+\w+3,502,413.34 ns60,104.536 ns0.9712 B1.20
CountMainaei38,764.39 ns528.311 ns1.00-NA
CountPRaei38,671.05 ns532.363 ns1.00-NA
CountMainaqj38,552.12 ns579.654 ns1.00-NA
CountPRaqj38,708.25 ns516.758 ns1.00-NA
CountMainHolmes50,202.09 ns78.982 ns1.00-NA
CountPRHolmes50,229.34 ns115.917 ns1.00-NA
CountMainHolmes.{0,25}(...).{0,25}Holmes [39]44,351.33 ns90.615 ns1.00-NA
CountPRHolmes.{0,25}(...).{0,25}Holmes [39]44,416.66 ns119.101 ns1.00-NA
CountMainSher[a-z]+|Hol[a-z]+48,711.02 ns113.935 ns1.00-NA
CountPRSher[a-z]+|Hol[a-z]+48,852.79 ns227.029 ns1.00-NA
CountMainSherlock58,662.58 ns2,239.132 ns1.00-NA
CountPRSherlock59,492.49 ns2,839.858 ns1.02-NA
CountMainSherlock Holmes59,536.37 ns2,887.051 ns1.01-NA
CountPRSherlock Holmes59,734.87 ns2,760.671 ns1.01-NA
CountMainSherlock\s+Holmes59,934.57 ns2,381.314 ns1.00-NA
CountPRSherlock\s+Holmes60,715.04 ns3,024.286 ns1.02-NA
CountMainSherlock|Holmes44,782.94 ns106.657 ns1.00-NA
CountPRSherlock|Holmes44,866.33 ns114.894 ns1.00-NA
CountMainSherlock|Holmes|Watson58,630.95 ns77.246 ns1.00-NA
CountPRSherlock|Holmes|Watson59,046.70 ns110.269 ns1.01-NA
CountMainSherlock|Holm(...)er|John|Baker [45]109,855.32 ns133.155 ns1.00-NA
CountPRSherlock|Holm(...)er|John|Baker [45]109,887.13 ns89.847 ns1.00-NA
CountMainSherlock|Street25,047.25 ns62.195 ns1.00-NA
CountPRSherlock|Street25,004.97 ns85.354 ns1.00-NA
CountMainthe179,288.74 ns632.033 ns1.001 B1.00
CountPRthe179,042.42 ns643.054 ns1.001 B1.00
CountMainThe54,675.24 ns84.398 ns1.00-NA
CountPRThe54,504.36 ns156.081 ns1.00-NA
CountMainthe\s+\w+282,854.54 ns12,723.264 ns1.001 B1.00
CountPRthe\s+\w+288,927.85 ns11,924.621 ns1.031 B1.00
CountMainzqj38,776.08 ns523.611 ns1.00-NA
CountPRzqj38,694.38 ns541.346 ns1.00-NA

Vector512<byte> secondBitmap512 = state.SecondBitmap512;
Vector512<ushort> offset512 = Vector512.Create(state.Offset);

if (searchSpaceLength > 2 * Vector512<short>.Count)

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.

@GrabYourPitchforks, in the UTF8 experiment you shared with me, you had code that, after having validated that Vector256 was hardware accelerated, aligned an address and then read a full vector, without concern for whether that vector read under or overread the target region. Is that safe to do on all platforms? It seems we could avoid some branching with similar techniques in many of our implementations that use vectorization.

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.

Is that safe to do on all platforms?

As long as the data is pinned, otherwise GC can interrupt at any point and break the alignment assumption

@MihaZupan
MihaZupanforce-pushed the searchvalues-asciiWithSecondSet branch 2 times, most recently from f378aca to e747fd0CompareJuly 10, 2024 22:48
@stephentoub

Copy link
Copy Markdown
Member

the Ascii-only search is at ~1.5x the throughput of the two bitmaps impl.

Does this mean someone searching mostly ASCII text with IgnoreCase for [a-z] will see a 50% regression?

@MihaZupan

MihaZupan commented Jul 11, 2024

Copy link
Copy Markdown
MemberAuthor

Does this mean someone searching mostly ASCII text with IgnoreCase for [a-z] will see a 50% regression?

If you had a long run of text that was just ASCII and didn't match, yes, it would be slower.
But if you were stopping on matches along the way (even if they were ASCII), the two sets can be cheaper due to the lower overhead on matches (mainly since there's an extra method involved in the ascii+probmap implementation).

I reran Sherlock, and the throughput difference doesn't seem to be affecting it: MihuBot/runtime-utils#505 (comment)

We could also choose to use the two bitmaps approach only on -Except paths to avoid the scalar fallbacks.

@MihaZupan

Copy link
Copy Markdown
MemberAuthor

Any concerns with merging this one as-is and seeing if benchmarks complain?

@stephentoub

Copy link
Copy Markdown
Member

Ok, let's give it a try but be ready to back it out if any meaningful regressions pop up.

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.

6 participants

@MihaZupan@danmoseley@MihuBot@EgorBo@EgorBot@stephentoub