From 73e1fe2232b73584bd5538043752c66d8c1a7d44 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 1 Mar 2023 23:51:03 -0500 Subject: [PATCH 1/2] Improve DateTime parsing perf for invariant culture Speed up the handling of ddd, dddd, MMM, and MMMM parts of a date time format string when using the invariant culture, which is very commonly used in parsing. Today, when one of these is encountered, the relevant array of comparison strings is retrieved from the DateTimeFormatInfo, and each is compared as a prefix against the current position in the input, using a linguistic ignore-case comparison. But for the invariant culture, we don't need to consult any arrays, and can do the comparison much more quickly. These parts dominate the processing of a format like that for RFC1123. --- .../src/System/Globalization/DateTimeParse.cs | 229 ++++++++++++++---- 1 file changed, 179 insertions(+), 50 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs index 28ba7c96a9c5d2..30a91aff90821e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Runtime.CompilerServices; using System.Text; @@ -3299,25 +3300,56 @@ private static bool MatchAbbreviatedMonthName(ref __DTString str, DateTimeFormat result = -1; if (str.GetNext()) { - // - // Scan the month names (note that some calendars has 13 months) and find - // the matching month name which has the max string length. - // We need to do this because some cultures (e.g. "cs-CZ") which have - // abbreviated month names with the same prefix. - // - int monthsInYear = (dtfi.GetMonthName(13).Length == 0 ? 12 : 13); - for (int i = 1; i <= monthsInYear; i++) + if (ReferenceEquals(dtfi, DateTimeFormat.InvariantFormatInfo)) { - string searchStr = dtfi.GetAbbreviatedMonthName(i); - int matchStrLen = searchStr.Length; - if (dtfi.HasSpacesInMonthNames - ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen) - : str.MatchSpecifiedWord(searchStr)) + // Invariant data. Do a fast lookup on the known abbreviated month names. + ReadOnlySpan span = str.Value.Slice(str.Index); + if (span.Length >= 3) { - if (matchStrLen > maxMatchStrLen) + uint m0 = span[0], m1 = span[1], m2 = span[2]; + if ((m0 | m1 | m2) <= 0x7F) { - maxMatchStrLen = matchStrLen; - result = i; + // Combine all the characters into a single uint, lowercased. + maxMatchStrLen = 3; // assume we'll successfully match + switch ((m0 << 16) | (m1 << 8) | m2 | 0x202020) + { + case 0x6a616e: /* 'jan' */ result = 1; break; + case 0x666562: /* 'feb' */ result = 2; break; + case 0x6d6172: /* 'mar' */ result = 3; break; + case 0x617072: /* 'apr' */ result = 4; break; + case 0x6d6179: /* 'may' */ result = 5; break; + case 0x6a756e: /* 'jun' */ result = 6; break; + case 0x6a756c: /* 'jul' */ result = 7; break; + case 0x617567: /* 'aug' */ result = 8; break; + case 0x736570: /* 'sep' */ result = 9; break; + case 0x6f6374: /* 'oct' */ result = 10; break; + case 0x6e6f76: /* 'nov' */ result = 11; break; + case 0x646563: /* 'dec' */ result = 12; break; + default: maxMatchStrLen = 0; break; // undo match assumption + } + } + } + } + else + { + // Scan the month names (note that some calendars has 13 months) and find + // the matching month name which has the max string length. + // We need to do this because some cultures (e.g. "cs-CZ") which have + // abbreviated month names with the same prefix. + int monthsInYear = (dtfi.GetMonthName(13).Length == 0 ? 12 : 13); + for (int i = 1; i <= monthsInYear; i++) + { + string searchStr = dtfi.GetAbbreviatedMonthName(i); + int matchStrLen = searchStr.Length; + if (dtfi.HasSpacesInMonthNames + ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen) + : str.MatchSpecifiedWord(searchStr)) + { + if (matchStrLen > maxMatchStrLen) + { + maxMatchStrLen = matchStrLen; + result = i; + } } } } @@ -3370,25 +3402,54 @@ private static bool MatchMonthName(ref __DTString str, DateTimeFormatInfo dtfi, result = -1; if (str.GetNext()) { - // - // Scan the month names (note that some calendars has 13 months) and find - // the matching month name which has the max string length. - // We need to do this because some cultures (e.g. "vi-VN") which have - // month names with the same prefix. - // - int monthsInYear = (dtfi.GetMonthName(13).Length == 0 ? 12 : 13); - for (int i = 1; i <= monthsInYear; i++) + if (ReferenceEquals(dtfi, DateTimeFormat.InvariantFormatInfo)) { - string searchStr = dtfi.GetMonthName(i); - int matchStrLen = searchStr.Length; - if (dtfi.HasSpacesInMonthNames - ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen) - : str.MatchSpecifiedWord(searchStr)) + // Invariant data. Do a fast lookup on the known month names. + ReadOnlySpan span = str.Value.Slice(str.Index); + if (span.Length >= 3) { - if (matchStrLen > maxMatchStrLen) + uint m0 = span[0], m1 = span[1], m2 = span[2]; + if ((m0 | m1 | m2) <= 0x7F) { - maxMatchStrLen = matchStrLen; - result = i; + // Combine all the characters into a single uint, lowercased. + switch ((m0 << 16) | (m1 << 8) | m2 | 0x202020) + { + case 0x6a616e: /* 'jan' */ SetIfStartsWith(span, "January", 1, ref result, ref maxMatchStrLen); break; + case 0x666562: /* 'feb' */ SetIfStartsWith(span, "February", 2, ref result, ref maxMatchStrLen); break; + case 0x6d6172: /* 'mar' */ SetIfStartsWith(span, "March", 3, ref result, ref maxMatchStrLen); break; + case 0x617072: /* 'apr' */ SetIfStartsWith(span, "April", 4, ref result, ref maxMatchStrLen); break; + case 0x6d6179: /* 'may' */ SetIfStartsWith(span, "May", 5, ref result, ref maxMatchStrLen); break; + case 0x6a756e: /* 'jun' */ SetIfStartsWith(span, "June", 6, ref result, ref maxMatchStrLen); break; + case 0x6a756c: /* 'jul' */ SetIfStartsWith(span, "July", 7, ref result, ref maxMatchStrLen); break; + case 0x617567: /* 'aug' */ SetIfStartsWith(span, "August", 8, ref result, ref maxMatchStrLen); break; + case 0x736570: /* 'sep' */ SetIfStartsWith(span, "September", 9, ref result, ref maxMatchStrLen); break; + case 0x6f6374: /* 'oct' */ SetIfStartsWith(span, "October", 10, ref result, ref maxMatchStrLen); break; + case 0x6e6f76: /* 'nov' */ SetIfStartsWith(span, "November", 11, ref result, ref maxMatchStrLen); break; + case 0x646563: /* 'dec' */ SetIfStartsWith(span, "December", 12, ref result, ref maxMatchStrLen); break; + } + } + } + } + else + { + // Scan the month names (note that some calendars has 13 months) and find + // the matching month name which has the max string length. + // We need to do this because some cultures (e.g. "vi-VN") which have + // month names with the same prefix. + int monthsInYear = (dtfi.GetMonthName(13).Length == 0 ? 12 : 13); + for (int i = 1; i <= monthsInYear; i++) + { + string searchStr = dtfi.GetMonthName(i); + int matchStrLen = searchStr.Length; + if (dtfi.HasSpacesInMonthNames + ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen) + : str.MatchSpecifiedWord(searchStr)) + { + if (matchStrLen > maxMatchStrLen) + { + maxMatchStrLen = matchStrLen; + result = i; + } } } } @@ -3442,18 +3503,46 @@ private static bool MatchAbbreviatedDayName(ref __DTString str, DateTimeFormatIn result = -1; if (str.GetNext()) { - for (DayOfWeek i = DayOfWeek.Sunday; i <= DayOfWeek.Saturday; i++) + if (ReferenceEquals(dtfi, DateTimeFormat.InvariantFormatInfo)) { - string searchStr = dtfi.GetAbbreviatedDayName(i); - int matchStrLen = searchStr.Length; - if (dtfi.HasSpacesInDayNames - ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen) - : str.MatchSpecifiedWord(searchStr)) + // Invariant data. Do a fast lookup on the known abbreviated day names. + ReadOnlySpan span = str.Value.Slice(str.Index); + if (span.Length >= 3) { - if (matchStrLen > maxMatchStrLen) + uint d0 = span[0], d1 = span[1], d2 = span[2]; + if ((d0 | d1 | d2) <= 0x7F) { - maxMatchStrLen = matchStrLen; - result = (int)i; + // Combine all the characters into a single uint, lowercased. + maxMatchStrLen = 3; // assume we'll successfully match + switch ((d0 << 16) | (d1 << 8) | d2 | 0x202020) + { + case 0x73756E /* 'sun' */: result = 0; break; + case 0x6d6f6e /* 'mon' */: result = 1; break; + case 0x747565 /* 'tue' */: result = 2; break; + case 0x776564 /* 'wed' */: result = 3; break; + case 0x746875 /* 'thu' */: result = 4; break; + case 0x667269 /* 'fri' */: result = 5; break; + case 0x736174 /* 'sat' */: result = 6; break; + default: maxMatchStrLen = 0; break; // undo match assumption + } + } + } + } + else + { + for (DayOfWeek i = DayOfWeek.Sunday; i <= DayOfWeek.Saturday; i++) + { + string searchStr = dtfi.GetAbbreviatedDayName(i); + int matchStrLen = searchStr.Length; + if (dtfi.HasSpacesInDayNames + ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen) + : str.MatchSpecifiedWord(searchStr)) + { + if (matchStrLen > maxMatchStrLen) + { + maxMatchStrLen = matchStrLen; + result = (int)i; + } } } } @@ -3481,18 +3570,44 @@ private static bool MatchDayName(ref __DTString str, DateTimeFormatInfo dtfi, sc result = -1; if (str.GetNext()) { - for (DayOfWeek i = DayOfWeek.Sunday; i <= DayOfWeek.Saturday; i++) + if (ReferenceEquals(dtfi, DateTimeFormat.InvariantFormatInfo)) { - string searchStr = dtfi.GetDayName(i); - int matchStrLen = searchStr.Length; - if (dtfi.HasSpacesInDayNames - ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen) - : str.MatchSpecifiedWord(searchStr)) + // Invariant data. Do a fast lookup on the known day names. + ReadOnlySpan span = str.Value.Slice(str.Index); + if (span.Length >= 3) { - if (matchStrLen > maxMatchStrLen) + uint d0 = span[0], d1 = span[1], d2 = span[2]; + if ((d0 | d1 | d2) <= 0x7F) { - maxMatchStrLen = matchStrLen; - result = (int)i; + // Combine all the characters into a single uint, lowercased. + switch ((d0 << 16) | (d1 << 8) | d2 | 0x202020) + { + case 0x73756E /* 'sun' */: SetIfStartsWith(span, "Sunday", 0, ref result, ref maxMatchStrLen); break; + case 0x6d6f6e /* 'mon' */: SetIfStartsWith(span, "Monday", 1, ref result, ref maxMatchStrLen); break; + case 0x747565 /* 'tue' */: SetIfStartsWith(span, "Tuesday", 2, ref result, ref maxMatchStrLen); break; + case 0x776564 /* 'wed' */: SetIfStartsWith(span, "Wednesday", 3, ref result, ref maxMatchStrLen); break; + case 0x746875 /* 'thu' */: SetIfStartsWith(span, "Thursday", 4, ref result, ref maxMatchStrLen); break; + case 0x667269 /* 'fri' */: SetIfStartsWith(span, "Friday", 5, ref result, ref maxMatchStrLen); break; + case 0x736174 /* 'sat' */: SetIfStartsWith(span, "Saturday", 6, ref result, ref maxMatchStrLen); break; + } + } + } + } + else + { + for (DayOfWeek i = DayOfWeek.Sunday; i <= DayOfWeek.Saturday; i++) + { + string searchStr = dtfi.GetDayName(i); + int matchStrLen = searchStr.Length; + if (dtfi.HasSpacesInDayNames + ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen) + : str.MatchSpecifiedWord(searchStr)) + { + if (matchStrLen > maxMatchStrLen) + { + maxMatchStrLen = matchStrLen; + result = (int)i; + } } } } @@ -3505,6 +3620,20 @@ private static bool MatchDayName(ref __DTString str, DateTimeFormatInfo dtfi, sc return false; } + /// + /// Sets to and to 's Length + /// if starts with with an ordinal ignore-case comparison. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] // exposes StartsWith to constant `match` + private static void SetIfStartsWith(ReadOnlySpan span, [ConstantExpected] string match, int matchResult, scoped ref int result, ref int maxMatchStrLen) + { + if (span.StartsWith(match, StringComparison.OrdinalIgnoreCase)) + { + result = matchResult; + maxMatchStrLen = match.Length; + } + } + /*=================================MatchEraName================================== **Action: Parse era name from string starting at str.Index. **Returns: An era value. From 50121789e14b72a420904612b96e2a46b063561f Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 2 Mar 2023 15:03:20 -0500 Subject: [PATCH 2/2] Address PR feedback --- .../src/System/Globalization/DateTimeFormatInfo.cs | 2 +- .../src/System/Globalization/DateTimeParse.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormatInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormatInfo.cs index 26a182843ac8b6..68a3bd417a2989 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormatInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormatInfo.cs @@ -150,7 +150,7 @@ public sealed class DateTimeFormatInfo : IFormatProvider, ICloneable private DateTimeFormatFlags formatFlags = DateTimeFormatFlags.NotInitialized; - private string CultureName => _name ??= _cultureData.CultureName; + internal string CultureName => _name ??= _cultureData.CultureName; private CultureInfo Culture => _cultureInfo ??= CultureInfo.GetCultureInfo(CultureName); diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs index 4c34f78219d6c5..9995578c1f7c57 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs @@ -3300,7 +3300,7 @@ private static bool MatchAbbreviatedMonthName(ref __DTString str, DateTimeFormat result = -1; if (str.GetNext()) { - if (ReferenceEquals(dtfi, DateTimeFormat.InvariantFormatInfo)) + if (dtfi.CultureName == "") { // Invariant data. Do a fast lookup on the known abbreviated month names. ReadOnlySpan span = str.Value.Slice(str.Index); @@ -3402,7 +3402,7 @@ private static bool MatchMonthName(ref __DTString str, DateTimeFormatInfo dtfi, result = -1; if (str.GetNext()) { - if (ReferenceEquals(dtfi, DateTimeFormat.InvariantFormatInfo)) + if (dtfi.CultureName == "") { // Invariant data. Do a fast lookup on the known month names. ReadOnlySpan span = str.Value.Slice(str.Index); @@ -3503,7 +3503,7 @@ private static bool MatchAbbreviatedDayName(ref __DTString str, DateTimeFormatIn result = -1; if (str.GetNext()) { - if (ReferenceEquals(dtfi, DateTimeFormat.InvariantFormatInfo)) + if (dtfi.CultureName == "") { // Invariant data. Do a fast lookup on the known abbreviated day names. ReadOnlySpan span = str.Value.Slice(str.Index); @@ -3570,7 +3570,7 @@ private static bool MatchDayName(ref __DTString str, DateTimeFormatInfo dtfi, sc result = -1; if (str.GetNext()) { - if (ReferenceEquals(dtfi, DateTimeFormat.InvariantFormatInfo)) + if (dtfi.CultureName == "") { // Invariant data. Do a fast lookup on the known day names. ReadOnlySpan span = str.Value.Slice(str.Index);