Skip to content

Commit 65d29fb

Browse files
committed
Unroll Equals too
1 parent 0a3531e commit 65d29fb

File tree

3 files changed

+159
-56
lines changed

3 files changed

+159
-56
lines changed

src/coreclr/jit/inlinepolicy.cpp

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -735,15 +735,6 @@ double DefaultPolicy::DetermineMultiplier()
735735
JITDUMP("\nInline candidate has arg that feeds range check. Multiplier increased to %g.", multiplier);
736736
}
737737

738-
if (m_ConstArgFeedsIsKnownConst || (m_ArgFeedsIsKnownConst && m_IsPrejitRoot))
739-
{
740-
// if we use RuntimeHelpers.IsKnownConstant we most likely expect our function to be always inlined
741-
// at least in the case of constant arguments. In IsPrejitRoot we don't have callsite info so let's
742-
// assume we have a constant here in order to avoid "baked" noinline
743-
multiplier += 20;
744-
JITDUMP("\nConstant argument feeds RuntimeHelpers.IsKnownConstant. Multiplier increased to %g.", multiplier);
745-
}
746-
747738
if (m_ConstantArgFeedsConstantTest > 0)
748739
{
749740
multiplier += 3.0;
@@ -1629,6 +1620,15 @@ double ExtendedDefaultPolicy::DetermineMultiplier()
16291620
}
16301621
}
16311622

1623+
if (m_ConstArgFeedsIsKnownConst || (m_ArgFeedsIsKnownConst && m_IsPrejitRoot))
1624+
{
1625+
// if we use RuntimeHelpers.IsKnownConstant we most likely expect our function to be always inlined
1626+
// at least in the case of constant arguments. In IsPrejitRoot we don't have callsite info so let's
1627+
// assume we have a constant here in order to avoid "baked" noinline
1628+
multiplier += 20;
1629+
JITDUMP("\nConstant argument feeds RuntimeHelpers.IsKnownConstant. Multiplier increased to %g.", multiplier);
1630+
}
1631+
16321632
if (m_ArgFeedsConstantTest > 0)
16331633
{
16341634
multiplier += m_IsPrejitRoot ? 3.0 : 1.0;

src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,5 +127,8 @@ internal static bool IsPrimitiveType(this CorElementType et)
127127

128128
[Intrinsic]
129129
internal static bool IsKnownConstant(char t) => false;
130+
131+
[Intrinsic]
132+
internal static bool IsKnownConstant(int t) => false;
130133
}
131134
}

src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs

Lines changed: 147 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Runtime.CompilerServices;
1010
using System.Runtime.InteropServices;
1111
using System.Text.Unicode;
12+
using System.Buffers.Binary;
1213

1314
using Internal.Runtime.CompilerServices;
1415

@@ -681,31 +682,85 @@ public bool Equals([NotNullWhen(true)] string? value, StringComparison compariso
681682
}
682683

683684
// Determines whether two Strings match.
685+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
684686
public static bool Equals(string? a, string? b)
685687
{
686-
// Transform 'str == ""' to 'str != null && str.Length == 0' if either a or b are jit-time
687-
// constants. Otherwise, these two blocks are eliminated
688-
if (RuntimeHelpers.IsKnownConstant(a) && a != null && a.Length == 0)
688+
#if TARGET_64BIT && !MONO && !BIGENDIAN
689+
if (RuntimeHelpers.IsKnownConstant(b) && b != null && b.Length <= 4)
689690
{
690-
return b != null && b.Length == 0;
691+
return EqualsUnrolled(a, b);
691692
}
692693

693-
if (RuntimeHelpers.IsKnownConstant(b) && b != null && b.Length == 0)
694+
if (RuntimeHelpers.IsKnownConstant(a) && a != null && a.Length <= 4)
694695
{
695-
return a != null && a.Length == 0;
696+
return EqualsUnrolled(b, a);
696697
}
697698

698-
if (object.ReferenceEquals(a, b))
699+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
700+
static bool EqualsUnrolled(string? a, string b)
699701
{
700-
return true;
702+
// if both are constants - EqualsInternal will handle it just fine
703+
if (RuntimeHelpers.IsKnownConstant(a))
704+
{
705+
return EqualsInternal(a, b);
706+
}
707+
708+
if (b.Length == 0)
709+
{
710+
return a != null && a.Length == 0;
711+
}
712+
713+
if (b.Length == 1)
714+
{
715+
return a != null &&
716+
// Length: [ 0 ][ 1 ], ch1: [ X ][ \0 ] - we can compare Length and firstChar using a single
717+
// cmp operation, it's safe because there is also '\0' char we can rely on
718+
Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<char, byte>(ref Unsafe.Add(ref a._firstChar, -2))) ==
719+
(((ulong)b[0] << 32) | 1UL);
720+
}
721+
722+
if (b.Length == 2)
723+
{
724+
// Same here, compare Length and two chars in a single cmp
725+
return a != null &&
726+
Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<char, byte>(ref Unsafe.Add(ref a._firstChar, -2))) ==
727+
(((ulong)b[1] << 48) | ((ulong)b[0] << 32) | 2UL);
728+
}
729+
730+
if (b.Length == 3)
731+
{
732+
// it's safe to load 64bit here because of '\0' but we need to mask the last char out
733+
return a != null && a.Length == 3 &&
734+
Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<char, byte>(ref a._firstChar)) ==
735+
(((ulong)b[2] << 32) | ((ulong)b[1] << 16) | (ulong)b[0]);
736+
}
737+
738+
if (b.Length == 4)
739+
{
740+
return a != null && a.Length == 4 &&
741+
Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<char, byte>(ref a._firstChar)) ==
742+
(((ulong)b[3] << 48) | ((ulong)b[2] << 32) | ((ulong)b[1] << 16) | b[0]);
743+
}
744+
745+
return EqualsInternal(a, b);
701746
}
747+
#endif
748+
return EqualsInternal(a, b);
702749

703-
if (a is null || b is null || a.Length != b.Length)
750+
static bool EqualsInternal(string? a, string? b)
704751
{
705-
return false;
706-
}
752+
if (object.ReferenceEquals(a, b))
753+
{
754+
return true;
755+
}
707756

708-
return EqualsHelper(a, b);
757+
if (a is null || b is null || a.Length != b.Length)
758+
{
759+
return false;
760+
}
761+
762+
return EqualsHelper(a, b);
763+
}
709764
}
710765

711766
public static bool Equals(string? a, string? b, StringComparison comparisonType)
@@ -953,59 +1008,104 @@ public bool StartsWith(string value)
9531008
{
9541009
throw new ArgumentNullException(nameof(value));
9551010
}
956-
return StartsWith(value, StringComparison.CurrentCulture);
1011+
return CultureInfo.CurrentCulture.CompareInfo.IsPrefix(this, value, GetCaseCompareOfComparisonCulture(StringComparison.CurrentCulture));
9571012
}
9581013

1014+
// A hint for the inliner to always prescan this method
1015+
// and take the constant input path
9591016
public bool StartsWith(string value, StringComparison comparisonType)
9601017
{
9611018
if (value is null)
9621019
{
9631020
throw new ArgumentNullException(nameof(value));
9641021
}
9651022

966-
if ((object)this == (object)value)
1023+
#if TARGET_64BIT && !MONO && !BIGENDIAN
1024+
if (RuntimeHelpers.IsKnownConstant(value) && RuntimeHelpers.IsKnownConstant((int)comparisonType) &&
1025+
comparisonType == StringComparison.Ordinal && value.Length <= 4)
9671026
{
968-
CheckStringComparison(comparisonType);
969-
return true;
1027+
return StartsWithUnrolledOrdinal(value);
9701028
}
9711029

972-
if (value.Length == 0)
1030+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
1031+
bool StartsWithUnrolledOrdinal(string value)
9731032
{
974-
CheckStringComparison(comparisonType);
975-
return true;
1033+
if (value.Length == 0)
1034+
{
1035+
return true;
1036+
}
1037+
if (value.Length == 1)
1038+
{
1039+
return _stringLength >= 1 && _firstChar == value[0];
1040+
}
1041+
if (value.Length == 2)
1042+
{
1043+
return _stringLength >= 2 && Unsafe.ReadUnaligned<uint>(ref Unsafe.As<char, byte>(ref _firstChar)) ==
1044+
(((uint)value[1] << 16) | value[0]);
1045+
}
1046+
if (value.Length == 3)
1047+
{
1048+
// it's safe to load 64bit here because of '\0' but we need to mask the last char out
1049+
return _stringLength >= 3 && Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<char, byte>(ref _firstChar)) << 16 ==
1050+
(((ulong)value[2] << 48) | ((ulong)value[1] << 32) | (ulong)value[0] << 16);
1051+
}
1052+
if (value.Length == 4)
1053+
{
1054+
return _stringLength >= 4 && Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<char, byte>(ref _firstChar)) ==
1055+
(((ulong)value[3] << 48) | ((ulong)value[2] << 32) | ((ulong)value[1] << 16) | value[0]);
1056+
}
1057+
return StartsWithInternal(value, StringComparison.Ordinal);
9761058
}
1059+
#endif
9771060

