From 06be68e39634b0fa6490a2d500912e83dc78ed1e Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 21 Mar 2018 17:35:56 -0400 Subject: [PATCH] Improve DateTime{Offset} "r" and "o" formatting performance (#17092) Two main changes: 1. Rewrote the formatting to use span, only to then discover that we already had almost exactly the same implementation in Utf8Formatter. As that one had some extra optimizations around JIT behaviors, I ported that over instead. 2. Avoided [ThreadStatic] lookups unless necessary. ToString/TryFormat for "o"/"O" improve by ~2.5x. ToString/TryFormat for "r"/"R" improve by ~3x. --- src/mscorlib/shared/System/DateTime.cs | 18 +- src/mscorlib/shared/System/DateTimeOffset.cs | 10 +- .../System/Globalization/DateTimeFormat.cs | 304 +++++++++++++----- 3 files changed, 244 insertions(+), 88 deletions(-) diff --git a/src/mscorlib/shared/System/DateTime.cs b/src/mscorlib/shared/System/DateTime.cs index d3116ee25a92..9c3b3989e4a6 100644 --- a/src/mscorlib/shared/System/DateTime.cs +++ b/src/mscorlib/shared/System/DateTime.cs @@ -1255,46 +1255,46 @@ internal DateTime ToLocalTime(bool throwOnOverflow) public String ToLongDateString() { - return DateTimeFormat.Format(this, "D", DateTimeFormatInfo.CurrentInfo); + return DateTimeFormat.Format(this, "D", null); } public String ToLongTimeString() { - return DateTimeFormat.Format(this, "T", DateTimeFormatInfo.CurrentInfo); + return DateTimeFormat.Format(this, "T", null); } public String ToShortDateString() { - return DateTimeFormat.Format(this, "d", DateTimeFormatInfo.CurrentInfo); + return DateTimeFormat.Format(this, "d", null); } public String ToShortTimeString() { - return DateTimeFormat.Format(this, "t", DateTimeFormatInfo.CurrentInfo); + return DateTimeFormat.Format(this, "t", null); } public override String ToString() { - return DateTimeFormat.Format(this, null, DateTimeFormatInfo.CurrentInfo); + return DateTimeFormat.Format(this, null, null); } public String ToString(String format) { - return DateTimeFormat.Format(this, format, DateTimeFormatInfo.CurrentInfo); + return DateTimeFormat.Format(this, format, null); } public String ToString(IFormatProvider provider) { - return DateTimeFormat.Format(this, null, DateTimeFormatInfo.GetInstance(provider)); + return DateTimeFormat.Format(this, null, provider); } public String ToString(String format, IFormatProvider provider) { - return DateTimeFormat.Format(this, format, DateTimeFormatInfo.GetInstance(provider)); + return DateTimeFormat.Format(this, format, provider); } public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider provider = null) => - DateTimeFormat.TryFormat(this, destination, out charsWritten, format, DateTimeFormatInfo.GetInstance(provider)); + DateTimeFormat.TryFormat(this, destination, out charsWritten, format, provider); public DateTime ToUniversalTime() { diff --git a/src/mscorlib/shared/System/DateTimeOffset.cs b/src/mscorlib/shared/System/DateTimeOffset.cs index 1498f9365c89..3c3f3f42a299 100644 --- a/src/mscorlib/shared/System/DateTimeOffset.cs +++ b/src/mscorlib/shared/System/DateTimeOffset.cs @@ -755,26 +755,26 @@ internal DateTimeOffset ToLocalTime(bool throwOnOverflow) public override String ToString() { - return DateTimeFormat.Format(ClockDateTime, null, DateTimeFormatInfo.CurrentInfo, Offset); + return DateTimeFormat.Format(ClockDateTime, null, null, Offset); } public String ToString(String format) { - return DateTimeFormat.Format(ClockDateTime, format, DateTimeFormatInfo.CurrentInfo, Offset); + return DateTimeFormat.Format(ClockDateTime, format, null, Offset); } public String ToString(IFormatProvider formatProvider) { - return DateTimeFormat.Format(ClockDateTime, null, DateTimeFormatInfo.GetInstance(formatProvider), Offset); + return DateTimeFormat.Format(ClockDateTime, null, formatProvider, Offset); } public String ToString(String format, IFormatProvider formatProvider) { - return DateTimeFormat.Format(ClockDateTime, format, DateTimeFormatInfo.GetInstance(formatProvider), Offset); + return DateTimeFormat.Format(ClockDateTime, format, formatProvider, Offset); } public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider formatProvider = null) => - DateTimeFormat.TryFormat(ClockDateTime, destination, out charsWritten, format, DateTimeFormatInfo.GetInstance(formatProvider), Offset); + DateTimeFormat.TryFormat(ClockDateTime, destination, out charsWritten, format, formatProvider, Offset); public DateTimeOffset ToUniversalTime() { diff --git a/src/mscorlib/shared/System/Globalization/DateTimeFormat.cs b/src/mscorlib/shared/System/Globalization/DateTimeFormat.cs index cd3a1500334e..092ad0365d5d 100644 --- a/src/mscorlib/shared/System/Globalization/DateTimeFormat.cs +++ b/src/mscorlib/shared/System/Globalization/DateTimeFormat.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.Text; using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; namespace System { @@ -852,9 +853,15 @@ private static void FormatCustomizedRoundripTimeZone(DateTime dateTime, TimeSpan offset = offset.Negate(); } - AppendNumber(result, offset.Hours, 2); + Append2DigitNumber(result, offset.Hours); result.Append(':'); - AppendNumber(result, offset.Minutes, 2); + Append2DigitNumber(result, offset.Minutes); + } + + private static void Append2DigitNumber(StringBuilder result, int val) + { + result.Append((char)('0' + (val / 10))); + result.Append((char)('0' + (val % 10))); } internal static String GetRealFormat(ReadOnlySpan format, DateTimeFormatInfo dtfi) @@ -981,19 +988,65 @@ private static String ExpandPredefinedFormat(ReadOnlySpan format, ref Date return GetRealFormat(format, dtfi); } - internal static String Format(DateTime dateTime, String format, DateTimeFormatInfo dtfi) + internal static String Format(DateTime dateTime, String format, IFormatProvider provider) { - return Format(dateTime, format, dtfi, NullOffset); + return Format(dateTime, format, provider, NullOffset); } - internal static string Format(DateTime dateTime, String format, DateTimeFormatInfo dtfi, TimeSpan offset) => - StringBuilderCache.GetStringAndRelease(FormatStringBuilder(dateTime, format, dtfi, offset)); + internal static string Format(DateTime dateTime, String format, IFormatProvider provider, TimeSpan offset) + { + if (format != null && format.Length == 1) + { + // Optimize for these standard formats that are not affected by culture. + switch (format[0]) + { + // Round trip format + case 'o': + case 'O': + const int MinFormatOLength = 27, MaxFormatOLength = 33; + Span span = stackalloc char[MaxFormatOLength]; + TryFormatO(dateTime, offset, span, out int ochars); + Debug.Assert(ochars >= MinFormatOLength && ochars <= MaxFormatOLength); + return span.Slice(0, ochars).ToString(); + + // RFC1123 + case 'r': + case 'R': + const int FormatRLength = 29; + string str = string.FastAllocateString(FormatRLength); + TryFormatR(dateTime, offset, new Span(ref str.GetRawStringData(), str.Length), out int rchars); + Debug.Assert(rchars == str.Length); + return str; + } + } + + DateTimeFormatInfo dtfi = DateTimeFormatInfo.GetInstance(provider); + return StringBuilderCache.GetStringAndRelease(FormatStringBuilder(dateTime, format, dtfi, offset)); + } - internal static bool TryFormat(DateTime dateTime, Span destination, out int charsWritten, ReadOnlySpan format, DateTimeFormatInfo dtfi) => - TryFormat(dateTime, destination, out charsWritten, format, dtfi, NullOffset); + internal static bool TryFormat(DateTime dateTime, Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider provider) => + TryFormat(dateTime, destination, out charsWritten, format, provider, NullOffset); - internal static bool TryFormat(DateTime dateTime, Span destination, out int charsWritten, ReadOnlySpan format, DateTimeFormatInfo dtfi, TimeSpan offset) + internal static bool TryFormat(DateTime dateTime, Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider provider, TimeSpan offset) { + if (format.Length == 1) + { + // Optimize for these standard formats that are not affected by culture. + switch (format[0]) + { + // Round trip format + case 'o': + case 'O': + return TryFormatO(dateTime, offset, destination, out charsWritten); + + // RFC1123 + case 'r': + case 'R': + return TryFormatR(dateTime, offset, destination, out charsWritten); + } + } + + DateTimeFormatInfo dtfi = DateTimeFormatInfo.GetInstance(provider); StringBuilder sb = FormatStringBuilder(dateTime, format, dtfi, offset); bool success = sb.Length <= destination.Length; @@ -1011,7 +1064,7 @@ internal static bool TryFormat(DateTime dateTime, Span destination, out in return success; } - internal static StringBuilder FormatStringBuilder(DateTime dateTime, ReadOnlySpan format, DateTimeFormatInfo dtfi, TimeSpan offset) + private static StringBuilder FormatStringBuilder(DateTime dateTime, ReadOnlySpan format, DateTimeFormatInfo dtfi, TimeSpan offset) { Debug.Assert(dtfi != null); if (format.Length == 0) @@ -1060,101 +1113,204 @@ internal static StringBuilder FormatStringBuilder(DateTime dateTime, ReadOnlySpa if (format.Length == 1) { - switch (format[0]) + format = ExpandPredefinedFormat(format, ref dateTime, ref dtfi, ref offset); + } + + return FormatCustomized(dateTime, format, dtfi, offset, result: null); + } + + // Roundtrippable format. One of + // 012345678901234567890123456789012 + // --------------------------------- + // 2017-06-12T05:30:45.7680000-07:00 + // 2017-06-12T05:30:45.7680000Z (Z is short for "+00:00" but also distinguishes DateTimeKind.Utc from DateTimeKind.Local) + // 2017-06-12T05:30:45.7680000 (interpreted as local time wrt to current time zone) + private static bool TryFormatO(DateTime dateTime, TimeSpan offset, Span destination, out int charsWritten) + { + const int MinimumBytesNeeded = 27; + + int charsRequired = MinimumBytesNeeded; + DateTimeKind kind = DateTimeKind.Local; + + if (offset == NullOffset) + { + kind = dateTime.Kind; + if (kind == DateTimeKind.Local) { - case 'O': - case 'o': - return FastFormatRoundtrip(dateTime, offset); - case 'R': - case 'r': - return FastFormatRfc1123(dateTime, offset, dtfi); + offset = TimeZoneInfo.Local.GetUtcOffset(dateTime); + charsRequired += 6; + } + else if (kind == DateTimeKind.Utc) + { + charsRequired += 1; } + } + else + { + charsRequired += 6; + } - format = ExpandPredefinedFormat(format, ref dateTime, ref dtfi, ref offset); + if (destination.Length < charsRequired) + { + charsWritten = 0; + return false; } + charsWritten = charsRequired; + + // Hoist most of the bounds checks on destination. + { var unused = destination[MinimumBytesNeeded - 1]; } + + WriteFourDecimalDigits((uint)dateTime.Year, destination, 0); + destination[4] = '-'; + WriteTwoDecimalDigits((uint)dateTime.Month, destination, 5); + destination[7] = '-'; + WriteTwoDecimalDigits((uint)dateTime.Day, destination, 8); + destination[10] = 'T'; + WriteTwoDecimalDigits((uint)dateTime.Hour, destination, 11); + destination[13] = ':'; + WriteTwoDecimalDigits((uint)dateTime.Minute, destination, 14); + destination[16] = ':'; + WriteTwoDecimalDigits((uint)dateTime.Second, destination, 17); + destination[19] = '.'; + WriteDigits((uint)((ulong)dateTime.Ticks % (ulong)TimeSpan.TicksPerSecond), destination.Slice(20, 7)); + + if (kind == DateTimeKind.Local) + { + char sign; + if (offset < default(TimeSpan) /* a "const" version of TimeSpan.Zero */) + { + sign = '-'; + offset = TimeSpan.FromTicks(-offset.Ticks); + } + else + { + sign = '+'; + } - return FormatCustomized(dateTime, format, dtfi, offset, result: null); + // Writing the value backward allows the JIT to optimize by + // performing a single bounds check against buffer. + WriteTwoDecimalDigits((uint)offset.Minutes, destination, 31); + destination[30] = ':'; + WriteTwoDecimalDigits((uint)offset.Hours, destination, 28); + destination[27] = sign; + } + else if (kind == DateTimeKind.Utc) + { + destination[27] = 'Z'; + } + + return true; } - internal static StringBuilder FastFormatRfc1123(DateTime dateTime, TimeSpan offset, DateTimeFormatInfo dtfi) + // Rfc1123 + // 01234567890123456789012345678 + // ----------------------------- + // Tue, 03 Jan 2017 08:08:05 GMT + private static bool TryFormatR(DateTime dateTime, TimeSpan offset, Span destination, out int charsWritten) { - // ddd, dd MMM yyyy HH:mm:ss GMT - const int Rfc1123FormatLength = 29; - StringBuilder result = StringBuilderCache.Acquire(Rfc1123FormatLength); + // Writing the check in this fashion elides all bounds checks on 'destination' + // for the remainder of the method. + if (28 >= (uint)destination.Length) + { + charsWritten = 0; + return false; + } if (offset != NullOffset) { - // Convert to UTC invariants + // Convert to UTC invariants. dateTime = dateTime - offset; } dateTime.GetDatePart(out int year, out int month, out int day); - result.Append(InvariantAbbreviatedDayNames[(int)dateTime.DayOfWeek]); - result.Append(','); - result.Append(' '); - AppendNumber(result, day, 2); - result.Append(' '); - result.Append(InvariantAbbreviatedMonthNames[month - 1]); - result.Append(' '); - AppendNumber(result, year, 4); - result.Append(' '); - AppendHHmmssTimeOfDay(result, dateTime); - result.Append(' '); - result.Append(Gmt); - return result; + string dayAbbrev = InvariantAbbreviatedDayNames[(int)dateTime.DayOfWeek]; + Debug.Assert(dayAbbrev.Length == 3); + + string monthAbbrev = InvariantAbbreviatedMonthNames[month - 1]; + Debug.Assert(monthAbbrev.Length == 3); + + destination[0] = dayAbbrev[0]; + destination[1] = dayAbbrev[1]; + destination[2] = dayAbbrev[2]; + destination[3] = ','; + destination[4] = ' '; + WriteTwoDecimalDigits((uint)day, destination, 5); + destination[7] = ' '; + destination[8] = monthAbbrev[0]; + destination[9] = monthAbbrev[1]; + destination[10] = monthAbbrev[2]; + destination[11] = ' '; + WriteFourDecimalDigits((uint)year, destination, 12); + destination[16] = ' '; + WriteTwoDecimalDigits((uint)dateTime.Hour, destination, 17); + destination[19] = ':'; + WriteTwoDecimalDigits((uint)dateTime.Minute, destination, 20); + destination[22] = ':'; + WriteTwoDecimalDigits((uint)dateTime.Second, destination, 23); + destination[25] = ' '; + destination[26] = 'G'; + destination[27] = 'M'; + destination[28] = 'T'; + + charsWritten = 29; + return true; } - internal static StringBuilder FastFormatRoundtrip(DateTime dateTime, TimeSpan offset) + /// + /// Writes a value [ 00 .. 99 ] to the buffer starting at the specified offset. + /// This method performs best when the starting index is a constant literal. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteTwoDecimalDigits(uint value, Span destination, int offset) { - // yyyy-MM-ddTHH:mm:ss.fffffffK - const int roundTripFormatLength = 28; - StringBuilder result = StringBuilderCache.Acquire(roundTripFormatLength); + Debug.Assert(0 <= value && value <= 99); - dateTime.GetDatePart(out int year, out int month, out int day); - AppendNumber(result, year, 4); - result.Append('-'); - AppendNumber(result, month, 2); - result.Append('-'); - AppendNumber(result, day, 2); - result.Append('T'); - AppendHHmmssTimeOfDay(result, dateTime); - result.Append('.'); + uint temp = '0' + value; + value /= 10; + destination[offset + 1] = (char)(temp - (value * 10)); + destination[offset] = (char)('0' + value); + } - long fraction = dateTime.Ticks % TimeSpan.TicksPerSecond; - AppendNumber(result, fraction, 7); + /// + /// Writes a value [ 0000 .. 9999 ] to the buffer starting at the specified offset. + /// This method performs best when the starting index is a constant literal. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteFourDecimalDigits(uint value, Span buffer, int startingIndex = 0) + { + Debug.Assert(0 <= value && value <= 9999); - FormatCustomizedRoundripTimeZone(dateTime, offset, result); + uint temp = '0' + value; + value /= 10; + buffer[startingIndex + 3] = (char)(temp - (value * 10)); - return result; - } + temp = '0' + value; + value /= 10; + buffer[startingIndex + 2] = (char)(temp - (value * 10)); - private static void AppendHHmmssTimeOfDay(StringBuilder result, DateTime dateTime) - { - // HH:mm:ss - AppendNumber(result, dateTime.Hour, 2); - result.Append(':'); - AppendNumber(result, dateTime.Minute, 2); - result.Append(':'); - AppendNumber(result, dateTime.Second, 2); + temp = '0' + value; + value /= 10; + buffer[startingIndex + 1] = (char)(temp - (value * 10)); + + buffer[startingIndex] = (char)('0' + value); } - internal static void AppendNumber(StringBuilder builder, long val, int digits) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteDigits(ulong value, Span buffer) { - for (int i = 0; i < digits; i++) - { - builder.Append('0'); - } + // We can mutate the 'value' parameter since it's a copy-by-value local. + // It'll be used to represent the value left over after each division by 10. - int index = 1; - while (val > 0 && index <= digits) + for (int i = buffer.Length - 1; i >= 1; i--) { - builder[builder.Length - index] = (char)('0' + (val % 10)); - val = val / 10; - index++; + ulong temp = '0' + value; + value /= 10; + buffer[i] = (char)(temp - (value * 10)); } - Debug.Assert(val == 0, "DateTimeFormat.AppendNumber(): digits less than size of val"); + Debug.Assert(value < 10); + buffer[0] = (char)('0' + value); } internal static String[] GetAllDateTimes(DateTime dateTime, char format, DateTimeFormatInfo dtfi)