Skip to content

UriHelper.BuildRelative: opportunity for performance improvement #28904

Open
@paulomorgado

Description

@paulomorgado

Summary

UriHelper.BuildRelative creates an intermediary string for the combined path that is used only for concatenating with the other components to create the final URL.

public static string BuildRelative(
    PathString pathBase = new PathString(),
    PathString path = new PathString(),
    QueryString query = new QueryString(),
    FragmentString fragment = new FragmentString())
{
    string combinePath = (pathBase.HasValue || path.HasValue) ? (pathBase + path).ToString() : "/";
    return combinePath + query.ToString() + fragment.ToString();
}

Motivation and goals

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

Detailed design

Single_Concat

Given that the final URL is composed of only 3 or 4 parts, the use of string.Concat is both time and memory efficient.

String_Create

string.Create could be used. But it's just as memory efficient as string.Concat and less time efficient is most cases.

Benchmarks

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.21277
Intel Core i7-8650U CPU 1.90GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=5.0.200-preview.20614.14
  [Host]     : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT
  DefaultJob : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT

Method pathBase path query fragment Mean Error StdDev Median Ratio RatioSD Gen 0 Gen 1 Gen 2 Allocated
UriHelper_BuildRelative 10.334 ns 0.3064 ns 0.4948 ns 10.278 ns 1.00 0.00 - - - -
Single_Concat 7.557 ns 0.1385 ns 0.1295 ns 7.534 ns 0.71 0.04 - - - -
String_Create 8.398 ns 0.2614 ns 0.4646 ns 8.210 ns 0.82 0.07 - - - -
UriHelper_BuildRelative #fragment 29.557 ns 0.5777 ns 0.8098 ns 29.433 ns 1.00 0.00 0.0114 - - 48 B
Single_Concat #fragment 25.987 ns 0.5162 ns 0.4576 ns 25.986 ns 0.87 0.04 0.0114 - - 48 B
String_Create #fragment 37.347 ns 0.7226 ns 0.9395 ns 37.080 ns 1.26 0.04 0.0114 - - 48 B
UriHelper_BuildRelative ?param1=value1&param2=value2&param3=value3 99.900 ns 1.4810 ns 1.2367 ns 99.743 ns 1.00 0.00 0.0267 - - 112 B
Single_Concat ?param1=value1&param2=value2&param3=value3 100.739 ns 2.1000 ns 3.4504 ns 99.189 ns 1.01 0.04 0.0267 - - 112 B
String_Create ?param1=value1&param2=value2&param3=value3 108.600 ns 1.1783 ns 1.0445 ns 108.788 ns 1.09 0.02 0.0267 - - 112 B
UriHelper_BuildRelative ?param1=value1&param2=value2&param3=value3 #fragment 103.621 ns 1.6522 ns 1.9668 ns 103.039 ns 1.00 0.00 0.0305 - - 128 B
Single_Concat ?param1=value1&param2=value2&param3=value3 #fragment 103.340 ns 2.1289 ns 2.7682 ns 102.687 ns 1.00 0.03 0.0305 - - 128 B
String_Create ?param1=value1&param2=value2&param3=value3 #fragment 114.593 ns 2.3370 ns 2.2953 ns 114.116 ns 1.10 0.03 0.0305 - - 128 B
UriHelper_BuildRelative /path/one/two/three 145.257 ns 2.9859 ns 2.9325 ns 144.811 ns 1.00 0.00 - - - -
Single_Concat /path/one/two/three 139.083 ns 2.5119 ns 2.2268 ns 139.028 ns 0.96 0.02 - - - -
String_Create /path/one/two/three 139.504 ns 2.0265 ns 1.8956 ns 139.482 ns 0.96 0.03 - - - -
UriHelper_BuildRelative /path/one/two/three #fragment 185.283 ns 3.7939 ns 4.5163 ns 184.090 ns 1.00 0.00 0.0191 - - 80 B
Single_Concat /path/one/two/three #fragment 172.680 ns 2.7930 ns 2.8682 ns 171.917 ns 0.93 0.03 0.0191 - - 80 B
String_Create /path/one/two/three #fragment 181.909 ns 1.8544 ns 1.6439 ns 181.797 ns 0.98 0.03 0.0191 - - 80 B
UriHelper_BuildRelative /path/one/two/three ?param1=value1&param2=value2&param3=value3 246.532 ns 4.9295 ns 5.2746 ns 245.078 ns 1.00 0.00 0.0343 - - 144 B
Single_Concat /path/one/two/three ?param1=value1&param2=value2&param3=value3 236.133 ns 2.2584 ns 1.8859 ns 236.346 ns 0.96 0.02 0.0343 - - 144 B
String_Create /path/one/two/three ?param1=value1&param2=value2&param3=value3 256.853 ns 3.0590 ns 2.5544 ns 256.886 ns 1.04 0.02 0.0343 - - 144 B
UriHelper_BuildRelative /path/one/two/three ?param1=value1&param2=value2&param3=value3 #fragment 250.724 ns 4.9369 ns 4.8486 ns 250.070 ns 1.00 0.00 0.0401 - - 168 B
Single_Concat /path/one/two/three ?param1=value1&param2=value2&param3=value3 #fragment 246.514 ns 4.1097 ns 5.4863 ns 244.180 ns 0.99 0.03 0.0401 - - 168 B
String_Create /path/one/two/three ?param1=value1&param2=value2&param3=value3 #fragment 262.577 ns 4.2791 ns 4.0026 ns 261.847 ns 1.05 0.03 0.0401 - - 168 B
UriHelper_BuildRelative /base-path 78.592 ns 1.3297 ns 1.4780 ns 78.622 ns 1.00 0.00 - - - -
Single_Concat /base-path 76.572 ns 1.6192 ns 2.4236 ns 75.729 ns 0.98 0.04 - - - -
String_Create /base-path 78.741 ns 1.2896 ns 1.1432 ns 78.702 ns 1.00 0.03 - - - -
UriHelper_BuildRelative /base-path #fragment 109.622 ns 1.8629 ns 1.7426 ns 109.826 ns 1.00 0.00 0.0153 - - 64 B
Single_Concat /base-path #fragment 95.689 ns 1.2692 ns 1.1872 ns 95.927 ns 0.87 0.02 0.0153 - - 64 B
String_Create /base-path #fragment 104.016 ns 0.7687 ns 0.6814 ns 104.075 ns 0.95 0.02 0.0153 - - 64 B
UriHelper_BuildRelative /base-path ?param1=value1&param2=value2&param3=value3 181.487 ns 3.3279 ns 4.7728 ns 180.207 ns 1.00 0.00 0.0305 - - 128 B
Single_Concat /base-path ?param1=value1&param2=value2&param3=value3 169.635 ns 3.2086 ns 3.8196 ns 168.079 ns 0.93 0.04 0.0305 - - 128 B
String_Create /base-path ?param1=value1&param2=value2&param3=value3 180.100 ns 2.3870 ns 1.9932 ns 179.820 ns 0.98 0.04 0.0305 - - 128 B
UriHelper_BuildRelative /base-path ?param1=value1&param2=value2&param3=value3 #fragment 185.033 ns 3.4618 ns 4.2513 ns 184.494 ns 1.00 0.00 0.0343 - - 144 B
Single_Concat /base-path ?param1=value1&param2=value2&param3=value3 #fragment 178.615 ns 3.6554 ns 8.4720 ns 174.128 ns 0.99 0.06 0.0343 - - 144 B
String_Create /base-path ?param1=value1&param2=value2&param3=value3 #fragment 185.893 ns 3.5433 ns 2.7663 ns 185.250 ns 1.00 0.03 0.0343 - - 144 B
UriHelper_BuildRelative /base-path /path/one/two/three 240.085 ns 3.2488 ns 2.8800 ns 239.380 ns 1.00 0.00 0.0191 - - 80 B
Single_Concat /base-path /path/one/two/three 224.022 ns 2.7651 ns 2.4512 ns 223.712 ns 0.93 0.01 0.0191 - - 80 B
String_Create /base-path /path/one/two/three 246.030 ns 4.8594 ns 4.9903 ns 244.354 ns 1.03 0.03 0.0191 - - 80 B
UriHelper_BuildRelative /base-path /path/one/two/three #fragment 250.323 ns 3.2801 ns 2.9077 ns 249.957 ns 1.00 0.00 0.0439 - - 184 B
Single_Concat /base-path /path/one/two/three #fragment 242.748 ns 2.3555 ns 2.0881 ns 242.596 ns 0.97 0.01 0.0248 - - 104 B
String_Create /base-path /path/one/two/three #fragment 257.290 ns 3.4151 ns 2.8517 ns 257.640 ns 1.03 0.02 0.0248 - - 104 B
UriHelper_BuildRelative /base-path /path/one/two/three ?param1=value1&param2=value2&param3=value3 330.477 ns 3.8466 ns 3.0032 ns 330.757 ns 1.00 0.00 0.0591 - - 248 B
Single_Concat /base-path /path/one/two/three ?param1=value1&param2=value2&param3=value3 317.222 ns 5.0941 ns 6.6238 ns 315.922 ns 0.96 0.02 0.0401 - - 168 B
String_Create /base-path /path/one/two/three ?param1=value1&param2=value2&param3=value3 316.704 ns 5.4259 ns 5.3290 ns 315.759 ns 0.96 0.02 0.0401 - - 168 B
UriHelper_BuildRelative /base-path /path/one/two/three ?param1=value1&param2=value2&param3=value3 #fragment 340.993 ns 3.4605 ns 3.0676 ns 341.252 ns 1.00 0.00 0.0629 - - 264 B
Single_Concat /base-path /path/one/two/three ?param1=value1&param2=value2&param3=value3 #fragment 318.045 ns 5.1664 ns 4.5799 ns 317.869 ns 0.93 0.02 0.0439 - - 184 B
String_Create /base-path /path/one/two/three ?param1=value1&param2=value2&param3=value3 #fragment 337.393 ns 6.1599 ns 8.6353 ns 335.176 ns 0.99 0.03 0.0439 - - 184 B

