为什么通过字符串或 ToString() 将 F# 可区分联合转换为字符串如此缓慢?

Why is converting an F# discriminated union to a string via string or ToString() so slow?

有没有快速的方法将可区分的联合转换为字符串?

我试图弄清楚为什么使用各种方法将大量记录保存到 csv 文件需要几个小时。我尝试了 CsvProvider.Save、sprintf、string builder 等,但都非常慢。我想我已经将问题追溯到受歧视的联合类型转换。

我下面的例子说明了这个问题。有没有更好的方法,或者我的 "manual conversion" 是最好的选择。

#time
open System

type Field = | Ying | Yang
let manual = function | Ying -> "Ying" | Yang -> "Yang"

// Discriminated Union versions

[for i = 0 to 100000 do yield (Ying).ToString()] |> ignore
//Real: 00:00:12.963, CPU: 00:00:13.281, GC gen0: 10, gen1: 0, gen2: 0

[for i = 0 to 100000 do yield (Ying) |> manual] |> ignore
//Real: 00:00:00.004, CPU: 00:00:00.015, GC gen0: 0, gen1: 0, gen2: 0

// Others for comparison

[for i = 0 to 100000 do yield (1).ToString()] |> ignore
//Real: 00:00:00.011, CPU: 00:00:00.015, GC gen0: 0, gen1: 0, gen2: 0
[for i = 0 to 100000 do yield (1.0).ToString()] |> ignore
//Real: 00:00:00.054, CPU: 00:00:00.062, GC gen0: 0, gen1: 0, gen2: 0
[for i = 0 to 100000 do yield (1.0m).ToString()] |> ignore
//Real: 00:00:00.014, CPU: 00:00:00.015, GC gen0: 0, gen1: 0, gen2: 0


转换为字符串很慢,因为 DU 案例名称实际上是代码的一部分,而不是程序的数据。将其转换为字符串实际上是一种元编程技术,必须超出程序的正常 运行 时间,即 .NET 中的反射。

一般而言,标识符名称不会影响程序的 运行 是件好事,因为这意味着像重命名标识符这样的重构是完全安全的。

但是,如果你真的想这样做并让它变得更快,我认为最实用的解决方案是使用记忆:

let memoize fn =
    let cache = System.Collections.Concurrent.ConcurrentDictionary<'a, 'b>()
    (fun x -> cache.GetOrAdd(x, fun _ -> fn x))

let showField : Field -> string = memoize string

memoize 函数接受一个函数并创建一个函数版本,该版本缓存每个输入的输出。在每个 DU 案例 运行 一次后,showField 函数现在应该与您的 manual 函数一样快。

如果您对格式不太挑剔,也许使用 NewtonSoft.Json 序列化集合会更快。

或者您可以尝试将每个 DU 值附加到 StringBuilder,然后在 StringBuilder 上调用 ToString 以获取完整的字符串。