978-
switch (comparisonType)
979-
{
980-
case StringComparison.CurrentCulture:
981-
case StringComparison.CurrentCultureIgnoreCase:
982-
return CultureInfo.CurrentCulture.CompareInfo.IsPrefix(this, value, GetCaseCompareOfComparisonCulture(comparisonType));
983-
984-
case StringComparison.InvariantCulture:
985-
case StringComparison.InvariantCultureIgnoreCase:
986-
return CompareInfo.Invariant.IsPrefix(this, value, GetCaseCompareOfComparisonCulture(comparisonType));
1061+
return StartsWithInternal(value, comparisonType);
9871062

988-
case StringComparison.Ordinal:
989-
if (this.Length < value.Length || _firstChar != value._firstChar)
990-
{
991-
return false;
992-
}
993-
return (value.Length == 1) ?
994-
true : // First char is the same and thats all there is to compare
995-
SpanHelpers.SequenceEqual(
996-
ref Unsafe.As<char, byte>(ref this.GetRawStringData()),
997-
ref Unsafe.As<char, byte>(ref value.GetRawStringData()),
998-
((nuint)value.Length) * 2);
1063+
bool StartsWithInternal(string value, StringComparison comparisonType)
1064+
{
1065+
if ((object)this == (object)value)
1066+
{
1067+
CheckStringComparison(comparisonType);
1068+
return true;
1069+
}
9991070

1000-
case StringComparison.OrdinalIgnoreCase:
1001-
if (this.Length < value.Length)
1002-
{
1003-
return false;
1004-
}
1005-
return Ordinal.EqualsIgnoreCase(ref this.GetRawStringData(), ref value.GetRawStringData(), value.Length);
1071+
if (value.Length == 0)
1072+
{
1073+
CheckStringComparison(comparisonType);
1074+
return true;
1075+
}
10061076

1007-
default:
1008-
throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
1077+
switch (comparisonType)
1078+
{
1079+
case StringComparison.CurrentCulture:
1080+
case StringComparison.CurrentCultureIgnoreCase:
1081+
return CultureInfo.CurrentCulture.CompareInfo.IsPrefix(this, value, GetCaseCompareOfComparisonCulture(comparisonType));
1082+
1083+
case StringComparison.InvariantCulture:
1084+
case StringComparison.InvariantCultureIgnoreCase:
1085+
return CompareInfo.Invariant.IsPrefix(this, value, GetCaseCompareOfComparisonCulture(comparisonType));
1086+
1087+
case StringComparison.Ordinal:
1088+
if (this.Length < value.Length || _firstChar != value._firstChar)
1089+
{
1090+
return false;
1091+
}
1092+
return (value.Length == 1) ?
1093+
true : // First char is the same and thats all there is to compare
1094+
SpanHelpers.SequenceEqual(
1095+
ref Unsafe.As<char, byte>(ref this.GetRawStringData()),
1096+
ref Unsafe.As<char, byte>(ref value.GetRawStringData()),
1097+
((nuint)value.Length) * 2);
1098+
1099+
case StringComparison.OrdinalIgnoreCase:
1100+
if (this.Length < value.Length)
1101+
{
1102+
return false;
1103+
}
1104+
return Ordinal.EqualsIgnoreCase(ref this.GetRawStringData(), ref value.GetRawStringData(), value.Length);
1105+
1106+
default:
1107+
throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
1108+
}
10091109
}
10101110
}
10111111

0 commit comments

Comments
 (0)