Michal Sznajder
Michal Sznajder

Reputation: 9406

How do I improve the performance of code using DateTime.ToString?

In my binary to text decoding application (.NET 2.0) I found that the line:

logEntryTime.ToString("dd.MM.yy HH:mm:ss:fff")

takes 33% of total processing time. Does anyone have any ideas on how to make it faster?

EDIT: This app is used to process some binary logs and it currently takes 15 hours to run. So 1/3 of this will be 5 hours.

EDIT: I am using NProf for profiling. App is processing around 17 GBytes of binary logs.

Upvotes: 23

Views: 5778

Answers (7)

SteveHansen
SteveHansen

Reputation: 401

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');
        }
    }
}

Upvotes: 3

Jon Skeet
Jon Skeet

Reputation: 1502935

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!

Upvotes: 24

Thomas
Thomas

Reputation: 12127

Just for reference, in F#:

module DateTimeFormatter =

    let inline private valueToDigit (value: int) : char =
        char (value + int '0')
        
    let inline private write2Characters (c: char[]) offset value =
        c.[offset + 0] <- valueToDigit (value / 10)
        c.[offset + 1] <- valueToDigit (value % 10)

    let inline private write3Characters (c: char[]) offset value =
        c.[offset + 0] <- valueToDigit (value / 100)
        c.[offset + 1] <- valueToDigit ((value % 100) / 10)
        c.[offset + 2] <- valueToDigit (value % 10)

    let format (dateTime: DateTime) =
        let c = Array.zeroCreate<char> 23
        write2Characters c 0 (dateTime.Year / 100)
        write2Characters c 2 (dateTime.Year % 100)
        c.[4] <- '/'
        write2Characters c 5 dateTime.Month
        c.[7] <- '/'
        write2Characters c 8 dateTime.Day
        c.[10] <- ' '
        write2Characters c 11 dateTime.Hour
        c.[13] <- ':'
        write2Characters c 14 dateTime.Minute
        c.[16] <- ':'
        write2Characters c 17 dateTime.Second
        c.[19] <- '.'
        write3Characters c 20 dateTime.Millisecond

        new string(c)

It is 4x faster than:

ToString("yyyy/MM/dd hh:mm:ss.fff")

Upvotes: 0

Desmond
Desmond

Reputation: 613

Expanding on Jon Skeet's reply.

If you want maximum speed you can cut the method calls and it would shave up more processing time.

For example like this for the format yyMMdd:

    public static string ATextoyyMMdd(this DateTime fechaHora) {

        var chars = new char[6];
        int valor = fechaHora.Year % 100;     
        chars[0] = (char)(valor / 10 + '0');
        chars[1] = (char)(valor % 10 + '0');
        valor = fechaHora.Month;
        chars[2] = (char)(valor / 10 + '0');
        chars[3] = (char)(valor % 10 + '0');
        valor = fechaHora.Day;
        chars[4] = (char)(valor / 10 + '0');
        chars[5] = (char)(valor % 10 + '0');
        return new string(chars);

    }

Upvotes: 0

Marcel
Marcel

Reputation: 15732

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)

Upvotes: 3

Martin Brown
Martin Brown

Reputation: 25330

Do you know how big each record in the binary and text logs are going to be? If so you can split the processing of the log file across a number of threads which would give better use of a multi core/processor PC. If you don't mind the result being in separate files it would be a good idea to have one hard disk per core that way you will reduce the amount the disk heads have to move.

Upvotes: 0

Marc Gravell
Marc Gravell

Reputation: 1063814

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));
    }
}

Upvotes: 9

Related Questions