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

UriHelper.GetDisplayUrl: opportunity for performance improvement #28906

Open
paulomorgado opened this issue Dec 29, 2020 · 1 comment · May be fixed by #55611
Open

UriHelper.GetDisplayUrl: opportunity for performance improvement #28906

paulomorgado opened this issue Dec 29, 2020 · 1 comment · May be fixed by #55611
Labels
area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions design-proposal This issue represents a design proposal for a different issue, linked in the description feature-http-abstractions
Milestone

Comments

@paulomorgado
Copy link
Contributor

paulomorgado commented Dec 29, 2020

Notes:

  • May 8th, 2024
    Updated to .NET 8.0

Summary

UriHelper.GetDisplayUrl uses a non-pooled StringBuilder that is instantiated on every invocation. Although optimized in size, it is a heap allocation with an intermediary buffer.

public static string GetDisplayUrl(this HttpRequest request)
{
    var scheme = request.Scheme ?? string.Empty;
    var host = request.Host.Value ?? string.Empty;
    var pathBase = request.PathBase.Value ?? string.Empty;
    var path = request.Path.Value ?? string.Empty;
    var queryString = request.QueryString.Value ?? string.Empty;

    // PERF: Calculate string length to allocate correct buffer size for StringBuilder.
    var length = scheme.Length + SchemeDelimiter.Length + host.Length
        + pathBase.Length + path.Length + queryString.Length;

    return new StringBuilder(length)
        .Append(scheme)
        .Append(SchemeDelimiter)
        .Append(host)
        .Append(pathBase)
        .Append(path)
        .Append(queryString)
        .ToString();
}

Motivation and goals

This method is frequently used in hot paths like redirect and rewrite rules.

From the benchmarks below, we can see that, compared to the current implementation using a StringBuilder with enough capacity, string interpolation is around 3 times better in terms of duration and around 4 times in memory used.

String.Create is even more performant.

Benchmarks

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3527/23H2/2023Update/SunValley3)
13th Gen Intel Core i9-13900K, 1 CPU, 32 logical and 24 physical cores
.NET SDK 8.0.300-preview.24203.14
  [Host]     : .NET 8.0.4 (8.0.424.16909), X64 RyuJIT AVX2
  DefaultJob : .NET 8.0.4 (8.0.424.16909), X64 RyuJIT AVX2

Method scheme host basePath path query Mean Ratio Gen0 Allocated Alloc Ratio
StringBuilder http cname.domain.tld / 69.988 ns 1.00 0.0288 544 B 1.00
String_Interpolation http cname.domain.tld / 26.739 ns 0.38 0.0038 72 B 0.13
String_Create http cname.domain.tld / 8.194 ns 0.12 0.0038 72 B 0.13
StringBuilder http cname.domain.tld / ?para(...)alue3 [42] 98.486 ns 1.00 0.0446 840 B 1.00
String_Interpolation http cname.domain.tld / ?para(...)alue3 [42] 31.592 ns 0.32 0.0085 160 B 0.19
String_Create http cname.domain.tld / ?para(...)alue3 [42] 15.580 ns 0.16 0.0085 160 B 0.19
StringBuilder http cname.domain.tld /path/one/two/three 80.926 ns 1.00 0.0314 592 B 1.00
String_Interpolation http cname.domain.tld /path/one/two/three 27.104 ns 0.34 0.0059 112 B 0.19
String_Create http cname.domain.tld /path/one/two/three 10.069 ns 0.12 0.0059 112 B 0.19
StringBuilder http cname.domain.tld /path/one/two/three ?para(...)alue3 [42] 100.374 ns 1.00 0.0467 880 B 1.00
String_Interpolation http cname.domain.tld /path/one/two/three ?para(...)alue3 [42] 32.507 ns 0.32 0.0102 192 B 0.22
String_Create http cname.domain.tld /path/one/two/three ?para(...)alue3 [42] 15.831 ns 0.16 0.0102 192 B 0.22
StringBuilder http cname.domain.tld /base-path / 71.221 ns 1.00 0.0305 576 B 1.00
String_Interpolation http cname.domain.tld /base-path / 25.770 ns 0.36 0.0051 96 B 0.17
String_Create http cname.domain.tld /base-path / 11.728 ns 0.16 0.0051 96 B 0.17
StringBuilder http cname.domain.tld /base-path / ?para(...)alue3 [42] 101.443 ns 1.00 0.0459 864 B 1.00
String_Interpolation http cname.domain.tld /base-path / ?para(...)alue3 [42] 31.538 ns 0.31 0.0093 176 B 0.20
String_Create http cname.domain.tld /base-path / ?para(...)alue3 [42] 17.074 ns 0.17 0.0093 176 B 0.20
StringBuilder http cname.domain.tld /base-path /path/one/two/three 76.368 ns 1.00 0.0327 616 B 1.00
String_Interpolation http cname.domain.tld /base-path /path/one/two/three 27.561 ns 0.36 0.0068 128 B 0.21
String_Create http cname.domain.tld /base-path /path/one/two/three 11.338 ns 0.15 0.0068 128 B 0.21
StringBuilder http cname.domain.tld /base-path /path/one/two/three ?para(...)alue3 [42] 97.275 ns 1.00 0.0479 904 B 1.00
String_Interpolation http cname.domain.tld /base-path /path/one/two/three ?para(...)alue3 [42] 34.144 ns 0.35 0.0114 216 B 0.24
String_Create http cname.domain.tld /base-path /path/one/two/three ?para(...)alue3 [42] 17.378 ns 0.18 0.0115 216 B 0.24

StringBuilder

This benchmark uses the same implementation as UriHelper.GetDisplayUrl.

String_Interpolation

This benchmark uses string interpolation to build the URL.

String_Create

This benchmark uses String.Create and spans to build the URL.

Code

[MemoryDiagnoser]
[HideColumns("Error", "StdDev", "Median", "RatioSD")]
public class DisplayUrlBenchmark
{
    private static readonly string SchemeDelimiter = Uri.SchemeDelimiter;

    private static readonly string[] schemes = ["http"];
    private static readonly string[] hosts = ["cname.domain.tld"];
    private static readonly string[] basePaths = [null, "/base-path",];
    private static readonly string[] paths = ["/", "/path/one/two/three",];
    private static readonly string[] queries = [null, "?param1=value1&param2=value2&param3=value3",];

    public IEnumerable<object[]> Data()
    {
        foreach (var scheme in schemes)
        {
            foreach (var host in hosts)
            {
                foreach (var basePath in basePaths)
                {
                    foreach (var path in paths)
                    {
                        foreach (var query in queries)
                        {
                            yield return new object[] { scheme, new HostString(host), new PathString(basePath), new PathString(path), new QueryString(query), };
                        }
                    }
                }
            }
        }
    }

    [Benchmark(Baseline = true)]
    [ArgumentsSource(nameof(Data))]
    public string StringBuilder(string scheme, HostString host, PathString basePath, PathString path, QueryString query)
    {
        var schemeValue = scheme ?? string.Empty;
        var hostValue = host.Value ?? string.Empty;
        var basePathValue = basePath.Value ?? string.Empty;
        var pathValue = path.Value ?? string.Empty;
        var queryValue = query.Value ?? string.Empty;

        var length =
            +schemeValue.Length
            + SchemeDelimiter
            + hostValue.Length
            + basePathValue.Length
            + pathValue.Length
            + queryValue.Length;

        return new StringBuilder(length)
                .Append(schemeValue)
                .Append(SchemeDelimiter)
                .Append(hostValue)
                .Append(basePathValue)
                .Append(pathValue)
                .Append(queryValue)
                .ToString();
    }

    [Benchmark]
    [ArgumentsSource(nameof(Data))]
    public string String_Interpolation(string scheme, HostString host, PathString basePath, PathString path, QueryString query)
    {
        return $"{scheme}://{host.Value}{basePath.Value}{path.Value}{query.Value}";
    }

    [Benchmark]
    [ArgumentsSource(nameof(Data))]
    public string String_Create(string scheme, HostString host, PathString basePath, PathString path, QueryString query)
    {
        var schemeValue = scheme ?? string.Empty;
        var hostValue = host.Value ?? string.Empty;
        var basePathValue = basePath.Value ?? string.Empty;
        var pathValue = path.Value ?? string.Empty;
        var queryValue = query.Value ?? string.Empty;

        var length =
            +schemeValue.Length
            + SchemeDelimiter.Length
            + hostValue.Length
            + basePathValue.Length
            + pathValue.Length
            + queryValue.Length;

        return string.Create(
            length,
            (schemeValue, hostValue, basePathValue, pathValue, queryValue),
            static (buffer, uriParts) =>
            {
                var (scheme, host, basePath, path, query) = uriParts;

                if (scheme.Length > 0)
                {
                    scheme.CopyTo(buffer);
                    buffer = buffer.Slice(scheme.Length);
                }

                SchemeDelimiter.CopyTo(buffer);
                buffer = buffer.Slice(SchemeDelimiter.Length);

                if (host.Length > 0)
                {
                    host.CopyTo(buffer);
                    buffer = buffer.Slice(host.Length);
                }

                if (basePath.Length > 0)
                {
                    basePath.CopyTo(buffer);
                    buffer = buffer.Slice(basePath.Length);
                }

                if (path.Length > 0)
                {
                    path.CopyTo(buffer);
                    buffer = buffer.Slice(path.Length);
                }

                if (query.Length > 0)
                {
                    query.CopyTo(buffer);
                }
            });
    }
}
@paulomorgado paulomorgado added the design-proposal This issue represents a design proposal for a different issue, linked in the description label Dec 29, 2020
@BrennanConroy BrennanConroy added this to the Backlog milestone Dec 30, 2020
@ghost
Copy link

ghost commented Dec 30, 2020

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions design-proposal This issue represents a design proposal for a different issue, linked in the description feature-http-abstractions
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants