代码之家  ›  专栏  ›  技术社区  ›  Thomas

DateTime to string,在F#中,这可以进一步优化吗?

  •  0
  • Thomas  · 技术社区  · 5 年前

    我有一个系统,可以进行一些模拟,并输出大量带有时间戳的日志信息。

    看到这个约会时间我很惊讶。ToString()在循环中非常昂贵(它的调用非常多),所以我尝试制作一个更快的版本,输出日期、时间和毫秒。

    这能更快地完成吗(输出需要毫秒)?我没有尝试使用指针,因为我认为(可能是错误的)对于这样一小段代码,固定对象的开销会更高。

    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)
    

    以下是测试代码:

    let a = DateTime.UtcNow
    let iterations = 10_000_000
    let sw = Stopwatch()
    
    sw.Start()
    for i = 0 to iterations do
        a.ToString() |> ignore
    sw.Stop()
    printfn $"original no ms display {sw.ElapsedMilliseconds} ms"
    
    sw.Reset()
    sw.Start()
    for i = 0 to iterations do
        a.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture) |> ignore
    sw.Stop()
    printfn $"original with ms display {sw.ElapsedMilliseconds} ms"
    
    sw.Reset()
    sw.Start()
    for i = 0 to iterations do
        a |> DateTimeFormatter.format |> ignore
    sw.Stop()
    printfn $"new with ms display {sw.ElapsedMilliseconds} ms"
    

    以及测试结果(MBP i7 2019):

    original no ms display   2892 ms
    
    original with ms display 4042 ms
    new with ms display      1435 ms
    
    0 回复  |  直到 5 年前
        1
  •  1
  •   Sergey Berezovskiy    5 年前

    我需要改进的地方

    • 避免重新创建字符数组

    • 避免在字符数组中重新分配分隔符——它们永远不会改变

    • 避免重新分配未更改的部分日期。如果你担心毫秒,我会假设年、月、日、小时、分钟甚至秒不会经常改变。

    • 避免重新计算 int '0' -它永远不会改变

    • 避免额外的函数调用

      let format =
           let mutable year = -1
           let mutable month = -1
           let mutable day = -1
           let mutable hour = -1
           let mutable minute = -1
           let mutable second = -1
           let array = "0000-00-00 00:00:00.000".ToCharArray()
           let zeroChar = int '0'
      
           fun (dateTime: DateTime) ->
               if dateTime.Year <> year then
                   year <- dateTime.Year
                   array.[0] <- char (zeroChar + year / 1000)
                   array.[1] <- char (zeroChar + (year % 1000) / 100)
                   array.[2] <- char (zeroChar + (year % 100) / 10)
                   array.[3] <- char (zeroChar + (year % 10))
      
               if dateTime.Month <> month then
                   month <- dateTime.Month
                   array.[5] <- char (zeroChar + month / 10)
                   array.[6] <- char (zeroChar + month % 10)
      
               if dateTime.Day <> day then
                   day <- dateTime.Day
                   array.[8] <- char (zeroChar + day / 10)
                   array.[9] <- char (zeroChar + day % 10)
      
               if dateTime.Hour <> hour then
                   hour <- dateTime.Hour
                   array.[11] <- char (zeroChar + hour / 10)
                   array.[12] <- char (zeroChar + hour % 10)
      
               if dateTime.Minute <> minute then
                   minute <- dateTime.Minute
                   array.[14] <- char (zeroChar + minute / 10)
                   array.[15] <- char (zeroChar + minute % 10)
      
               if dateTime.Second <> second then
                   second <- dateTime.Second
                   array.[17] <- char (zeroChar + second / 10)
                   array.[18] <- char (zeroChar + second % 10)
      
               let ms = dateTime.Millisecond
               array.[20] <- char (zeroChar + ms / 100)
               array.[21] <- char (zeroChar + (ms % 100) / 10)
               array.[22] <- char (zeroChar + ms % 10)
      
               new string(array)
      

    在测试用例中运行它会显示x2相对于您的解决方案的性能,以及x5相对于原始解决方案的性能。

    original no ms display 2354 ms
    original with ms display 3545 ms
    new with ms display 1221 ms
    newest with ms display 691 ms
    

    进一步的优化可能是避免DateTime属性调用,并根据滴答声手动计算值。

        2
  •  1
  •   kaefer    5 年前

    注释中已经提到了重用数组和不重写常量字符。进一步考虑:

    • 使用 inline 关键字似乎不会影响“内联”相应表达式的编译器优化;通过避免对数组索引进行算术运算,可以更好地实现这一目标
    • 打电话给 System.DateTime 房地产开发商似乎很昂贵
    • 返回值是通过调用该类型的构造函数生成的 System.String ,为了什么 new string(c) 这是C#的称呼吗

    因此,请看一下这一点——1的额外分区似乎并没有减缓速度:

    let internal c = "0000-00-00T00:00:00.000".ToCharArray()
    let internal (%&) x m = char(48 + (x / m) % 10)
    let format (a : System.DateTime) =
        let y, m, d, h, min, s, ms =
            a.Year, a.Month, a.Day,
            a.Hour, a.Minute, a.Second,
            a.Millisecond
        c.[ 0] <- y   %& 1000
        c.[ 1] <- y   %& 100
        c.[ 2] <- y   %& 10
        c.[ 3] <- y   %& 1
        c.[ 5] <- m   %& 10
        c.[ 6] <- m   %& 1
        c.[ 8] <- d   %& 10
        c.[ 9] <- d   %& 1
        c.[11] <- h   %& 10
        c.[12] <- h   %& 1
        c.[14] <- min %& 10
        c.[15] <- min %& 1
        c.[17] <- s   %& 10
        c.[18] <- s   %& 1
        c.[20] <- ms  %& 100
        c.[21] <- ms  %& 10
        c.[22] <- ms  %& 1
        System.String c
    

    这有望被编译成以下C#代码:

    public static string format(DateTime a)
    {
        int year = a.Year;
        ...
        c[0] = (char)(48 + year / 1000 % 10);
        c[1] = (char)(48 + year / 100 % 10);
        c[2] = (char)(48 + year / 10 % 10);
        c[3] = (char)(48 + year / 1 % 10);
        ...
        return new string(c);
    }