From 03f1be91cb09bbe85daa16ffd87adc13d914f019 Mon Sep 17 00:00:00 2001 From: pl752 Date: Mon, 29 Dec 2025 11:30:01 +0500 Subject: [PATCH 1/3] Reworked the rune operations to not spam char[1 or 2] allocations and leverage simd for BMP-only strings --- .../Client/Managed/Version10/GdsStatement.cs | 38 ++++++------- .../Common/DbField.cs | 12 ++-- .../Common/DbValue.cs | 8 +-- .../Common/Extensions.cs | 56 +++++++++++++++++++ 4 files changed, 84 insertions(+), 30 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs index 54b5698cc..d8171219b 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs @@ -1533,16 +1533,7 @@ protected object ReadRawValue(IXdrReader xdr, DbField field) else { var s = xdr.ReadString(innerCharset, field.Length); - var runes = s.EnumerateRunesToChars().ToList(); - if ((field.Length % field.Charset.BytesPerCharacter) == 0 && - runes.Count > field.CharCount) - { - return new string([.. runes.Take(field.CharCount).SelectMany(x => x)]); - } - else - { - return s; - } + return TruncateStringByRuneCount(s, field); } case DbDataType.VarChar: @@ -1631,16 +1622,7 @@ protected async ValueTask ReadRawValueAsync(IXdrReader xdr, DbField fiel else { var s = await xdr.ReadStringAsync(innerCharset, field.Length, cancellationToken).ConfigureAwait(false); - var runes = s.EnumerateRunesToChars().ToList(); - if ((field.Length % field.Charset.BytesPerCharacter) == 0 && - runes.Count > field.CharCount) - { - return new string([.. runes.Take(field.CharCount).SelectMany(x => x)]); - } - else - { - return s; - } + return TruncateStringByRuneCount(s, field); } case DbDataType.VarChar: @@ -1797,6 +1779,22 @@ protected virtual async ValueTask ReadRowAsync(CancellationToken canc return row; } + private static string TruncateStringByRuneCount(string s, DbField field) + { + if ((field.Length % field.Charset.BytesPerCharacter) != 0) + { + return s; + } + + var runeCount = s.CountRunes(); + if (runeCount <= field.CharCount) + { + return s; + } + + return new string(s.TruncateStringToRuneCount(field.CharCount)); + } + #endregion #region Protected Internal Methods diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/DbField.cs b/src/FirebirdSql.Data.FirebirdClient/Common/DbField.cs index 3be89be74..efc23524a 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/DbField.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/DbField.cs @@ -325,12 +325,12 @@ public void SetValue(byte[] buffer) else { var s = Charset.GetString(buffer, 0, buffer.Length); - - var runes = s.EnumerateRunesToChars().ToList(); - if ((Length % Charset.BytesPerCharacter) == 0 && - runes.Count > CharCount) - { - s = new string([.. runes.Take(CharCount).SelectMany(x => x)]); + if((Length % Charset.BytesPerCharacter) == 0) + { + var runes = s.CountRunes(); + if(runes > CharCount) { + s = new string(s.TruncateStringToRuneCount(CharCount)); + } } DbValue.SetValue(s); diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/DbValue.cs b/src/FirebirdSql.Data.FirebirdClient/Common/DbValue.cs index 44b79a0f1..f4fe8abee 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/DbValue.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/DbValue.cs @@ -424,7 +424,7 @@ public byte[] GetBytes() else { var svalue = GetString(); - if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.EnumerateRunesToChars().Count() > Field.CharCount) + if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.CountRunes() > Field.CharCount) { throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation }); } @@ -460,7 +460,7 @@ public byte[] GetBytes() else { var svalue = GetString(); - if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.EnumerateRunesToChars().Count() > Field.CharCount) + if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.CountRunes() > Field.CharCount) { throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation }); } @@ -639,7 +639,7 @@ public async ValueTask GetBytesAsync(CancellationToken cancellationToken else { var svalue = await GetStringAsync(cancellationToken).ConfigureAwait(false); - if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.EnumerateRunesToChars().Count() > Field.CharCount) + if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.CountRunes() > Field.CharCount) { throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation }); } @@ -675,7 +675,7 @@ public async ValueTask GetBytesAsync(CancellationToken cancellationToken else { var svalue = await GetStringAsync(cancellationToken).ConfigureAwait(false); - if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.EnumerateRunesToChars().Count() > Field.CharCount) + if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.CountRunes() > Field.CharCount) { throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation }); } diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs b/src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs index 6143cc839..abd71a004 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs @@ -102,4 +102,60 @@ public static Encoding GetANSIEncoding() } } } + + public static int CountRunes(this ReadOnlySpan text) + { + var length = text.Length; + if(length == 0) + return 0; + + var i = text.IndexOfAnyInRange('\uD800', '\uDBFF'); + if(i < 0) + return length; + + var count = i; + while(i < length) + { + if(char.IsHighSurrogate(text[i]) && i + 1 < length && char.IsLowSurrogate(text[i + 1])) + { + i += 2; + } + else + { + i++; + } + count++; + } + return count; + } + + public static ReadOnlySpan TruncateStringToRuneCount(this ReadOnlySpan text, int maxRuneCount) + { + if(maxRuneCount <= 0 || text.IsEmpty) + return ReadOnlySpan.Empty; + + var length = text.Length; + if(maxRuneCount >= length) + return text; + + var prefix = text[..maxRuneCount]; + var i = prefix.IndexOfAnyInRange('\uD800', '\uDBFF'); + if(i < 0) + return prefix; + + var remaining = maxRuneCount - i; + while(i < length && remaining > 0) + { + if(char.IsHighSurrogate(text[i]) && i + 1 < length && char.IsLowSurrogate(text[i + 1])) + { + i += 2; + } + else + { + i++; + } + remaining--; + } + return text[..i]; + } } From 842ff2393b339cf033303391fc0e5afaa5410cae Mon Sep 17 00:00:00 2001 From: pl752 Date: Wed, 31 Dec 2025 10:31:01 +0500 Subject: [PATCH 2/3] Fixed somehow skipped few remaining unoptimal counts --- .../Client/Managed/Version10/GdsStatement.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs index d8171219b..15cbbd6a4 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs @@ -1247,7 +1247,7 @@ protected void WriteRawParameter(IXdrWriter xdr, DbField field) else { var svalue = field.DbValue.GetString(); - if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.EnumerateRunesToChars().Count() > field.CharCount) + if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.CountRunes() > field.CharCount) { throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation }); } @@ -1272,7 +1272,7 @@ protected void WriteRawParameter(IXdrWriter xdr, DbField field) else { var svalue = field.DbValue.GetString(); - if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.EnumerateRunesToChars().Count() > field.CharCount) + if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.CountRunes() > field.CharCount) { throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation }); } @@ -1395,7 +1395,7 @@ protected async ValueTask WriteRawParameterAsync(IXdrWriter xdr, DbField field, else { var svalue = await field.DbValue.GetStringAsync(cancellationToken).ConfigureAwait(false); - if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.EnumerateRunesToChars().Count() > field.CharCount) + if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.CountRunes() > field.CharCount) { throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation }); } @@ -1420,7 +1420,7 @@ protected async ValueTask WriteRawParameterAsync(IXdrWriter xdr, DbField field, else { var svalue = await field.DbValue.GetStringAsync(cancellationToken).ConfigureAwait(false); - if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.EnumerateRunesToChars().Count() > field.CharCount) + if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.CountRunes() > field.CharCount) { throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation }); } From 822b1a260d4096d1f27a42e2b85fad2056f9ca73 Mon Sep 17 00:00:00 2001 From: pl752 Date: Wed, 31 Dec 2025 10:32:22 +0500 Subject: [PATCH 3/3] Removed necessity to count twice and allocate similar string --- .../Client/Managed/Version10/GdsStatement.cs | 8 ++------ src/FirebirdSql.Data.FirebirdClient/Common/DbField.cs | 6 +++--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs index 15cbbd6a4..958459e70 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs @@ -1786,13 +1786,9 @@ private static string TruncateStringByRuneCount(string s, DbField field) return s; } - var runeCount = s.CountRunes(); - if (runeCount <= field.CharCount) - { - return s; - } + var truncated = s.TruncateStringToRuneCount(field.CharCount); - return new string(s.TruncateStringToRuneCount(field.CharCount)); + return truncated == s.AsSpan() ? s : new string(truncated); } #endregion diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/DbField.cs b/src/FirebirdSql.Data.FirebirdClient/Common/DbField.cs index efc23524a..f5166de25 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/DbField.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/DbField.cs @@ -327,9 +327,9 @@ public void SetValue(byte[] buffer) var s = Charset.GetString(buffer, 0, buffer.Length); if((Length % Charset.BytesPerCharacter) == 0) { - var runes = s.CountRunes(); - if(runes > CharCount) { - s = new string(s.TruncateStringToRuneCount(CharCount)); + var truncated = s.TruncateStringToRuneCount(CharCount); + if(s.AsSpan() != truncated) { + s = new string(truncated); } }