Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Speed up KeyAnalyser #87590

Closed
wants to merge 1 commit into from

Conversation

IDisposable
Copy link
Contributor

@IDisposable IDisposable commented Jun 15, 2023

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.

NOT SURE if this actually will speed things up at all, hoping to leverage some /azp magic to do the performance evaluations.

@ghost ghost added the community-contribution Indicates that the PR has been added by a community member label Jun 15, 2023
@ghost
Copy link

ghost commented Jun 15, 2023

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

Issue Details

null

Author: IDisposable
Assignees: -
Labels:

area-System.Collections, community-contribution

Milestone: -

@azure-pipelines
Copy link

Supported commands
  • help:
    • Get descriptions, examples and documentation about supported commands
    • Example: help "command_name"
  • list:
    • List all pipelines for this repository using a comment.
    • Example: "list"
  • run:
    • Run all pipelines or specific pipelines for this repository using a comment. Use this command by itself to trigger all related pipelines, or specify specific pipelines to run.
    • Example: "run" or "run pipeline_name, pipeline_name, pipeline_name"
  • where:
    • Report back the Azure DevOps orgs that are related to this repository and org
    • Example: "where"

See additional documentation.

@azure-pipelines
Copy link

Commenter does not have sufficient privileges for PR 87590 in repo dotnet/runtime

@@ -124,7 +120,7 @@ private static bool TryUseSubstring(ReadOnlySpan<string> uniqueStrings, bool ign
// actually perform the comparison as case-sensitive even if case-insensitive
// was requested, as there's nothing that would compare equally to the substring
// other than the substring itself.
bool canSwitchIgnoreCaseToCaseSensitive = ignoreCase;
bool canSwitchIgnoreCaseToCaseSensitive = true;
Copy link
Contributor Author

@IDisposable IDisposable Jun 15, 2023

Choose a reason for hiding this comment

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

This is demonstrably true because we're inside the if (ignoreCase) immediately above and could be a distinct PR, but it actually changes nothing but the readability and not hoping the Compiler/JIT groks this truth.

@azure-pipelines
Copy link

Commenter does not have sufficient privileges for PR 87590 in repo dotnet/runtime

@IDisposable
Copy link
Contributor Author

Hey @adamsitnik, can you do the /azp magic to make this run the perf tests?

@adamsitnik
Copy link
Member

Hey @adamsitnik, can you do the /azp magic to make this run the perf tests?

I am afraid we don't have that in dotnet/runtime yet (cc @sblom)

For now you need to follow the docs I provided in #87510 (comment) and benchmark it locally

@IDisposable
Copy link
Contributor Author

IDisposable commented Jun 23, 2023

Sadly @adamsitnik , I can't even get the dotnet/performance stuff to execute at all on my machine... :( dotnet/performance#3096

Fixed that problem by whacking my nuget caches!

@IDisposable IDisposable force-pushed the faster-freeze-strings branch 3 times, most recently from 5f231fa to fae39cb Compare June 29, 2023 05:19
@IDisposable
Copy link
Contributor Author

IDisposable commented Jun 29, 2023

dotnet run -c Release -f net8.0 \ 
 --filter *Frozen* \
 --statisticalTest 2ms \
 --coreRun \
 "C:\Dev\dotnet\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe"  \
 "C:\Dev\dotnet\runtime_baseline\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe"

Initial results from using @adamsitnik benchmarks as of dotnet/performance@28e9ab3

BenchmarkDotNet=v0.13.2.2052-nightly, OS=Windows 11 (10.0.22631.1906)
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
Change: .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2 from IDisposable@fae39cb
Baseline : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2 from cfae69f

PowerPlanMode=00000000-0000-0000-0000-000000000000 Arguments=/p:EnableUnsafeBinaryFormatterSerialization=true IterationTime=250.0000 ms
MaxIterationCount=20 MinIterationCount=15 WarmupCount=1

Method Toolchain Size Mean Error StdDev Median Min Max Ratio MannWhitney(2ms) Allocated Alloc Ratio
FrozenDictionary Change 512 6.363 us 0.0315 us 0.0263 us 6.364 us 6.317 us 6.404 us 1.00 Base - NA
FrozenDictionary Baseline 512 6.447 us 0.0726 us 0.0644 us 6.438 us 6.360 us 6.577 us 1.01 Same - NA

@IDisposable
Copy link
Contributor Author

IDisposable commented Jun 29, 2023

So, is a ratio of 1.01 (old code) vs. 1.00 (new code) worthwhile? It isn't slower, and it (to me) is less coupled. Also, I note that the Baseline version has a higher SD and Error, which (to me) means it's more influenced by branch mispredictions.

Also, realized I should have presented the Baseline first in the command, future runs will do so... sorry, learning here.

@IDisposable IDisposable marked this pull request as ready for review June 29, 2023 07:39
@@ -263,25 +259,32 @@ public AnalysisResults(bool ignoreCase, bool allAsciiIfIgnoreCase, int hashIndex
public bool RightJustifiedSubstring => HashIndex < 0;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ReadOnlySpan<char> SliceLeft(string s, int index, int count) => s!.AsSpan(index, count);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This encapsulates the login on old line 75

public static ReadOnlySpan<char> SliceLeft(string s, int index, int count) => s!.AsSpan(index, count);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ReadOnlySpan<char> SliceRight(string s, int index, int count) => s!.AsSpan(s!.Length + index, count);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This encapsulates the login on old line 100

public abstract bool Equals(string? x, string? y);
public abstract int GetHashCode(string s);
}

private sealed class JustifiedSubstringComparer : SubstringComparer
{
public override bool Equals(string? x, string? y) => x.AsSpan(IsLeft ? Index : (x!.Length + Index), Count).SequenceEqual(y.AsSpan(IsLeft ? Index : (y!.Length + Index), Count));
public override int GetHashCode(string s) => Hashing.GetHashCodeOrdinal(s.AsSpan(IsLeft ? Index : (s.Length + Index), Count));
public override bool Equals(string? x, string? y) => Slicer(x!, Index, Count).SequenceEqual(Slicer(y!, Index, Count));
Copy link
Contributor Author

@IDisposable IDisposable Jun 29, 2023

Choose a reason for hiding this comment

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

Here we can just call Slicer, which no longer has an IsLeft conditional test inside a hot loop, so calling through to the GetSpan-like method that was set on lines 60 (for the IsLeft == true logic) and 84 (for the IsLeft == false logic). This (to my reasoning) will result in much better low-level branch-prediction and since the actual delegate bodies are [MethodImpl(MethodImplOptions.AggressiveInlining)] they should be inlined stubs.

We also get to reuse those stubs when we do the construction (instead of "similar" ones coded up for the calls to CreateAnalysisResults)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am not confident about the various not-null hints ! that I injected, but I think they're right, if not overkill. Not sure it really would affect the code generated and welcome input on that from low-level ninjas

@IDisposable
Copy link
Contributor Author

I'm not sure this is an endpoint or a jumping off for more or simply an exploration that should be tabled. I welcome input from @adamsitnik and @MichalStrehovsky or anyone else :)

The slicing is really only changed once per Count, so move the
IsLeft-dependent logic into delegates to see if that makes
things faster.

A little more refactoring
Encourage inlining.
@IDisposable
Copy link
Contributor Author

Closing this because it's gotten messy

@IDisposable IDisposable closed this Jul 7, 2023
@ghost ghost locked as resolved and limited conversation to collaborators Aug 6, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Collections community-contribution Indicates 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