Skip to content

Move the IsLeft/IsRight decision out of the loop and use computed substring set - #88516

Closed
IDisposable wants to merge 5 commits into
dotnet:mainfrom
IDisposable:faster-freeze-strings
Closed

Move the IsLeft/IsRight decision out of the loop and use computed substring set#88516
IDisposable wants to merge 5 commits into
dotnet:mainfrom
IDisposable:faster-freeze-strings

Conversation

@IDisposable

@IDisposableIDisposable commented Jul 7, 2023

Copy link
Copy Markdown
Contributor

While reviewing #87510, I noticed the inline code in the comparers seems like since it's in the hot-loop path, might be faster to move the IsLeft conditionalized code out of the loop by adding a static Slicer to the comparator. The Slicer is the same logic as was originally in the bodies of the Equals and GetHashCode methods, and also matches the delegates that were being passed to the internal CreateAnalysisResults method.

The slicing is really only changed once per Count, so move the IsLeft-dependent logic into aggressively inlined Slicer extension method because that makes things a bit faster and slightly reduces allocations.

Builds on #88709 as that's a trivially true change. Has been merged now.

The summary of changes:

  • Eliminate the the inner TryUseSubstring because we can just early return the calculated results as we build them
  • Hoist the calculation of acceptableNonUniqueCount out to the top level since it never changes (which means we pass that into the HasSufficientUniquenessFactor method for it to "use up" internally (passed by value, so unchanged at call-site)
  • Eliminated the delegate ReadOnlySpan<char> GetSpan and use, which helps reduce dynamic dispatch overhead in the CreateAnalysisResults method
  • Eliminated the IsLeft field of the SubstringComparer since we can tell by the Index being negative that we're doing right-justified slicing (and documented that on the class)
  • Changed the logic managing the Index and Count on the comparer for right-justified substrings.
  • Added [MethodImpl(MethodImplOptions.AggressiveInlining)] to the Equals and GetHashCode overrides.

@ghostghost added community-contribution Indicates that the PR has been added by a community member area-System.Collections labels Jul 7, 2023
@ghost

ghost commented Jul 7, 2023

Copy link
Copy Markdown

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

Issue Details

While reviewing #87510, I noticed the inline code in the Comparers seems like since it's in the hot-loop path, might be faster to move the IsLeft conditionalized code out of the loop by adding a Slicer to the comparator. The Slicer is the same logic as was in the bodies of the Equals and GetHashCode methods, and indeed also matches the delegate that was passed to the internal CreateAnalysisResults method.

The slicing is really only changed once per Count, so move the IsLeft-dependent logic into aggressively inlined SliceLeft and SliceRight to see if that makes things faster. Also reuses the same slicers for the calls to the CreateAnalysisResults so the will get the same JIT perf benefits.

Also made a subtle tweak because we know that ignoreCase is true (due to the test above), so can set the starting state of canSwitchIgnoreCaseToCaseSensitive explicitly to true.

BenchmarkDotNet=v0.13.2.2052-nightly, OS=Windows 11 (10.0.22631.1972)
Intel Core i7-10875H CPU 2.30GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK=8.0.100-preview.7.23322.33
[Host] : .NET 8.0.0 (8.0.23.32106), X64 RyuJIT AVX2
Job-ZWELZX : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-KUEFVA : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
PowerPlanMode=00000000-0000-0000-0000-000000000000 Arguments=/p:EnableUnsafeBinaryFormatterSerialization=true IterationTime=250.0000 ms MaxIterationCount=20 MinIterationCount=15 WarmupCount=1 
MethodJobToolchainCountMeanErrorStdDevMedianMinMaxRatioMannWhitney(3ms)RatioSDGen0Gen1Gen2AllocatedAlloc Ratio
ToFrozenDictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe102,057.56 ns23.550 ns20.877 ns2,053.70 ns2,004.57 ns2,095.98 ns1.26Same0.030.2027--1720 B1.00
ToFrozenDictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe101,644.78 ns32.011 ns35.580 ns1,631.83 ns1,603.70 ns1,715.13 ns1.00Base0.000.2047--1720 B1.00
TryGetValue_True_FrozenDictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe1063.10 ns0.752 ns0.628 ns63.24 ns61.43 ns63.91 ns1.00Same0.02----NA
TryGetValue_True_FrozenDictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe1063.30 ns0.842 ns0.788 ns63.31 ns62.34 ns65.07 ns1.00Base0.00----NA
ToFrozenDictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe10011,800.30 ns139.610 ns123.760 ns11,764.33 ns11,681.12 ns12,075.98 ns1.26Same0.011.40980.0470-12112 B1.00
ToFrozenDictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe1009,348.64 ns91.765 ns76.628 ns9,367.20 ns9,209.04 ns9,481.99 ns1.00Base0.001.41450.0372-12112 B1.00
TryGetValue_True_FrozenDictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe100647.76 ns12.870 ns12.640 ns645.45 ns630.03 ns677.18 ns1.00Same0.02----NA
TryGetValue_True_FrozenDictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe100645.03 ns8.896 ns8.321 ns645.45 ns631.23 ns658.04 ns1.00Base0.00----NA
ToFrozenDictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe1000107,105.57 ns1,078.411 ns955.984 ns106,742.36 ns106,269.73 ns109,294.56 ns1.26Same0.0111.13012.5685-94864 B1.00
ToFrozenDictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe100084,805.63 ns1,384.405 ns1,294.973 ns84,801.32 ns82,494.28 ns87,164.12 ns1.00Base0.0011.24342.6455-94864 B1.00
TryGetValue_True_FrozenDictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe10007,823.96 ns106.727 ns99.833 ns7,841.29 ns7,668.40 ns7,999.15 ns1.04Same0.02----NA
TryGetValue_True_FrozenDictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe10007,542.00 ns104.779 ns92.883 ns7,555.03 ns7,364.24 ns7,702.98 ns1.00Base0.00----NA
ToFrozenDictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe100001,641,164.67 ns23,876.639 ns22,334.223 ns1,633,353.12 ns1,612,245.00 ns1,676,639.38 ns1.18Same0.02143.7500143.7500143.7500926133 B1.00
ToFrozenDictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe100001,393,440.90 ns17,878.950 ns16,723.981 ns1,394,471.88 ns1,360,370.31 ns1,421,018.75 ns1.00Base0.00145.8333145.8333145.8333926134 B1.00
TryGetValue_True_FrozenDictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe10000129,167.96 ns2,134.712 ns1,996.811 ns129,264.68 ns124,220.63 ns132,173.42 ns1.01Same0.02----NA
TryGetValue_True_FrozenDictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe10000128,107.54 ns1,706.036 ns1,595.827 ns127,981.50 ns126,341.48 ns131,397.43 ns1.00Base0.00----NA
ToDictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe10138.87 ns1.287 ns1.204 ns138.58 ns136.88 ns141.00 ns0.94Same0.020.0526--440 B1.00
ToDictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe10147.84 ns2.518 ns2.355 ns147.02 ns145.32 ns153.22 ns1.00Base0.000.0526--440 B1.00
ToImmutableDictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe101,299.27 ns83.702 ns96.391 ns1,250.37 ns1,202.89 ns1,479.53 ns1.12Same0.080.0860--736 B1.00
ToImmutableDictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe101,178.81 ns16.472 ns15.407 ns1,171.80 ns1,158.97 ns1,204.85 ns1.00Base0.000.0847--736 B1.00
TryGetValue_True_DictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe10128.08 ns1.673 ns1.483 ns128.16 ns125.29 ns130.57 ns1.00Same0.01----NA
TryGetValue_True_DictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe10128.21 ns1.885 ns1.763 ns128.02 ns125.36 ns131.44 ns1.00Base0.00----NA
TryGetValue_True_ImmutableDictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe10164.46 ns2.012 ns1.882 ns164.70 ns162.56 ns168.03 ns1.00Same0.01----NA
TryGetValue_True_ImmutableDictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe10164.62 ns2.097 ns1.962 ns163.06 ns162.89 ns167.53 ns1.00Base0.00----NA
ToDictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe1001,097.04 ns21.699 ns20.298 ns1,095.36 ns1,072.24 ns1,134.88 ns1.13Same0.030.37140.0043-3128 B1.00
ToDictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe100972.18 ns16.163 ns15.119 ns968.03 ns958.70 ns1,002.61 ns1.00Base0.000.37060.0039-3128 B1.00
ToImmutableDictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe10017,821.67 ns327.754 ns306.582 ns17,853.37 ns17,381.97 ns18,333.97 ns0.95Same0.020.7301--6496 B1.00
ToImmutableDictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe10018,780.61 ns259.705 ns230.222 ns18,796.94 ns18,384.63 ns19,208.59 ns1.00Base0.000.7449--6496 B1.00
TryGetValue_True_DictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe1001,442.70 ns19.214 ns17.973 ns1,442.10 ns1,410.93 ns1,475.64 ns0.95Same0.01----NA
TryGetValue_True_DictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe1001,524.14 ns21.632 ns18.064 ns1,521.86 ns1,490.48 ns1,565.28 ns1.00Base0.00----NA
TryGetValue_True_ImmutableDictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe1001,772.76 ns24.517 ns22.934 ns1,769.24 ns1,735.35 ns1,807.67 ns0.88Same0.02----NA
TryGetValue_True_ImmutableDictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe1002,006.92 ns29.358 ns26.025 ns2,011.73 ns1,962.37 ns2,050.82 ns1.00Base0.00----NA
ToDictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe10009,776.74 ns168.881 ns149.708 ns9,747.11 ns9,596.95 ns10,043.39 ns0.98Same0.023.68330.3877-31016 B1.00
ToDictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe100010,041.68 ns178.905 ns167.348 ns9,997.95 ns9,819.95 ns10,373.81 ns1.00Base0.003.66940.4032-31016 B1.00
ToImmutableDictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe1000281,817.08 ns4,379.124 ns4,300.886 ns281,971.82 ns275,266.78 ns288,499.65 ns1.04Same0.036.94441.1574-64097 B1.00
ToImmutableDictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe1000272,101.95 ns5,216.817 ns5,357.287 ns270,768.10 ns266,252.05 ns285,728.56 ns1.00Base0.007.54312.1552-64097 B1.00
TryGetValue_True_DictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe100017,205.75 ns183.564 ns162.725 ns17,180.39 ns16,956.92 ns17,508.42 ns0.98Same0.02----NA
TryGetValue_True_DictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe100017,615.34 ns196.353 ns183.669 ns17,640.08 ns17,242.77 ns17,890.94 ns1.00Base0.00----NA
TryGetValue_True_ImmutableDictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe100065,528.70 ns303.425 ns253.374 ns65,455.07 ns65,180.80 ns66,062.89 ns0.99Same0.01----NA
TryGetValue_True_ImmutableDictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe100066,317.31 ns919.599 ns860.194 ns65,766.51 ns65,670.49 ns68,352.98 ns1.00Base0.00----NA
ToDictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe10000135,225.46 ns1,934.785 ns1,809.799 ns134,627.07 ns132,521.68 ns137,873.59 ns1.07Same0.0450.000046.739145.1087283324 B1.00
ToDictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe10000126,728.45 ns3,881.702 ns4,470.174 ns126,718.90 ns117,977.22 ns134,411.62 ns1.00Base0.0046.487642.871941.8388283336 B1.00
ToImmutableDictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe100003,905,016.98 ns64,558.467 ns60,388.030 ns3,887,075.00 ns3,836,512.50 ns4,007,631.25 ns1.00Same0.0262.500046.8750-640108 B1.00
ToImmutableDictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe100003,921,738.06 ns46,705.911 ns41,403.573 ns3,921,446.88 ns3,879,031.25 ns4,003,846.88 ns1.00Base0.0062.500046.8750-640108 B1.00
TryGetValue_True_DictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe10000239,574.73 ns3,271.035 ns2,731.463 ns240,082.88 ns233,648.37 ns243,980.29 ns1.00Same0.01----NA
TryGetValue_True_DictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe10000239,909.95 ns2,255.089 ns1,999.078 ns239,680.77 ns235,758.37 ns243,502.01 ns1.00Base0.00----NA
TryGetValue_True_ImmutableDictionaryJob-ZWELZX\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe100001,116,305.32 ns5,426.525 ns4,531.395 ns1,114,173.66 ns1,113,416.52 ns1,127,808.48 ns0.99Same0.01----NA
TryGetValue_True_ImmutableDictionaryJob-KUEFVA\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe100001,133,305.93 ns16,927.154 ns15,005.481 ns1,127,919.87 ns1,121,172.32 ns1,168,315.62 ns1.00Base0.00----NA

In the JustifiedSubstringComparer, uses the current Slicer

Author:IDisposable
Assignees:-
Labels:

area-System.Collections, community-contribution

Milestone:-

@IDisposable
IDisposableforce-pushed the faster-freeze-strings branch 5 times, most recently from 00e52d1 to d9d188bCompareJuly 12, 2023 07:41

@IDisposableIDisposableJul 12, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The Slicer does the left/right based on the sign of the Index value now, which should JIT down better.

I really wish that String.AsSpan understood the use of -1 start intrinsically as string.Length - 1... that would make the ternary unneeded.

@IDisposableIDisposableJul 12, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Here's a real improvement. We do the math for setting a negative index (e.g. from the right side) starting with count characters from the right (as before). Then on line 81 we keep decrementing comparer.Index as we go on moving the "cursor" left.

@IDisposable
IDisposableforce-pushed the faster-freeze-strings branch 3 times, most recently from 8c2b2e4 to 865a9eeCompareJuly 21, 2023 05:50

@IDisposableIDisposableJul 21, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This ternary is the sole remaining conditional jump in the hot loop, but there's no good way to avoid that simultaneous avoiding delegate overhead so make it as simple as possible. The number of jumps (old vs. new) is identical, but this is a tiny tiny block of JITtable goodness.

@IDisposableIDisposableJul 30, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I have tried running this as a jumpless method body, but it makes little difference and is harder to grok; passing fromRight = 0 for left, or fromRight = 1 for right which requires the SubstringComparers to carry the left/right multiplier with them (so more state...):

publicstaticReadOnlySpan<char>Slicer(thisstrings,bytefromRight,intindex,intcount)=>s.AsSpan((s.Length*fromRight)+index), count);

Also, if we COULD swap the HashSet<string>'s comparer out for left vs. right the we could just have that knowledge embedded with a trait and thus fully jumpless, but that would require allocating two HashSet<string>s which might be an allocation regression nobody wants :(

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I did something like that in PR #89689 which is a huge win... since we can't swap the comparer, I ended up creating both a left and right HashSet with backing comparer that "hard codes" the left/right logic. HUGE WINS, see that PR.

@IDisposableIDisposable changed the title Move the IsLeft/IsRight decision out of the loopMove the IsLeft/IsRight decision out of the loop and pass computed setJul 21, 2023
@IDisposableIDisposable changed the title Move the IsLeft/IsRight decision out of the loop and pass computed setMove the IsLeft/IsRight decision out of the loop and use computed substring setJul 21, 2023
@IDisposable
IDisposable marked this pull request as draft July 21, 2023 08:48
@IDisposable
IDisposableforce-pushed the faster-freeze-strings branch from 865a9ee to b11d49fCompareJuly 21, 2023 15:40
@IDisposable
IDisposable marked this pull request as ready for review July 27, 2023 23:15

@IDisposableIDisposableJul 27, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

We need to re-slice the string again here. It would be awesome if we could have a HashSet<ReadOnlySpan<char> but that's not going to happen as those would be structs not objects.

The slicing is really only changed once per Count, so move the
IsLeft-dependent logic into `Slicer` method and eliminate the `GetSpan` delegate.
Changed to also pass the already-computed `set` of unique substrings to the `CreateAnalysisResults` method, so we don't recompute the slices twice. In order than either the set or the original `uniqueStrings` can be passed, swapped that argument for the `Analyze` method to take the `uniqueStrings` as a `string[]` (which it already is at all call-sites).
Subtle bug in that the entire string is being placed in the set, not the span.
@IDisposable
IDisposableforce-pushed the faster-freeze-strings branch from b11d49f to c8c8801CompareJuly 29, 2023 07:17
Since we are working with the same set of input strings in each strategy there's no reason to take the length every time we make an attempt (per count, both left and right justified).
Hoist the calculation of the acceptable number of collisions out to the top, do it once, and pass that number into the `HasSufficientUniquenessFactor` method for it to (locally) use up.
@IDisposable
IDisposableforce-pushed the faster-freeze-strings branch from f97e491 to a168767CompareJuly 29, 2023 07:44
Benchmarks ever so slightly better.
Looks like the overhead of IEnumerable<string> is not worth the savings for the benchmark test data. Perhaps it would matter less if we were freezing more strings, but not likely
@IDisposable

Copy link
Copy Markdown
ContributorAuthor

