Reputation: 9406
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
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
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
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
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
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
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
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