Code

[MemoryDiagnoser]
public class BuildRelativeBenchmark
{
    public IEnumerable<object[]> Data() => TestData.PathBasePathQueryFragment();

    [Benchmark(Baseline = true)]
    [ArgumentsSource(nameof(Data))]
    public string UriHelper_BuildRelative(PathString pathBase, PathString path, QueryString query, FragmentString fragment)
        => UriHelper.BuildRelative(pathBase, path, query, fragment);

    [Benchmark]
    [ArgumentsSource(nameof(Data))]
    public string Single_Concat(PathString pathBase, PathString path, QueryString query, FragmentString fragment)
    {
        if (pathBase.HasValue || path.HasValue)
        {
            return pathBase.ToUriComponent() + path.ToUriComponent() + query.ToUriComponent() + fragment.ToUriComponent();
        }
        else
        {
            return "/" + query.ToUriComponent() + fragment.ToUriComponent();
        }
    }

    private static readonly SpanAction<char, (string pathBase, string path, string query, string fragment)> InitializeStringAction = new(InitializeString);

    [Benchmark]
    [ArgumentsSource(nameof(Data))]
    public string String_Create(PathString pathBaseString, PathString pathString, QueryString queryString, FragmentString fragmentString)
    {
        var pathBase = pathBaseString.ToUriComponent();
        var path = pathString.ToUriComponent();
        var query = queryString.ToUriComponent();
        var fragment = fragmentString.ToUriComponent();

        var length = pathBase.Length + path.Length + query.Length + fragment.Length;

        if (string.IsNullOrEmpty(pathBase) && string.IsNullOrEmpty(path))
        {
            if (length == 0)
            {
                return "/";
            }

            path = "/";
            length++;
        }
        else
        {
            if (!string.IsNullOrEmpty(pathBase) && pathBase.Length == length)
            {
                return pathBase;
            }

            if (path.Length == length)
            {
                return path;
            }
        }

        return string.Create(length, (pathBase, path, query, fragment), InitializeStringSpanAction);
    }

