-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Description
Summary
UriHelper.BuildAbsolute
creates an intermediary string for the combined path that is used only for concatenating with the other components to create the final URL.
It also 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 BuildAbsolute(
string scheme,
HostString host,
PathString pathBase = new PathString(),
PathString path = new PathString(),
QueryString query = new QueryString(),
FragmentString fragment = new FragmentString())
{
if (scheme == null)
{
throw new ArgumentNullException(nameof(scheme));
}
var combinedPath = (pathBase.HasValue || path.HasValue) ? (pathBase + path).ToString() : "/";
var encodedHost = host.ToString();
var encodedQuery = query.ToString();
var encodedFragment = fragment.ToString();
// PERF: Calculate string length to allocate correct buffer size for StringBuilder.
var length = scheme.Length + SchemeDelimiter.Length + encodedHost.Length
+ combinedPath.Length + encodedQuery.Length + encodedFragment.Length;
return new StringBuilder(length)
.Append(scheme)
.Append(SchemeDelimiter)
.Append(encodedHost)
.Append(combinedPath)
.Append(encodedQuery)
.Append(encodedFragment)
.ToString();
}
Motivation and goals
This method is frequently use in hot paths like redirect and rewrite rules.
Detailed design
StringBuilder_WithoutCombinedPathGeneration
Just by not generating the intermediary combinePath
, there are memory usage improvements in the when the number of components is highier. There are also time improvements in those cases, but it's wrost in the other cases.
String_Concat_WithArrayArgument
Given that the final URL is composed of more than 4 parts, the use of string.Concat
incurs in an array allocation.
But it still always performs better in terms of time and memory usage that using a StringBuilder
.
String_Create
string.Create
excels here in comparison to all the other options. It was created exactly for these use 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 | host | pathBase | path | query | fragment | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
UriHelper_BuildRelative | cname.domain.tld | 201.1 ns | 9.05 ns | 25.52 ns | 191.8 ns | 1.00 | 0.00 | 0.0477 | - | - | 200 B | ||||
StringBuilder_WithoutCombinedPathGeneration | cname.domain.tld | 189.3 ns | 5.81 ns | 15.91 ns | 188.4 ns | 0.96 | 0.16 | 0.0477 | - | - | 200 B | ||||
String_Concat_WithArrayArgument | cname.domain.tld | 142.8 ns | 2.91 ns | 4.70 ns | 141.6 ns | 0.74 | 0.10 | 0.0381 | - | - | 160 B | ||||
String_Create | cname.domain.tld | 129.9 ns | 3.74 ns | 10.96 ns | 129.0 ns | 0.66 | 0.10 | 0.0172 | - | - | 72 B | ||||
UriHelper_BuildRelative | cname.domain.tld | #fragment | 170.9 ns | 4.67 ns | 13.54 ns | 168.8 ns | 1.00 | 0.00 | 0.0572 | - | - | 240 B | |||
StringBuilder_WithoutCombinedPathGeneration | cname.domain.tld | #fragment | 181.8 ns | 6.66 ns | 19.22 ns | 179.5 ns | 1.07 | 0.15 | 0.0572 | - | - | 240 B | |||
String_Concat_WithArrayArgument | cname.domain.tld | #fragment | 151.2 ns | 3.09 ns | 4.24 ns | 150.2 ns | 0.86 | 0.08 | 0.0420 | - | - | 176 B | |||
String_Create | cname.domain.tld | #fragment | 111.3 ns | 2.26 ns | 2.32 ns | 111.2 ns | 0.65 | 0.05 | 0.0229 | - | - | 96 B | |||
UriHelper_BuildRelative | cname.domain.tld | ?param1=value1¶m2=value2¶m3=value3 | 236.3 ns | 3.59 ns | 3.35 ns | 235.6 ns | 1.00 | 0.00 | 0.0877 | - | - | 368 B | |||
StringBuilder_WithoutCombinedPathGeneration | cname.domain.tld | ?param1=value1¶m2=value2¶m3=value3 | 247.0 ns | 5.05 ns | 4.72 ns | 247.5 ns | 1.05 | 0.03 | 0.0877 | - | - | 368 B | |||
String_Concat_WithArrayArgument | cname.domain.tld | ?param1=value1¶m2=value2¶m3=value3 | 227.2 ns | 4.64 ns | 9.47 ns | 225.7 ns | 0.99 | 0.04 | 0.0572 | - | - | 240 B | |||
String_Create | cname.domain.tld | ?param1=value1¶m2=value2¶m3=value3 | 189.4 ns | 3.90 ns | 8.22 ns | 185.9 ns | 0.82 | 0.04 | 0.0381 | - | - | 160 B | |||
UriHelper_BuildRelative | cname.domain.tld | ?param1=value1¶m2=value2¶m3=value3 | #fragment | 246.6 ns | 5.04 ns | 8.15 ns | 244.2 ns | 1.00 | 0.00 | 0.0954 | - | - | 400 B | ||
StringBuilder_WithoutCombinedPathGeneration | cname.domain.tld | ?param1=value1¶m2=value2¶m3=value3 | #fragment | 252.3 ns | 5.12 ns | 10.35 ns | 250.2 ns | 1.03 | 0.06 | 0.0954 | - | - | 400 B | ||
String_Concat_WithArrayArgument | cname.domain.tld | ?param1=value1¶m2=value2¶m3=value3 | #fragment | 224.9 ns | 2.22 ns | 1.74 ns | 225.3 ns | 0.92 | 0.03 | 0.0610 | - | - | 256 B | ||
String_Create | cname.domain.tld | ?param1=value1¶m2=value2¶m3=value3 | #fragment | 192.0 ns | 3.62 ns | 5.08 ns | 192.2 ns | 0.78 | 0.04 | 0.0420 | - | - | 176 B | ||
UriHelper_BuildRelative | cname.domain.tld | /path/one/two/three | 309.4 ns | 3.35 ns | 2.97 ns | 309.7 ns | 1.00 | 0.00 | 0.0648 | - | - | 272 B | |||
StringBuilder_WithoutCombinedPathGeneration | cname.domain.tld | /path/one/two/three | 321.7 ns | 4.44 ns | 3.93 ns | 321.1 ns | 1.04 | 0.01 | 0.0648 | - | - | 272 B | |||
String_Concat_WithArrayArgument | cname.domain.tld | /path/one/two/three | 300.6 ns | 5.86 ns | 12.74 ns | 296.0 ns | 1.01 | 0.04 | 0.0477 | - | - | 200 B | |||
String_Create | cname.domain.tld | /path/one/two/three | 247.0 ns | 5.06 ns | 7.25 ns | 244.7 ns | 0.81 | 0.03 | 0.0267 | - | - | 112 B | |||
UriHelper_BuildRelative | cname.domain.tld | /path/one/two/three | #fragment | 319.5 ns | 4.68 ns | 4.15 ns | 318.9 ns | 1.00 | 0.00 | 0.0725 | - | - | 304 B | ||
StringBuilder_WithoutCombinedPathGeneration | cname.domain.tld | /path/one/two/three | #fragment | 307.3 ns | 4.89 ns | 4.58 ns | 306.4 ns | 0.96 | 0.02 | 0.0725 | - | - | 304 B | ||
String_Concat_WithArrayArgument | cname.domain.tld | /path/one/two/three | #fragment | 302.8 ns | 6.11 ns | 8.77 ns | 300.1 ns | 0.95 | 0.04 | 0.0515 | - | - | 216 B | ||
String_Create | cname.domain.tld | /path/one/two/three | #fragment | 253.0 ns | 5.12 ns | 8.13 ns | 251.5 ns | 0.79 | 0.03 | 0.0305 | - | - | 128 B | ||
UriHelper_BuildRelative | cname.domain.tld | /path/one/two/three | ?param1=value1¶m2=value2¶m3=value3 | 421.7 ns | 8.49 ns | 15.09 ns | 418.3 ns | 1.00 | 0.00 | 0.1049 | - | - | 440 B | ||
StringBuilder_WithoutCombinedPathGeneration | cname.domain.tld | /path/one/two/three | ?param1=value1¶m2=value2¶m3=value3 | 395.9 ns | 4.71 ns | 4.18 ns | 395.1 ns | 0.94 | 0.02 | 0.1049 | - | - | 440 B | ||
String_Concat_WithArrayArgument | cname.domain.tld | /path/one/two/three | ?param1=value1¶m2=value2¶m3=value3 | 372.2 ns | 6.86 ns | 5.73 ns | 370.3 ns | 0.88 | 0.03 | 0.0687 | - | - | 288 B | ||
String_Create | cname.domain.tld | /path/one/two/three | ?param1=value1¶m2=value2¶m3=value3 | 325.6 ns | 6.51 ns | 6.69 ns | 323.5 ns | 0.78 | 0.02 | 0.0458 | - | - | 192 B | ||
UriHelper_BuildRelative | cname.domain.tld | /path/one/two/three | ?param1=value1¶m2=value2¶m3=value3 | #fragment | 396.1 ns | 7.65 ns | 7.16 ns | 395.3 ns | 1.00 | 0.00 | 0.1144 | - | - | 480 B | |
StringBuilder_WithoutCombinedPathGeneration | cname.domain.tld | /path/one/two/three | ?param1=value1¶m2=value2¶m3=value3 | #fragment | 394.3 ns | 3.10 ns | 2.42 ns | 393.8 ns | 0.99 | 0.02 | 0.1144 | - | - | 480 B | |
String_Concat_WithArrayArgument | cname.domain.tld | /path/one/two/three | ?param1=value1¶m2=value2¶m3=value3 | #fragment | 377.6 ns | 4.22 ns | 3.74 ns | 377.8 ns | 0.95 | 0.02 | 0.0725 | - | - | 304 B | |
String_Create | cname.domain.tld | /path/one/two/three | ?param1=value1¶m2=value2¶m3=value3 | #fragment | 329.2 ns | 6.29 ns | 6.46 ns | 327.9 ns | 0.83 | 0.02 | 0.0515 | - | - | 216 B | |
UriHelper_BuildRelative | cname.domain.tld | /base-path | 241.0 ns | 2.40 ns | 2.25 ns | 241.4 ns | 1.00 | 0.00 | 0.0572 | - | - | 240 B | |||
StringBuilder_WithoutCombinedPathGeneration | cname.domain.tld | /base-path | 245.4 ns | 4.99 ns | 4.66 ns | 245.3 ns | 1.02 | 0.02 | 0.0572 | - | - | 240 B | |||
String_Concat_WithArrayArgument | cname.domain.tld | /base-path | 215.0 ns | 2.70 ns | 2.11 ns | 214.9 ns | 0.89 | 0.01 | 0.0439 | - | - | 184 B | |||
String_Create | cname.domain.tld | /base-path | 164.7 ns | 1.27 ns | 1.19 ns | 164.8 ns | 0.68 | 0.01 | 0.0229 | - | - | 96 B | |||
UriHelper_BuildRelative | cname.domain.tld | /base-path | #fragment | 243.2 ns | 4.56 ns | 11.85 ns | 240.1 ns | 1.00 | 0.00 | 0.0648 | - | - | 272 B | ||
StringBuilder_WithoutCombinedPathGeneration | cname.domain.tld | /base-path | #fragment | 236.3 ns | 4.75 ns | 6.66 ns | 234.3 ns | 0.96 | 0.05 | 0.0648 | - | - | 272 B | ||
String_Concat_WithArrayArgument | cname.domain.tld | /base-path | #fragment | 233.3 ns | 3.86 ns | 3.22 ns | 233.0 ns | 0.95 | 0.05 | 0.0477 | - | - | 200 B | ||
String_Create | cname.domain.tld | /base-path | #fragment | 171.6 ns | 2.93 ns | 2.88 ns | 170.8 ns | 0.70 | 0.04 | 0.0267 | - | - | 112 B | ||
UriHelper_BuildRelative | cname.domain.tld | /base-path | ?param1=value1¶m2=value2¶m3=value3 | 324.3 ns | 6.38 ns | 7.59 ns | 322.2 ns | 1.00 | 0.00 | 0.0954 | - | - | 400 B | ||
StringBuilder_WithoutCombinedPathGeneration | cname.domain.tld | /base-path | ?param1=value1¶m2=value2¶m3=value3 | 324.8 ns | 6.06 ns | 5.37 ns | 323.4 ns | 1.00 | 0.03 | 0.0954 | - | - | 400 B | ||
String_Concat_WithArrayArgument | cname.domain.tld | /base-path | ?param1=value1¶m2=value2¶m3=value3 | 298.9 ns | 5.48 ns | 4.58 ns | 298.2 ns | 0.92 | 0.03 | 0.0629 | - | - | 264 B | ||
String_Create | cname.domain.tld | /base-path | ?param1=value1¶m2=value2¶m3=value3 | 252.5 ns | 5.12 ns | 8.26 ns | 250.9 ns | 0.78 | 0.03 | 0.0420 | - | - | 176 B | ||
UriHelper_BuildRelative | cname.domain.tld | /base-path | ?param1=value1¶m2=value2¶m3=value3 | #fragment | 319.1 ns | 4.57 ns | 4.06 ns | 318.7 ns | 1.00 | 0.00 | 0.1049 | - | - | 440 B | |
StringBuilder_WithoutCombinedPathGeneration | cname.domain.tld | /base-path | ?param1=value1¶m2=value2¶m3=value3 | #fragment | 320.0 ns | 4.47 ns | 4.18 ns | 319.2 ns | 1.00 | 0.02 | 0.1049 | - | - | 440 B | |
String_Concat_WithArrayArgument | cname.domain.tld | /base-path | ?param1=value1¶m2=value2¶m3=value3 | #fragment | 320.8 ns | 6.39 ns | 11.19 ns | 315.0 ns | 1.02 | 0.05 | 0.0687 | - | - | 288 B | |
String_Create | cname.domain.tld | /base-path | ?param1=value1¶m2=value2¶m3=value3 | #fragment | 249.8 ns | 5.07 ns | 5.21 ns | 250.2 ns | 0.78 | 0.02 | 0.0458 | - | - | 192 B | |
UriHelper_BuildRelative | cname.domain.tld | /base-path | /path/one/two/three | 421.9 ns | 6.69 ns | 6.25 ns | 419.6 ns | 1.00 | 0.00 | 0.0935 | - | - | 392 B | ||
StringBuilder_WithoutCombinedPathGeneration | cname.domain.tld | /base-path | /path/one/two/three | 389.2 ns | 6.62 ns | 7.08 ns | 387.0 ns | 0.92 | 0.02 | 0.0744 | - | - | 312 B | ||
String_Concat_WithArrayArgument | cname.domain.tld | /base-path | /path/one/two/three | 362.6 ns | 6.17 ns | 6.85 ns | 361.0 ns | 0.86 | 0.02 | 0.0534 | - | - | 224 B | ||
String_Create | cname.domain.tld | /base-path | /path/one/two/three | 318.4 ns | 6.41 ns | 8.56 ns | 315.5 ns | 0.76 | 0.03 | 0.0305 | - | - | 128 B | ||
UriHelper_BuildRelative | cname.domain.tld | /base-path | /path/one/two/three | #fragment | 412.6 ns | 4.19 ns | 3.72 ns | 412.5 ns | 1.00 | 0.00 | 0.1030 | - | - | 432 B | |
StringBuilder_WithoutCombinedPathGeneration | cname.domain.tld | /base-path | /path/one/two/three | #fragment | 390.0 ns | 7.68 ns | 18.84 ns | 381.2 ns | 0.94 | 0.04 | 0.0839 | - | - | 352 B | |
String_Concat_WithArrayArgument | cname.domain.tld | /base-path | /path/one/two/three | #fragment | 360.9 ns | 5.15 ns | 4.30 ns | 359.0 ns | 0.87 | 0.01 | 0.0572 | - | - | 240 B | |
String_Create | cname.domain.tld | /base-path | /path/one/two/three | #fragment | 322.3 ns | 5.97 ns | 5.30 ns | 320.7 ns | 0.78 | 0.01 | 0.0362 | - | - | 152 B | |
UriHelper_BuildRelative | cname.domain.tld | /base-path | /path/one/two/three | ?param1=value1¶m2=value2¶m3=value3 | 488.9 ns | 6.15 ns | 5.75 ns | 490.3 ns | 1.00 | 0.00 | 0.1335 | - | - | 560 B | |
StringBuilder_WithoutCombinedPathGeneration | cname.domain.tld | /base-path | /path/one/two/three | ?param1=value1¶m2=value2¶m3=value3 | 476.3 ns | 9.62 ns | 15.81 ns | 470.6 ns | 0.98 | 0.04 | 0.1144 | - | - | 480 B | |
String_Concat_WithArrayArgument | cname.domain.tld | /base-path | /path/one/two/three | ?param1=value1¶m2=value2¶m3=value3 | 439.7 ns | 7.73 ns | 7.23 ns | 438.8 ns | 0.90 | 0.02 | 0.0725 | - | - | 304 B | |
String_Create | cname.domain.tld | /base-path | /path/one/two/three | ?param1=value1¶m2=value2¶m3=value3 | 396.9 ns | 7.76 ns | 7.62 ns | 393.0 ns | 0.81 | 0.02 | 0.0515 | - | - | 216 B | |
UriHelper_BuildRelative | cname.domain.tld | /base-path | /path/one/two/three | ?param1=value1¶m2=value2¶m3=value3 | #fragment | 496.5 ns | 9.76 ns | 13.68 ns | 494.4 ns | 1.00 | 0.00 | 0.1411 | - | - | 592 B |
StringBuilder_WithoutCombinedPathGeneration | cname.domain.tld | /base-path | /path/one/two/three | ?param1=value1¶m2=value2¶m3=value3 | #fragment | 525.1 ns | 16.74 ns | 49.10 ns | 517.1 ns | 1.01 | 0.08 | 0.1221 | - | - | 512 B |
String_Concat_WithArrayArgument | cname.domain.tld | /base-path | /path/one/two/three | ?param1=value1¶m2=value2¶m3=value3 | #fragment | 449.7 ns | 8.55 ns | 9.50 ns | 447.6 ns | 0.90 | 0.03 | 0.0763 | - | - | 320 B |
String_Create | cname.domain.tld | /base-path | /path/one/two/three | ?param1=value1¶m2=value2¶m3=value3 | #fragment | 398.3 ns | 6.32 ns | 5.60 ns | 397.2 ns | 0.79 | 0.02 | 0.0553 | - | - | 232 B |
Code
[MemoryDiagnoser]
public class BuildAbsoluteBenchmark
{
public IEnumerable<object[]> Data() => TestData.HostPathBasePathQueryFragment();
[Benchmark(Baseline = true)]
[ArgumentsSource(nameof(Data))]
public string UriHelper_BuildRelative(HostString host, PathString pathBase, PathString path, QueryString query, FragmentString fragment)
=> UriHelper.BuildAbsolute("https", host, pathBase, path, query, fragment);
[Benchmark]
[ArgumentsSource(nameof(Data))]
public string StringBuilder_WithoutCombinedPathGeneration(HostString host, PathString pathBase, PathString path, QueryString query, FragmentString fragment)
{
var scheme = "https";
var SchemeDelimiter = Uri.SchemeDelimiter;
var encodedHost = host.ToUriComponent();
var encodedPathBase = pathBase.ToUriComponent();
var encodedPath = path.ToUriComponent();
var encodedQuery = query.ToUriComponent();
var encodedFragment = fragment.ToUriComponent();
// PERF: Calculate string length to allocate correct buffer size for StringBuilder.
var length =
scheme.Length +
SchemeDelimiter.Length +
encodedHost.Length +
encodedPathBase.Length +
encodedPath.Length +
encodedQuery.Length +
encodedFragment.Length;
if (!pathBase.HasValue && !path.HasValue)
{
length++;
}
var builder = new StringBuilder(length)
.Append(scheme)
.Append(SchemeDelimiter)
.Append(encodedHost);
if (!pathBase.HasValue && !path.HasValue)
{
builder.Append("/");
}
else
{
builder
.Append(encodedPathBase)
.Append(encodedPath);
}
return builder
.Append(encodedQuery)
.Append(encodedFragment)
.ToString();
}
[Benchmark]
[ArgumentsSource(nameof(Data))]
public string String_Concat_WithArrayArgument(HostString host, PathString pathBase, PathString path, QueryString query, FragmentString fragment)
{
var scheme = "https";
var SchemeDelimiter = Uri.SchemeDelimiter;
if (!pathBase.HasValue && !path.HasValue)
{
return scheme + SchemeDelimiter + "/" + host.ToUriComponent() + "/" + query.ToUriComponent() + fragment.ToUriComponent();
}
else
{
return scheme + SchemeDelimiter + pathBase.ToUriComponent() + path.ToUriComponent() + host.ToUriComponent() + "/" + query.ToUriComponent() + fragment.ToUriComponent();
}
}
private static readonly SpanAction<char, (string scheme, string host, string pathBase, string path, string query, string fragment)> InitializeStringAction = new(InitializeString);
[Benchmark]
[ArgumentsSource(nameof(Data))]
public string String_Create(HostString hostString, PathString pathBaseString, PathString pathString, QueryString queryString, FragmentString fragmentString)
{
var scheme = "https";
var host = hostString.ToUriComponent();
var pathBase = pathBaseString.ToUriComponent();
var path = pathString.ToUriComponent();
var query = queryString.ToUriComponent();
var fragment = fragmentString.ToUriComponent();
// PERF: Calculate string length to allocate correct buffer size for string.Create.
var length =
scheme.Length +
Uri.SchemeDelimiter.Length +
host.Length +
pathBase.Length +
path.Length +
query.Length +
fragment.Length;
if (string.IsNullOrEmpty(pathBase) && string.IsNullOrEmpty(path))
{
path = "/";
length++;
}
return string.Create(length, (scheme, host, pathBase, path, query, fragment), InitializeStringSpanAction);
}
private static void InitializeString(Span<char> buffer, (string scheme, string host, string pathBase, string path, string query, string fragment) uriParts)
{
var index = 0;
index = Copy(buffer, index, uriParts.scheme);
index = Copy(buffer, index, Uri.SchemeDelimiter);
index = Copy(buffer, index, uriParts.host);
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[] hosts = new[] { "cname.domain.tld" };
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¶m2=value2¶m3=value3", };
private static readonly string[] fragments = new[] { "", "#fragment", };
public static IEnumerable<object[]> HostPathBasePathQueryFragment()
{
foreach (var host in hosts)
{
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 HostString(host), new PathString(basePath), new PathString(path), new QueryString(query), new FragmentString(fragment), };
}
}
}
}
}
}
}