Skip to content

Avoid allocations in more cases #9788

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

Merged
merged 2 commits into from
Apr 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Http/HttpAbstractions.sln
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HttpOv
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.IISIntegration", "..\Servers\IIS\IISIntegration\src\Microsoft.AspNetCore.Server.IISIntegration.csproj", "{1062FCDE-E145-40EC-B175-FDBCAA0C59A0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.WebUtilities.Performance", "WebUtilities\perf\Microsoft.AspNetCore.WebUtilities.Performance\Microsoft.AspNetCore.WebUtilities.Performance.csproj", "{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.WebUtilities.Performance", "WebUtilities\perf\Microsoft.AspNetCore.WebUtilities.Performance\Microsoft.AspNetCore.WebUtilities.Performance.csproj", "{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -642,7 +642,7 @@ Global
{1A866315-5FD5-4F96-BFAC-1447E3CB4514} = {80A090C8-ED02-4DE3-875A-30DCCDBD84BA}
{068A1DA0-C7DF-4E3C-9933-4E79A141EFF8} = {80A090C8-ED02-4DE3-875A-30DCCDBD84BA}
{8C635944-51F0-4BB0-A89E-CA49A7D9BE7F} = {FB2DCA0F-EB9E-425B-ABBC-D543DBEC090F}
{1A74D674-5D19-4575-B443-8B7ED433EF2B} = {793FFE24-138A-4C3D-81AB-18D625E36230}
{1A74D674-5D19-4575-B443-8B7ED433EF2B} = {14A7B3DE-46C8-4245-B0BD-9AFF3795C163}
{B8812D83-0F76-48F4-B716-C7356DB51E72} = {14A7B3DE-46C8-4245-B0BD-9AFF3795C163}
{215E7408-A123-4B5F-B625-59ED22031109} = {DC519C5E-CA6E-48CA-BF35-B46305B83013}
{8B64326C-A87F-4157-8337-22B5C4D7A4B7} = {DC519C5E-CA6E-48CA-BF35-B46305B83013}
Expand Down
58 changes: 37 additions & 21 deletions src/Http/Routing/src/Matching/CandidateSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
/// </summary>
public sealed class CandidateSet
{
private const int BitVectorSize = 32;

private CandidateState[] _candidates;
internal CandidateState[] Candidates;

/// <summary>
/// <para>
Expand Down Expand Up @@ -59,26 +57,32 @@ public CandidateSet(Endpoint[] endpoints, RouteValueDictionary[] values, int[] s
throw new ArgumentException($"The provided {nameof(endpoints)}, {nameof(values)}, and {nameof(scores)} must have the same length.");
}

_candidates = new CandidateState[endpoints.Length];
Candidates = new CandidateState[endpoints.Length];
for (var i = 0; i < endpoints.Length; i++)
{
_candidates[i] = new CandidateState(endpoints[i], values[i], scores[i]);
Candidates[i] = new CandidateState(endpoints[i], values[i], scores[i]);
}
}

// Used in tests.
internal CandidateSet(Candidate[] candidates)
{
_candidates = new CandidateState[candidates.Length];
Candidates = new CandidateState[candidates.Length];
for (var i = 0; i < candidates.Length; i++)
{
_candidates[i] = new CandidateState(candidates[i].Endpoint, candidates[i].Score);
Candidates[i] = new CandidateState(candidates[i].Endpoint, candidates[i].Score);
}
}

internal CandidateSet(CandidateState[] candidates)
{
Candidates = candidates;
}

/// <summary>
/// Gets the count of candidates in the set.
/// </summary>
public int Count => _candidates.Length;
public int Count => Candidates.Length;

/// <summary>
/// Gets the <see cref="CandidateState"/> associated with the candidate <see cref="Endpoint"/>
Expand All @@ -103,7 +107,7 @@ public ref CandidateState this[int index]
ThrowIndexArgumentOutOfRangeException();
}

return ref _candidates[index];
return ref Candidates[index];
}
}

Expand All @@ -124,7 +128,12 @@ public bool IsValidCandidate(int index)
ThrowIndexArgumentOutOfRangeException();
}

return _candidates[index].Score >= 0;
return IsValidCandidate(ref Candidates[index]);
}

internal static bool IsValidCandidate(ref CandidateState candidate)
{
return candidate.Score >= 0;
}

/// <summary>
Expand All @@ -142,8 +151,15 @@ public void SetValidity(int index, bool value)
ThrowIndexArgumentOutOfRangeException();
}

ref var original = ref _candidates[index];
_candidates[index] = new CandidateState(original.Endpoint, original.Values, original.Score >= 0 ^ value ? ~original.Score : original.Score);
ref var original = ref Candidates[index];
SetValidity(ref original, value);
}

internal static void SetValidity(ref CandidateState candidate, bool value)
{
var originalScore = candidate.Score;
var score = originalScore >= 0 ^ value ? ~originalScore : originalScore;
candidate = new CandidateState(candidate.Endpoint, candidate.Values, score);
}

/// <summary>
Expand All @@ -168,7 +184,7 @@ public void ReplaceEndpoint(int index, Endpoint endpoint, RouteValueDictionary v
ThrowIndexArgumentOutOfRangeException();
}

_candidates[index] = new CandidateState(endpoint, values, _candidates[index].Score);
Candidates[index] = new CandidateState(endpoint, values, Candidates[index].Score);

if (endpoint == null)
{
Expand Down Expand Up @@ -229,18 +245,18 @@ public void ExpandEndpoint(int index, IReadOnlyList<Endpoint> endpoints, ICompar
break;

case 1:
ReplaceEndpoint(index, endpoints[0], _candidates[index].Values);
ReplaceEndpoint(index, endpoints[0], Candidates[index].Values);
break;

default:

var score = GetOriginalScore(index);
var values = _candidates[index].Values;
var values = Candidates[index].Values;

// Adding candidates requires expanding the array and computing new score values for the new candidates.
var original = _candidates;
var original = Candidates;
var candidates = new CandidateState[original.Length - 1 + endpoints.Count];
_candidates = candidates;
Candidates = candidates;

// Since the new endpoints have an unknown ordering relationship to each other, we need to:
// - order them
Expand Down Expand Up @@ -293,12 +309,12 @@ public void ExpandEndpoint(int index, IReadOnlyList<Endpoint> endpoints, ICompar
scoreOffset++;
}

_candidates[i + index] = new CandidateState(buffer[i], values, score + scoreOffset);
Candidates[i + index] = new CandidateState(buffer[i], values, score + scoreOffset);
}

for (var i = index + 1; i < original.Length; i++)
{
_candidates[i + endpoints.Count - 1] = new CandidateState(original[i].Endpoint, original[i].Values, original[i].Score + scoreOffset);
Candidates[i + endpoints.Count - 1] = new CandidateState(original[i].Endpoint, original[i].Values, original[i].Score + scoreOffset);
}

break;
Expand All @@ -311,7 +327,7 @@ public void ExpandEndpoint(int index, IReadOnlyList<Endpoint> endpoints, ICompar
// This is the original score and used to determine if there are ambiguities.
private int GetOriginalScore(int index)
{
var score = _candidates[index].Score;
var score = Candidates[index].Score;
return score >= 0 ? score : ~score;
}

Expand All @@ -320,7 +336,7 @@ private void ValidateUniqueScore(int index)
var score = GetOriginalScore(index);

var count = 0;
var candidates = _candidates;
var candidates = Candidates;
for (var i = 0; i < candidates.Length; i++)
{
if (GetOriginalScore(i) == score)
Expand Down
40 changes: 24 additions & 16 deletions src/Http/Routing/src/Matching/DefaultEndpointSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace Microsoft.AspNetCore.Routing.Matching
{
internal class DefaultEndpointSelector : EndpointSelector
internal sealed class DefaultEndpointSelector : EndpointSelector
{
public override Task SelectAsync(
HttpContext httpContext,
Expand All @@ -31,9 +31,18 @@ public override Task SelectAsync(
throw new ArgumentNullException(nameof(candidateSet));
}

Select(httpContext, context, candidateSet.Candidates);
return Task.CompletedTask;
}

internal static void Select(
HttpContext httpContext,
EndpointSelectorContext context,
CandidateState[] candidateState)
{
// Fast path: We can specialize for trivial numbers of candidates since there can
// be no ambiguities
switch (candidateSet.Count)
switch (candidateState.Length)
{
case 0:
{
Expand All @@ -43,9 +52,9 @@ public override Task SelectAsync(

case 1:
{
if (candidateSet.IsValidCandidate(0))
ref var state = ref candidateState[0];
if (CandidateSet.IsValidCandidate(ref state))
{
ref var state = ref candidateSet[0];
context.Endpoint = state.Endpoint;
context.RouteValues = state.Values;
}
Expand All @@ -57,30 +66,28 @@ public override Task SelectAsync(
{
// Slow path: There's more than one candidate (to say nothing of validity) so we
// have to process for ambiguities.
ProcessFinalCandidates(httpContext, context, candidateSet);
ProcessFinalCandidates(httpContext, context, candidateState);
break;
}
}

return Task.CompletedTask;
}

private static void ProcessFinalCandidates(
HttpContext httpContext,
EndpointSelectorContext context,
CandidateSet candidateSet)
CandidateState[] candidateState)
{
Endpoint endpoint = null;
RouteValueDictionary values = null;
int? foundScore = null;
for (var i = 0; i < candidateSet.Count; i++)
for (var i = 0; i < candidateState.Length; i++)
{
if (!candidateSet.IsValidCandidate(i))
ref var state = ref candidateState[i];
if (!CandidateSet.IsValidCandidate(ref state))
{
continue;
}

ref var state = ref candidateSet[i];
if (foundScore == null)
{
// This is the first match we've seen - speculatively assign it.
Expand All @@ -103,7 +110,7 @@ private static void ProcessFinalCandidates(
//
// Don't worry about the 'null == state.Score' case, it returns false.

ReportAmbiguity(candidateSet);
ReportAmbiguity(candidateState);

// Unreachable, ReportAmbiguity always throws.
throw new NotSupportedException();
Expand All @@ -117,16 +124,17 @@ private static void ProcessFinalCandidates(
}
}

private static void ReportAmbiguity(CandidateSet candidates)
private static void ReportAmbiguity(CandidateState[] candidateState)
{
// If we get here it's the result of an ambiguity - we're OK with this
// being a littler slower and more allocatey.
var matches = new List<Endpoint>();
for (var i = 0; i < candidates.Count; i++)
for (var i = 0; i < candidateState.Length; i++)
{
if (candidates.IsValidCandidate(i))
ref var state = ref candidateState[i];
if (CandidateSet.IsValidCandidate(ref state))
{
matches.Add(candidates[i].Endpoint);
matches.Add(state.Endpoint);
}
}

Expand Down
44 changes: 25 additions & 19 deletions src/Http/Routing/src/Matching/DfaMatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public sealed override Task MatchAsync(HttpContext httpContext, EndpointSelector
// set of endpoints before we call the EndpointSelector.
//
// `candidateSet` is the mutable state that we pass to the EndpointSelector.
var candidateSet = new CandidateSet(candidates);
var candidateState = new CandidateState[candidateCount];

for (var i = 0; i < candidateCount; i++)
{
Expand All @@ -111,17 +111,13 @@ public sealed override Task MatchAsync(HttpContext httpContext, EndpointSelector
// candidate: readonly data about the endpoint and how to match
// state: mutable storarge for our processing
ref var candidate = ref candidates[i];
ref var state = ref candidateSet[i];
ref var state = ref candidateState[i];
state = new CandidateState(candidate.Endpoint, candidate.Score);

var flags = candidate.Flags;

// First process all of the parameters and defaults.
RouteValueDictionary values;
if ((flags & Candidate.CandidateFlags.HasSlots) == 0)
{
values = new RouteValueDictionary();
}
else
if ((flags & Candidate.CandidateFlags.HasSlots) != 0)
{
// The Slots array has the default values of the route values in it.
//
Expand All @@ -145,29 +141,29 @@ public sealed override Task MatchAsync(HttpContext httpContext, EndpointSelector
ProcessCatchAll(slots, candidate.CatchAll, path, segments);
}

values = RouteValueDictionary.FromArray(slots);
state.Values = RouteValueDictionary.FromArray(slots);
}

state.Values = values;

// Now that we have the route values, we need to process complex segments.
// Complex segments go through an old API that requires a fully-materialized
// route value dictionary.
var isMatch = true;
if ((flags & Candidate.CandidateFlags.HasComplexSegments) != 0)
{
if (!ProcessComplexSegments(candidate.Endpoint, candidate.ComplexSegments, path, segments, values))
state.Values ??= new RouteValueDictionary();
if (!ProcessComplexSegments(candidate.Endpoint, candidate.ComplexSegments, path, segments, state.Values))
{
candidateSet.SetValidity(i, false);
CandidateSet.SetValidity(ref state, false);
isMatch = false;
}
}

if ((flags & Candidate.CandidateFlags.HasConstraints) != 0)
{
if (!ProcessConstraints(candidate.Endpoint, candidate.Constraints, httpContext, values))
state.Values ??= new RouteValueDictionary();
if (!ProcessConstraints(candidate.Endpoint, candidate.Constraints, httpContext, state.Values))
{
candidateSet.SetValidity(i, false);
CandidateSet.SetValidity(ref state, false);
isMatch = false;
}
}
Expand All @@ -185,13 +181,23 @@ public sealed override Task MatchAsync(HttpContext httpContext, EndpointSelector
}
}

if (policyCount == 0)
if (policyCount == 0 && _isDefaultEndpointSelector)
{
// Fast path that avoids allocating the candidate set.
//
// We can use this when there are no policies and we're using the default selector.
DefaultEndpointSelector.Select(httpContext, context, candidateState);
return Task.CompletedTask;
}
else if (policyCount == 0)
{
// Perf: avoid a state machine if there are no polices
return _selector.SelectAsync(httpContext, context, candidateSet);
// Fast path that avoids a state machine.
//
// We can use this when there are no policies and a non-default selector.
return _selector.SelectAsync(httpContext, context, new CandidateSet(candidateState));
}

return SelectEndpointWithPoliciesAsync(httpContext, context, policies, candidateSet);
return SelectEndpointWithPoliciesAsync(httpContext, context, policies, new CandidateSet(candidateState));
}

internal (Candidate[] candidates, IEndpointSelectorPolicy[] policies) FindCandidateSet(
Expand Down
Loading