Skip to content

Conversation

@EgorBo
Copy link
Member

@EgorBo EgorBo commented Jul 8, 2025

Jit knows how to unroll string.Equals/MemoryExtensions.SequenceEquals when one of its args is a constant string. When it does so, it doesn't check for reference equality, which can lead to a regression compared to the non-unrolled behavior (can be quite significant). It's hard to say how often that happens on practice (we may involve Dynamic PGO for that, I might do that in a separate PR), but looks like the overhead from the reference check is not too bad. This issue led to terrible benchmark results for this benchmark: #117427 (comment) when that PR causes the unrolling to kick in and the previous fast path with reference equality was not presented in the codegen.

Example:

bool Test(string str)
{
    return str == "hello";
}

Asm diff: https://www.diffchecker.com/6ZrwEyJb/

@github-actions github-actions bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Jul 8, 2025
@EgorBo

This comment was marked as resolved.

@EgorBo
Copy link
Member Author

EgorBo commented Jul 8, 2025

@EgorBot -arm -amd

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);

public class BestCase
{
    string _empty;
    [Benchmark] public bool Best_StringEq_Empty() => _empty == "";

    string _short = "Hi";
    [Benchmark] public bool Best_StringEq_Short() => _short == "H1";
    [Benchmark] public bool Best_StringEq_Short_IgnoreCase() => string.Equals(_short, "H1", StringComparison.OrdinalIgnoreCase);

    string _medium = "Hello World";
    [Benchmark] public bool Best_StringEq_Medium() => _medium == "Hello World";
    [Benchmark] public bool Best_StringEq_Medium_IgnoreCase() => string.Equals(_medium, "Hello World", StringComparison.OrdinalIgnoreCase);

    string _long = "using System.Runtime.CompilerServices";
    [Benchmark] public bool Best_StringEq_Long() => _long == "using System.Runtime.CompilerServices";
    [Benchmark] public bool Best_StringEq_Long_IgnoreCase() => string.Equals(_long, "using System.Runtime.CompilerServices", StringComparison.OrdinalIgnoreCase);
    [Benchmark] public bool Best_SpanEq_Long() => _long.AsSpan().Equals("using System.Runtime.CompilerServices", StringComparison.Ordinal);
    [Benchmark] public bool Best_SpanEq_Long_IgnoreCase() => _long.AsSpan().Equals("using System.Runtime.CompilerServices", StringComparison.OrdinalIgnoreCase);

}

public class WorstCase
{
    // Don't know how to create an empty string that will be different from the interned one
    // I think it's almost impossible in practice.

    string _short = "Qi";
    [Benchmark] public bool Worst_StringEq_Short() => _short == "H1";
    [Benchmark] public bool Worst_StringEq_Short_IgnoreCase() => string.Equals(_short, "H1", StringComparison.OrdinalIgnoreCase);

    string _medium = "Qello World";
    [Benchmark] public bool Worst_StringEq_Medium() => _medium == "Hello World";
    [Benchmark] public bool Worst_StringEq_Medium_IgnoreCase() => string.Equals(_medium, "Hello World", StringComparison.OrdinalIgnoreCase);

    string _long = "Qsing System.Runtime.CompilerServices";
    [Benchmark] public bool Worst_StringEq_Long() => _long == "using System.Runtime.CompilerServices";
    [Benchmark] public bool Worst_StringEq_Long_IgnoreCase() => string.Equals(_long, "using System.Runtime.CompilerServices", StringComparison.OrdinalIgnoreCase);
    [Benchmark] public bool Worst_SpanEq_Long() => _long.AsSpan().Equals("Qsing System.Runtime.CompilerServices", StringComparison.Ordinal);
    [Benchmark] public bool Worst_SpanEq_Long_IgnoreCase() => _long.AsSpan().Equals("Qsing System.Runtime.CompilerServices", StringComparison.OrdinalIgnoreCase);
}

@EgorBo EgorBo marked this pull request as ready for review July 8, 2025 19:40
Copilot AI review requested due to automatic review settings July 8, 2025 19:40
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR enhances JIT unrolled comparisons by reinserting a reference equality check when comparing strings or spans against constant strings for the Equals case.

  • Wraps the existing unrolled string comparison in a ternary (str == const ? true : unrolled) to restore the fast path.
  • Applies an equivalent reference check for span comparisons.
  • Leaves StartsWith/EndsWith unmodified.
Comments suppressed due to low confidence (1)

src/coreclr/jit/importervectorization.cpp:510

  • Add JIT unit tests that verify string.Equals against constant strings uses the reference‐equality fast path when the instance matches the constant, and correctly falls back to the unrolled comparison otherwise.
        // Wrap with the reference equality check for Equals.

@EgorBo
Copy link
Member Author

EgorBo commented Jul 8, 2025

PTAL @dotnet/jit-contrib

This adds a missing reference equality fast-path to the unrolling. It does exists in the reference version:

if (ReferenceEquals(a, b))
{
return true;
}

so when the unrolling triggers, it might regress the performance. The overhead seems to be around noise.
Diffs aren't too bad (obviously, it's a size regression due to extra check)

@EgorBo EgorBo requested a review from jakobbotsch July 8, 2025 20:09
@EgorBo EgorBo added area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Jul 8, 2025
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants