Quantcast
Channel: Andrew Lock | .NET Escapades
Viewing all articles
Browse latest Browse all 743

Using default interface methods for performance gains in IHeaderDictionary

$
0
0

In this post I show an example of how the C# 8 feature, default interface methods, can be used to improve performance. I describe a pull request to ASP.NET Core that shows how default interface methods can also be a way to improve performance, by giving a way for implementations to optimise specific usages of the interface.

Default interface methods

In my previous post I described how default interface methods can be used to allow you to evolve and interface by adding members without breaking consumers of the interface. The feature requires that you provide a method body for your implementation, which implementers can override, but which is used when no alternative is provided.

For example, take the following simple interface:

public interface IShape
{
    bool NumberOfSides { get; }
}

and you want to add a new method to it, GetName():

public interface IShape
{
    bool NumberOfSides { get; }
    string GetName(); // 👈 New method
}

Unfortunately, adding this method is a breaking change, which would require a new major version of your library and would put the onus on consumers to implement the new method.

If you instead make the method a default interface method, you can avoid the breaking change entirely, leaving consumers to opt-in to overriding the method at a later point if they wish:

public interface IShape
{
    bool NumberOfSides { get; }
    string GetName() => "IShape"; // 👈 Default implementation
}

Implementers can choose to override the method if they wish, but they don't have to; the code will still compile:

public class Rectangle : IShape { } // No override
public class Square : IShape
{
    string GetName() => "Square"; // Override the implementation
}

Consumers of IShape need to ensure they cast implementations to IShape before they can access the default method:

IShape square = new Square(); // Implcit cast to IShape
Console.WriteLine(square.GetName()); // Prints "Square"

var rectangle = (IShape)new Rectangle(); // Explicit cast to IShape
Console.WriteLine(rectangle.GetName()); // Prints "IShape"

Rectangle error = new Rectangle(); // No cast...
Console.WriteLine(error.GetName()); // ...won't compile because it needs to be cast to IShape

There are a number of caveats and complexities when working with default interface methods, so if you haven't already, I suggest reading my previous post, which describes some of the sharp-edges of the feature.

Now we have a basic understanding of default interface methods, we're going to look at a different potential use for them: improving performance in ASP.NET Core.

Using default interface methods to improve performance

As already described, the driving factor for default interface methods was to allow library authors to evolve interfaces without breaking changes.

Default interface members also allow for easier interoperability with similar features that exist in Android and iOS. They also offer some of the characteristics of traits which are an approach for building systems that focuses on composability over inheritance.

Even though extensibility is one of the main benefits, in response to my previous post about StringValues, Ben Adams pointed me to a PR that uses default interface methods for performance reasons:

I'm not going to dig into all the details of that PR here, instead I give an overview of the changes and how they managed to make getting and setting headers in IHeaderDictionary 3× faster in .NET 6! 🤯

Introducing the IHeaderDictionary interface

In a previous post I described how the StringValues can be used to represent zero, one, or more strings. In that post I described a hypothetical header "dictionary" that looked something like this:

public class HeaderDictionary : Dictionary<string, StringValues> { }

Well, funnily enough, that's almost exactly what we have, but in interface form:

public interface IHeaderDictionary : IDictionary<string, StringValues>
{
    /// <summary>
    /// IHeaderDictionary has a different indexer contract than IDictionary, where it will return StringValues.Empty for missing entries.
    /// </summary>
    /// <param name="key"></param>
    /// <returns>The stored value, or StringValues.Empty if the key is not present.</returns>
    new StringValues this[string key] { get; set; }

    /// <summary>
    /// Strongly typed access to the Content-Length header. Implementations must keep this in sync with the string representation.
    /// </summary>
    long? ContentLength { get; set; }
}

This header dictionary is exposed on both HttpRequest and HttpResponse. Prior to the PR in question, to fetch a header, you would call the indexer with the name of the header. For example:

WebApplicationBuilder builder = WebApplication.CreateBuilder();
WebApplication app = builder.Build();

// return the Content-Type header:
app.MapGet("/", (HttpContext context) => 
    context.Request.Headers[HeaderNames.ContentType]);

app.Run();

Given IHeaderDictionary is almost identical to a standard Dictionary<>, how is it implemented in the framework?

Optimising IHeaderDictionary implementations

You could almost implement IHeaderDictionary directly as a Dictionary<string, StringValues>, but the performance for that would be terrible. It would likely require ASP.NET Core to deserialize all of the headers sent in a request (or being sent in a response) regardless of whether they were actually accessed, which would create a lot of allocations.

Consequently, the IHeaderDictionary implementations are heavily optimised. There are two important features we're interested in:

  1. The header values are stored in a named field that can be directly accessed
  2. The code to figure out which header to return when the string name is provided is heavily optimised.

Thinking about point 1, the fields are defined as you might expect:

internal partial class HttpRequestHeaders
{
    private HeaderReferences _headers;

    private struct HeaderReferences
    {
        public StringValues _CacheControl;
        public StringValues _Connection;
        public StringValues _Date;
        public StringValues _GrpcEncoding;
        public StringValues _KeepAlive;
        public StringValues _Pragma;
        // ... other known headers
    }
}

The real complexity occurs when you try to request one of these headers using code like context.Request.Headers[HeaderNames.ContentType]. The method that does this mapping is TryGetValueFast(), but if you look at the code, you might be surprised by the complexity:

internal partial class HttpRequestHeaders
{
    protected override bool TryGetValueFast(string key, out StringValues value)
      {
          value = default;
          switch (key.Length)
          {
              case 2:
              {
                  if (ReferenceEquals(HeaderNames.TE, key))
                  {
                      if ((_bits & 0x100000000000L) != 0)
                      {
                          value = _headers._TE;
                          return true;
                      }
                      return false;
                  }

                  if (HeaderNames.TE.Equals(key, StringComparison.OrdinalIgnoreCase))
                  {
                      if ((_bits & 0x100000000000L) != 0)
                      {
                          value = _headers._TE;
                          return true;
                      }
                      return false;
                  }
                  break;
              }
              case 3:
              {
                  // ... and on and on for 1200 more lines!

This code is auto-generated, and is designed to try to make the lookup for a given header as fast as possible. It first checks the length of the header name key, and uses that to quickly switch between possible headers. It then does a ReferenceEquals for the key, which will be true if the string is the exact header, and finally does a case-insensitive comparison. If these checks pass, it returns the header from the field.

This lookup aims to be as efficient as possible, using code generation and bit twiddling to make this hot-path code as performant as it can be, but the reality is there is still a lot of code here that has to run with every lookup.

And that's where the PR we're interested in comes in.

Improving the API with default interface methods

Ben's PR made a lot of changes, as it both introduced changes to the IHeaderDictionary and put them to use throughout the code base. But the crucial change was the addition of a whole host of default interface implementations:

public partial interface IHeaderDictionary
{
    /// <summary>Gets or sets the <c>Accept</c> HTTP header.</summary>
    StringValues Accept { get => this[HeaderNames.Accept]; set => this[HeaderNames.Accept] = value; }

    /// <summary>Gets or sets the <c>Accept-Charset</c> HTTP header.</summary>
    StringValues AcceptCharset { get => this[HeaderNames.AcceptCharset]; set => this[HeaderNames.AcceptCharset] = value; }

    /// <summary>Gets or sets the <c>Accept-Encoding</c> HTTP header.</summary>
    StringValues AcceptEncoding { get => this[HeaderNames.AcceptEncoding]; set => this[HeaderNames.AcceptEncoding] = value; }

    /// <summary>Gets or sets the <c>Accept-Language</c> HTTP header.</summary>
    StringValues AcceptLanguage { get => this[HeaderNames.AcceptLanguage]; set => this[HeaderNames.AcceptLanguage] = value; }

    // ... Many, many more! 
}

Each of these implementations simply defers to the standard indexer version to retrieve a header. That means that by default, both of the following endpoint definitions are identical:

app.MapGet("/indexer", (HttpContext context) => 
    context.Request.Headers[HeaderNames.ContentType]); // Using the "old" way, with the accessor

app.MapGet("/property", (HttpContext context) => 
    context.Request.Headers.ContentType); // Using the new default interface property

Regardless of other changes this is a nice improvement to the API in a lot of ways, as it ensures you only request "known" headers by default, and removes the risk of any typos in string header names.

In case you're wondering, yes this file is also auto-generated!

Ben updated all the usages of the IHeaderDictionary indexer to use the new APIs, and I think you can agree it looks much cleaner:

-   httpContext.Response.Headers[HeaderNames.XFrameOptions] = "SAMEORIGIN";
+   httpContext.Response.Headers.XFrameOptions = "SAMEORIGIN";

However, the real reason for this addition was the potential performance improvements…

Improving performance with default interface methods

Ben's real reason for introducing the default interface members, as it meant he could provide specific implementations in the IHeaderDictionary implementations. The benefit of this is that it completely bypasses the complex TryGetValue lookup for all the implemented headers:

internal partial class HttpRequestHeaders : IHeaderDictionary
{
    StringValues IHeaderDictionary.Accept
    {
        get
        {
            var value = _headers._Accept;
            if ((_bits & 0x1000000L) != 0)
            {
                return value;
            }
            return default;
        }
        set
        {
            if (_isReadOnly) { ThrowHeadersReadOnlyException(); }

            var flag = 0x1000000L;
            if (value.Count > 0)
            {
                _bits |= flag;
                _headers._Accept = value;
            }
            else
            {
                _bits &= ~flag;
                _headers._Accept = default;
            }
        }
    }

    // ... and so on for all implemented headers
}

As you can see in the above code, the property goes directly to the header field and returns it if it's available, and similarly for the setter. No need for elaborate switch statements, just direct access!

Of course, the code to access via the string header name is still there, so you can still access the methods by using the string name with the indexer, you just shouldn't if possible! 🙂

If you're wondering how much improvement this gives, the answer is "it depends". If we consider just "implemented" methods (that is, headers which are expected in a request such as Content-Type or Connection) then we see and impressive 3× speedup for the getter, and an incredible 6× speedup for the setter!

MethodBranchMeanOp/sDelta
GetHeadersmain121.355 ns8,240,299.3-
GetHeadersPR37.598 ns26,597,474.6+222.8%
SetHeadersmain635.060 ns1,574,654.3-
SetHeadersPR108.041 ns9,255,723.7+487.7%

Handling un-implemented headers

One of the slight downsides to the PR is that it provides concrete APIs for headers that don't necessarily make sense, as described in this comment. For example, the "Content-Security-Policy" header is a response header only, so it's implemented in HttpResponseHeaders but not in HttpRequestHeaders.

Nevertheless, even though it doesn't make semantic sense to send "Content-Security-Policy" in a request, it's still valid HTTP. Similarly, it's perfectly legal to retrieve the header from the HttpRequestHeaders object, as it might be there.

Consequently, the IHeaderCollection implementations do maintain a Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase) of headers that were sent/received but were not "expected". Reading from this dictionary is always the final step to take if a string header name isn't matched in the IHeaderDictionary indexer.

The "problem" with the new API, is that the same IHeaderDictionary interface is used for both request and response headers. And consequently the default interface methods apply to both cases. That means there is explicitly a ContentSecurityPolicy policy on the request headers, even though that doesn't make sense:

// This is legal, but it doesn't really make sense
// which isn't apparent in the API
var csp = httpContext.Request.Headers.ContentSecurityPolicy;

// You could still do this with the "old" way of course
csp = httpContext.Request.Headers[HeaderNames.ContentSecurityPolicy];

The good news is that even though they're "unexpected", the PR still improves the performance of getting and setting these headers compared to using the indexer! In the previous implementation, you would have to go all the way through the TryGetValueFast() method before finally falling out the bottom and calling TryGetUnknown().

internal partial class HttpRequestHeaders : IHeaderDictionary
{
    StringValues IHeaderDictionary.ContentSecurityPolicy
    {
        get
        {
            StringValues value = default;
            // Go directly to the dictionary lookup
            if (!TryGetUnknown(HeaderNames.ContentSecurityPolicy, ref value))
            {
                value = default;
            }
            return value;
        }
        set
        {
            if (_isReadOnly) { ThrowHeadersReadOnlyException(); }

            SetValueUnknown(HeaderNames.ContentSecurityPolicy, value);
        }
    }
    // ...
}

After the PR, the APIs for these "Unknown" headers can go directly to the TryGetUnknown call, bypassing the giant switch statement (which was always a worst-case lookup at runtime). Looking at how that impacts performance, the relative performance boost isn't as noticeable as for the known headers (because you still have to call the slow TryGetUnknown method), but the absolute speed up is huge:

MethodBranchMeanOp/sDelta
GetHeadersmain366.456 ns2,728,840.7-
GetHeadersPR223.472 ns4,474,824.0+64.0%
SetHeadersmain1,439.945 ns694,470.8-
SetHeadersPR517.067 ns1,933,985.7+178.4%

There are some other interesting changes in that PR, but we've covered the bulk of them. Given that default interface methods are typically touted as allowing extensibility of interfaces, it's interesting to see them being used primarily for performance reasons in this PR, so thanks again to Ben Adams for all his work (and for pointing me to it)!

Summary

In this post I described how Ben Adams used default interface members to improve the speed of accessing headers in ASP.NET Core in .NET 6 by 3×. I described the main changes he made, how they improved the API exposed on IHeaderDictionary, and why this allowed for performance improvements. Finally, I discussed the improvements seen both for implemented headers and unimplemented headers.


Viewing all articles
Browse latest Browse all 743

Trending Articles