标准的xml解析器在Golang中的性能非常低

sg3maiej  于 2023-08-01  发布在  Go
关注(0)|答案(2)|浏览(139)

我有一个100 GB的XML文件,并在下面的代码中使用SAX方法解析它

file, err := os.Open(filename)
    handle(err)
    defer file.Close()
    buffer := bufio.NewReaderSize(file, 1024*1024*256) // 33554432
    decoder := xml.NewDecoder(buffer)
    for {
            t, _ := decoder.Token()
            if t == nil {
                break
            }
            switch se := t.(type) {
            case xml.StartElement:
                if se.Name.Local == "House" {
                    house := House{}
                    err := decoder.DecodeElement(&house, &se)
                    handle(err)
                }
            }
        }

字符串
但是golang的工作速度非常慢,它似乎是由执行时间和磁盘使用。我的硬盘能够读取数据的速度约100-120 MB/s,但golang只使用10-13 MB/s.
为了实验,我用C#重写了以下代码:

using (XmlReader reader = XmlReader.Create(filename)
                {
                    while (reader.Read())
                    {
                        switch (reader.NodeType)
                        {
                            case XmlNodeType.Element:
                                if (reader.Name == "House")
                                {
                                    //Code
                                }
                                break;
                        }
                    }
                }


我得到了完整的硬盘加载,C#读取数据与100-110 MB/s的速度。执行时间大约降低10倍。
如何使用golang提高XML解析性能?

6l7fqoea

6l7fqoea1#

这5件事可以帮助使用encoding/xml库提高速度:

  • (针对XMB测试,有75 k条目,20 MB,%s应用于前一个项目符号)*

1.使用定义明确的结构

  • 在所有结构上实施xml.Unmarshaller
  • 大量的代码
  • 节省20%的时间和15%的分配
  • d.DecodeElement(&foo, &token)替换为foo.UnmarshallXML(d, &token)
  • 几乎百分之百安全
  • 节省10%的时间和分配
  • 使用d.RawToken()代替d.Token()
  • 需要手动处理嵌套对象和命名空间
  • 节省10%的时间和20%的分配
  • 如果使用d.Skip(),则使用d.RawToken()重新实现它

在我的特定用例上,我减少了40%的时间和分配,代价是更多的代码,样板文件,以及潜在的更糟糕的极端情况处理,但我的输入相当一致,但这还不够。

benchstat first.bench.txt parseraw.bench.txt 
name          old time/op    new time/op    delta
Unmarshal-16     1.06s ± 6%     0.66s ± 4%  -37.55%  (p=0.008 n=5+5)

name          old alloc/op   new alloc/op   delta
Unmarshal-16     461MB ± 0%     280MB ± 0%  -39.20%  (p=0.029 n=4+4)

name          old allocs/op  new allocs/op  delta
Unmarshal-16     8.42M ± 0%     5.03M ± 0%  -40.26%  (p=0.016 n=4+5)

字符串
在我的实验中,lack of memoizing issue是XML解析器上大量时间/分配的原因,这会显著减慢速度,主要是因为Go通过值复制。

kwvwclae

kwvwclae2#

回答您的问题 “如何使用golang提高xml解析性能?“
使用常见的xml.NewDecoder/decoder.Token,我在本地看到50 MB/s。通过使用https://github.com/tamerh/xml-stream-parser,我能够将解析速度提高一倍。
为了进行测试,我使用了https://archive.org/details/stackexchange存档torrent中的Posts.xml(68 GB)。

package main

import (
    "bufio"
    "fmt"
    "github.com/tamerh/xml-stream-parser"
    "os"
    "time"
)

func main() {
    // Using `Posts.xml` (68 GB) from https://archive.org/details/stackexchange (in the torrent)
    f, err := os.Open("Posts.xml")
    if err != nil {
        panic(err)
    }
    defer f.Close()

    br := bufio.NewReaderSize(f, 1024*1024)
    parser := xmlparser.NewXmlParser(br, "row")

    started := time.Now()
    var previous int64 = 0

    for x := range *parser.Stream() {
        elapsed := int64(time.Since(started).Seconds())
        if elapsed > previous {
            kBytesPerSecond := int64(parser.TotalReadSize) / elapsed / 1024
            fmt.Printf("\r%ds elapsed, read %d kB/s (last post.Id %s)", elapsed, kBytesPerSecond, x.Attrs["Id"])
            previous = elapsed
        }
    }
}

字符串
这将输出如下内容:

...s elapsed, read ... kB/s (last post.Id ...)


唯一需要注意的是,这并不能方便地解组为结构。
正如https://github.com/golang/go/issues/21823中所讨论的,速度似乎是Golang中XML实现的普遍问题,需要重写/重新考虑标准库的这一部分。

相关问题