如何在F#中有效地向文件写入数据?

10

我想为测试目的生成大型xml文件,但最终编写的代码非常慢,随着我写入文件的行数增加,时间呈指数级增长。下面的示例显示,写入100行只需毫秒级别的时间,但写入1000行需要超过20秒(在我的电脑上)。我真的无法确定是什么导致了这个问题,因为我认为写入1000行不应该需要那么长时间。此外,写入200行所需的时间大约是写入100行的4倍,这不好。要运行代码,您可能需要更改StreamWriter的路径。

open System.IO
open System.Diagnostics

let xmlSeq = Seq.initInfinite (fun index -> sprintf "<author><name>name%d</name><age>%d</age><books><book>book%d</book></books></author>" index index index)

let createFile (seq: string seq) numberToTake fileName =
    use streamWriter = new StreamWriter("C:\\tmp\\FSharpXmlTest\\FSharpXmlTest\\" + fileName, false)
    streamWriter.WriteLine("<startTag>")
    let rec internalWriter (seq: string seq) (sw:StreamWriter) i (endTag:string) =
        match i with
        | 0 -> (sw.WriteLine(Seq.head seq);
            sw.WriteLine(endTag))
        | _ -> (sw.WriteLine(Seq.head seq);
            internalWriter (Seq.skip 1 seq) sw (i-1) endTag)
    internalWriter seq streamWriter numberToTake "</startTag>"

let funcTimer fn =
    let stopWatch = Stopwatch.StartNew()
    printfn "Timing started"
    fn()
    stopWatch.Stop()
    printfn "Time elased: %A" stopWatch.Elapsed


(funcTimer (fun () -> createFile xmlSeq 100 "file100.xml"))
(funcTimer (fun () -> createFile xmlSeq 1000 "file1000.xml"))
3个回答

6

您在操作序列时观察到了二次行为 O(n^2)。当您调用 Seq.skip 时,将创建一个全新的序列,因此您会隐式地遍历其余部分。更详细的解释可以在https://dev59.com/QHM_5IYBdhLWcg3wmkeu#1306267找到。

在这个例子中,您不需要分解序列。将您的内部函数替换为:

let internalWriter (seq: string seq) (sw:StreamWriter) i (endTag:string) =
    for node in Seq.take i seq do
        sw.WriteLine(node)
    sw.WriteLine(endTag)

我可以在短短的几分之一秒内写入10000行代码。

您可以通过删除此内部函数并将其主体复制到父函数来进一步重构代码。

正如上面的链接中提到的,如果您需要分解序列,最好使用LazyList


谢谢您澄清我每次都在创建新序列。然而,我应该替换内部函数,我可以只删除内部函数并将for循环添加到外部函数中。 - Tomas Jansson

5

Pad在他的答案中指出了减速的原因。另一种惯用的方法可能是,不是生成无限序列,而是使用Seq.unfold生成所需长度的序列,这使得代码真正轻松:

let xmlSeq n = Seq.unfold (fun i ->
    if i = 0 then None
    else Some((sprintf "<author><name>name%d</name><age>%d</age><books><book>book%d</book></books></author>" i i i), i - 1)) n

let createFile seqLen fileName =
    use streamWriter = new StreamWriter("C:\\tmp\\FSharpXmlTest\\" + fileName, false)
    streamWriter.WriteLine("<startTag>")
    seqLen |> xmlSeq |> Seq.iter streamWriter.WriteLine
    streamWriter.WriteLine("</startTag>")

(funcTimer (fun () -> createFile  10000 "file10000.xml"))

在我的笔记本电脑上生成10000个元素大约需要500毫秒。

感谢您填写其他有用的信息。其他答案更针对我的问题,但您提供了一些非常有用的额外信息。 - Tomas Jansson

2
我想到了以下解决方案:
namespace FSharpBasics

module Program2 =

    open System
    open System.IO
    open System.Diagnostics

    let seqTest count : seq<string> =
        let template = "<author>\
                    <name>Name {0}</name>\
                    <age>{0}</age>\
                    <books>\
                    <book>Book {0}</book>\
                    </books>\
                    </author>"

        let row (i: int) =
            String.Format (template, i)

        seq {
            yield "<authors>"
            for x in [ 1..count ] do
                yield row x
            yield "</authors>"
        }

    [<EntryPoint>]
    let main argv =
        printfn "File will be written now"

        let stopwatch = Stopwatch.StartNew()
        File.WriteAllLines (@".\test.xml", seqTest 10000) |> ignore
        stopwatch.Stop()

        printf "Ended, took %f seconds" stopwatch.Elapsed.TotalSeconds

        System.Console.ReadKey() |> ignore

        0

在我的笔记本电脑上,创建一个包含1万个作者的格式良好的test.xml文件只需要不到90毫秒。


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