Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.

Commit

Permalink
Improve TimeSpan.ToString/TryFormat throughput for default format (#1…
Browse files Browse the repository at this point in the history
  • Loading branch information
stephentoub authored Jul 18, 2018
1 parent c06c61a commit 965ad0c
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 67 deletions.
253 changes: 191 additions & 62 deletions src/System.Private.CoreLib/shared/System/Globalization/TimeSpanFormat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Buffers.Text;
using System.Text;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace System.Globalization
Expand Down Expand Up @@ -35,66 +37,53 @@ private static unsafe void AppendNonNegativeInt32(StringBuilder sb, int n, int d
internal static readonly FormatLiterals PositiveInvariantFormatLiterals = TimeSpanFormat.FormatLiterals.InitInvariant(isNegative: false);
internal static readonly FormatLiterals NegativeInvariantFormatLiterals = TimeSpanFormat.FormatLiterals.InitInvariant(isNegative: true);

internal enum Pattern
{
None = 0,
Minimum = 1,
Full = 2,
}

/// <summary>Main method called from TimeSpan.ToString.</summary>
internal static string Format(TimeSpan value, string format, IFormatProvider formatProvider) =>
StringBuilderCache.GetStringAndRelease(FormatToBuilder(value, format, formatProvider));
internal static string Format(TimeSpan value, string format, IFormatProvider formatProvider)
{
return IsFormatC(format) ? // special-case to optimize the default TimeSpan format
FormatC(value) : // formatProvider ignored, as "c" is invariant
StringBuilderCache.GetStringAndRelease(FormatToBuilder(value, format, formatProvider));
}

/// <summary>Main method called from TimeSpan.TryFormat.</summary>
internal static bool TryFormat(TimeSpan value, Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider formatProvider)
{
if (IsFormatC(format)) // special-case to optimize the default TimeSpan format
{
return TryFormatC(value, destination, out charsWritten); // formatProvider ignored, as "c" is invariant
}

StringBuilder sb = FormatToBuilder(value, format, formatProvider);

if (sb.Length <= destination.Length)
{
charsWritten = sb.Length;
sb.CopyTo(0, destination, sb.Length);
charsWritten = sb.Length;
StringBuilderCache.Release(sb);
return true;
}
else
{
StringBuilderCache.Release(sb);
charsWritten = 0;
return false;
}

charsWritten = 0;
StringBuilderCache.Release(sb);
return false;
}

private static StringBuilder FormatToBuilder(TimeSpan value, ReadOnlySpan<char> format, IFormatProvider formatProvider)
{
if (format.Length == 0)
{
format = "c";
}

// Standard formats
// Standard formats other than 'c'/'t'/'T', which should have already been handled.
if (format.Length == 1)
{
char f = format[0];
switch (f)
{
case 'c':
case 't':
case 'T':
return FormatStandard(
value,
isInvariant: true,
format: format,
pattern: Pattern.Minimum);

case 'g':
case 'G':
DateTimeFormatInfo dtfi = DateTimeFormatInfo.GetInstance(formatProvider);
return FormatStandard(
return FormatG(
value,
isInvariant: false,
format: value.Ticks < 0 ? dtfi.FullTimeSpanNegativePattern : dtfi.FullTimeSpanPositivePattern,
pattern: f == 'g' ? Pattern.Minimum : Pattern.Full);
full: f == 'G');

default:
throw new FormatException(SR.Format_InvalidString);
Expand All @@ -105,10 +94,166 @@ private static StringBuilder FormatToBuilder(TimeSpan value, ReadOnlySpan<char>
return FormatCustomized(value, format, DateTimeFormatInfo.GetInstance(formatProvider), result: null);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsFormatC(ReadOnlySpan<char> format) =>
format.Length == 0 ||
(format.Length == 1 && (format[0] == 'c' || (format[0] | 0x20) == 't'));

internal static string FormatC(TimeSpan value)
{
Span<char> destination = stackalloc char[26]; // large enough for any "c" TimeSpan
TryFormatC(value, destination, out int charsWritten);
return new string(destination.Slice(0, charsWritten));
}

private static bool TryFormatC(TimeSpan value, Span<char> destination, out int charsWritten)
{
// First, calculate how large an output buffer is needed to hold the entire output.
int requiredOutputLength = 8; // start with "hh:mm:ss" and adjust as necessary

uint fraction;
ulong totalSecondsRemaining;
{
// Turn this into a non-negative TimeSpan if possible.
long ticks = value.Ticks;
if (ticks < 0)
{
requiredOutputLength = 9; // requiredOutputLength + 1 for the leading '-' sign
ticks = -ticks;
if (ticks < 0)
{
Debug.Assert(ticks == long.MinValue /* -9223372036854775808 */);

// We computed these ahead of time; they're straight from the decimal representation of Int64.MinValue.
fraction = 4775808;
totalSecondsRemaining = 922337203685;
goto AfterComputeFraction;
}
}

totalSecondsRemaining = Math.DivRem((ulong)ticks, TimeSpan.TicksPerSecond, out ulong fraction64);
fraction = (uint)fraction64;
}

AfterComputeFraction:
// Only write out the fraction if it's non-zero, and in that
// case write out the entire fraction (all digits).
int fractionDigits = 0;
if (fraction != 0)
{
Debug.Assert(fraction < 10_000_000);
fractionDigits = DateTimeFormat.MaxSecondsFractionDigits;
requiredOutputLength += fractionDigits + 1; // If we're going to write out a fraction, also need to write the leading decimal.
}

ulong totalMinutesRemaining = 0, seconds = 0;
if (totalSecondsRemaining > 0)
{
// Only compute minutes if the TimeSpan has an absolute value of >= 1 minute.
totalMinutesRemaining = Math.DivRem(totalSecondsRemaining, 60 /* seconds per minute */, out seconds);
Debug.Assert(seconds < 60);
}

ulong totalHoursRemaining = 0, minutes = 0;
if (totalMinutesRemaining > 0)
{
// Only compute hours if the TimeSpan has an absolute value of >= 1 hour.
totalHoursRemaining = Math.DivRem(totalMinutesRemaining, 60 /* minutes per hour */, out minutes);
Debug.Assert(minutes < 60);
}

// At this point, we can switch over to 32-bit DivRem since the data has shrunk far enough.
Debug.Assert(totalHoursRemaining <= uint.MaxValue);

uint days = 0, hours = 0;
if (totalHoursRemaining > 0)
{
// Only compute days if the TimeSpan has an absolute value of >= 1 day.
days = Math.DivRem((uint)totalHoursRemaining, 24 /* hours per day */, out hours);
Debug.Assert(hours < 24);
}

int dayDigits = 0;
if (days > 0)
{
dayDigits = FormattingHelpers.CountDigits(days);
Debug.Assert(dayDigits <= 8);
requiredOutputLength += dayDigits + 1; // for the leading "d."
}

if (destination.Length < requiredOutputLength)
{
charsWritten = 0;
return false;
}

// Write leading '-' if necessary
int idx = 0;
if (value.Ticks < 0)
{
destination[idx++] = '-';
}

// Write day and separator, if necessary
if (dayDigits != 0)
{
WriteDigits(days, destination.Slice(idx, dayDigits));
idx += dayDigits;
destination[idx++] = '.';
}

// Write "hh:mm:ss"
WriteTwoDigits(hours, destination.Slice(idx));
idx += 2;
destination[idx++] = ':';
WriteTwoDigits((uint)minutes, destination.Slice(idx));
idx += 2;
destination[idx++] = ':';
WriteTwoDigits((uint)seconds, destination.Slice(idx));
idx += 2;

// Write fraction and separator, if necessary
if (fractionDigits != 0)
{
destination[idx++] = '.';
WriteDigits(fraction, destination.Slice(idx, fractionDigits));
idx += fractionDigits;
}

Debug.Assert(idx == requiredOutputLength);
charsWritten = requiredOutputLength;
return true;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteTwoDigits(uint value, Span<char> buffer)
{
Debug.Assert(buffer.Length >= 2);
uint temp = '0' + value;
value /= 10;
buffer[1] = (char)(temp - (value * 10));
buffer[0] = (char)('0' + value);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteDigits(uint value, Span<char> buffer)
{
Debug.Assert(buffer.Length > 0);

for (int i = buffer.Length - 1; i >= 1; i--)
{
uint temp = '0' + value;
value /= 10;
buffer[i] = (char)(temp - (value * 10));
}

Debug.Assert(value < 10);
buffer[0] = (char)('0' + value);
}

/// <summary>Format the TimeSpan instance using the specified format.</summary>
private static StringBuilder FormatStandard(TimeSpan value, bool isInvariant, ReadOnlySpan<char> format, Pattern pattern)
private static StringBuilder FormatG(TimeSpan value, ReadOnlySpan<char> format, bool full)
{
StringBuilder sb = StringBuilderCache.Acquire(InternalGlobalizationHelper.StringBuilderDefaultCapacity);
int day = (int)(value.Ticks / TimeSpan.TicksPerDay);
long time = value.Ticks % TimeSpan.TicksPerDay;

Expand All @@ -122,30 +267,21 @@ private static StringBuilder FormatStandard(TimeSpan value, bool isInvariant, Re
int seconds = (int)(time / TimeSpan.TicksPerSecond % 60);
int fraction = (int)(time % TimeSpan.TicksPerSecond);

FormatLiterals literal;
if (isInvariant)
{
literal = value.Ticks < 0 ?
NegativeInvariantFormatLiterals :
PositiveInvariantFormatLiterals;
}
else
{
literal = new FormatLiterals();
literal.Init(format, pattern == Pattern.Full);
}
FormatLiterals literal = new FormatLiterals();
literal.Init(format, full);

if (fraction != 0)
{
// truncate the partial second to the specified length
fraction = (int)(fraction / TimeSpanParse.Pow10(DateTimeFormat.MaxSecondsFractionDigits - literal.ff));
}

// Pattern.Full: [-]dd.hh:mm:ss.fffffff
// Pattern.Minimum: [-][d.]hh:mm:ss[.fffffff]
// full: [-]dd.hh:mm:ss.fffffff
// !full: [-][d.]hh:mm:ss[.fffffff]

StringBuilder sb = StringBuilderCache.Acquire(InternalGlobalizationHelper.StringBuilderDefaultCapacity);
sb.Append(literal.Start); // [-]
if (pattern == Pattern.Full || day != 0)
if (full || day != 0)
{
sb.Append(day); // [dd]
sb.Append(literal.DayHourSep); // [.]
Expand All @@ -155,28 +291,21 @@ private static StringBuilder FormatStandard(TimeSpan value, bool isInvariant, Re
AppendNonNegativeInt32(sb, minutes, literal.mm); // mm
sb.Append(literal.MinuteSecondSep); // :
AppendNonNegativeInt32(sb, seconds, literal.ss); // ss
if (!isInvariant && pattern == Pattern.Minimum)
if (!full)
{
int effectiveDigits = literal.ff;
while (effectiveDigits > 0)
while (effectiveDigits > 0 && fraction % 10 == 0)
{
if (fraction % 10 == 0)
{
fraction = fraction / 10;
effectiveDigits--;
}
else
{
break;
}
fraction = fraction / 10;
effectiveDigits--;
}
if (effectiveDigits > 0)
{
sb.Append(literal.SecondFractionSep); // [.FFFFFFF]
sb.Append((fraction).ToString(DateTimeFormat.fixedNumberFormats[effectiveDigits - 1], CultureInfo.InvariantCulture));
}
}
else if (pattern == Pattern.Full || fraction != 0)
else
{
sb.Append(literal.SecondFractionSep); // [.]
AppendNonNegativeInt32(sb, fraction, literal.ff); // [fffffff]
Expand Down
18 changes: 14 additions & 4 deletions src/System.Private.CoreLib/shared/System/Math.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,25 @@ public static int DivRem(int a, int b, out int result)

public static long DivRem(long a, long b, out long result)
{
// TODO https://github.com/dotnet/coreclr/issues/3439:
// Restore to using % and / when the JIT is able to eliminate one of the idivs.
// In the meantime, a * and - is measurably faster than an extra /.

long div = a / b;
result = a - (div * b);
return div;
}

internal static uint DivRem(uint a, uint b, out uint result)
{
uint div = a / b;
result = a - (div * b);
return div;
}

internal static ulong DivRem(ulong a, ulong b, out ulong result)
{
ulong div = a / b;
result = a - (div * b);
return div;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static decimal Ceiling(decimal d)
{
Expand Down
2 changes: 1 addition & 1 deletion src/System.Private.CoreLib/shared/System/TimeSpan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ public static bool TryParseExact(ReadOnlySpan<char> input, string[] formats, IFo
}
public override string ToString()
{
return TimeSpanFormat.Format(this, null, null);
return TimeSpanFormat.FormatC(this);
}
public string ToString(string format)
{
Expand Down

0 comments on commit 965ad0c

Please sign in to comment.