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!