【问题标题】:Can't find why this datetime test fails, in F#在 F# 中找不到此日期时间测试失败的原因
【发布时间】:2021-08-19 16:02:30
【问题描述】:

我有以下功能,这是在一些人的帮助下制作的:

module DateTimeFormatter =

    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 dateTimeArray  = "xxxx-xx-xx xx:xx:xx.xxx".ToCharArray()
    let timeArray      = "xx:xx:xx.xxx".ToCharArray()
    let zeroChar       = int '0'

    // format the datetime into a date and a time with milliseconds
    let format (dateTime: DateTime) =

        if dateTime.Year <> year then
            year <- dateTime.Year
            dateTimeArray.[0] <- char (zeroChar + year / 1000)
            dateTimeArray.[1] <- char (zeroChar + (year % 1000) / 100)
            dateTimeArray.[2] <- char (zeroChar + (year % 100) / 10)
            dateTimeArray.[3] <- char (zeroChar + (year % 10))

        if dateTime.Month <> month then
            month <- dateTime.Month
            dateTimeArray.[5] <- char (zeroChar + month / 10)
            dateTimeArray.[6] <- char (zeroChar + month % 10)

        if dateTime.Day <> day then
            day <- dateTime.Day
            dateTimeArray.[8] <- char (zeroChar + day / 10)
            dateTimeArray.[9] <- char (zeroChar + day % 10)

        if dateTime.Hour <> hour then
            hour <- dateTime.Hour
            dateTimeArray.[11] <- char (zeroChar + hour / 10)
            dateTimeArray.[12] <- char (zeroChar + hour % 10)

        if dateTime.Minute <> minute then
            minute <- dateTime.Minute
            dateTimeArray.[14] <- char (zeroChar + minute / 10)
            dateTimeArray.[15] <- char (zeroChar + minute % 10)

        if dateTime.Second <> second then
            second <- dateTime.Second
            dateTimeArray.[17] <- char (zeroChar + second / 10)
            dateTimeArray.[18] <- char (zeroChar + second % 10)

        let ms = dateTime.Millisecond
        dateTimeArray.[20] <- char (zeroChar + ms / 100)
        dateTimeArray.[21] <- char (zeroChar + (ms % 100) / 10)
        dateTimeArray.[22] <- char (zeroChar + ms % 10)

        new string(dateTimeArray)

这个想法是尽可能快地构建日期时间 + 毫秒时间戳。

但有时,但并非总是如此,我会得到以下输出:

2021-06-01 xx:xx:53.648

而且失败总是在小时/分钟,但有时它工作正常,有时却不行。并且在构建字符串时我没有看到测试有任何问题,但看起来小时和分钟即使是第一次测试也失败了,因为有占位符“x”。

这可能是这些超级简单的事情之一,需要另一双眼睛才能看到它:)

【问题讨论】:

  • 你是否有机会同时运行多次?
  • 是的,它用于一堆异步/并行调用;我想可以想象一个调用通过了相等性测试,而另一个调用只是设置了值并且还没有设置字符,但是问题会发生一次,然后字符会在那之后设置
  • @Thomas 为每个线程创建格式化程序。否则即使你没有观察到'xx',你也会得到不可预知的结果。例如。小时可能会被刚刚开始格式化的线程修改,它会影响同时创建新字符串的线程。
  • C# 中没有编译器机制来请求每个线程的字段吗?我想知道是否有 F# 等价物。我认为有一些属性
  • 好吧,如果您声称它是间歇性发生的,而不仅仅是在前几次通过时,那么恐怕我们需要重新制作。

标签: f#


【解决方案1】:

它失败了,因为模块中的可变变量和dateTimeArray 正在从不同的线程中遍历。这不是线程安全的代码。

我发布了一些我现在已经删除的 cmets,因为我看代码的时间不够长,无法理解其意图。意图显然是优化,但它失败了。简化函数实际上会提高性能。额外的逻辑和额外的变量都没有帮助。下面的代码仍然更快。

我有点怀疑线程本地存储是否可以帮助原始代码击败这段代码,因为我认为在这种逻辑中找不到任何性能优势。我认为DateTime.ToString 的好处在于这个简化代码中剩下的内容:

let dateTimeArray = "xxxx-xx-xx xx:xx:xx.xxx".ToCharArray()
let [<Literal>] zeroChar = 48 // '0'

let format (dateTime: DateTime) =

    let dateTimeArray = Array.copy dateTimeArray

    dateTimeArray.[0] <- char (zeroChar + dateTime.Year / 1000)
    dateTimeArray.[1] <- char (zeroChar + (dateTime.Year % 1000) / 100)
    dateTimeArray.[2] <- char (zeroChar + (dateTime.Year % 100) / 10)
    dateTimeArray.[3] <- char (zeroChar + (dateTime.Year % 10))

    dateTimeArray.[5] <- char (zeroChar + dateTime.Month / 10)
    dateTimeArray.[6] <- char (zeroChar + dateTime.Month % 10)

    dateTimeArray.[8] <- char (zeroChar + dateTime.Day / 10)
    dateTimeArray.[9] <- char (zeroChar + dateTime.Day % 10)

    dateTimeArray.[11] <- char (zeroChar + dateTime.Hour / 10)
    dateTimeArray.[12] <- char (zeroChar + dateTime.Hour % 10)

    dateTimeArray.[14] <- char (zeroChar + dateTime.Minute / 10)
    dateTimeArray.[15] <- char (zeroChar + dateTime.Minute % 10)

    dateTimeArray.[17] <- char (zeroChar + dateTime.Second / 10)
    dateTimeArray.[18] <- char (zeroChar + dateTime.Second % 10)

    dateTimeArray.[20] <- char (zeroChar + dateTime.Millisecond / 100)
    dateTimeArray.[21] <- char (zeroChar + (dateTime.Millisecond % 100) / 10)
    dateTimeArray.[22] <- char (zeroChar + dateTime.Millisecond % 10)

    new string(dateTimeArray)

多次读取dateTime 的属性不会减慢函数的速度。读取一次属性,例如将dateTime.Year 读入一次本地值,然后在四个地方使用它,实际上会使函数变慢。

看起来这将节省更多时间,并且不再需要外部的dateTimeArray

let dateTimeArray: char[] =
    [|
        char (zeroChar + dateTime.Year / 1000)
        char (zeroChar + (dateTime.Year % 1000) / 100)
        char (zeroChar + (dateTime.Year % 100) / 10)
        char (zeroChar + (dateTime.Year % 10))
        '-'
        char (zeroChar + dateTime.Month / 10)
        char (zeroChar + dateTime.Month % 10)
        '-'
        char (zeroChar + dateTime.Day / 10)
        char (zeroChar + dateTime.Day % 10)
        ' '
        char (zeroChar + dateTime.Hour / 10)
        char (zeroChar + dateTime.Hour % 10)
        ':'
        char (zeroChar + dateTime.Minute / 10)
        char (zeroChar + dateTime.Minute % 10)
        ':'
        char (zeroChar + dateTime.Second / 10)
        char (zeroChar + dateTime.Second % 10)
        '.'
        char (zeroChar + dateTime.Millisecond / 100)
        char (zeroChar + (dateTime.Millisecond % 100) / 10)
        char (zeroChar + dateTime.Millisecond % 10)
    |]

我还有一个版本。 Math.DivRem 的使用似乎稍微减少了一点,但是查找数组 digits 增加了时间,尽管它带来了一些优雅。

let format (dateTime: DateTime) =
    let monthHigh, monthLow = Math.DivRem (dateTime.Month, 10)
    let dayHigh, dayLow = Math.DivRem (dateTime.Day, 10)
    let hourHigh, hourLow = Math.DivRem (dateTime.Hour, 10)
    let minuteHigh, minuteLow = Math.DivRem (dateTime.Minute, 10)
    let secondHigh, secondLow = Math.DivRem (dateTime.Second, 10)
    let digit = [| '0'; '1'; '2'; '3'; '4'; '5'; '6'; '7'; '8'; '9' |]
    [|
        digit.[dateTime.Year / 1000]
        digit.[(dateTime.Year % 1000) / 100]
        digit.[(dateTime.Year % 100) / 10]
        digit.[dateTime.Year % 10]
        '-'
        digit.[monthHigh]
        digit.[monthLow]
        '-'
        digit.[dayHigh]
        digit.[dayLow]
        ' '
        digit.[hourHigh]
        digit.[hourLow]
        ':'
        digit.[minuteHigh]
        digit.[minuteLow]
        ':'
        digit.[secondHigh]
        digit.[secondLow]
        '.'
        digit.[dateTime.Millisecond / 100]
        digit.[(dateTime.Millisecond % 100) / 10]
        digit.[dateTime.Millisecond % 10]
    |] |> (fun a -> new String(a))

【讨论】:

  • 感谢您仔细研究此问题。该代码最初是为来自流的大量数据添加时间戳;虽然它显然不是线程安全的,但我仍然不明白“x”字符被多次打印的问题。有一次我可以理解一个调用设置了测试中的变量,而另一个代码在 x 被覆盖之前通过了测试,但我不明白它怎么会发生多次。
  • 我不明白的一点是:“读取一个属性一次,[...] 到一个本地值,然后在四个地方使用它,实际上会使函数变慢。”我不知道 dotnet 编译器是如何工作的,但是,最初来自 ASM/C 世界,我无法理解这是怎么可能的,因为您会将值加载到寄存器中并从那里多次使用它,特别是因为该值的使用非常本地化,并且所涉及的代码不会在很多值之间进行处理。怎么会慢?
  • 当从多个线程不安全地访问可变数据时,所有规则都会消失。试图理解你所看到的毫无意义。你会得到这些非常奇怪的魔法效果。修改没有生效只是随之而来的混乱的一小部分。它与每个线程临时将值保存在寄存器中有关,并且不期望其他线程篡改相同的内存,这只是复杂性的一部分。那个“x”可能在内存中,或者在某个线程的缓存或寄存器中,而另一个线程实际上已经更新了内存。
  • 稍微偏离主题:我有时使用布尔值来表示从一个线程到另一个线程的信号。由于这些缓存效果,这在开箱即用时不能可靠地工作。实际上,您可能会被愚弄,只有在编译发布后才会发现问题,甚至只是在部署后才偶尔看到。但我的观点是;有一些方法可以解决它 - 但我对它们没有很好的概述。上次我做这样的事情时,我相信我使用了 .NET 调用以某种方式将布尔值标记为易失性。 F# 有一个 volatile 关键字,但不确定这是否可以解决问题 - 可能保留供将来使用。
猜你喜欢
  • 2016-08-21
  • 1970-01-01
  • 2017-12-12
  • 1970-01-01
  • 1970-01-01
  • 2018-06-03
  • 1970-01-01
  • 2017-04-10
  • 2019-03-19
相关资源
最近更新 更多