From cb1917aa5990a701b92fc44f28f76b0718c76197 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Thu, 27 Dec 2018 02:00:39 +0000 Subject: [PATCH] Don't allocate in BeginChunkBytes (#5688) --- .../Core/src/Internal/Http/ChunkWriter.cs | 74 ++++++++++--------- .../Core/src/Internal/Http/HttpProtocol.cs | 10 +-- .../Kestrel/Core/test/ChunkWriterTests.cs | 33 ++++++--- .../ChunkWriterBenchmark.cs | 63 ++++++++++++++++ ...pNetCore.Server.Kestrel.Performance.csproj | 1 + 5 files changed, 126 insertions(+), 55 deletions(-) create mode 100644 src/Servers/Kestrel/perf/Kestrel.Performance/ChunkWriterBenchmark.cs diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/ChunkWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http/ChunkWriter.cs index 3d8cc4566b..7bb2a3781f 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/ChunkWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/ChunkWriter.cs @@ -5,59 +5,63 @@ using System; using System.Buffers; using System.IO.Pipelines; using System.Text; +using System.Runtime.CompilerServices; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { internal static class ChunkWriter { - private static readonly ArraySegment _endChunkBytes = CreateAsciiByteArraySegment("\r\n"); private static readonly byte[] _hex = Encoding.ASCII.GetBytes("0123456789abcdef"); - private static ArraySegment CreateAsciiByteArraySegment(string text) + public static int BeginChunkBytes(int dataCount, Span span) { - var bytes = Encoding.ASCII.GetBytes(text); - return new ArraySegment(bytes); - } - - public static ArraySegment BeginChunkBytes(int dataCount) - { - var bytes = new byte[10] - { - _hex[((dataCount >> 0x1c) & 0x0f)], - _hex[((dataCount >> 0x18) & 0x0f)], - _hex[((dataCount >> 0x14) & 0x0f)], - _hex[((dataCount >> 0x10) & 0x0f)], - _hex[((dataCount >> 0x0c) & 0x0f)], - _hex[((dataCount >> 0x08) & 0x0f)], - _hex[((dataCount >> 0x04) & 0x0f)], - _hex[((dataCount >> 0x00) & 0x0f)], - (byte)'\r', - (byte)'\n', - }; - // Determine the most-significant non-zero nibble int total, shift; - total = (dataCount > 0xffff) ? 0x10 : 0x00; - dataCount >>= total; - shift = (dataCount > 0x00ff) ? 0x08 : 0x00; - dataCount >>= shift; + var count = dataCount; + total = (count > 0xffff) ? 0x10 : 0x00; + count >>= total; + shift = (count > 0x00ff) ? 0x08 : 0x00; + count >>= shift; total |= shift; - total |= (dataCount > 0x000f) ? 0x04 : 0x00; + total |= (count > 0x000f) ? 0x04 : 0x00; - var offset = 7 - (total >> 2); - return new ArraySegment(bytes, offset, 10 - offset); + count = (total >> 2) + 3; + + var offset = 0; + ref var startHex = ref _hex[0]; + + for (shift = total; shift >= 0; shift -= 4) + { + // Using Unsafe.Add to elide the bounds check on _hex as the & 0x0f definately + // constrains it to the range 0x0 - 0xf, matching the bounds of the array + span[offset] = Unsafe.Add(ref startHex, ((dataCount >> shift) & 0x0f)); + offset++; + } + + span[count - 2] = (byte)'\r'; + span[count - 1] = (byte)'\n'; + + return count; } - internal static int WriteBeginChunkBytes(ref BufferWriter start, int dataCount) + internal static void WriteBeginChunkBytes(this ref BufferWriter start, int dataCount) { - var chunkSegment = BeginChunkBytes(dataCount); - start.Write(new ReadOnlySpan(chunkSegment.Array, chunkSegment.Offset, chunkSegment.Count)); - return chunkSegment.Count; + // 10 bytes is max length + \r\n + start.Ensure(10); + + var count = BeginChunkBytes(dataCount, start.Span); + start.Advance(count); } - internal static void WriteEndChunkBytes(ref BufferWriter start) + internal static void WriteEndChunkBytes(this ref BufferWriter start) { - start.Write(new ReadOnlySpan(_endChunkBytes.Array, _endChunkBytes.Offset, _endChunkBytes.Count)); + start.Ensure(2); + var span = start.Span; + + // CRLF done in reverse order so the 1st index will elide the bounds check for the 0th index + span[1] = (byte)'\n'; + span[0] = (byte)'\r'; + start.Advance(2); } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index 72bedf1be6..773e5cc112 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -927,9 +927,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { var writer = new BufferWriter(writableBuffer); - ChunkWriter.WriteBeginChunkBytes(ref writer, buffer.Length); + writer.WriteBeginChunkBytes(buffer.Length); writer.Write(buffer.Span); - ChunkWriter.WriteEndChunkBytes(ref writer); + writer.WriteEndChunkBytes(); writer.Commit(); bytesWritten = writer.BytesCommitted; @@ -938,12 +938,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http return bytesWritten; } - private static ArraySegment CreateAsciiByteArraySegment(string text) - { - var bytes = Encoding.ASCII.GetBytes(text); - return new ArraySegment(bytes); - } - public void ProduceContinue() { if (HasResponseStarted) diff --git a/src/Servers/Kestrel/Core/test/ChunkWriterTests.cs b/src/Servers/Kestrel/Core/test/ChunkWriterTests.cs index 722c0281ab..2b6f3a3c65 100644 --- a/src/Servers/Kestrel/Core/test/ChunkWriterTests.cs +++ b/src/Servers/Kestrel/Core/test/ChunkWriterTests.cs @@ -1,7 +1,7 @@ // 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.Linq; +using System; using System.Text; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Xunit; @@ -11,28 +11,37 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests public class ChunkWriterTests { [Theory] - [InlineData(1, "1\r\n")] - [InlineData(10, "a\r\n")] + [InlineData(0x00, "0\r\n")] + [InlineData(0x01, "1\r\n")] [InlineData(0x08, "8\r\n")] - [InlineData(0x10, "10\r\n")] + [InlineData(0x0a, "a\r\n")] + [InlineData(0x0f, "f\r\n")] + [InlineData(0x010, "10\r\n")] [InlineData(0x080, "80\r\n")] - [InlineData(0x100, "100\r\n")] + [InlineData(0x0ff, "ff\r\n")] + [InlineData(0x0100, "100\r\n")] [InlineData(0x0800, "800\r\n")] - [InlineData(0x1000, "1000\r\n")] + [InlineData(0x0fff, "fff\r\n")] + [InlineData(0x01000, "1000\r\n")] [InlineData(0x08000, "8000\r\n")] - [InlineData(0x10000, "10000\r\n")] + [InlineData(0x0ffff, "ffff\r\n")] + [InlineData(0x010000, "10000\r\n")] [InlineData(0x080000, "80000\r\n")] - [InlineData(0x100000, "100000\r\n")] + [InlineData(0x0fffff, "fffff\r\n")] + [InlineData(0x0100000, "100000\r\n")] [InlineData(0x0800000, "800000\r\n")] - [InlineData(0x1000000, "1000000\r\n")] + [InlineData(0x0ffffff, "ffffff\r\n")] + [InlineData(0x01000000, "1000000\r\n")] [InlineData(0x08000000, "8000000\r\n")] - [InlineData(0x10000000, "10000000\r\n")] + [InlineData(0x0fffffff, "fffffff\r\n")] + [InlineData(0x010000000, "10000000\r\n")] [InlineData(0x7fffffffL, "7fffffff\r\n")] public void ChunkedPrefixMustBeHexCrLfWithoutLeadingZeros(int dataCount, string expected) { - var beginChunkBytes = ChunkWriter.BeginChunkBytes(dataCount); + Span span = new byte[10]; + var count = ChunkWriter.BeginChunkBytes(dataCount, span); - Assert.Equal(Encoding.ASCII.GetBytes(expected), beginChunkBytes.ToArray()); + Assert.Equal(Encoding.ASCII.GetBytes(expected), span.Slice(0, count).ToArray()); } } } diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/ChunkWriterBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/ChunkWriterBenchmark.cs new file mode 100644 index 0000000000..3b24e49cab --- /dev/null +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/ChunkWriterBenchmark.cs @@ -0,0 +1,63 @@ +// 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.Buffers; +using System.IO.Pipelines; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; + +namespace Microsoft.AspNetCore.Server.Kestrel.Performance +{ + public class ChunkWriterBenchmark + { + private const int InnerLoopCount = 1024; + + private PipeReader _reader; + private PipeWriter _writer; + private MemoryPool _memoryPool; + + [GlobalSetup] + public void Setup() + { + _memoryPool = KestrelMemoryPool.Create(); + var pipe = new Pipe(new PipeOptions(_memoryPool)); + _reader = pipe.Reader; + _writer = pipe.Writer; + } + + [Params(0x0, 0x1, 0x10, 0x100, 0x1_000, 0x10_000, 0x100_000, 0x1_000_000)] + public int DataLength { get; set; } + + [Benchmark(OperationsPerInvoke = InnerLoopCount)] + public async Task WriteBeginChunkBytes() + { + WriteBeginChunkBytes_Write(); + + var flushResult = _writer.FlushAsync(); + + var result = await _reader.ReadAsync(); + _reader.AdvanceTo(result.Buffer.End, result.Buffer.End); + await flushResult; + } + + private void WriteBeginChunkBytes_Write() + { + var writer = new BufferWriter(_writer); + var dataLength = DataLength; + for (int i = 0; i < InnerLoopCount; i++) + { + ChunkWriter.WriteBeginChunkBytes(ref writer, dataLength); + } + + writer.Commit(); + } + + [GlobalCleanup] + public void Cleanup() + { + _memoryPool.Dispose(); + } + } +} diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Microsoft.AspNetCore.Server.Kestrel.Performance.csproj b/src/Servers/Kestrel/perf/Kestrel.Performance/Microsoft.AspNetCore.Server.Kestrel.Performance.csproj index 09655f7ca8..0015869a26 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Microsoft.AspNetCore.Server.Kestrel.Performance.csproj +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Microsoft.AspNetCore.Server.Kestrel.Performance.csproj @@ -6,6 +6,7 @@ true true false + false