为什么通过字符串或 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 以获取完整的字符串。
有没有快速的方法将可区分的联合转换为字符串?
我试图弄清楚为什么使用各种方法将大量记录保存到 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 以获取完整的字符串。