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

Improve DateTime{Offset} "r" and "o" formatting performance #17092

Merged
merged 1 commit into from
Mar 21, 2018

Conversation

stephentoub
Copy link
Member

Two main changes:

  1. Rewrote the formatting to use span directly rather than going through StringBuilder, 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. But I also took advantage of a few things that can be done only in corelib.
  2. Avoided [ThreadStatic] lookups unless necessary.

Before:

Method Mean Error StdDev
ToStringO 2.201 ms 0.0434 ms 0.0445 ms
ToStringR 1.881 ms 0.0363 ms 0.0484 ms
TryFormatO 2.098 ms 0.0387 ms 0.0362 ms
TryFormatR 1.809 ms 0.0359 ms 0.0399 ms

After:

Method Mean Error StdDev
ToStringO 942.6 us 5.217 us 4.356 us
ToStringR 591.4 us 5.026 us 4.701 us
TryFormatO 827.9 us 6.094 us 5.402 us
TryFormatR 521.1 us 3.701 us 3.462 us

Test:

using System;
using BenchmarkDotNet.Attributes.Jobs;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

[InProcess]
public class Program
{
    public static void Main() => BenchmarkRunner.Run<Program>();
    private const int Iters = 10_000;
    private static readonly DateTime s_now = DateTime.UtcNow;
    private static readonly char[] s_buffer = new char[100];

    [Benchmark]
    public void ToStringO()
    {
        DateTime dt = s_now;
        for (int i = 0; i < Iters; i++) dt.ToString("O");
    }

    [Benchmark]
    public void ToStringR()
    {
        DateTime dt = s_now;
        for (int i = 0; i < Iters; i++) dt.ToString("R");
    }

    [Benchmark]
    public void TryFormatO()
    {
        DateTime dt = s_now;
        Span<char> arr = s_buffer;
        for (int i = 0; i < Iters; i++) dt.TryFormat(arr, out int charsWritten, "O");
    }

    [Benchmark]
    public void TryFormatR()
    {
        DateTime dt = s_now;
        Span<char> arr = s_buffer;
        for (int i = 0; i < Iters; i++) dt.TryFormat(arr, out int charsWritten, "R");
    }
}

cc: @danmosemsft, @jkotas, @ahsonkhan

@stephentoub
Copy link
Member Author

stephentoub commented Mar 21, 2018

FYI @Petermarcu, since you previously did some optimizations for allocations with these formats.

@Petermarcu
Copy link
Member

Nice win!

@danmoseley danmoseley requested a review from tarekgh March 21, 2018 16:17
}

public String ToString(IFormatProvider formatProvider)
{
return DateTimeFormat.Format(ClockDateTime, null, DateTimeFormatInfo.GetInstance(formatProvider), Offset);
return DateTimeFormat.Format(ClockDateTime, null, null, Offset);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we pass the formatProvider here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops! Yes, thanks.

}

public String ToString(String format, IFormatProvider formatProvider)
{
return DateTimeFormat.Format(ClockDateTime, format, DateTimeFormatInfo.GetInstance(formatProvider), Offset);
return DateTimeFormat.Format(ClockDateTime, format, null, Offset);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we pass the formatProvider here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops! Yes, thanks.

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.
private static void Append2DigitNumber(StringBuilder result, int val)
{
result.Append((char)('0' + (val / 10)));
result.Append((char)('0' + (val % 10)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could drop the the two idivs?

int tens = (int)((val * 205u) >> 11); // div10, valid to 1028
result.Append((char)('0' + tens);
result.Append((char)('0' + (val - (tens * 10));

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually the Jit will already do this since its a const

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually the Jit will already do this since its a const

Yeah, all of the ones that have been commented on here involve a const denominator. I'm going to let the JIT do its thing.

private static void Append2DigitNumber(StringBuilder result, int val)
{
result.Append((char)('0' + (val / 10)));
result.Append((char)('0' + (val % 10)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the DivMod helper method that we use in Utf8Formatter?

image

private static void Append2DigitNumber(StringBuilder result, int val)
{
    result.Append((char)('0' + (val / 10)));
    result.Append((char)('0' + (val % 10)));
}

private static void Append2DigitNumber2(StringBuilder result, int val)
{
    uint div = DivMod((uint)val, out uint modulo);
    result.Append((char)('0' + div));
    result.Append((char)('0' + modulo));
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint DivMod(uint numerator, out uint modulo)
{
    uint div = numerator / 10;
    modulo = numerator - (div * 10);
    return div;
}

Copy link
Member Author

@stephentoub stephentoub Mar 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the DivMod helper method that we use in Utf8Formatter?

I can switch to Math.DivRem if it makes a difference.

case 'r':
case 'R':
const int FormatRLength = 29;
string str = string.FastAllocateString(FormatRLength);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please explain why we use a stackalloc span for 'o' (and then call ToString), but create a string for 'r'?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because 'r' is always a fixed result length; 'o' is not.

charsWritten = charsRequired;

// Hoist most of the bounds checks on destination.
{ var unused = destination[MinimumBytesNeeded - 1]; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting trick. @AndyAyersMS, the JIT can exclude bounds checks within loops. Any way for it to optimize out the bounds checks for multiple uses outside of a loop, such as this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This is just ported from the Utf8Formatter implementation.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, if the array length is not known, and the code is writing at fixed indices, write or access the largest first, and the jit should know the subsequent smaller indices are in bounds. More or less what is done here.

If indices contain unknown values the jit can sometimes reason about similar patterns, but not always.

val = val / 10;
index++;
ulong temp = '0' + value;
value /= 10;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you add the DivMod helper, we can call it in all these places.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you add the DivMod helper

Math already has a DivRem method.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, we have access to that here.

Copy link
Member

@ahsonkhan ahsonkhan Mar 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DivRem isn't much better (difference between int parameters vs uint parameters):
image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also note that this code came from Utf8Formatter.

Copy link
Member

@ahsonkhan ahsonkhan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants