【问题标题】:How do I improve the performance of code using DateTime.ToString?如何使用 DateTime.ToString 提高代码的性能?
【发布时间】:2009-07-24 07:47:02
【问题描述】:

在我的二进制文本解码应用程序 (.NET 2.0) 中,我发现以下行:

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

占用总处理时间的 33%。有没有人知道如何让它更快?

编辑:此应用程序用于处理一些二进制日志,目前需要 15 小时才能运行。因此,其中的 1/3 将是 5 小时。

编辑:我使用NProf 进行分析。应用正在处理大约 17 GB 的二进制日志。

【问题讨论】:

  • 33% 什么? 33% 的 2 毫秒处理时间甚至可能不值得重构。
  • 另外,您使用什么工具来确定 33%?不同的工具有时会测量细微的不同事物,因此值得了解...
  • 如何将结果字符串添加到输出中?字符串生成器?
  • @jonelf:StringBuilder 不适合这种类型的数据;你需要一个 TextWriter 或类似的,直接写入底层流(文件)。
  • @Martin:为什么优化 ToString 不会有成效?我们比 ToString 拥有更多的信息 - 我们在 编译时 知道模式是什么,这意味着我们不必经历解析它等的所有痛苦。

标签: c# optimization datetime formatting


【解决方案1】:

不幸的是,.NET 没有一种可以解析模式并记住它的“格式化程序”类型。

如果您总是使用相同的格式,您可能需要手工制作一个格式化程序来完全做到这一点。大致如下:

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

这很丑陋,但它可能更有效...当然是基准测试!

【讨论】:

  • ...和傻瓜很少有区别:)
  • 我为我的 ToString("s") 调用编写了一个类似的格式化程序,它的运行速度确实比原来的调用快了大约 3 倍。
  • 您可以在 .NET Core 2.1+ 中使用string.Create 进一步改进这一点。 :) 如果您引用 System.Memory 并添加几个帮助程序,即使 .NET Framework 4.7.2 也可以参与其中。
  • 自原始答案以来的小改进,使用 Span chars = stackalloc char[21];
  • @SteveHansen:在Span 可用的情况下,我可能会使用String.Create&lt;TState&gt;(Int32, TState, SpanAction&lt;Char,TState&gt;) 方法,在构造过程中直接写入字符串。我添加了一条评论提到它,但我不会回去做整个事情......
【解决方案2】:

将原始答案更新为使用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');
}

基准测试结果

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 |

基准测试

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

【讨论】:

    【解决方案3】:

    这本身不是一个答案,而是对Jon Skeet 的出色答案 的补充,提供了“s”(ISO)格式的变体:

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

    使用 4 位序列化器为:

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

    它的运行速度大约是原来的 3 倍。 (使用 VS2015 性能分析器测量)

    【讨论】:

      【解决方案4】:

      您确定需要 33% 的时间吗?你是怎么测量的?这对我来说听起来有点可疑......

      这会让事情变得稍微快一点:

      Basic: 2342ms
      Custom: 1319ms
      

      或者如果我们切断 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));
          }
      }
      

      【讨论】:

      • 我并不完全不相信——当我进行一些日志测试时,我发现格式化和解析日期和时间主导了 CPU 访问。诚然,除了记录之外,它几乎什么也没做……
      • 您的微基准测试受 IO 限制,如果您想测量 CPU 周期,您可能应该放弃写入文件。
      • 是的,但是 OP 正在写入文件,因此忽略这一点有点人为……但实际上,使用 Stream.Null 会显示出更多差异。
      【解决方案5】:

      您知道二进制和文本日志中的每条记录有多大吗?如果是这样,您可以将日志文件的处理拆分为多个线程,这将更好地利用多核/处理器 PC。如果您不介意将结果放在单独的文件中,那么每个内核都有一个硬盘是个好主意,这样可以减少磁盘磁头必须移动的数量。

      【讨论】:

      • 我认为这个答案超出了原始问题的范围。
      • @Dr.MAF 可能是这样,但最初的问题是:“有没有人知道如何让它更快?”我认为在更多的 CPU 上运行它是一种让它更快的方法,即使它不是最好的方法。
      • 是的,先生。无论如何,这很有帮助。谢谢。
      【解决方案6】:

      扩展 Jon Skeet 的回复。

      如果你想要最大速度,你可以减少方法调用,这样会节省更多的处理时间。

      例如格式为 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);
      
          }
      

      【讨论】:

        【解决方案7】:

        仅供参考,在 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)
        

        比以下速度快 4 倍:

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

        【讨论】:

          猜你喜欢
          • 2016-01-14
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多