How do I improve the performance of code using DateTime.ToString?
Are you sure it takes 33% of the time? How have you measured that? It sounds more than a little suspicious to me...
This makes things a little bit quicker:
Basic: 2342ms
Custom: 1319ms
Or if we cut out the IO (Stream.Null
):
Basic: 2275ms
Custom: 839ms
using System.Diagnostics;
using System;
using System.IO;
static class Program
{
static void Main()
{
DateTime when = DateTime.Now;
const int LOOP = 1000000;
Stopwatch basic = Stopwatch.StartNew();
using (TextWriter tw = new StreamWriter("basic.txt"))
{
for (int i = 0; i < LOOP; i++)
{
tw.Write(when.ToString("dd.MM.yy HH:mm:ss:fff"));
}
}
basic.Stop();
Console.WriteLine("Basic: " + basic.ElapsedMilliseconds + "ms");
char[] buffer = new char[100];
Stopwatch custom = Stopwatch.StartNew();
using (TextWriter tw = new StreamWriter("custom.txt"))
{
for (int i = 0; i < LOOP; i++)
{
WriteDateTime(tw, when, buffer);
}
}
custom.Stop();
Console.WriteLine("Custom: " + custom.ElapsedMilliseconds + "ms");
}
static void WriteDateTime(TextWriter output, DateTime when, char[] buffer)
{
buffer[2] = buffer[5] = '.';
buffer[8] = ' ';
buffer[11] = buffer[14] = buffer[17] = ':';
Write2(buffer, when.Day, 0);
Write2(buffer, when.Month, 3);
Write2(buffer, when.Year % 100, 6);
Write2(buffer, when.Hour, 9);
Write2(buffer, when.Minute, 12);
Write2(buffer, when.Second, 15);
Write3(buffer, when.Millisecond, 18);
output.Write(buffer, 0, 21);
}
static void Write2(char[] buffer, int value, int offset)
{
buffer[offset++] = (char)('0' + (value / 10));
buffer[offset] = (char)('0' + (value % 10));
}
static void Write3(char[] buffer, int value, int offset)
{
buffer[offset++] = (char)('0' + (value / 100));
buffer[offset++] = (char)('0' + ((value / 10) % 10));
buffer[offset] = (char)('0' + (value % 10));
}
}
Updated the original answer to use Span
.
public static string FormatDateTime(DateTime dateTime)
{
return string.Create(21, dateTime, (chars, dt) =>
{
Write2Chars(chars, 0, dt.Day);
chars[2] = '.';
Write2Chars(chars, 3, dt.Month);
chars[5] = '.';
Write2Chars(chars, 6, dt.Year % 100);
chars[8] = ' ';
Write2Chars(chars, 9, dt.Hour);
chars[11] = ' ';
Write2Chars(chars, 12, dt.Minute);
chars[14] = ' ';
Write2Chars(chars, 15, dt.Second);
chars[17] = ' ';
Write2Chars(chars, 18, dt.Millisecond / 10);
chars[20] = Digit(dt.Millisecond % 10);
});
}
private static void Write2Chars(in Span<char> chars, int offset, int value)
{
chars[offset] = Digit(value / 10);
chars[offset + 1] = Digit(value % 10);
}
private static char Digit(int value)
{
return (char)(value + '0');
}
Benchmark Results
BenchmarkDotNet=v0.13.1, OS=ubuntu 20.04
Intel Xeon W-1290P CPU 3.70GHz, 1 CPU, 20 logical and 10 physical cores
.NET SDK=6.0.202
[Host] : .NET 6.0.4 (6.0.422.16404), X64 RyuJIT
DefaultJob : .NET 6.0.4 (6.0.422.16404), X64 RyuJIT
| Method | Mean | Error | StdDev | Gen 0 | Allocated |
|-------------------------- |----------:|---------:|---------:|-------:|----------:|
| DateTime_Format | 225.35 ns | 1.211 ns | 1.011 ns | 0.0060 | 64 B |
| Custom_Formatter_Original | 43.00 ns | 0.188 ns | 0.147 ns | 0.0130 | 136 B |
| Custom_Formatter_Updated | 37.15 ns | 0.140 ns | 0.117 ns | 0.0061 | 64 B |
Benchmark
using System.Globalization;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
namespace Benchmark
{
[MemoryDiagnoser]
public class DateTimeToString
{
private const string DateTimeFormat = "dd.MM.yy HH:mm:ss:fff";
private readonly DateTime _now;
public DateTimeToString()
{
_now = DateTime.UtcNow;
}
[Benchmark]
public string DateTime_Format() => _now.ToString(DateTimeFormat, CultureInfo.InvariantCulture);
[Benchmark]
public string Custom_Formatter_Original()
{
return DateTimeWriterHelper.FormatDateTimeOriginal(_now);
}
[Benchmark]
public string Custom_Formatter_Updated()
{
return DateTimeWriterHelper.FormatDateTimeUpdated(_now);
}
}
public class Program
{
public static void Main(string[] args)
{
var summary = BenchmarkRunner.Run(typeof(Program).Assembly);
Console.Write(summary);
}
}
static class DateTimeWriterHelper
{
public static string FormatDateTimeOriginal(DateTime dt)
{
char[] chars = new char[21];
Write2Chars(chars, 0, dt.Day);
chars[2] = '.';
Write2Chars(chars, 3, dt.Month);
chars[5] = '.';
Write2Chars(chars, 6, dt.Year % 100);
chars[8] = ' ';
Write2Chars(chars, 9, dt.Hour);
chars[11] = ' ';
Write2Chars(chars, 12, dt.Minute);
chars[14] = ' ';
Write2Chars(chars, 15, dt.Second);
chars[17] = ' ';
Write2Chars(chars, 18, dt.Millisecond / 10);
chars[20] = Digit(dt.Millisecond % 10);
return new string(chars);
}
public static string FormatDateTimeUpdated(DateTime dateTime)
{
return string.Create(21, dateTime, (chars, dt) =>
{
Write2Chars(chars, 0, dt.Day);
chars[2] = '.';
Write2Chars(chars, 3, dt.Month);
chars[5] = '.';
Write2Chars(chars, 6, dt.Year % 100);
chars[8] = ' ';
Write2Chars(chars, 9, dt.Hour);
chars[11] = ' ';
Write2Chars(chars, 12, dt.Minute);
chars[14] = ' ';
Write2Chars(chars, 15, dt.Second);
chars[17] = ' ';
Write2Chars(chars, 18, dt.Millisecond / 10);
chars[20] = Digit(dt.Millisecond % 10);
});
}
private static void Write2Chars(in Span<char> chars, int offset, int value)
{
chars[offset] = Digit(value / 10);
chars[offset + 1] = Digit(value % 10);
}
private static char Digit(int value)
{
return (char)(value + '0');
}
}
}
This is not an answer in itself, but rather an addedum to Jon Skeet's execellent answer, offering a variant for the "s" (ISO) format:
/// <summary>
/// Implements a fast method to write a DateTime value to string, in the ISO "s" format.
/// </summary>
/// <param name="dateTime">The date time.</param>
/// <returns></returns>
/// <devdoc>
/// This implementation exists just for performance reasons, it is semantically identical to
/// <code>
/// text = value.HasValue ? value.Value.ToString("s") : string.Empty;
/// </code>
/// However, it runs about 3 times as fast. (Measured using the VS2015 performace profiler)
/// </devdoc>
public static string ToIsoStringFast(DateTime? dateTime) {
if (!dateTime.HasValue) {
return string.Empty;
}
DateTime dt = dateTime.Value;
char[] chars = new char[19];
Write4Chars(chars, 0, dt.Year);
chars[4] = '-';
Write2Chars(chars, 5, dt.Month);
chars[7] = '-';
Write2Chars(chars, 8, dt.Day);
chars[10] = 'T';
Write2Chars(chars, 11, dt.Hour);
chars[13] = ':';
Write2Chars(chars, 14, dt.Minute);
chars[16] = ':';
Write2Chars(chars, 17, dt.Second);
return new string(chars);
}
With the 4 digit serializer as:
private static void Write4Chars(char[] chars, int offset, int value) {
chars[offset] = Digit(value / 1000);
chars[offset + 1] = Digit(value / 100 % 10);
chars[offset + 2] = Digit(value / 10 % 10);
chars[offset + 3] = Digit(value % 10);
}
This runs about 3 times as fast. (Measured using the VS2015 performance profiler)
It's unfortunate that .NET doesn't have a sort of "formatter" type which can parse a pattern and remember it.
If you're always using the same format, you might want to hand-craft a formatter to do exactly that. Something along the lines of:
public static string FormatDateTime(DateTime dt)
{
// Note: there are more efficient approaches using Span<char> these days.
char[] chars = new char[21];
Write2Chars(chars, 0, dt.Day);
chars[2] = '.';
Write2Chars(chars, 3, dt.Month);
chars[5] = '.';
Write2Chars(chars, 6, dt.Year % 100);
chars[8] = ' ';
Write2Chars(chars, 9, dt.Hour);
chars[11] = ' ';
Write2Chars(chars, 12, dt.Minute);
chars[14] = ' ';
Write2Chars(chars, 15, dt.Second);
chars[17] = ' ';
Write2Chars(chars, 18, dt.Millisecond / 10);
chars[20] = Digit(dt.Millisecond % 10);
return new string(chars);
}
private static void Write2Chars(char[] chars, int offset, int value)
{
chars[offset] = Digit(value / 10);
chars[offset+1] = Digit(value % 10);
}
private static char Digit(int value)
{
return (char) (value + '0');
}
This is pretty ugly, but it's probably a lot more efficient... benchmark it, of course!