【问题标题】:JsonConvert.Serialize performance in F#F# 中的 JsonConvert.Serialize 性能
【发布时间】:2018-02-10 15:24:15
【问题描述】:

我创建一个记录类型如下:

type CombEmp =
    {
        empid:int
        empname:string
        email:string                       
    }

let defCombEmp:CombEmp =
    {
        empid = 0
        empname = ""
        email = ""
    }

然后我创建记录实例:

let chrE1 = {defCombEmp with empid = 100; empname = "Wayne Rooney"; email = "wroo@mun.com"}
let chrE2 = {defCombEmp with empid = 100; empname = "Wayne R"; email = "war@mun.com"}
let chrE3 = {defCombEmp with empid = 100; empname = "Wayne R"; email = "rooney@mun.com"}    

然后我使用上述实例创建一个包含 65-70 条记录的列表:

let hrLst = [|chrE1;chrE2;chrE3;chrE1;chrE2;chrE3;chrE1; ...... |] |> Array.toList

我现在已经编写了如下所示的代码。

函数GetByteCount获取序列化数据的大小(这里使用NewtonSoft)。

函数loop1 使用上面的hrLst 创建一个长度约为500k 的长列表。

函数loop2在每次迭代中将输入序列减少1000,并为剩余的列表调用GetByteCount

let GetByteCount data = //can improve this algorithm?
    let stopWatch = System.Diagnostics.Stopwatch.StartNew()
    let x = data
            |> JsonConvert.SerializeObject
            |> Encoding.UTF8.GetByteCount
    stopWatch.Stop()
    Console.WriteLine("time reqd: " + stopWatch.Elapsed.TotalMilliseconds.ToString() + " milliseconds")
    x

let PerfTestLoop() =
    let rec loop1 ctr l =
        if ctr%100 = 0 then Console.WriteLine("" + ctr.ToString())
        match ctr with
        |500 -> l
        |_ ->
            let l2 = l @ hrLst
            loop1 (ctr+1) l2
    let l = loop1 1 hrLst
    let len = l.Length
    let s = l @ l |> List.toSeq
    Console.WriteLine("input length: " + (Seq.length s).ToString())
    Console.WriteLine("Start Measuring ..")
    let stopWatch = System.Diagnostics.Stopwatch.StartNew()

    let rec loop2 ctr s =
        match ctr with
        |100 -> s |> GetByteCount
        |_ -> 
            let news = Seq.skip 2000 s        
            let size = news |> GetByteCount
            loop2 (ctr+1) news
    let x = loop2 1 s

let res =  PerfTestLoop() // becomes slow gradually

观察到在loop2 的每次迭代中执行GetByteCount 所花费的时间继续增加,即使序列的大小正在减少!为什么会这样?在任务管理器中,CPU 和内存使用率保持稳定。有没有其他方法可以找到数据的字节数或以某种方式减少执行GetByteCount 所需的时间?

如果在loop2 中删除Seq.skip 行并在每次迭代中使用相同的序列,则每次迭代所需的时间相似并且变化不大。

【问题讨论】:

  • Seq.skip 不会像您认为的那样做。它创建了一个新的序列,在枚举时会引起底层序列的枚举,但会跳过前 N 个元素。当您一遍又一遍地执行此操作时,您会创建一个包含 Seq.skip 调用的“嵌套娃娃”,每个调用都会枚举前一个,最终总是枚举整个原始 500K 序列。
  • 哇,谢谢,不知道这个。那么我如何真正“拆分”一个序列?

标签: performance serialization f# json.net


【解决方案1】:

此代码有许多可能的改进,但这里的主要问题是不了解何时根据它们在语义和性能方面的相对优缺点来选择列表、数组或序列。

F# 列表是一个不可变的单链表,可以快速从前面快速添加和删除项目。它在连接(@ 运算符)或访问某个索引处的项目时并不快。

F# 数组是一个 .NET 数组:它是可变的,并且具有快速索引访问等。但是,大多数用于处理数组的 F# 函数将避免突变并进行复制。

seq 是一个 .NET IEnumerable<T>。这是一个您可以从头开始枚举的序列。这些值是在序列使用者需要时计算的,并且计算起来可能很昂贵。每个值可能取决于先前的值,具体取决于seq 的实现,这就是为什么如果您创建一个带有跳过值的新序列并将其传递到其他地方,新的调用站点仍需要按顺序重新评估跳过的值得到它实际使用的那些。在 F# 中解决此问题的一种方法是使用 Seq.cache。这会将一个普通的 seq 转换为一个在评估时缓存每个项目的序列,这样重复访问就不会导致重新计算。ver,大多数用于处理数组的 F# 函数将避免突变并进行复制。


那么为什么首先需要使用seq?你有一个list 开头,它并不懒惰,因此你将所有这些数据保存在内存中。你做let s = l @ l |> List.toSeq。将其更改为 List.toArray 并稍后使用 Array.skip 可能会更快。

将所有列表更改为数组也可能会有所帮助,因为您没有利用列表的好处。如果您将 hrLst 保留为数组,则可以完全删除 loop1 并执行以下操作:Array.replicate 500 hrLst |> Array.concat


一般来说,我建议您非常熟悉 F# 集合:list、array、seq、map 和 set。浏览这些模块中的函数及其文档,因为它们在几乎所有 F# 代码中都很有用。内置函数通常会消除使用递归的需要并产生更简单的代码。我怀疑loop2 也可以用Array.chunkBySize 的一些用法代替,但我不清楚您的代码要做什么。

【讨论】:

  • 其实上面给出的只是一种测试代码。主要目的是将一个大的 seq(作为输入接收)分成优化大小的块,这些块在大小(以字节为单位)限制内。这个问题的重点是为什么GetByteCount 的执行时间会随着每次迭代而增加。谢谢!
  • “一般来说,我建议你非常熟悉 F# 集合:list、array、seq、map 和 set” - 有什么好的书籍/链接可以推荐吗?那将不胜感激。谢谢。
  • @ronilk Choosing between collections functions 是一个很好的概述。 Seq 的实现本身在 F# 中可能很慢。还探索通过 Array.Parallel 或其他方法进行并行化的方法(想到 PSeq 和 Nessos Streams)。
猜你喜欢
  • 1970-01-01
  • 2019-04-20
  • 2011-02-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-01
  • 1970-01-01
  • 2014-08-27
相关资源
最近更新 更多