Speed up WritableBuffer.WriteXxx (#1504)

* Speed up WritableBuffer.WriteAscii
* Add Benchmarks
* Non-standard header vals are still ascii
* Speedup WriteNumeric
* Don't advance for write
* Remove cruft
This commit is contained in:
Ben Adams 2017-03-18 19:25:10 +00:00 committed by David Fowler
parent f4c6e0b151
commit 5b814a55ac
4 changed files with 413 additions and 23 deletions

View File

@ -49,7 +49,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
output.Write(_CrLf);
output.WriteAscii(kv.Key);
output.Write(_colonSpace);
output.Write(value);
output.WriteAscii(value);
}
}
}

View File

@ -13,6 +13,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
public static class PipelineExtensions
{
private const int _maxULongByteLength = 20;
[ThreadStatic]
private static byte[] _numericBytesScratch;
public static ValueTask<ArraySegment<byte>> PeekAsync(this IPipeReader pipelineReader)
{
var input = pipelineReader.ReadAsync();
@ -90,18 +95,237 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
return result;
}
public static void WriteAscii(this WritableBuffer buffer, string data)
public unsafe static void WriteAscii(this WritableBuffer buffer, string data)
{
buffer.Write(Encoding.ASCII.GetBytes(data));
}
public static void Write(this WritableBuffer buffer, string data)
{
buffer.Write(Encoding.UTF8.GetBytes(data));
if (!string.IsNullOrEmpty(data))
{
if (buffer.Memory.IsEmpty)
{
buffer.Ensure();
}
// Fast path, try copying to the available memory directly
if (data.Length <= buffer.Memory.Length)
{
fixed (char* input = data)
fixed (byte* output = &buffer.Memory.Span.DangerousGetPinnableReference())
{
EncodeAsciiCharsToBytes(input, output, data.Length);
}
buffer.Advance(data.Length);
}
else
{
buffer.WriteAsciiMultiWrite(data);
}
}
}
public static void WriteNumeric(this WritableBuffer buffer, ulong number)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe static void WriteNumeric(this WritableBuffer buffer, ulong number)
{
buffer.Write(number.ToString());
const byte AsciiDigitStart = (byte)'0';
if (buffer.Memory.IsEmpty)
{
buffer.Ensure();
}
// Fast path, try copying to the available memory directly
var bytesLeftInBlock = buffer.Memory.Length;
var simpleWrite = true;
fixed (byte* output = &buffer.Memory.Span.DangerousGetPinnableReference())
{
var start = output;
if (number < 10 && bytesLeftInBlock >= 1)
{
*(start) = (byte)(((uint)number) + AsciiDigitStart);
buffer.Advance(1);
}
else if (number < 100 && bytesLeftInBlock >= 2)
{
var val = (uint)number;
var tens = (byte)((val * 205u) >> 11); // div10, valid to 1028
*(start) = (byte)(tens + AsciiDigitStart);
*(start + 1) = (byte)(val - (tens * 10) + AsciiDigitStart);
buffer.Advance(2);
}
else if (number < 1000 && bytesLeftInBlock >= 3)
{
var val = (uint)number;
var digit0 = (byte)((val * 41u) >> 12); // div100, valid to 1098
var digits01 = (byte)((val * 205u) >> 11); // div10, valid to 1028
*(start) = (byte)(digit0 + AsciiDigitStart);
*(start + 1) = (byte)(digits01 - (digit0 * 10) + AsciiDigitStart);
*(start + 2) = (byte)(val - (digits01 * 10) + AsciiDigitStart);
buffer.Advance(3);
}
else
{
simpleWrite = false;
}
}
if (!simpleWrite)
{
buffer.WriteNumericMultiWrite(number);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static unsafe void WriteNumericMultiWrite(this WritableBuffer buffer, ulong number)
{
const byte AsciiDigitStart = (byte)'0';
var value = number;
var position = _maxULongByteLength;
var byteBuffer = NumericBytesScratch;
do
{
// Consider using Math.DivRem() if available
var quotient = value / 10;
byteBuffer[--position] = (byte)(AsciiDigitStart + (value - quotient * 10)); // 0x30 = '0'
value = quotient;
}
while (value != 0);
var length = _maxULongByteLength - position;
buffer.Write(new ReadOnlySpan<byte>(byteBuffer, position, length));
}
[MethodImpl(MethodImplOptions.NoInlining)]
private unsafe static void WriteAsciiMultiWrite(this WritableBuffer buffer, string data)
{
var remaining = data.Length;
fixed (char* input = data)
{
var inputSlice = input;
while (remaining > 0)
{
var writable = Math.Min(remaining, buffer.Memory.Length);
buffer.Ensure(writable);
if (writable == 0)
{
continue;
}
fixed (byte* output = &buffer.Memory.Span.DangerousGetPinnableReference())
{
EncodeAsciiCharsToBytes(inputSlice, output, writable);
}
inputSlice += writable;
remaining -= writable;
buffer.Advance(writable);
}
}
}
private unsafe static void EncodeAsciiCharsToBytes(char* input, byte* output, int length)
{
// Note: Not BIGENDIAN or check for non-ascii
const int Shift16Shift24 = (1 << 16) | (1 << 24);
const int Shift8Identity = (1 << 8) | (1);
// Encode as bytes upto the first non-ASCII byte and return count encoded
int i = 0;
// Use Intrinsic switch
if (IntPtr.Size == 8) // 64 bit
{
if (length < 4) goto trailing;
int unaligned = (int)(((ulong)input) & 0x7) >> 1;
// Unaligned chars
for (; i < unaligned; i++)
{
char ch = *(input + i);
*(output + i) = (byte)ch; // Cast convert
}
// Aligned
int ulongDoubleCount = (length - i) & ~0x7;
for (; i < ulongDoubleCount; i += 8)
{
ulong inputUlong0 = *(ulong*)(input + i);
ulong inputUlong1 = *(ulong*)(input + i + 4);
// Pack 16 ASCII chars into 16 bytes
*(uint*)(output + i) =
((uint)((inputUlong0 * Shift16Shift24) >> 24) & 0xffff) |
((uint)((inputUlong0 * Shift8Identity) >> 24) & 0xffff0000);
*(uint*)(output + i + 4) =
((uint)((inputUlong1 * Shift16Shift24) >> 24) & 0xffff) |
((uint)((inputUlong1 * Shift8Identity) >> 24) & 0xffff0000);
}
if (length - 4 > i)
{
ulong inputUlong = *(ulong*)(input + i);
// Pack 8 ASCII chars into 8 bytes
*(uint*)(output + i) =
((uint)((inputUlong * Shift16Shift24) >> 24) & 0xffff) |
((uint)((inputUlong * Shift8Identity) >> 24) & 0xffff0000);
i += 4;
}
trailing:
for (; i < length; i++)
{
char ch = *(input + i);
*(output + i) = (byte)ch; // Cast convert
}
}
else // 32 bit
{
// Unaligned chars
if ((unchecked((int)input) & 0x2) != 0)
{
char ch = *input;
i = 1;
*(output) = (byte)ch; // Cast convert
}
// Aligned
int uintCount = (length - i) & ~0x3;
for (; i < uintCount; i += 4)
{
uint inputUint0 = *(uint*)(input + i);
uint inputUint1 = *(uint*)(input + i + 2);
// Pack 4 ASCII chars into 4 bytes
*(ushort*)(output + i) = (ushort)(inputUint0 | (inputUint0 >> 8));
*(ushort*)(output + i + 2) = (ushort)(inputUint1 | (inputUint1 >> 8));
}
if (length - 1 > i)
{
uint inputUint = *(uint*)(input + i);
// Pack 2 ASCII chars into 2 bytes
*(ushort*)(output + i) = (ushort)(inputUint | (inputUint >> 8));
i += 2;
}
if (i < length)
{
char ch = *(input + i);
*(output + i) = (byte)ch; // Cast convert
i = length;
}
}
}
private static byte[] NumericBytesScratch => _numericBytesScratch ?? CreateNumericBytesScratch();
[MethodImpl(MethodImplOptions.NoInlining)]
private static byte[] CreateNumericBytesScratch()
{
var bytes = new byte[_maxULongByteLength];
_numericBytesScratch = bytes;
return bytes;
}
}
}

View File

@ -3,16 +3,16 @@
using System.Runtime.CompilerServices;
using System.Text;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Testing;
using BenchmarkDotNet.Attributes;
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
{
[Config(typeof(CoreConfig))]
public class ResponseHeadersBenchmark
public class ResponseHeaderCollectionBenchmark
{
private const int InnerLoopCount = 512;
@ -21,27 +21,42 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
private FrameResponseHeaders _responseHeadersDirect;
private HttpResponse _response;
[Params("ContentLengthNumeric", "ContentLengthString", "Plaintext", "Common", "Unknown")]
public string Type { get; set; }
public enum BenchmarkTypes
{
ContentLengthNumeric,
ContentLengthString,
Plaintext,
Common,
Unknown
}
[Params(
BenchmarkTypes.ContentLengthNumeric,
BenchmarkTypes.ContentLengthString,
BenchmarkTypes.Plaintext,
BenchmarkTypes.Common,
BenchmarkTypes.Unknown
)]
public BenchmarkTypes Type { get; set; }
[Benchmark(OperationsPerInvoke = InnerLoopCount)]
public void SetHeaders()
{
switch (Type)
{
case "ContentLengthNumeric":
case BenchmarkTypes.ContentLengthNumeric:
ContentLengthNumeric(InnerLoopCount);
break;
case "ContentLengthString":
case BenchmarkTypes.ContentLengthString:
ContentLengthString(InnerLoopCount);
break;
case "Plaintext":
case BenchmarkTypes.Plaintext:
Plaintext(InnerLoopCount);
break;
case "Common":
case BenchmarkTypes.Common:
Common(InnerLoopCount);
break;
case "Unknown":
case BenchmarkTypes.Unknown:
Unknown(InnerLoopCount);
break;
}
@ -163,19 +178,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
switch (Type)
{
case "ContentLengthNumeric":
case BenchmarkTypes.ContentLengthNumeric:
ContentLengthNumeric(1);
break;
case "ContentLengthString":
case BenchmarkTypes.ContentLengthString:
ContentLengthString(1);
break;
case "Plaintext":
case BenchmarkTypes.Plaintext:
Plaintext(1);
break;
case "Common":
case BenchmarkTypes.Common:
Common(1);
break;
case "Unknown":
case BenchmarkTypes.Unknown:
Unknown(1);
break;
}

View File

@ -0,0 +1,151 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO.Pipelines;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing;
using BenchmarkDotNet.Attributes;
using Moq;
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
{
[Config(typeof(CoreConfig))]
public class ResponseHeadersWritingBenchmark
{
private static readonly byte[] _helloWorldPayload = Encoding.ASCII.GetBytes("Hello, World!");
private readonly TestFrame<object> _frame;
public ResponseHeadersWritingBenchmark()
{
_frame = MakeFrame();
}
public enum BenchmarkTypes
{
TechEmpowerPlaintext,
PlaintextChunked,
PlaintextWithCookie,
PlaintextChunkedWithCookie,
LiveAspNet
}
[Params(
BenchmarkTypes.TechEmpowerPlaintext,
BenchmarkTypes.PlaintextChunked,
BenchmarkTypes.PlaintextWithCookie,
BenchmarkTypes.PlaintextChunkedWithCookie,
BenchmarkTypes.LiveAspNet
)]
public BenchmarkTypes Type { get; set; }
[Benchmark]
public async Task Output()
{
_frame.Reset();
_frame.StatusCode = 200;
Task writeTask = Task.CompletedTask;
switch (Type)
{
case BenchmarkTypes.TechEmpowerPlaintext:
writeTask = TechEmpowerPlaintext();
break;
case BenchmarkTypes.PlaintextChunked:
writeTask = PlaintextChunked();
break;
case BenchmarkTypes.PlaintextWithCookie:
writeTask = PlaintextWithCookie();
break;
case BenchmarkTypes.PlaintextChunkedWithCookie:
writeTask = PlaintextChunkedWithCookie();
break;
case BenchmarkTypes.LiveAspNet:
writeTask = LiveAspNet();
break;
}
await writeTask;
await _frame.ProduceEndAsync();
}
private Task TechEmpowerPlaintext()
{
var responseHeaders = _frame.ResponseHeaders;
responseHeaders["Content-Type"] = "text/plain";
responseHeaders.ContentLength = _helloWorldPayload.Length;
return _frame.WriteAsync(new ArraySegment<byte>(_helloWorldPayload), default(CancellationToken));
}
private Task PlaintextChunked()
{
var responseHeaders = _frame.ResponseHeaders;
responseHeaders["Content-Type"] = "text/plain";
return _frame.WriteAsync(new ArraySegment<byte>(_helloWorldPayload), default(CancellationToken));
}
private Task LiveAspNet()
{
var responseHeaders = _frame.ResponseHeaders;
responseHeaders["Content-Encoding"] = "gzip";
responseHeaders["Content-Type"] = "text/html; charset=utf-8";
responseHeaders["Strict-Transport-Security"] = "max-age=31536000; includeSubdomains";
responseHeaders["Vary"] = "Accept-Encoding";
responseHeaders["X-Powered-By"] = "ASP.NET";
return _frame.WriteAsync(new ArraySegment<byte>(_helloWorldPayload), default(CancellationToken));
}
private Task PlaintextWithCookie()
{
var responseHeaders = _frame.ResponseHeaders;
responseHeaders["Content-Type"] = "text/plain";
responseHeaders["Set-Cookie"] = "prov=20629ccd-8b0f-e8ef-2935-cd26609fc0bc; __qca=P0-1591065732-1479167353442; _ga=GA1.2.1298898376.1479167354; _gat=1; sgt=id=9519gfde_3347_4762_8762_df51458c8ec2; acct=t=why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric&s=why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric";
responseHeaders.ContentLength = _helloWorldPayload.Length;
return _frame.WriteAsync(new ArraySegment<byte>(_helloWorldPayload), default(CancellationToken));
}
private Task PlaintextChunkedWithCookie()
{
var responseHeaders = _frame.ResponseHeaders;
responseHeaders["Content-Type"] = "text/plain";
responseHeaders["Set-Cookie"] = "prov=20629ccd-8b0f-e8ef-2935-cd26609fc0bc; __qca=P0-1591065732-1479167353442; _ga=GA1.2.1298898376.1479167354; _gat=1; sgt=id=9519gfde_3347_4762_8762_df51458c8ec2; acct=t=why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric&s=why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric";
return _frame.WriteAsync(new ArraySegment<byte>(_helloWorldPayload), default(CancellationToken));
}
private TestFrame<object> MakeFrame()
{
var socketInput = new PipeFactory().Create();
var serviceContext = new ServiceContext
{
DateHeaderValueManager = new DateHeaderValueManager(),
ServerOptions = new KestrelServerOptions(),
Log = Mock.Of<IKestrelTrace>()
};
var listenerContext = new ListenerContext(serviceContext)
{
ListenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 5000))
};
var connectionContext = new ConnectionContext(listenerContext)
{
Input = socketInput,
Output = new MockSocketOutput(),
ConnectionControl = Mock.Of<IConnectionControl>()
};
connectionContext.ListenerContext.ServiceContext.HttpParserFactory = f => new Internal.Http.KestrelHttpParser(log: null);
var frame = new TestFrame<object>(application: null, context: connectionContext);
frame.Reset();
frame.InitializeHeaders();
return frame;
}
}
}