Skip to content

UriHelper.GetDisplayUrl: opportunity for performance improvement #28906

@paulomorgado

Description

@paulomorgado

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);
                }
            });
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-networkingIncludes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractionsdesign-proposalThis issue represents a design proposal for a different issue, linked in the descriptionfeature-http-abstractions

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions