Skip to content

Commit 73e1fe2

Browse files
committed
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.
1 parent e303a45 commit 73e1fe2

File tree

1 file changed

+179
-50
lines changed

1 file changed

+179
-50
lines changed

src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs

Lines changed: 179 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics;
5+
using System.Diagnostics.CodeAnalysis;
56
using System.Globalization;
67
using System.Runtime.CompilerServices;
78
using System.Text;
@@ -3299,25 +3300,56 @@ private static bool MatchAbbreviatedMonthName(ref __DTString str, DateTimeFormat
32993300
result = -1;
33003301
if (str.GetNext())
33013302
{
3302-
//
3303-
// Scan the month names (note that some calendars has 13 months) and find
3304-
// the matching month name which has the max string length.
3305-
// We need to do this because some cultures (e.g. "cs-CZ") which have
3306-
// abbreviated month names with the same prefix.
3307-
//
3308-
int monthsInYear = (dtfi.GetMonthName(13).Length == 0 ? 12 : 13);
3309-
for (int i = 1; i <= monthsInYear; i++)
3303+
if (ReferenceEquals(dtfi, DateTimeFormat.InvariantFormatInfo))
33103304
{
3311-
string searchStr = dtfi.GetAbbreviatedMonthName(i);
3312-
int matchStrLen = searchStr.Length;
3313-
if (dtfi.HasSpacesInMonthNames
3314-
? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3315-
: str.MatchSpecifiedWord(searchStr))
3305+
// Invariant data. Do a fast lookup on the known abbreviated month names.
3306+
ReadOnlySpan<char> span = str.Value.Slice(str.Index);
3307+
if (span.Length >= 3)
33163308
{
3317-
if (matchStrLen > maxMatchStrLen)
3309+
uint m0 = span[0], m1 = span[1], m2 = span[2];
3310+
if ((m0 | m1 | m2) <= 0x7F)
33183311
{
3319-
maxMatchStrLen = matchStrLen;
3320-
result = i;
3312+
// Combine all the characters into a single uint, lowercased.
3313+
maxMatchStrLen = 3; // assume we'll successfully match
3314+
switch ((m0 << 16) | (m1 << 8) | m2 | 0x202020)
3315+
{
3316+
case 0x6a616e: /* 'jan' */ result = 1; break;
3317+
case 0x666562: /* 'feb' */ result = 2; break;
3318+
case 0x6d6172: /* 'mar' */ result = 3; break;
3319+
case 0x617072: /* 'apr' */ result = 4; break;
3320+
case 0x6d6179: /* 'may' */ result = 5; break;
3321+
case 0x6a756e: /* 'jun' */ result = 6; break;
3322+
case 0x6a756c: /* 'jul' */ result = 7; break;
3323+
case 0x617567: /* 'aug' */ result = 8; break;
3324+
case 0x736570: /* 'sep' */ result = 9; break;
3325+
case 0x6f6374: /* 'oct' */ result = 10; break;
3326+
case 0x6e6f76: /* 'nov' */ result = 11; break;
3327+
case 0x646563: /* 'dec' */ result = 12; break;
3328+
default: maxMatchStrLen = 0; break; // undo match assumption
3329+
}
3330+
}
3331+
}
3332+
}
3333+
else
3334+
{
3335+
// Scan the month names (note that some calendars has 13 months) and find
3336+
// the matching month name which has the max string length.
3337+
// We need to do this because some cultures (e.g. "cs-CZ") which have
3338+
// abbreviated month names with the same prefix.
3339+
int monthsInYear = (dtfi.GetMonthName(13).Length == 0 ? 12 : 13);
3340+
for (int i = 1; i <= monthsInYear; i++)
3341+
{
3342+
string searchStr = dtfi.GetAbbreviatedMonthName(i);
3343+
int matchStrLen = searchStr.Length;
3344+
if (dtfi.HasSpacesInMonthNames
3345+
? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3346+
: str.MatchSpecifiedWord(searchStr))
3347+
{
3348+
if (matchStrLen > maxMatchStrLen)
3349+
{
3350+
maxMatchStrLen = matchStrLen;
3351+
result = i;
3352+
}
33213353
}
33223354
}
33233355
}
@@ -3370,25 +3402,54 @@ private static bool MatchMonthName(ref __DTString str, DateTimeFormatInfo dtfi,
33703402
result = -1;
33713403
if (str.GetNext())
33723404
{
3373-
//
3374-
// Scan the month names (note that some calendars has 13 months) and find
3375-
// the matching month name which has the max string length.
3376-
// We need to do this because some cultures (e.g. "vi-VN") which have
3377-
// month names with the same prefix.
3378-
//
3379-
int monthsInYear = (dtfi.GetMonthName(13).Length == 0 ? 12 : 13);
3380-
for (int i = 1; i <= monthsInYear; i++)
3405+
if (ReferenceEquals(dtfi, DateTimeFormat.InvariantFormatInfo))
33813406
{
3382-
string searchStr = dtfi.GetMonthName(i);
3383-
int matchStrLen = searchStr.Length;
3384-
if (dtfi.HasSpacesInMonthNames
3385-
? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3386-
: str.MatchSpecifiedWord(searchStr))
3407+
// Invariant data. Do a fast lookup on the known month names.
3408+
ReadOnlySpan<char> span = str.Value.Slice(str.Index);
3409+
if (span.Length >= 3)
33873410
{
3388-
if (matchStrLen > maxMatchStrLen)
3411+
uint m0 = span[0], m1 = span[1], m2 = span[2];
3412+
if ((m0 | m1 | m2) <= 0x7F)
33893413
{
3390-
maxMatchStrLen = matchStrLen;
3391-
result = i;
3414+
// Combine all the characters into a single uint, lowercased.
3415+
switch ((m0 << 16) | (m1 << 8) | m2 | 0x202020)
3416+
{
3417+
case 0x6a616e: /* 'jan' */ SetIfStartsWith(span, "January", 1, ref result, ref maxMatchStrLen); break;
3418+
case 0x666562: /* 'feb' */ SetIfStartsWith(span, "February", 2, ref result, ref maxMatchStrLen); break;
3419+
case 0x6d6172: /* 'mar' */ SetIfStartsWith(span, "March", 3, ref result, ref maxMatchStrLen); break;
3420+
case 0x617072: /* 'apr' */ SetIfStartsWith(span, "April", 4, ref result, ref maxMatchStrLen); break;
3421+
case 0x6d6179: /* 'may' */ SetIfStartsWith(span, "May", 5, ref result, ref maxMatchStrLen); break;
3422+
case 0x6a756e: /* 'jun' */ SetIfStartsWith(span, "June", 6, ref result, ref maxMatchStrLen); break;
3423+
case 0x6a756c: /* 'jul' */ SetIfStartsWith(span, "July", 7, ref result, ref maxMatchStrLen); break;
3424+
case 0x617567: /* 'aug' */ SetIfStartsWith(span, "August", 8, ref result, ref maxMatchStrLen); break;
3425+
case 0x736570: /* 'sep' */ SetIfStartsWith(span, "September", 9, ref result, ref maxMatchStrLen); break;
3426+
case 0x6f6374: /* 'oct' */ SetIfStartsWith(span, "October", 10, ref result, ref maxMatchStrLen); break;
3427+
case 0x6e6f76: /* 'nov' */ SetIfStartsWith(span, "November", 11, ref result, ref maxMatchStrLen); break;
3428+
case 0x646563: /* 'dec' */ SetIfStartsWith(span, "December", 12, ref result, ref maxMatchStrLen); break;
3429+
}
3430+
}
3431+
}
3432+
}
3433+
else
3434+
{
3435+
// Scan the month names (note that some calendars has 13 months) and find
3436+
// the matching month name which has the max string length.
3437+
// We need to do this because some cultures (e.g. "vi-VN") which have
3438+
// month names with the same prefix.
3439+
int monthsInYear = (dtfi.GetMonthName(13).Length == 0 ? 12 : 13);
3440+
for (int i = 1; i <= monthsInYear; i++)
3441+
{
3442+
string searchStr = dtfi.GetMonthName(i);
3443+
int matchStrLen = searchStr.Length;
3444+
if (dtfi.HasSpacesInMonthNames
3445+
? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3446+
: str.MatchSpecifiedWord(searchStr))
3447+
{
3448+
if (matchStrLen > maxMatchStrLen)
3449+
{
3450+
maxMatchStrLen = matchStrLen;
3451+
result = i;
3452+
}
33923453
}
33933454
}
33943455
}
@@ -3442,18 +3503,46 @@ private static bool MatchAbbreviatedDayName(ref __DTString str, DateTimeFormatIn
34423503
result = -1;
34433504
if (str.GetNext())
34443505
{
3445-
for (DayOfWeek i = DayOfWeek.Sunday; i <= DayOfWeek.Saturday; i++)
3506+
if (ReferenceEquals(dtfi, DateTimeFormat.InvariantFormatInfo))
34463507
{
3447-
string searchStr = dtfi.GetAbbreviatedDayName(i);
3448-
int matchStrLen = searchStr.Length;
3449-
if (dtfi.HasSpacesInDayNames
3450-
? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3451-
: str.MatchSpecifiedWord(searchStr))
3508+
// Invariant data. Do a fast lookup on the known abbreviated day names.
3509+
ReadOnlySpan<char> span = str.Value.Slice(str.Index);
3510+
if (span.Length >= 3)
34523511
{
3453-
if (matchStrLen > maxMatchStrLen)
3512+
uint d0 = span[0], d1 = span[1], d2 = span[2];
3513+
if ((d0 | d1 | d2) <= 0x7F)
34543514
{
3455-
maxMatchStrLen = matchStrLen;
3456-
result = (int)i;
3515+
// Combine all the characters into a single uint, lowercased.
3516+
maxMatchStrLen = 3; // assume we'll successfully match
3517+
switch ((d0 << 16) | (d1 << 8) | d2 | 0x202020)
3518+
{
3519+
case 0x73756E /* 'sun' */: result = 0; break;
3520+
case 0x6d6f6e /* 'mon' */: result = 1; break;
3521+
case 0x747565 /* 'tue' */: result = 2; break;
3522+
case 0x776564 /* 'wed' */: result = 3; break;
3523+
case 0x746875 /* 'thu' */: result = 4; break;
3524+
case 0x667269 /* 'fri' */: result = 5; break;
3525+
case 0x736174 /* 'sat' */: result = 6; break;
3526+
default: maxMatchStrLen = 0; break; // undo match assumption
3527+
}
3528+
}
3529+
}
3530+
}
3531+
else
3532+
{
3533+
for (DayOfWeek i = DayOfWeek.Sunday; i <= DayOfWeek.Saturday; i++)
3534+
{
3535+
string searchStr = dtfi.GetAbbreviatedDayName(i);
3536+
int matchStrLen = searchStr.Length;
3537+
if (dtfi.HasSpacesInDayNames
3538+
? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3539+
: str.MatchSpecifiedWord(searchStr))
3540+
{
3541+
if (matchStrLen > maxMatchStrLen)
3542+
{
3543+
maxMatchStrLen = matchStrLen;
3544+
result = (int)i;
3545+
}
34573546
}
34583547
}
34593548
}
@@ -3481,18 +3570,44 @@ private static bool MatchDayName(ref __DTString str, DateTimeFormatInfo dtfi, sc
34813570
result = -1;
34823571
if (str.GetNext())
34833572
{
3484-
for (DayOfWeek i = DayOfWeek.Sunday; i <= DayOfWeek.Saturday; i++)
3573+
if (ReferenceEquals(dtfi, DateTimeFormat.InvariantFormatInfo))
34853574
{
3486-
string searchStr = dtfi.GetDayName(i);
3487-
int matchStrLen = searchStr.Length;
3488-
if (dtfi.HasSpacesInDayNames
3489-
? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3490-
: str.MatchSpecifiedWord(searchStr))
3575+
// Invariant data. Do a fast lookup on the known day names.
3576+
ReadOnlySpan<char> span = str.Value.Slice(str.Index);
3577+
if (span.Length >= 3)
34913578
{
3492-
if (matchStrLen > maxMatchStrLen)
3579+
uint d0 = span[0], d1 = span[1], d2 = span[2];
3580+
if ((d0 | d1 | d2) <= 0x7F)
34933581
{
3494-
maxMatchStrLen = matchStrLen;
3495-
result = (int)i;
3582+
// Combine all the characters into a single uint, lowercased.
3583+
switch ((d0 << 16) | (d1 << 8) | d2 | 0x202020)
3584+
{
3585+
case 0x73756E /* 'sun' */: SetIfStartsWith(span, "Sunday", 0, ref result, ref maxMatchStrLen); break;
3586+
case 0x6d6f6e /* 'mon' */: SetIfStartsWith(span, "Monday", 1, ref result, ref maxMatchStrLen); break;
3587+
case 0x747565 /* 'tue' */: SetIfStartsWith(span, "Tuesday", 2, ref result, ref maxMatchStrLen); break;
3588+
case 0x776564 /* 'wed' */: SetIfStartsWith(span, "Wednesday", 3, ref result, ref maxMatchStrLen); break;
3589+
case 0x746875 /* 'thu' */: SetIfStartsWith(span, "Thursday", 4, ref result, ref maxMatchStrLen); break;
3590+
case 0x667269 /* 'fri' */: SetIfStartsWith(span, "Friday", 5, ref result, ref maxMatchStrLen); break;
3591+
case 0x736174 /* 'sat' */: SetIfStartsWith(span, "Saturday", 6, ref result, ref maxMatchStrLen); break;
3592+
}
3593+
}
3594+
}
3595+
}
3596+
else
3597+
{
3598+
for (DayOfWeek i = DayOfWeek.Sunday; i <= DayOfWeek.Saturday; i++)
3599+
{
3600+
string searchStr = dtfi.GetDayName(i);
3601+
int matchStrLen = searchStr.Length;
3602+
if (dtfi.HasSpacesInDayNames
3603+
? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3604+
: str.MatchSpecifiedWord(searchStr))
3605+
{
3606+
if (matchStrLen > maxMatchStrLen)
3607+
{
3608+
maxMatchStrLen = matchStrLen;
3609+
result = (int)i;
3610+
}
34963611
}
34973612
}
34983613
}
@@ -3505,6 +3620,20 @@ private static bool MatchDayName(ref __DTString str, DateTimeFormatInfo dtfi, sc
35053620
return false;
35063621
}
35073622

3623+
/// <summary>
3624+
/// Sets <paramref name="result"/> to <paramref name="matchResult"/> and <paramref name="maxMatchStrLen"/> to <paramref name="match"/>'s Length
3625+
/// if <paramref name="span"/> starts with <paramref name="match"/> with an ordinal ignore-case comparison.
3626+
/// </summary>
3627+
[MethodImpl(MethodImplOptions.AggressiveInlining)] // exposes StartsWith to constant `match`
3628+
private static void SetIfStartsWith(ReadOnlySpan<char> span, [ConstantExpected] string match, int matchResult, scoped ref int result, ref int maxMatchStrLen)
3629+
{
3630+
if (span.StartsWith(match, StringComparison.OrdinalIgnoreCase))
3631+
{
3632+
result = matchResult;
3633+
maxMatchStrLen = match.Length;
3634+
}
3635+
}
3636+
35083637
/*=================================MatchEraName==================================
35093638
**Action: Parse era name from string starting at str.Index.
35103639
**Returns: An era value.

0 commit comments

Comments
 (0)