FSharp.Data CsvProvider 性能

7
我有一个包含6列和678,552行的csv文件。不幸的是我无法分享任何数据样本,但类型很简单:int64int64datedatestringstring,并且没有缺失值。
使用read.table在R中加载此数据到dataframe中的时间:约3秒。
使用F#中的CsvFile.Load加载此数据的时间:约3秒。
使用F#中的Deedle dataframe加载此数据的时间:约7秒。
添加inferTypes=false并提供Deedle's Frame.ReadCsv的模式可以将时间缩短到约3秒。
使用F#中的CsvProvider加载此数据的时间:大约5分钟。
这5分钟甚至是在我在Schema参数中定义了类型之后,预计消除了F#用于推断它们的时间。
我理解类型提供程序需要做比R或CsvFile.Load更多的工作,以便将数据解析为正确的数据类型,但我对x100倍速度惩罚感到惊讶。更加令人困惑的是Deedle加载数据所需的时间,因为它还需要推断类型并适当地转换,组织成序列等。实际上,我希望Deedle需要的时间比CsvProvider长。
这个问题中,CsvProvider的性能不佳是由于大量列引起的,而这不是我的情况。
我想知道我是否做错了什么或者是否有任何方法可以加快速度。
只是为了澄清:创建提供程序几乎是瞬间完成的。当我强制生成的序列被Seq.length df.Rows实现时,fsharpi提示需要约5分钟才能返回。
我在一个Linux系统上,使用F# v4.1和mono v4.6.1。
以下是CsvProvider的代码:
let [<Literal>] SEP = "|"
let [<Literal>] CULTURE = "sv-SE"
let [<Literal>] DATAFILE = dataroot + "all_diagnoses.csv"

type DiagnosesProvider = CsvProvider<DATAFILE, Separators=SEP, Culture=CULTURE>
let diagnoses = DiagnosesProvider()

EDIT1: 我添加了Deedle加载数据到框架中所需的时间。

EDIT2: 如果提供了模式并且inferTypes=false,我添加了Deedle所需的时间。

此外,在CsvProvider中提供CacheRows=false没有明显影响加载时间,正如评论中建议的那样。

EDIT3: 好的,我们正在取得进展。由于某种奇怪的原因,似乎 Culture 是罪魁祸首。如果我省略这个参数,CsvProvider会在约7秒内加载数据。我不确定是什么导致了这一问题。我的系统区域设置为en_US。但是数据来自具有瑞典区域设置的SQL Server,该区域的小数点用','代替'.'。但是此特定数据集没有任何小数点,所以我可以完全省略Culture。另一个数据集有2个小数列和超过1,000,000行。我的下一个任务是在我目前无法使用的Windows系统上进行测试。

EDIT4: 问题似乎已经解决,但我仍然不理解是什么原因导致了这个问题。如果我通过以下方式“全局”更改区域设置:

System.Globalization.CultureInfo.DefaultThreadCurrentCulture = CultureInfo("sv-SE")
System.Threading.Thread.CurrentThread.CurrentCulture = CultureInfo("sv-SE")

如果从 CsvProvider 中删除 Culture="sv-SE" 参数,加载时间可以缩短到约 6 秒,并且小数可以正确解析。我将保持开放状态,以便任何人都可以对此行为进行说明。


1
你尝试过设置 CacheRows=false 来避免急切地加载所有数据吗?参见这里:http://stackoverflow.com/questions/40852191/csvprovider-throws-outofmemoryexception - TheInnerLight
我实际上想要加载所有数据。此外,那不可能是问题所在。Deedle加载所有数据并且做了更多的工作(将其组织成系列等)比CsvProvider。 - kliron
1
我相信我已经找到了原因,并在FSharp.Data错误跟踪器上打开了一个新问题,以跟踪这个问题以及可能的解决方案。你可以在那里阅读有关它的所有信息。 - rmunn
@rmunn 很好,如果有人需要,我很乐意提供一个合成的数据样本来引起这个问题,尽管我不认为数据结构是问题所在。 - kliron
1
@kliron FYI:https://github.com/fsharp/FSharp.Data/pull/1033 - Just another metaprogrammer
显示剩余4条评论
2个回答

6

