Skip to content

Commit cb1917a

Browse files
benaadamspakrym
authored andcommitted
Don't allocate in BeginChunkBytes (#5688)
1 parent 0eab464 commit cb1917a

File tree

5 files changed

+126
-55
lines changed

5 files changed

+126
-55
lines changed

src/Servers/Kestrel/Core/src/Internal/Http/ChunkWriter.cs

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,59 +5,63 @@
55
using System.Buffers;
66
using System.IO.Pipelines;
77
using System.Text;
8+
using System.Runtime.CompilerServices;
89

910
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
1011
{
1112
internal static class ChunkWriter
1213
{
13-
private static readonly ArraySegment<byte> _endChunkBytes = CreateAsciiByteArraySegment("\r\n");
1414
private static readonly byte[] _hex = Encoding.ASCII.GetBytes("0123456789abcdef");
1515

16-
private static ArraySegment<byte> CreateAsciiByteArraySegment(string text)
16+
public static int BeginChunkBytes(int dataCount, Span<byte> span)
1717
{
18-
var bytes = Encoding.ASCII.GetBytes(text);
19-
return new ArraySegment<byte>(bytes);
20-
}
21-
22-
public static ArraySegment<byte> BeginChunkBytes(int dataCount)
23-
{
24-
var bytes = new byte[10]
25-
{
26-
_hex[((dataCount >> 0x1c) & 0x0f)],
27-
_hex[((dataCount >> 0x18) & 0x0f)],
28-
_hex[((dataCount >> 0x14) & 0x0f)],
29-
_hex[((dataCount >> 0x10) & 0x0f)],
30-
_hex[((dataCount >> 0x0c) & 0x0f)],
31-
_hex[((dataCount >> 0x08) & 0x0f)],
32-
_hex[((dataCount >> 0x04) & 0x0f)],
33-
_hex[((dataCount >> 0x00) & 0x0f)],
34-
(byte)'\r',
35-
(byte)'\n',
36-
};
37-
3818
// Determine the most-significant non-zero nibble
3919
int total, shift;
40-
total = (dataCount > 0xffff) ? 0x10 : 0x00;
41-
dataCount >>= total;
42-
shift = (dataCount > 0x00ff) ? 0x08 : 0x00;
43-
dataCount >>= shift;
20+
var count = dataCount;
21+
total = (count > 0xffff) ? 0x10 : 0x00;
22+
count >>= total;
23+
shift = (count > 0x00ff) ? 0x08 : 0x00;
24+
count >>= shift;
4425
total |= shift;
45-
total |= (dataCount > 0x000f) ? 0x04 : 0x00;
26+
total |= (count > 0x000f) ? 0x04 : 0x00;
27+
28+
count = (total >> 2) + 3;
4629

47-
var offset = 7 - (total >> 2);
48-
return new ArraySegment<byte>(bytes, offset, 10 - offset);
30+
var offset = 0;
31+
ref var startHex = ref _hex[0];
32+
33+
for (shift = total; shift >= 0; shift -= 4)
34+
{
35+
// Using Unsafe.Add to elide the bounds check on _hex as the & 0x0f definately
36+
// constrains it to the range 0x0 - 0xf, matching the bounds of the array
37+
span[offset] = Unsafe.Add(ref startHex, ((dataCount >> shift) & 0x0f));
38+
offset++;
39+
}
40+
41+
span[count - 2] = (byte)'\r';
42+
span[count - 1] = (byte)'\n';
43+
44+
return count;
4945
}
5046

51-
internal static int WriteBeginChunkBytes(ref BufferWriter<PipeWriter> start, int dataCount)
47+
internal static void WriteBeginChunkBytes(this ref BufferWriter<PipeWriter> start, int dataCount)
5248
{
53-
var chunkSegment = BeginChunkBytes(dataCount);
54-
start.Write(new ReadOnlySpan<byte>(chunkSegment.Array, chunkSegment.Offset, chunkSegment.Count));
55-
return chunkSegment.Count;
49+
// 10 bytes is max length + \r\n
50+
start.Ensure(10);
51+
52+
var count = BeginChunkBytes(dataCount, start.Span);
53+
start.Advance(count);
5654
}
5755

58-
internal static void WriteEndChunkBytes(ref BufferWriter<PipeWriter> start)
56+
internal static void WriteEndChunkBytes(this ref BufferWriter<PipeWriter> start)
5957
{
60-
start.Write(new ReadOnlySpan<byte>(_endChunkBytes.Array, _endChunkBytes.Offset, _endChunkBytes.Count));
58+
start.Ensure(2);
59+
var span = start.Span;
60+
61+
// CRLF done in reverse order so the 1st index will elide the bounds check for the 0th index
62+
span[1] = (byte)'\n';
63+
span[0] = (byte)'\r';
64+
start.Advance(2);
6165
}
6266
}
6367
}

src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -927,9 +927,9 @@ private static long WriteChunk(PipeWriter writableBuffer, ReadOnlyMemory<byte> b
927927
{
928928
var writer = new BufferWriter<PipeWriter>(writableBuffer);
929929

930-
ChunkWriter.WriteBeginChunkBytes(ref writer, buffer.Length);
930+
writer.WriteBeginChunkBytes(buffer.Length);
931931
writer.Write(buffer.Span);
932-
ChunkWriter.WriteEndChunkBytes(ref writer);
932+
writer.WriteEndChunkBytes();
933933
writer.Commit();
934934

935935
bytesWritten = writer.BytesCommitted;
@@ -938,12 +938,6 @@ private static long WriteChunk(PipeWriter writableBuffer, ReadOnlyMemory<byte> b
938938
return bytesWritten;
939939
}
940940

941-
private static ArraySegment<byte> CreateAsciiByteArraySegment(string text)
942-
{
943-
var bytes = Encoding.ASCII.GetBytes(text);
944-
return new ArraySegment<byte>(bytes);
945-
}
946-
947941
public void ProduceContinue()
948942
{
949943
if (HasResponseStarted)
Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
using System.Linq;
4+
using System;
55
using System.Text;
66
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
77
using Xunit;
@@ -11,28 +11,37 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
1111
public class ChunkWriterTests
1212
{
1313
[Theory]
14-
[InlineData(1, "1\r\n")]
15-
[InlineData(10, "a\r\n")]
14+
[InlineData(0x00, "0\r\n")]
15+
[InlineData(0x01, "1\r\n")]
1616
[InlineData(0x08, "8\r\n")]
17-
[InlineData(0x10, "10\r\n")]
17+
[InlineData(0x0a, "a\r\n")]
18+
[InlineData(0x0f, "f\r\n")]
19+
[InlineData(0x010, "10\r\n")]
1820
[InlineData(0x080, "80\r\n")]
19-
[InlineData(0x100, "100\r\n")]
21+
[InlineData(0x0ff, "ff\r\n")]
22+
[InlineData(0x0100, "100\r\n")]
2023
[InlineData(0x0800, "800\r\n")]
21-
[InlineData(0x1000, "1000\r\n")]
24+
[InlineData(0x0fff, "fff\r\n")]
25+
[InlineData(0x01000, "1000\r\n")]
2226
[InlineData(0x08000, "8000\r\n")]
23-
[InlineData(0x10000, "10000\r\n")]
27+
[InlineData(0x0ffff, "ffff\r\n")]
28+
[InlineData(0x010000, "10000\r\n")]
2429
[InlineData(0x080000, "80000\r\n")]
25-
[InlineData(0x100000, "100000\r\n")]
30+
[InlineData(0x0fffff, "fffff\r\n")]
31+
[InlineData(0x0100000, "100000\r\n")]
2632
[InlineData(0x0800000, "800000\r\n")]
27-
[InlineData(0x1000000, "1000000\r\n")]
33+
[InlineData(0x0ffffff, "ffffff\r\n")]
34+
[InlineData(0x01000000, "1000000\r\n")]
2835
[InlineData(0x08000000, "8000000\r\n")]
29-
[InlineData(0x10000000, "10000000\r\n")]
36+
[InlineData(0x0fffffff, "fffffff\r\n")]
37+
[InlineData(0x010000000, "10000000\r\n")]
3038
[InlineData(0x7fffffffL, "7fffffff\r\n")]
3139
public void ChunkedPrefixMustBeHexCrLfWithoutLeadingZeros(int dataCount, string expected)
3240
{
33-
var beginChunkBytes = ChunkWriter.BeginChunkBytes(dataCount);
41+
Span<byte> span = new byte[10];
42+
var count = ChunkWriter.BeginChunkBytes(dataCount, span);
3443

35-
Assert.Equal(Encoding.ASCII.GetBytes(expected), beginChunkBytes.ToArray());
44+
Assert.Equal(Encoding.ASCII.GetBytes(expected), span.Slice(0, count).ToArray());
3645
}
3746
}
3847
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Buffers;
5+
using System.IO.Pipelines;
6+
using System.Threading.Tasks;
7+
using BenchmarkDotNet.Attributes;
8+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
9+
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
10+
11+
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
12+
{
13+
public class ChunkWriterBenchmark
14+
{
15+
private const int InnerLoopCount = 1024;
16+
17+
private PipeReader _reader;
18+
private PipeWriter _writer;
19+
private MemoryPool<byte> _memoryPool;
20+
21+
[GlobalSetup]
22+
public void Setup()
23+
{
24+
_memoryPool = KestrelMemoryPool.Create();
25+
var pipe = new Pipe(new PipeOptions(_memoryPool));
26+
_reader = pipe.Reader;
27+
_writer = pipe.Writer;
28+
}
29+
30+
[Params(0x0, 0x1, 0x10, 0x100, 0x1_000, 0x10_000, 0x100_000, 0x1_000_000)]
31+
public int DataLength { get; set; }
32+
33+
[Benchmark(OperationsPerInvoke = InnerLoopCount)]
34+
public async Task WriteBeginChunkBytes()
35+
{
36+
WriteBeginChunkBytes_Write();
37+
38+
var flushResult = _writer.FlushAsync();
39+
40+
var result = await _reader.ReadAsync();
41+
_reader.AdvanceTo(result.Buffer.End, result.Buffer.End);
42+
await flushResult;
43+
}
44+
45+
private void WriteBeginChunkBytes_Write()
46+
{
47+
var writer = new BufferWriter<PipeWriter>(_writer);
48+
var dataLength = DataLength;
49+
for (int i = 0; i < InnerLoopCount; i++)
50+
{
51+
ChunkWriter.WriteBeginChunkBytes(ref writer, dataLength);
52+
}
53+
54+
writer.Commit();
55+
}
56+
57+
[GlobalCleanup]
58+
public void Cleanup()
59+
{
60+
_memoryPool.Dispose();
61+
}
62+
}
63+
}

src/Servers/Kestrel/perf/Kestrel.Performance/Microsoft.AspNetCore.Server.Kestrel.Performance.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<ServerGarbageCollection>true</ServerGarbageCollection>
77
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
88
<IsPackable>false</IsPackable>
9+
<TieredCompilation>false</TieredCompilation>
910
</PropertyGroup>
1011

1112
<ItemGroup>

0 commit comments

Comments
 (0)