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.BuildRelative: opportunity for performance improvement #28904

Open
paulomorgado opened this issue Dec 29, 2020 · 1 comment
Open

UriHelper.BuildRelative: opportunity for performance improvement #28904

paulomorgado opened this issue Dec 29, 2020 · 1 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
Milestone

Comments

@paulomorgado
Copy link
Contributor

paulomorgado commented Dec 29, 2020

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), };
                    }
                }
            }
        }
    }
}
@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.

@amcasey amcasey added area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions and removed area-runtime labels Jun 2, 2023
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

No branches or pull requests

6 participants