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

Use span.Equals instead of string.Compare in JumpTables #55724

Merged
merged 3 commits into from
May 20, 2024

Conversation

andrewjsaid
Copy link
Contributor

@andrewjsaid andrewjsaid commented May 14, 2024

This is a performance improvement for LinearSearchJumpTable and SingleEntryJumpTable which is used heavily in routing.

Description

Instead of relying on string.Compare returning 0, we use span.Equals. The latter is much more optimized for example with vectorization.

Not sure if I must wrap with #if NET???_OR_GREATER - would appreciate direction.

// * Summary *

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22621.3447/22H2/2022Update/SunValley2)
12th Gen Intel Core i7-1260P, 1 CPU, 16 logical and 12 physical cores
.NET SDK 8.0.202
  [Host]     : .NET 8.0.3 (8.0.324.11423), X64 RyuJIT AVX2
  DefaultJob : .NET 8.0.3 (8.0.324.11423), X64 RyuJIT AVX2


| Method       | Mean     | Error    | StdDev   | Median   |
|------------- |---------:|---------:|---------:|---------:|
| Compare_Hit  | 90.61 ns | 1.758 ns | 4.601 ns | 89.08 ns |
| Compare_Miss | 74.30 ns | 1.386 ns | 1.082 ns | 73.99 ns |
| Equals_Hit   | 44.69 ns | 1.351 ns | 3.897 ns | 42.87 ns |
| Equals_Miss  | 45.37 ns | 0.884 ns | 0.738 ns | 45.25 ns |

Standalone benchmark code

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkRunner.Run<Benchmarks>();

public class Benchmarks
{
    private readonly LinearSearchJumpTable_Compare _searchWithCompare;
    private readonly LinearSearchJumpTable_Equals _searchWithEquals;

    public Benchmarks()
    {
        var entries = new[]
        {
            ("Controller1", 1),
            ("Controller2", 2),
            ("Controller3", 3),
            ("Controller4", 4),
            ("Controller5", 5),
            ("Controller6", 6),
            ("Controller7", 7),
            ("Controller8", 8),
            ("Controller9", 9),
        };

        _searchWithCompare = new (-1, -1, entries);
        _searchWithEquals = new (-1, -1, entries);
    }

    [Benchmark]
    public int Compare_Hit()
    {
        return _searchWithCompare.GetDestination("controller9", new PathSegment(0, 11));
    }

    [Benchmark]
    public int Compare_Miss()
    {
        return _searchWithCompare.GetDestination("controllerZ", new PathSegment(0, 11));
    }

    [Benchmark]
    public int Equals_Hit()
    {
        return _searchWithEquals.GetDestination("controller9", new PathSegment(0, 11));
    }

    [Benchmark]
    public int Equals_Miss()
    {
        return _searchWithEquals.GetDestination("controllerZ", new PathSegment(0, 11));
    }
}

internal sealed class LinearSearchJumpTable_Compare
{
    private readonly int _defaultDestination;
    private readonly int _exitDestination;
    private readonly (string text, int destination)[] _entries;

    public LinearSearchJumpTable_Compare(
        int defaultDestination,
        int exitDestination,
        (string text, int destination)[] entries)
    {
        _defaultDestination = defaultDestination;
        _exitDestination = exitDestination;
        _entries = entries;
    }

    public int GetDestination(string path, PathSegment segment)
    {
        if (segment.Length == 0)
        {
            return _exitDestination;
        }

        var entries = _entries;
        for (var i = 0; i < entries.Length; i++)
        {
            var text = entries[i].text;
            if (segment.Length == text.Length &&
                string.Compare(
                    path,
                    segment.Start,
                    text,
                    0,
                    segment.Length,
                    StringComparison.OrdinalIgnoreCase) == 0)
            {
                return entries[i].destination;
            }
        }

        return _defaultDestination;
    }
}

internal sealed class LinearSearchJumpTable_Equals
{
    private readonly int _defaultDestination;
    private readonly int _exitDestination;
    private readonly (string text, int destination)[] _entries;

    public LinearSearchJumpTable_Equals(
        int defaultDestination,
        int exitDestination,
        (string text, int destination)[] entries)
    {
        _defaultDestination = defaultDestination;
        _exitDestination = exitDestination;
        _entries = entries;
    }

    public int GetDestination(string path, PathSegment segment)
    {
        if (segment.Length == 0)
        {
            return _exitDestination;
        }

        var entries = _entries;
        var pathSpan = path.AsSpan(segment.Start..(segment.Start + segment.Length));
        for (var i = 0; i < entries.Length; i++)
        {
            var text = entries[i].text;
            if (pathSpan.Equals(text, StringComparison.OrdinalIgnoreCase))
            {
                return entries[i].destination;
            }
        }

        return _defaultDestination;
    }
}

internal readonly struct PathSegment : IEquatable<PathSegment>
{
    public readonly int Start;
    public readonly int Length;

    public PathSegment(int start, int length)
    {
        Start = start;
        Length = length;
    }

    public override bool Equals(object? obj)
    {
        return obj is PathSegment segment ? Equals(segment) : false;
    }

    public bool Equals(PathSegment other)
    {
        return Start == other.Start && Length == other.Length;
    }

    public override int GetHashCode()
    {
        return Start;
    }

    public override string ToString()
    {
        return $"Segment({Start}:{Length})";
    }
}

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions label May 14, 2024
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label May 14, 2024
@andrewjsaid andrewjsaid changed the title Make LinearSearchJumpTable use span.Equals which is much faster than string.Compare Use span.Equals instead of string.Compare in JumpTables May 15, 2024
@andrewjsaid
Copy link
Contributor Author

andrewjsaid commented May 15, 2024

One thing I'd like to research in a future PR, when I have time to run the perf tests, is whether SingleEntryAsciiJumpTable is now actually slower than SingleEntryJumpTable and an anti-optimization because very quick rough-and-dirty tests indicate span.Equals(..., OrdinalIgnoreCase) is actually faster than Ascii.EqualsIgnoreCase(span, span).

i.e. deleting SingleEntryAsciiJumpTable would probably simplify the code and be faster.

@amcasey
Copy link
Member

amcasey commented May 15, 2024

Seems sensible. FYI @BrennanConroy

@amcasey
Copy link
Member

amcasey commented May 15, 2024

I'm rerunning the failed tests, but it's not trivially obvious that the failures are unrelated. Are you seeing them locally @andrewjsaid?

@amcasey
Copy link
Member

amcasey commented May 15, 2024

Huh: #55696

@javiercn javiercn added area-routing and removed area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions labels May 20, 2024
@javiercn
Copy link
Member

Changes look good to me!

@BrennanConroy BrennanConroy merged commit 12e4c64 into dotnet:main May 20, 2024
26 checks passed
@dotnet-policy-service dotnet-policy-service bot added this to the 9.0-preview5 milestone May 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-routing community-contribution Indicates that the PR has been added by a community member Perf
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants