Skip to content

Commit 4b5b897

Browse files
authored
Perf improvements (#5245)
Should bring Npgsql back to or better than pre #5123 performance numbers
1 parent 5c10602 commit 4b5b897

33 files changed

Lines changed: 1168 additions & 894 deletions

src/Npgsql.GeoJSON/Internal/GeoJSONConverter.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -291,11 +291,15 @@ static Position ReadPosition(PgReader reader, EwkbGeometryType type, bool little
291291
return position;
292292

293293
double ReadDouble(bool littleEndian)
294-
=> littleEndian
295-
// Netstandard is missing ReverseEndianness apis for double.
296-
? Unsafe.As<long, double>(ref Unsafe.AsRef(
297-
BinaryPrimitives.ReverseEndianness(Unsafe.As<double, long>(ref Unsafe.AsRef(reader.ReadDouble())))))
298-
: reader.ReadDouble();
294+
{
295+
if (littleEndian)
296+
{
297+
var value = BinaryPrimitives.ReverseEndianness(Unsafe.As<double, long>(ref Unsafe.AsRef(reader.ReadDouble())));
298+
return Unsafe.As<long, double>(ref value);
299+
}
300+
301+
return reader.ReadDouble();
302+
}
299303
}
300304
}
301305

src/Npgsql/BackendMessages/CommandCompleteMessage.cs

Lines changed: 42 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,61 @@
1-
using System.Diagnostics;
1+
using System;
2+
using System.Buffers.Text;
23
using Npgsql.Internal;
34

45
namespace Npgsql.BackendMessages;
56

67
sealed class CommandCompleteMessage : IBackendMessage
78
{
9+
uint _oid;
10+
ulong _rows;
811
internal StatementType StatementType { get; private set; }
9-
internal uint OID { get; private set; }
10-
internal ulong Rows { get; private set; }
12+
13+
internal uint OID => _oid;
14+
internal ulong Rows => _rows;
1115

1216
internal CommandCompleteMessage Load(NpgsqlReadBuffer buf, int len)
1317
{
14-
Rows = 0;
15-
OID = 0;
16-
17-
var bytes = buf.Buffer;
18-
var i = buf.ReadPosition;
18+
var bytes = buf.Span.Slice(0, len);
1919
buf.Skip(len);
20-
switch (bytes[i])
21-
{
22-
case (byte)'I':
23-
if (!AreEqual(bytes, i, "INSERT "))
24-
goto default;
25-
StatementType = StatementType.Insert;
26-
i += 7;
27-
OID = (uint) ParseNumber(bytes, ref i);
28-
i++;
29-
Rows = ParseNumber(bytes, ref i);
30-
return this;
31-
32-
case (byte)'D':
33-
if (!AreEqual(bytes, i, "DELETE "))
34-
goto default;
35-
StatementType = StatementType.Delete;
36-
i += 7;
37-
Rows = ParseNumber(bytes, ref i);
38-
return this;
39-
40-
case (byte)'U':
41-
if (!AreEqual(bytes, i, "UPDATE "))
42-
goto default;
43-
StatementType = StatementType.Update;
44-
i += 7;
45-
Rows = ParseNumber(bytes, ref i);
46-
return this;
4720

48-
case (byte)'S':
49-
if (!AreEqual(bytes, i, "SELECT "))
50-
goto default;
51-
StatementType = StatementType.Select;
52-
i += 7;
53-
Rows = ParseNumber(bytes, ref i);
54-
return this;
55-
56-
case (byte)'M':
57-
if (AreEqual(bytes, i, "MERGE "))
58-
{
59-
StatementType = StatementType.Merge;
60-
i += 6;
61-
}
62-
else if (AreEqual(bytes, i, "MOVE "))
63-
{
64-
StatementType = StatementType.Move;
65-
i += 5;
66-
}
67-
else
68-
goto default;
69-
Rows = ParseNumber(bytes, ref i);
70-
return this;
71-
72-
case (byte)'F':
73-
if (!AreEqual(bytes, i, "FETCH "))
74-
goto default;
75-
StatementType = StatementType.Fetch;
76-
i += 6;
77-
Rows = ParseNumber(bytes, ref i);
78-
return this;
79-
80-
case (byte)'C':
81-
if (AreEqual(bytes, i, "COPY "))
82-
{
83-
StatementType = StatementType.Copy;
84-
i += 5;
85-
Rows = ParseNumber(bytes, ref i);
86-
return this;
87-
}
88-
if (bytes[i + 4] == 0 && AreEqual(bytes, i, "CALL"))
89-
{
90-
StatementType = StatementType.Call;
91-
return this;
92-
}
21+
// PostgreSQL always writes these strings as ASCII, see https://github.com/postgres/postgres/blob/c8e1ba736b2b9e8c98d37a5b77c4ed31baf94147/src/backend/tcop/cmdtag.c#L130-L133
22+
(StatementType, var argumentsStart) = Convert.ToChar(bytes[0]) switch
23+
{
24+
'S' when bytes.StartsWith("SELECT "u8) => (StatementType.Select, "SELECT ".Length),
25+
'I' when bytes.StartsWith("INSERT "u8) => (StatementType.Insert, "INSERT ".Length),
26+
'U' when bytes.StartsWith("UPDATE "u8) => (StatementType.Update, "UPDATE ".Length),
27+
'D' when bytes.StartsWith("DELETE "u8) => (StatementType.Delete, "DELETE ".Length),
28+
'M' when bytes.StartsWith("MERGE "u8) => (StatementType.Merge, "MERGE ".Length),
29+
'C' when bytes.StartsWith("COPY "u8) => (StatementType.Copy, "COPY ".Length),
30+
'C' when bytes.StartsWith("CALL"u8) => (StatementType.Call, "CALL".Length),
31+
'M' when bytes.StartsWith("MOVE "u8) => (StatementType.Move, "MOVE ".Length),
32+
'F' when bytes.StartsWith("FETCH "u8) => (StatementType.Fetch, "FETCH ".Length),
33+
'C' when bytes.StartsWith("CREATE TABLE AS "u8) => (StatementType.CreateTableAs, "CREATE TABLE AS ".Length),
34+
_ => (StatementType.Other, 0)
35+
};
36+
37+
_oid = 0;
38+
_rows = 0;
39+
40+
// Slice away the null terminator.
41+
var arguments = bytes.Slice(argumentsStart, bytes.Length - argumentsStart - 1);
42+
switch (StatementType)
43+
{
44+
case StatementType.Other:
45+
case StatementType.Call:
46+
break;
47+
case StatementType.Insert:
48+
if (!Utf8Parser.TryParse(arguments, out _oid, out var nextArgumentOffset))
49+
throw new InvalidOperationException("Invalid bytes in command complete message.");
50+
arguments = arguments.Slice(nextArgumentOffset + 1);
9351
goto default;
94-
9552
default:
96-
StatementType = StatementType.Other;
97-
return this;
53+
if (!Utf8Parser.TryParse(arguments, out _rows, out _))
54+
throw new InvalidOperationException("Invalid bytes in command complete message.");
55+
break;
9856
}
99-
}
10057

101-
static bool AreEqual(byte[] bytes, int pos, string s)
102-
{
103-
for (var i = 0; i < s.Length; i++)
104-
{
105-
if (bytes[pos+i] != s[i])
106-
return false;
107-
}
108-
return true;
109-
}
110-
111-
static ulong ParseNumber(byte[] bytes, ref int pos)
112-
{
113-
Debug.Assert(bytes[pos] >= '0' && bytes[pos] <= '9');
114-
ulong result = 0;
115-
do
116-
{
117-
result = result * 10 + bytes[pos++] - '0';
118-
} while (bytes[pos] >= '0' && bytes[pos] <= '9');
119-
return result;
58+
return this;
12059
}
12160

12261
public BackendMessageCode Code => BackendMessageCode.CommandComplete;

src/Npgsql/BackendMessages/RowDescriptionMessage.cs

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@
1212

1313
namespace Npgsql.BackendMessages;
1414

15+
readonly struct ColumnInfo
16+
{
17+
public ColumnInfo(PgConverterInfo converterInfo, DataFormat dataFormat, bool asObject)
18+
{
19+
ConverterInfo = converterInfo;
20+
DataFormat = dataFormat;
21+
AsObject = asObject;
22+
}
23+
24+
public PgConverterInfo ConverterInfo { get; }
25+
public DataFormat DataFormat { get; }
26+
public bool AsObject { get; }
27+
}
28+
1529
/// <summary>
1630
/// A RowDescription message sent from the backend.
1731
/// </summary>
@@ -24,7 +38,7 @@ sealed class RowDescriptionMessage : IBackendMessage, IReadOnlyList<FieldDescrip
2438
FieldDescription?[] _fields;
2539
readonly Dictionary<string, int> _nameIndex;
2640
Dictionary<string, int>? _insensitiveIndex;
27-
PgConverterInfo[]? _lastConverterInfoCache;
41+
ColumnInfo[]? _lastConverterInfoCache;
2842

2943
internal RowDescriptionMessage(bool connectorOwned, int numFields = 10)
3044
{
@@ -119,14 +133,14 @@ public FieldDescription this[int index]
119133
}
120134
}
121135

122-
internal void SetConverterInfoCache(ReadOnlySpan<PgConverterInfo> values)
136+
internal void SetConverterInfoCache(ReadOnlySpan<ColumnInfo> values)
123137
{
124138
if (_connectorOwned || _lastConverterInfoCache is not null)
125139
return;
126140
Interlocked.CompareExchange(ref _lastConverterInfoCache, values.ToArray(), null);
127141
}
128142

129-
internal void LoadConverterInfoCache(PgConverterInfo[] values)
143+
internal void LoadConverterInfoCache(ColumnInfo[] values)
130144
{
131145
if (_lastConverterInfoCache is not { } cache)
132146
return;
@@ -328,17 +342,17 @@ internal void Populate(
328342

329343
internal Type FieldType => ObjectOrDefaultInfo.TypeToConvert;
330344

331-
PgConverterInfo _objectOrDefaultInfo;
345+
ColumnInfo _objectOrDefaultInfo;
332346
internal PgConverterInfo ObjectOrDefaultInfo
333347
{
334348
get
335349
{
336-
if (!_objectOrDefaultInfo.IsDefault)
337-
return _objectOrDefaultInfo;
350+
if (!_objectOrDefaultInfo.ConverterInfo.IsDefault)
351+
return _objectOrDefaultInfo.ConverterInfo;
338352

339353
ref var info = ref _objectOrDefaultInfo;
340-
GetInfo(null, ref _objectOrDefaultInfo, out _);
341-
return info;
354+
GetInfo(null, ref _objectOrDefaultInfo);
355+
return info.ConverterInfo;
342356
}
343357
}
344358

@@ -350,64 +364,60 @@ internal FieldDescription Clone()
350364
return field;
351365
}
352366

353-
internal void GetInfo(Type? type, ref PgConverterInfo lastConverterInfo, out bool asObject)
367+
internal void GetInfo(Type? type, ref ColumnInfo lastColumnInfo)
354368
{
355-
Debug.Assert(lastConverterInfo.IsDefault || (
356-
ReferenceEquals(_serializerOptions, lastConverterInfo.TypeInfo.Options) &&
357-
lastConverterInfo.TypeInfo.PgTypeId == _serializerOptions.ToCanonicalTypeId(PostgresType)), "Cache is bleeding over");
369+
Debug.Assert(lastColumnInfo.ConverterInfo.IsDefault || (
370+
ReferenceEquals(_serializerOptions, lastColumnInfo.ConverterInfo.TypeInfo.Options) &&
371+
lastColumnInfo.ConverterInfo.TypeInfo.PgTypeId == _serializerOptions.ToCanonicalTypeId(PostgresType)), "Cache is bleeding over");
358372

359-
if (!lastConverterInfo.IsDefault && lastConverterInfo.TypeToConvert == type)
360-
{
361-
asObject = lastConverterInfo.IsBoxingConverter;
373+
if (!lastColumnInfo.ConverterInfo.IsDefault && lastColumnInfo.ConverterInfo.TypeToConvert == type)
362374
return;
363-
}
364375

365-
var odfInfo = DataFormat is DataFormat.Text && type is not null ? ObjectOrDefaultInfo : _objectOrDefaultInfo;
376+
var odfInfo = DataFormat is DataFormat.Text && type is not null ? ObjectOrDefaultInfo : _objectOrDefaultInfo.ConverterInfo;
366377
if (odfInfo is { IsDefault: false })
367378
{
368379
if (typeof(object) == type)
369380
{
370-
lastConverterInfo = odfInfo;
371-
asObject = true;
381+
lastColumnInfo = new(odfInfo, DataFormat, true);
372382
return;
373383
}
374384
if (odfInfo.TypeToConvert == type)
375385
{
376-
lastConverterInfo = odfInfo;
377-
asObject = lastConverterInfo.IsBoxingConverter;
386+
lastColumnInfo = new(odfInfo, DataFormat, odfInfo.IsBoxingConverter);
378387
return;
379388
}
380389
}
381390

382-
GetInfoSlow(out lastConverterInfo, out asObject);
391+
GetInfoSlow(out lastColumnInfo);
383392

384393
[MethodImpl(MethodImplOptions.NoInlining)]
385-
void GetInfoSlow(out PgConverterInfo lastConverterInfo, out bool asObject)
394+
void GetInfoSlow(out ColumnInfo lastColumnInfo)
386395
{
387396
var typeInfo = AdoSerializerHelpers.GetTypeInfoForReading(type ?? typeof(object), PostgresType, _serializerOptions);
397+
PgConverterInfo converterInfo;
388398
switch (DataFormat)
389399
{
390400
case DataFormat.Binary:
391401
// If we don't support binary we'll just throw.
392-
lastConverterInfo = typeInfo.Bind(Field, DataFormat);
393-
asObject = typeof(object) == type || lastConverterInfo.IsBoxingConverter;
402+
converterInfo = typeInfo.Bind(Field, DataFormat);
403+
lastColumnInfo = new(converterInfo, DataFormat.Binary, typeof(object) == type || converterInfo.IsBoxingConverter);
394404
break;
395405
default:
396406
// For text we'll fall back to any available text converter for the expected clr type or throw.
397-
if (!typeInfo.TryBind(Field, DataFormat, out lastConverterInfo))
407+
if (!typeInfo.TryBind(Field, DataFormat, out converterInfo))
398408
{
399409
typeInfo = AdoSerializerHelpers.GetTypeInfoForReading(type ?? typeof(string), _serializerOptions.UnknownPgType, _serializerOptions);
400-
lastConverterInfo = typeInfo.Bind(Field, DataFormat);
401-
asObject = type != lastConverterInfo.TypeToConvert || lastConverterInfo.IsBoxingConverter;
410+
converterInfo = typeInfo.Bind(Field, DataFormat);
411+
lastColumnInfo = new(converterInfo, DataFormat, type != converterInfo.TypeToConvert || converterInfo.IsBoxingConverter);
402412
}
403413
else
404-
asObject = typeof(object) == type || lastConverterInfo.IsBoxingConverter;
414+
lastColumnInfo = new(converterInfo, DataFormat, typeof(object) == type || converterInfo.IsBoxingConverter);
405415
break;
406416
}
407417

408418
// We delay initializing ObjectOrDefaultInfo until after the first lookup (unless it is itself the first lookup).
409419
// When passed in an unsupported type it allows the error to be more specific, instead of just having object/null to deal with.
410-
if (_objectOrDefaultInfo.IsDefault && type is not null)
420+
if (_objectOrDefaultInfo.ConverterInfo.IsDefault && type is not null)
411421
_ = ObjectOrDefaultInfo;
412422
}
413423
}

src/Npgsql/Internal/Converters/ArrayConverter.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,9 +305,10 @@ private protected ArrayConverter(int? expectedDimensions, PgConverterResolution
305305
public override T Read(PgReader reader) => (T)_pgArrayConverter.Read(async: false, reader).Result;
306306

307307
public override ValueTask<T> ReadAsync(PgReader reader, CancellationToken cancellationToken = default)
308-
#pragma warning disable CS9193
309-
=> Unsafe.As<ValueTask<object>, ValueTask<T>>(ref Unsafe.AsRef(_pgArrayConverter.Read(async: true, reader, cancellationToken)));
310-
#pragma warning restore
308+
{
309+
var value = _pgArrayConverter.Read(async: true, reader, cancellationToken);
310+
return Unsafe.As<ValueTask<object>, ValueTask<T>>(ref value);
311+
}
311312

312313
public override Size GetSize(SizeContext context, T values, ref object? writeState)
313314
=> _pgArrayConverter.GetSize(context, values, ref writeState);

src/Npgsql/Internal/Converters/AsyncHelpers.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,10 @@ public Continuation(object handle, delegate*<Task, CompletionSource, void> conti
6666
public static unsafe ValueTask<T> ComposingReadAsync<T, TEffective>(this PgConverter<T> instance, PgConverter<TEffective> effectiveConverter, PgReader reader, CancellationToken cancellationToken)
6767
{
6868
if (!typeof(T).IsValueType && !typeof(TEffective).IsValueType)
69-
#pragma warning disable CS9193
70-
return Unsafe.As<ValueTask<TEffective>, ValueTask<T>>(ref Unsafe.AsRef(effectiveConverter.ReadAsync(reader, cancellationToken)));
71-
#pragma warning restore
69+
{
70+
var value = effectiveConverter.ReadAsync(reader, cancellationToken);
71+
return Unsafe.As<ValueTask<TEffective>, ValueTask<T>>(ref value);
72+
}
7273
// Easy if we have all the data.
7374
var task = effectiveConverter.ReadAsync(reader, cancellationToken);
7475
if (task.IsCompletedSuccessfully)
@@ -90,9 +91,10 @@ static void UnboxAndComplete(Task task, CompletionSource completionSource)
9091
public static unsafe ValueTask<T> ComposingReadAsObjectAsync<T>(this PgConverter<T> instance, PgConverter effectiveConverter, PgReader reader, CancellationToken cancellationToken)
9192
{
9293
if (!typeof(T).IsValueType)
93-
#pragma warning disable CS9193
94-
return Unsafe.As<ValueTask<object>, ValueTask<T>>(ref Unsafe.AsRef(effectiveConverter.ReadAsObjectAsync(reader, cancellationToken)));
95-
#pragma warning restore
94+
{
95+
var value = effectiveConverter.ReadAsObjectAsync(reader, cancellationToken);
96+
return Unsafe.As<ValueTask<object>, ValueTask<T>>(ref value);
97+
}
9698

9799
// Easy if we have all the data.
98100
var task = effectiveConverter.ReadAsObjectAsync(reader, cancellationToken);

0 commit comments

Comments
 (0)