我正在尝试重现您所见到的问题,因为您无法分享数据,所以我尝试生成了一些测试数据。但是,在我的机器上(.NET 4.6.2 F#4.1),我没有看到需要几分钟,而只需几秒钟。

也许您可以尝试查看我的示例应用程序在您的设置中的表现,然后我们可以从那里开始?

open System
open System.Diagnostics
open System.IO

let clock =
  let sw = Stopwatch ()
  sw.Start ()
  fun () ->
    sw.ElapsedMilliseconds

let time a =
  let before  = clock ()
  let v       = a ()
  let after   = clock ()
  after - before, v

let generateDataSet () =
  let random            = Random 19740531

  let firstDate         = DateTime(1970, 1, 1)

  let randomInt     ()  = random.Next () |> int64 |> (+) 10000000000L |> string
  let randomDate    ()  = (firstDate + (random.Next () |> float |> TimeSpan.FromSeconds)).ToString("s")
  let randomString  ()  = 
    let inline valid ch =
      match ch with
      | '"'
      | '\\'  -> ' '
      | _     -> ch
    let c   = random.Next () % 16
    let g i =
      if i = 0 || i = c + 1 then '"'
      else 32 + random.Next() % (127 - 32) |> char |> valid
    Array.init (c + 2) g |> String

  let columns =
    [|
      "Id"          , randomInt
      "ForeignId"   , randomInt
      "BirthDate"   , randomDate
      "OtherDate"   , randomDate
      "FirstName"   , randomString
      "LastName"    , randomString
    |]

  use sw      = new StreamWriter ("perf.csv")
  let headers = columns |> Array.map fst |> String.concat ";"
  sw.WriteLine headers
  for i = 0 to 700000 do
    let values = columns |> Array.map (fun (_, f) -> f ()) |> String.concat ";"
    sw.WriteLine values

open FSharp.Data

[<Literal>]
let sample = """Id;ForeignId;BirthDate;OtherDate;FirstName;LastName
11795679844;10287417237;2028-09-14T20:33:17;1993-07-21T17:03:25;",xS@ %aY)N*})Z";"ZP~;"
11127366946;11466785219;2028-02-22T08:39:57;2026-01-24T05:07:53;"H-/QA(";"g8}J?k~"
"""

type PerfFile = CsvProvider<sample, ";">

let readDataWithTp () =
  use streamReader  = new StreamReader ("perf.csv")
  let csvFile       = PerfFile.Load streamReader
  let length        = csvFile.Rows |> Seq.length
  printfn "%A" length

[<EntryPoint>]
let main argv = 
  Environment.CurrentDirectory <- AppDomain.CurrentDomain.BaseDirectory

  printfn "Generating dataset..."
  let ms, _ = time generateDataSet
  printfn "  took %d ms" ms

  printfn "Reading dataset..."
  let ms, _ = time readDataWithTp
  printfn "  took %d ms" ms

  0

性能指标(在我的桌面上使用.NET462):

Generating dataset...
  took 2162 ms
Reading dataset...
  took 6156 ms

性能数据(在我的Macbook Pro上运行Mono 4.6.2):

Generating dataset...
  took 4432 ms
Reading dataset...
  took 8304 ms

更新

事实证明,显式指定CsvProviderCulture会严重降低性能。这可以是任何文化,不仅限于sv-SE,但原因是什么呢?

如果检查提供程序为快速和慢速情况生成的代码,则会注意到差异:

快速情况

internal sealed class csvFile@78
{
  internal System.Tuple<long, long, System.DateTime, System.DateTime, string, string> Invoke(object arg1, string[] arg2)
  {
    Microsoft.FSharp.Core.FSharpOption<string> fSharpOption = TextConversions.AsString(arg2[0]);
    long arg_C9_0 = TextRuntime.GetNonOptionalValue<long>("Id", TextRuntime.ConvertInteger64("", fSharpOption), fSharpOption);
    fSharpOption = TextConversions.AsString(arg2[1]);
    long arg_C9_1 = TextRuntime.GetNonOptionalValue<long>("ForeignId", TextRuntime.ConvertInteger64("", fSharpOption), fSharpOption);
    fSharpOption = TextConversions.AsString(arg2[2]);
    System.DateTime arg_C9_2 = TextRuntime.GetNonOptionalValue<System.DateTime>("BirthDate", TextRuntime.ConvertDateTime("", fSharpOption), fSharpOption);
    fSharpOption = TextConversions.AsString(arg2[3]);
    System.DateTime arg_C9_3 = TextRuntime.GetNonOptionalValue<System.DateTime>("OtherDate", TextRuntime.ConvertDateTime("", fSharpOption), fSharpOption);
    fSharpOption = TextConversions.AsString(arg2[4]);
    string arg_C9_4 = TextRuntime.GetNonOptionalValue<string>("FirstName", TextRuntime.ConvertString(fSharpOption), fSharpOption);
    fSharpOption = TextConversions.AsString(arg2[5]);
    return new System.Tuple<long, long, System.DateTime, System.DateTime, string, string>(arg_C9_0, arg_C9_1, arg_C9_2, arg_C9_3, arg_C9_4, TextRuntime.GetNonOptionalValue<string>("LastName", TextRuntime.ConvertString(fSharpOption), fSharpOption));
  }
}

缓慢

internal sealed class csvFile@78
{
  internal System.Tuple<long, long, System.DateTime, System.DateTime, string, string> Invoke(object arg1, string[] arg2)
  {
    Microsoft.FSharp.Core.FSharpOption<string> fSharpOption = TextConversions.AsString(arg2[0]);
    long arg_C9_0 = TextRuntime.GetNonOptionalValue<long>("Id", TextRuntime.ConvertInteger64("sv-SE", fSharpOption), fSharpOption);
    fSharpOption = TextConversions.AsString(arg2[1]);
    long arg_C9_1 = TextRuntime.GetNonOptionalValue<long>("ForeignId", TextRuntime.ConvertInteger64("sv-SE", fSharpOption), fSharpOption);
    fSharpOption = TextConversions.AsString(arg2[2]);
    System.DateTime arg_C9_2 = TextRuntime.GetNonOptionalValue<System.DateTime>("BirthDate", TextRuntime.ConvertDateTime("sv-SE", fSharpOption), fSharpOption);
    fSharpOption = TextConversions.AsString(arg2[3]);
    System.DateTime arg_C9_3 = TextRuntime.GetNonOptionalValue<System.DateTime>("OtherDate", TextRuntime.ConvertDateTime("sv-SE", fSharpOption), fSharpOption);
    fSharpOption = TextConversions.AsString(arg2[4]);
    string arg_C9_4 = TextRuntime.GetNonOptionalValue<string>("FirstName", TextRuntime.ConvertString(fSharpOption), fSharpOption);
    fSharpOption = TextConversions.AsString(arg2[5]);
    return new System.Tuple<long, long, System.DateTime, System.DateTime, string, string>(arg_C9_0, arg_C9_1, arg_C9_2, arg_C9_3, arg_C9_4, TextRuntime.GetNonOptionalValue<string>("LastName", TextRuntime.ConvertString(fSharpOption), fSharpOption));
  }
}

更具体来说,这就是区别:
// Fast
TextRuntime.ConvertDateTime("", fSharpOption), fSharpOption)
// Slow
TextRuntime.ConvertDateTime("sv-SE", fSharpOption), fSharpOption)

当我们指定一个文化时,它被传递给ConvertDateTime,并被转发到GetCulture

static member GetCulture(cultureStr) =
  if String.IsNullOrWhiteSpace cultureStr 
  then CultureInfo.InvariantCulture 
  else CultureInfo cultureStr

这意味着,在默认情况下,我们使用 CultureInfo.InvariantCulture,但对于每个字段和行的任何其他情况,我们都会创建一个 CultureInfo 对象。可以进行缓存,但目前未实现。创建过程本身似乎不需要太多时间,但每次使用新的 CultureInfo 对象解析时会出现问题。
FSharp.Data 中,解析 DateTime 的本质是这样的。
let dateTimeStyles = DateTimeStyles.AllowWhiteSpaces ||| DateTimeStyles.RoundtripKind
match DateTime.TryParse(text, cultureInfo, dateTimeStyles) with

因此,让我们进行性能测试,其中一个使用缓存的 CultureInfo 对象,另一个则每次创建一个新的。

open System
open System.Diagnostics
open System.Globalization

let clock =
  let sw = Stopwatch ()
  sw.Start ()
  fun () ->
    sw.ElapsedMilliseconds

let time a =
  let before  = clock ()
  let v       = a ()
  let after   = clock ()
  after - before, v

let perfTest c cf () =
  let dateTimeStyles = DateTimeStyles.AllowWhiteSpaces ||| DateTimeStyles.RoundtripKind
  let text = DateTime.Now.ToString ("", cf ())
  for i = 1 to c do
    let culture = cf ()
    DateTime.TryParse(text, culture, dateTimeStyles) |> ignore

[<EntryPoint>]
let main argv = 
  Environment.CurrentDirectory <- AppDomain.CurrentDomain.BaseDirectory

  let ct    = "sv-SE"
  let cct   = CultureInfo ct
  let count = 10000

  printfn "Using cached CultureInfo object..."
  let ms, _ = time (perfTest count (fun () -> cct))
  printfn "  took %d ms" ms

  printfn "Using fresh CultureInfo object..."
  let ms, _ = time (perfTest count (fun () -> CultureInfo ct))
  printfn "  took %d ms" ms

  0

.NET 4.6.2 F#4.1 的性能数据:

Using cached CultureInfo object...
  took 16 ms
Using fresh CultureInfo object...
  took 5328 ms

因此,似乎在FSharp.Data中缓存CultureInfo对象应当显著提高指定区域性时CsvProvider的性能。


谢谢。我得到了这些结果:生成:3767毫秒,读取:3764毫秒。绝对不是分钟。现在我更加困惑了……你的合成数据唯一明显的区别是我使用管道字符作为分隔符和瑞典文化。我将用实际代码更新我的问题。 - kliron
1
@kliron 看起来你需要做的下一步是生成一些合成数据,以重现问题并分享给我们。 - N_A
@mydogisbox 我更新了我的问题。文化参数似乎是造成这个问题的原因。值得一提的是,如果我将Culture设置为"sv_SE"并使用FuleSnabel的数据集,加载该数据集需要几分钟的时间。 - kliron
文化肯定会导致问题。我曾经有一个应用程序因为撒米文化造成的字符串比较非常缓慢而挂起。我也可以检查一下 sv_SE 文化。 - Just another metaprogrammer
@FuleSnabel 奇怪的是,在你的情况下它并没有发生。数据几乎肯定不是问题,当我加载你的性能数据时,我得到完全相同的行为。使用显式设置的 Culture 完成需要近 6 分钟,而不使用则只需要 3 秒钟。 - kliron
显示剩余3条评论

2

问题是由于CsvProvider没有记忆明确设置的Culture而引起的。这个问题通过这个拉取请求得到解决。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接