    private static void InitializeString(Span<char> buffer, (string pathBase, string path, string query, string fragment) uriParts)
    {
        var index = 0;

        index = Copy(buffer, index, uriParts.pathBase);
        index = Copy(buffer, index, uriParts.path);
        index = Copy(buffer, index, uriParts.query);
        _ = Copy(buffer, index, uriParts.fragment);

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        static int Copy(Span<char> buffer, int index, string text)
        {
            if (!string.IsNullOrEmpty(text))
            {
                var span = text.AsSpan();
                span.CopyTo(buffer.Slice(index, span.Length));
                return index + span.Length;
            }

            return index;
        }
    }
}

public static class TestData
{
    private static readonly string[] basePaths = new[] { "", "/base-path", };
    private static readonly string[] paths = new[] { "", "/path/one/two/three", };
    private static readonly string[] queries = new[] { "", "?param1=value1&param2=value2&param3=value3", };
    private static readonly string[] fragments = new[] { "", "#fragment", };

    public static IEnumerable<object[]> PathBasePathQueryFragment()
    {
        foreach (var basePath in basePaths)
        {
            foreach (var path in paths)
            {
                foreach (var query in queries)
                {
                    foreach (var fragment in fragments)
                    {
                        yield return new object[] { new PathString(basePath), new PathString(path), new QueryString(query), new FragmentString(fragment), };
                    }
                }
            }
        }
    }
}

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

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions