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

Allow headers to match on ReferenceEquals before OrdinalIgnoreCase #9341

Merged
merged 6 commits into from
Apr 18, 2019

Conversation

benaadams
Copy link
Member

@benaadams benaadams commented Apr 13, 2019

Provide a performance advantage to using the Microsoft.Net.Http.Headers.HeaderNames constants.

Change HeaderNames to static properties rather than string constants; so reference equality works; prior to falling back to an OrdinalIgnoreCase characters comparison. (string constants embed directly in the calling assembly so don't end up being reference equal)

Use these properties in Kestrel header methods rather than its own string constants so the reference equality works there.

ResponseHeaderCollectionBenchmark (Common is the main change +18%)

      Method |                 Type |        Mean |         Op/s | Allocated |
 ----------- |--------------------- |------------:|-------------:|----------:|
- SetHeaders |               Common |   889.92 ns |  1,123,698.8 |       0 B |
+ SetHeaders |               Common |   751.84 ns |  1,330,077.7 |       0 B |
- SetHeaders |  ContentLengthString |    51.64 ns | 19,363,867.1 |       0 B |
+ SetHeaders |  ContentLengthString |    51.90 ns | 19,268,524.6 |       0 B |
- SetHeaders |            Plaintext |    55.45 ns | 18,034,860.0 |       0 B |
+ SetHeaders |            Plaintext |    51.40 ns | 19,455,753.2 |       0 B |
- SetHeaders |              Unknown | 1,539.01 ns |    649,770.4 |       0 B |
+ SetHeaders |              Unknown | 1,589.23 ns |    629,237.4 |       0 B |

The inlined string property becomes a constant mov at Jit time e.g.

if (ReferenceEquals(HeaderNames.Host, key))

becomes

G_M13073_IG06:
       mov      rdx, 0x15B8E12EBA8        ; HeaderNames:get_Host():ref
       cmp      gword ptr [rdx], rdi      ; ReferenceEquals
       jne      SHORT G_M13073_IG07

For plaintext with the default builder (including host filtering)

Before
image

After
image


Background

Noticed this while looking at something else #9359

image

If you use the HeaderNames class they don't match with reference equality e.g.

context.Request.Headers[HeaderNames.Host]

as they are defined in a different assembly to the string constants used in Kestrel; so it moves to an OrdinalIgnoreCase compare. Using the same HeaderNames header names in Kestrel means it will first match using ReferenceEquals in coreclr

public bool Equals(string? value, StringComparison comparisonType)
{
    if (object.ReferenceEquals(this, value))
    {
        CheckStringComparison(comparisonType);
        return true;
    }

and won't need to fallback to the OrdinalIgnoreCase when HeaderNames is used e.g when setting

response.ContentType = "text/plain";

as DefaultHttpResponse also uses these constants

set
{
    if (string.IsNullOrEmpty(value))
    {
        HttpResponseFeature.Headers.Remove(HeaderNames.ContentType);
    }
    else
    {
        HttpResponseFeature.Headers[HeaderNames.ContentType] = value;
    }
}

@benaadams benaadams changed the title Allow headers to match on reference Equality before OrdinalIgnoreCase [WIP] Allow headers to match on reference Equality before OrdinalIgnoreCase Apr 13, 2019
@benaadams benaadams changed the title [WIP] Allow headers to match on reference Equality before OrdinalIgnoreCase Allow headers to match on reference Equality before OrdinalIgnoreCase Apr 13, 2019
@benaadams
Copy link
Member Author

ResponseHeaderCollectionBenchmark

      Method |                 Type |        Mean |         Op/s | Allocated |
 ----------- |--------------------- |------------:|-------------:|----------:|
- SetHeaders |               Common |   889.92 ns |  1,123,698.8 |       0 B |
+ SetHeaders |               Common |   751.84 ns |  1,330,077.7 |       0 B |
- SetHeaders |  ContentLengthString |    51.64 ns | 19,363,867.1 |       0 B |
+ SetHeaders |  ContentLengthString |    51.90 ns | 19,268,524.6 |       0 B |
- SetHeaders |            Plaintext |    55.45 ns | 18,034,860.0 |       0 B |
+ SetHeaders |            Plaintext |    51.40 ns | 19,455,753.2 |       0 B |
- SetHeaders |              Unknown | 1,539.01 ns |    649,770.4 |       0 B |
+ SetHeaders |              Unknown | 1,589.23 ns |    629,237.4 |       0 B |

@benaadams benaadams mentioned this pull request Apr 14, 2019
@benaadams benaadams force-pushed the reference-equal-headernames branch 3 times, most recently from cc8986d to bfdf578 Compare April 14, 2019 19:07
@@ -26,7 +27,7 @@ public Task Invoke(HttpContext context)
throw new InvalidOperationException("RequestId should be null here");
}

var requestId = context.Request.Headers["RequestId"];
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this was "RequestId" but HostingApplicationDiagnostics.cs uses "Request-Id"; now they are both using the same value via HeaderNames.RequestId (of "Request-Id")

@benaadams benaadams changed the title Allow headers to match on reference Equality before OrdinalIgnoreCase Allow headers to match on ReferenceEquals before OrdinalIgnoreCase Apr 14, 2019
@@ -118,74 +118,87 @@ public partial class EntityTagHeaderValue
}
public static partial class HeaderNames
{
public const string Accept = "Accept";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a breaking change? These can't be used in attributes anymore?

Copy link
Member

@Tratcher Tratcher Apr 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. Do we have any examples where it's relevant? I have not seen these used in attributes.

Copy link
Member Author

@benaadams benaadams Apr 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was one in http2 tests; though that was testing the headers

image

Wouldn't think it was common? e.g. everything in ASP.NET Core repos work and this was the only change.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suppose its a choice:

  • Fast header string matching (extremely common; used in every request, 1258 uses in ASP.NET Core itself)

  • Use in attributes (extremely uncommon, 2 test uses in ASP.NET Core itself and those testing http/2 protocol psudeo headers)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s not common no. The only other thing is that I’d like to eventually burn the headers assembly to the ground

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping it would involve duplication, which would get pretty confusing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To caveat the scope, its source code breaking if used in attributes or other string const initalization; otherwise isn't source code breaking.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I didn't see the whole change. I think this change is fine then. The few cases I found on the internet were not in attribute cases where a const is required. They were just regular imperative code. Carry on.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use these quite a bit in case statements for what it's worth...though I guess not anymore. Bleh.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry :-/

"Request-Id",
"Correlation-Context",
"TraceParent",
"TraceState"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I see you did come up with a few more non-HTTP/2 headers. Do these make sense to you @Tratcher?

Copy link
Member Author

@benaadams benaadams Apr 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are from hosting as it was checking each header that was the same length with IgnoreOrdinalCase for

if (!headers.TryGetValue(TraceParentHeaderName, out var correlationId))
{
    headers.TryGetValue(RequestIdHeaderName, out correlationId);

etc

}}
break;")}
{{{Each(byLength.OrderBy(h => !h.PrimaryHeader), header => $@"
if (ReferenceEquals(HeaderNames.{header.Identifier}, key))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is manually calling object.ReferenceEquals and then following up with string.Equals StringComparison.OrdinalIgnoreCase really faster than doing the string comparison up front? If so, shouldn't we just add an object.ReferenceEquals fast path to the start of string.Equals? Or at the very least add a helper method to Kestrel that does this?

Also, AFIACT the body of this if block is identical to the body of the string.Equals if block below. Wouldn't it be better to just add || HeaderNames.{header.Identifier}.Equals(key, StringComparison.OrdinalIgnoreCase) to this if condition even if it isn't part of some helper method?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string.Equals does have a ReferenceEquals at the start (though its uninlinable since its kinda chunky)

The two issues this resolves are

  1. Quick inlined reference check
  2. Check all the references first before checking IgnoreOrdinalCase

So if there were 3 headers of that length; and it was the 3rd header it would do

ReferenceEquals, IgnoreOrdinalCase, ReferenceEquals, IgnoreOrdinalCase, ReferenceEquals => match

With this approach it instead does

ReferenceEquals, ReferenceEquals, ReferenceEquals => match

Only if none of the ReferenceEquals match does it then start doing IgnoreOrdinalCase.

Noticed this issue after adding TraceParent and trying to workout why ContentMD5 was being tested IgnoreOrdinalCase.

return false;")}
}}")}
{Each(byLength.OrderBy(h => !h.PrimaryHeader), header => $@"
if (HeaderNames.{header.Identifier}.Equals(key, StringComparison.OrdinalIgnoreCase))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everywhere there are two if blocks in a row with identical bodies and one checking object.ReferenceEquals and the other checking string.Equals, I have the same questions I did for TryGetValueFast.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above. Checks all the ReferenceEquals first; then does OrdinalIgnoreCase; rather than interleaving the IgnoreCase checks

Copy link
Member

@Tratcher Tratcher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anurse @Eilon for breaking change awareness.

@benaadams
Copy link
Member Author

Build: Windows x64/x86 Job failed on a YARN error (transient)

@Eilon
Copy link
Member

Eilon commented Apr 16, 2019

I think @javiercn was looking at YARN reliability.

@benaadams
Copy link
Member Author

I think @javiercn was looking at YARN reliability.

Can you restart that job? 😉 I only have the power to restart everything, not an individual item.

@halter73
Copy link
Member

Can you restart that job? 😉 I only have the power to restart everything, not an individual item.

Generally I wait until all the builds are complete before triaging and restarting the failed build. I'm not sure if you can restart just the failed build before then. I'm setting a reminder to requeue once this is finished.

@javiercn
Copy link
Member

I think @javiercn was looking at YARN reliability.

Yeah, but this PR needs to be rebased on top of master. I just moved the yarn install for all projects to the beginning of the build and made them happen sequentially, that seems to have fixed the issue you are seeing.

@halter73
Copy link
Member

Yeah, but this PR needs to be rebased on top of master.

Doesn't our PR validation automatically rebase on the target branch?

@javiercn
Copy link
Member

Doesn't our PR validation automatically rebase on the target branch?

It doesn't AFAIK. It simply checks out the repo/branch and builds it, you still have to deal with merges/rebases.

@halter73
Copy link
Member

Looks good to go. I'll merge this tomorrow so I remember to make the breaking change announcement at the same time.

@analogrelay
Copy link
Contributor

It doesn't AFAIK. It simply checks out the repo/branch and builds it, you still have to deal with merges/rebases.

It doesn't rebase, but it does use the "merge ref" from GitHub (refs/pull/9341/merge for example) which contains the merge of this PR and master. Though, I don't know when GitHub updates this ref (do they do it for every master commit or just for every commit to the PR).

@analogrelay
Copy link
Contributor

At a minimum, it's safe to assume the build is incorporating all changes from the most recent commit to the PR branch and all changes that were in master at the time the most recent PR commit was made

@Eilon
Copy link
Member

Eilon commented Apr 17, 2019

Can probably close & re-open the PR to get the master ref to update and re-try?

@benaadams
Copy link
Member Author

@davidfowl merge conflicted me

@benaadams
Copy link
Member Author

merge conflict resolved

@halter73
Copy link
Member

aspnet/Announcements#356

@halter73 halter73 merged commit 8fcadf7 into dotnet:master Apr 18, 2019
@benaadams benaadams deleted the reference-equal-headernames branch April 18, 2019 19:01
@amcasey amcasey added area-middleware Includes: URL rewrite, redirect, response cache/compression, session, and other general middlesware area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions and removed area-runtime labels Jun 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-middleware Includes: URL rewrite, redirect, response cache/compression, session, and other general middlesware area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions breaking-change This issue / pr will introduce a breaking change, when resolved / merged.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet