Медленный синтаксический анализ XML при выполнении теста - PullRequest
0 голосов
/ 07 февраля 2019

Я хочу измерить время, необходимое GoLang для анализа XML-файла.Итак, я решил написать тест.

У меня есть функция, которая генерирует io.Reader, который содержит XML-документ.

// PRIVATE: createSampleXMLReader creates an io.Reader instance that contains 10.000 '<Node />' elements which are
//          suitable for running a benchmark test.
func createSampleXMLReader(
    nodeElementCount int) io.Reader {
    xmlContents := new(strings.Builder)

    xmlContents.WriteString("<ROOT>\n")

    for i := 0; i < nodeElementCount; i++ {
        appendNodeXMLElement(xmlContents)
    }

    xmlContents.WriteString("</ROOT>")

    return strings.NewReader(xmlContents.String())
}

// PRIVATE: appendNodeXMLElement appends a '<Node />' elements to an existing io.Reader instance.
func appendNodeXMLElement(
    xmlDocument *strings.Builder) {

    xmlDocument.WriteString("<Node id=\"0\" position=\"0\" depth=\"0\" parent=\"0\">\n")
    xmlDocument.WriteString("    <Name>Name</Name>\n")
    xmlDocument.WriteString("    <Description>Description</Description>\n")
    xmlDocument.WriteString("    <OwnInformation>\n")
    xmlDocument.WriteString("        <Title>Title</Title>\n")
    xmlDocument.WriteString("        <Description>Description</Description>\n")
    xmlDocument.WriteString("    </OwnInformation>\n")
    xmlDocument.WriteString("    <Assets>\n")
    xmlDocument.WriteString("        <Asset id=\"0\" position=\"0\" type=\"0\" category=\"0\">\n")
    xmlDocument.WriteString("            <OriginalFile>OriginalFile</OriginalFile>\n")
    xmlDocument.WriteString("            <Description>Description</Description>\n")
    xmlDocument.WriteString("            <Uri>Uri</Uri>\n")
    xmlDocument.WriteString("        </Asset>\n")
    xmlDocument.WriteString("        <Asset id=\"1\" position=\"1\" type=\"1\" category=\"1\">\n")
    xmlDocument.WriteString("            <OriginalFile>OriginalFile</OriginalFile>\n")
    xmlDocument.WriteString("            <Description>Description</Description>\n")
    xmlDocument.WriteString("            <Uri>Uri</Uri>\n")
    xmlDocument.WriteString("        </Asset>\n")
    xmlDocument.WriteString("        <Asset id=\"2\" position=\"2\" type=\"2\" category=\"2\">\n")
    xmlDocument.WriteString("            <OriginalFile>OriginalFile</OriginalFile>\n")
    xmlDocument.WriteString("            <Description>Description</Description>\n")
    xmlDocument.WriteString("            <Uri>Uri</Uri>\n")
    xmlDocument.WriteString("        </Asset>\n")
    xmlDocument.WriteString("        <Asset id=\"3\" position=\"3\" type=\"3\" category=\"3\">\n")
    xmlDocument.WriteString("            <OriginalFile>OriginalFile</OriginalFile>\n")
    xmlDocument.WriteString("            <Description>Description</Description>\n")
    xmlDocument.WriteString("            <Uri>Uri</Uri>\n")
    xmlDocument.WriteString("        </Asset>\n")
    xmlDocument.WriteString("        <Asset id=\"4\" position=\"4\" type=\"4\" category=\"4\">\n")
    xmlDocument.WriteString("            <OriginalFile>OriginalFile</OriginalFile>\n")
    xmlDocument.WriteString("            <Description>Description</Description>\n")
    xmlDocument.WriteString("            <Uri>Uri</Uri>\n")
    xmlDocument.WriteString("        </Asset>\n")
    xmlDocument.WriteString("    </Assets>\n")
    xmlDocument.WriteString("    <Synonyms>\n")
    xmlDocument.WriteString("        <Synonym>Synonym 0</Synonym>\n")
    xmlDocument.WriteString("        <Synonym>Synonym 1</Synonym>\n")
    xmlDocument.WriteString("        <Synonym>Synonym 2</Synonym>\n")
    xmlDocument.WriteString("        <Synonym>Synonym 3</Synonym>\n")
    xmlDocument.WriteString("        <Synonym>Synonym 4</Synonym>\n")
    xmlDocument.WriteString("    </Synonyms>\n")
    xmlDocument.WriteString("</Node>\n")
}

Затем у меня есть функция, котораяфактически анализирует этот XML-документ.

// PRIVATE: parseXML parses an io.Reader instance into a 'Node' struct.
func parseXML(
    xmlReader io.Reader) {

    xmlDecoder := xml.NewDecoder(xmlReader)

    for {
        token, _ := xmlDecoder.Token()
        if token == nil {
            break
        }

        switch element := token.(type) {
        case xml.StartElement:
            if element.Name.Local == "Node" {
                decodeNodeElement(xmlDecoder, &element)
            }
        }
    }
}

// PRIVATE: decodeNodeElement decodes a '<Node />' element into a 'Node' struct.
func decodeNodeElement(
    xmlDecoder *xml.Decoder,
    element *xml.StartElement) {

    node := new(model.Node)

    xmlDecoder.DecodeElement(node, element)
}

Затем у меня есть функция, которая выполняет тест:

// PRIVATE: runBenchmarkParseXML performs a benchmark that parses an XML document that contains the given number of
//          '<Node />' element.
func runBenchmarkParseXML(
    nodeCount int,
    benchmark *testing.B) {

    // Arrange.
    xmlReader := createSampleXMLReader(nodeCount)

    // Act.
    for i := 0; i < benchmark.N; i++ {
        parseXML(xmlReader)
    }
}

И затем у меня есть 5 функций, которые выполняют тест.Они предназначены для элементов 1, 10, 100, 1000 и 10000 в документе XML.

func BenchmarkParseXML1(benchmark *testing.B)     { runBenchmarkParseXML(1, benchmark) }
func BenchmarkParseXML10(benchmark *testing.B)    { runBenchmarkParseXML(10, benchmark) }
func BenchmarkParseXML100(benchmark *testing.B)   { runBenchmarkParseXML(100, benchmark) }
func BenchmarkParseXML1000(benchmark *testing.B)  { runBenchmarkParseXML(1000, benchmark) }
func BenchmarkParseXML10000(benchmark *testing.B) { runBenchmarkParseXML(10000, benchmark) }

Когда я запускаю этот тест, я вижу следующий вывод:

BenchmarkParseXML1-4             5000000               226 ns/op
BenchmarkParseXML10-4           10000000               230 ns/op
BenchmarkParseXML100-4           5000000               226 ns/op
BenchmarkParseXML1000-4          5000000               254 ns/op
BenchmarkParseXML10000-4               1        1690998100 ns/op

Как это возможно, что есть такая разница в тесте, который анализирует XML-файл, который содержит 10.000 элементов, а остальные значения времени теста стабильны?

Является ли мой тест новым или реализация метода ParseXMLНеправильно.

Редактировать: Структура узла

// Node represents a '<Node />' element in the XML document.
type Node struct {
    ID             int    `xml:"id,attr"`
    Position       int    `xml:"position,attr"`
    Depth          int    `xml:"depth,attr"`
    Parent         string `xml:"parent,attr"`
    Name           string `xml:"Name"`
    Description    string `xml:"Description"`
    OwnInformation struct {
        Title       string `xml:"Title"`
        Description string `xml:"Description"`
    } `xml:"OwnInformation"`
    Assets []struct {
        ID           string `xml:"id,attr"`
        Position     int    `xml:"position,attr"`
        Type         string `xml:"type,attr"`
        Category     int    `xml:"category,attr"`
        OriginalFile string `xml:"OriginalFile"`
        Description  string `xml:"Description"`
        URI          string `xml:"Uri"`
    } `xml:"Assets>Asset"`
    Synonyms []string `xml:"Synonyms>Synonym"`
}

Заранее благодарен за любые указания.

1 Ответ

0 голосов
/ 07 февраля 2019

Ваш эталонный тест некорректен.Тот факт, что вы увеличиваете ввод в 10 раз, но длительность остается примерно постоянным, должен был вызвать у вас очень подозрительное отношение к этому тесту.

Вы повторно используете один и тот же считыватель каждый раз.Только первая итерация каждого бенчмарка будет делать что угодно.Дальнейшие вызовы будут считываться с устройства чтения, которое уже находится в EOF.

Измените настройку, чтобы она возвращала срез байта, и создайте новое средство чтения для каждой итерации.Это даст ожидаемые результаты:

func createSampleXMLDoc(nodeElementCount int) []byte {
    xmlContents := &bytes.Buffer{}

    xmlContents.WriteString("<ROOT>\n")

    for i := 0; i < nodeElementCount; i++ {
        appendNodeXMLElement(xmlContents)
    }

    xmlContents.WriteString("</ROOT>")
    return xmlContents.Bytes()
}

func runBenchmarkParseXML(nodeCount int, b *testing.B) {
    doc := createSampleXMLDoc(nodeCount)

    for i := 0; i < b.N; i++ {
        xmlReader := bytes.NewReader(doc)
        parseXML(xmlReader)
    }
}

Результаты на моей машине (как и ожидалось, увеличение ввода в 10 раз увеличивает время в 10 раз):

$ go test -benchtime=5s -bench .
goos: linux
goarch: amd64
BenchmarkParseXML1-8              100000            115978 ns/op
BenchmarkParseXML10-8              10000           1147605 ns/op
BenchmarkParseXML100-8              1000          11586980 ns/op
BenchmarkParseXML1000-8               50         124199120 ns/op
BenchmarkParseXML10000-8               5        1003668966 ns/op

-benchtime=5s увеличивает время по умолчаниюза тест от одной секунды до пяти секунд.Одной секунды недостаточно для более чем одной итерации в последнем случае, что дает ненадежные результаты.Вот почему также увидеть большое число в исходном тесте.Первая итерация медленная, но каждая последующая возвращается мгновенно, поэтому среднее время резко падает.Кстати, одна только итерация в тесте - это еще один красный флаг.На самом деле даже пять итераций все еще довольно низки для надежных измерений.

Мораль истории: Проверьте свои ошибки!

...