|  | 
| 9 | 9 | using System.Runtime.CompilerServices; | 
| 10 | 10 | using System.Runtime.InteropServices; | 
| 11 | 11 | using System.Text.Unicode; | 
|  | 12 | +using System.Buffers.Binary; | 
| 12 | 13 | 
 | 
| 13 | 14 | using Internal.Runtime.CompilerServices; | 
| 14 | 15 | 
 | 
| @@ -681,31 +682,85 @@ public bool Equals([NotNullWhen(true)] string? value, StringComparison compariso | 
| 681 | 682 |         } | 
| 682 | 683 | 
 | 
| 683 | 684 |         // Determines whether two Strings match. | 
|  | 685 | +        [MethodImpl(MethodImplOptions.AggressiveInlining)] | 
| 684 | 686 |         public static bool Equals(string? a, string? b) | 
| 685 | 687 |         { | 
| 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) | 
| 689 | 690 |             { | 
| 690 |  | -                return b != null && b.Length == 0; | 
|  | 691 | +                return EqualsUnrolled(a, b); | 
| 691 | 692 |             } | 
| 692 | 693 | 
 | 
| 693 |  | -            if (RuntimeHelpers.IsKnownConstant(b) && b != null && b.Length == 0) | 
|  | 694 | +            if (RuntimeHelpers.IsKnownConstant(a) && a != null && a.Length <= 4) | 
| 694 | 695 |             { | 
| 695 |  | -                return a != null && a.Length == 0; | 
|  | 696 | +                return EqualsUnrolled(b, a); | 
| 696 | 697 |             } | 
| 697 | 698 | 
 | 
| 698 |  | -            if (object.ReferenceEquals(a, b)) | 
|  | 699 | +            [MethodImpl(MethodImplOptions.AggressiveInlining)] | 
|  | 700 | +            static bool EqualsUnrolled(string? a, string b) | 
| 699 | 701 |             { | 
| 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); | 
| 701 | 746 |             } | 
|  | 747 | +#endif | 
|  | 748 | +            return EqualsInternal(a, b); | 
| 702 | 749 | 
 | 
| 703 |  | -            if (a is null || b is null || a.Length != b.Length) | 
|  | 750 | +            static bool EqualsInternal(string? a, string? b) | 
| 704 | 751 |             { | 
| 705 |  | -                return false; | 
| 706 |  | -            } | 
|  | 752 | +                if (object.ReferenceEquals(a, b)) | 
|  | 753 | +                { | 
|  | 754 | +                    return true; | 
|  | 755 | +                } | 
| 707 | 756 | 
 | 
| 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 | +            } | 
| 709 | 764 |         } | 
| 710 | 765 | 
 | 
| 711 | 766 |         public static bool Equals(string? a, string? b, StringComparison comparisonType) | 
| @@ -953,59 +1008,104 @@ public bool StartsWith(string value) | 
| 953 | 1008 |             { | 
| 954 | 1009 |                 throw new ArgumentNullException(nameof(value)); | 
| 955 | 1010 |             } | 
| 956 |  | -            return StartsWith(value, StringComparison.CurrentCulture); | 
|  | 1011 | +            return CultureInfo.CurrentCulture.CompareInfo.IsPrefix(this, value, GetCaseCompareOfComparisonCulture(StringComparison.CurrentCulture)); | 
| 957 | 1012 |         } | 
| 958 | 1013 | 
 | 
|  | 1014 | +        // A hint for the inliner to always prescan this method | 
|  | 1015 | +        // and take the constant input path | 
| 959 | 1016 |         public bool StartsWith(string value, StringComparison comparisonType) | 
| 960 | 1017 |         { | 
| 961 | 1018 |             if (value is null) | 
| 962 | 1019 |             { | 
| 963 | 1020 |                 throw new ArgumentNullException(nameof(value)); | 
| 964 | 1021 |             } | 
| 965 | 1022 | 
 | 
| 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) | 
| 967 | 1026 |             { | 
| 968 |  | -                CheckStringComparison(comparisonType); | 
| 969 |  | -                return true; | 
|  | 1027 | +                return StartsWithUnrolledOrdinal(value); | 
| 970 | 1028 |             } | 
| 971 | 1029 | 
 | 
| 972 |  | -            if (value.Length == 0) | 
|  | 1030 | +            [MethodImpl(MethodImplOptions.AggressiveInlining)] | 
|  | 1031 | +            bool StartsWithUnrolledOrdinal(string value) | 
| 973 | 1032 |             { | 
| 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); | 
| 976 | 1058 |             } | 
|  | 1059 | +#endif | 
| 977 | 1060 | 
 | 
| 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); | 
| 987 | 1062 | 
 | 
| 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 | +                } | 
| 999 | 1070 | 
 | 
| 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 | +                } | 
| 1006 | 1076 | 
 | 
| 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 | +                } | 
| 1009 | 1109 |             } | 
| 1010 | 1110 |         } | 
| 1011 | 1111 | 
 | 
|  | 
0 commit comments