Improve DateTime{Offset} "r" and "o" formatting performance (#17092)
authorStephen Toub <stoub@microsoft.com>
Wed, 21 Mar 2018 21:35:56 +0000 (17:35 -0400)
committerGitHub <noreply@github.com>
Wed, 21 Mar 2018 21:35:56 +0000 (17:35 -0400)
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
src/mscorlib/shared/System/DateTimeOffset.cs
src/mscorlib/shared/System/Globalization/DateTimeFormat.cs

index d3116ee..9c3b398 100644 (file)
@@ -1255,46 +1255,46 @@ namespace System
 
         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<char> destination, out int charsWritten, ReadOnlySpan<char> 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()
         {
index 1498f93..3c3f3f4 100644 (file)
@@ -755,26 +755,26 @@ namespace System
 
         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<char> destination, out int charsWritten, ReadOnlySpan<char> 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()
         {
index cd3a150..092ad03 100644 (file)
@@ -7,6 +7,7 @@ using System.Diagnostics;
 using System.Globalization;
 using System.Text;
 using System.Runtime.InteropServices;
+using System.Runtime.CompilerServices;
 
 namespace System
 {
@@ -852,9 +853,15 @@ namespace System
                 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<char> format, DateTimeFormatInfo dtfi)
@@ -981,19 +988,65 @@ namespace System
             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<char> 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<char>(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<char> destination, out int charsWritten, ReadOnlySpan<char> format, DateTimeFormatInfo dtfi) =>
-            TryFormat(dateTime, destination, out charsWritten, format, dtfi, NullOffset);
+        internal static bool TryFormat(DateTime dateTime, Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider provider) =>
+            TryFormat(dateTime, destination, out charsWritten, format, provider, NullOffset);
 
-        internal static bool TryFormat(DateTime dateTime, Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, DateTimeFormatInfo dtfi, TimeSpan offset)
+        internal static bool TryFormat(DateTime dateTime, Span<char> destination, out int charsWritten, ReadOnlySpan<char> 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 @@ namespace System
             return success;
         }
 
-        internal static StringBuilder FormatStringBuilder(DateTime dateTime, ReadOnlySpan<char> format, DateTimeFormatInfo dtfi, TimeSpan offset)
+        private static StringBuilder FormatStringBuilder(DateTime dateTime, ReadOnlySpan<char> format, DateTimeFormatInfo dtfi, TimeSpan offset)
         {
             Debug.Assert(dtfi != null);
             if (format.Length == 0)
@@ -1060,101 +1113,204 @@ namespace System
 
             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<char> 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<char> 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)
+        /// <summary>
+        /// 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.
+        /// </summary>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static void WriteTwoDecimalDigits(uint value, Span<char> 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);
+        /// <summary>
+        /// 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.
+        /// </summary>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static void WriteFourDecimalDigits(uint value, Span<char> 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<char> 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)