Performance tests (note, PR #89689 wipes the floor on this, so we should merge that instead)

  • Baseline is the main as of d5c4a4e
  • Faster Freeze is the code in this which shows between a 1%-3% performance improvement and unchanged allocation
BenchmarkDotNet=v0.13.2.2052-nightly, OS=Windows 11 (10.0.22631.2115)
Intel Core i7-10875H CPU 2.30GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK=8.0.100-preview.7.23322.33
[Host] : .NET 8.0.0 (8.0.23.32106), X64 RyuJIT AVX2
Job-PMFJSX : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-XUZNBJ : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-ORKLIM : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
PowerPlanMode=00000000-0000-0000-0000-000000000000 Arguments=/p:EnableUnsafeBinaryFormatterSerialization=true IterationTime=250.0000 ms MaxIterationCount=20 MinIterationCount=15 WarmupCount=1 
MethodJobToolchainCountMeanErrorStdDevMedianMinMaxRatioMannWhitney(2ms)RatioSDGen0Gen1Gen2AllocatedAlloc Ratio
ToFrozenDictionaryJob-XUZNBJBaseline101,271.2 ns24.41 ns28.11 ns1,275.2 ns1,216.0 ns1,302.7 ns1.00Base0.000.2055--1.68 KB1.00
ToFrozenDictionaryJob-ORKLIMFaster Freeze101,246.0 ns27.06 ns31.17 ns1,263.2 ns1,194.8 ns1,283.6 ns0.98Same0.020.2033--1.67 KB1.00
ToFrozenDictionaryJob-XUZNBJBaseline1007,568.4 ns168.25 ns193.76 ns7,598.1 ns7,197.6 ns7,828.9 ns1.00Base0.001.42810.0304-11.83 KB1.00
ToFrozenDictionaryJob-ORKLIMFaster Freeze1007,523.3 ns156.77 ns180.54 ns7,583.8 ns7,023.0 ns7,818.5 ns0.99Same0.031.43430.0305-11.82 KB1.00
ToFrozenDictionaryJob-XUZNBJBaseline100070,430.6 ns1,377.38 ns1,586.20 ns70,729.1 ns67,759.9 ns73,232.6 ns1.00Base0.0011.31472.6940-92.64 KB1.00
ToFrozenDictionaryJob-ORKLIMFaster Freeze100068,147.0 ns1,356.55 ns1,507.80 ns68,636.0 ns64,592.7 ns69,908.1 ns0.97Same0.0211.12292.6483-92.63 KB1.00
ToFrozenDictionaryJob-XUZNBJBaseline100001,144,862.8 ns21,895.81 ns20,481.36 ns1,150,661.8 ns1,108,326.4 ns1,179,999.3 ns1.00Base0.00145.8333145.8333145.8333904.43 KB1.00
ToFrozenDictionaryJob-ORKLIMFaster Freeze100001,131,728.1 ns21,741.06 ns19,272.89 ns1,134,184.4 ns1,105,237.5 ns1,158,713.3 ns0.99Same0.03148.4375148.4375148.4375904.42 KB1.00

@IDisposable

Copy link
Copy Markdown
ContributorAuthor

Did much better in PR #89689

@ghostghost locked as resolved and limited conversation to collaborators Sep 8, 2023
@IDisposable
IDisposable deleted the faster-freeze-strings branch February 15, 2025 20:07
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Collectionscommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@IDisposable@krwq