Производительность XPath.evaluate замедляется (нелепо) при нескольких вызовах - PullRequest
22 голосов
/ 24 сентября 2010

Я пытаюсь использовать пакет javax.xml.xpath для запуска выражений XPath в документе с несколькими пространствами имен, и у меня возникают глупые проблемы с производительностью.

Мой тестовый документ извлечен из реального,производственный пример.Это около 600 КБ XML.Документ представляет собой довольно сложную ленту Atom.

Я понимаю, что то, что я делаю с XPath, может быть сделано без.Однако та же реализация на других, значительно уступающих платформах работает абсурдно лучше.Сейчас восстановление моей системы без использования XPath выходит за рамки того, что я могу сделать за то время, которое у меня есть.

Мой тестовый код выглядит примерно так:



void testXPathPerformance()
{
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setNamespaceAware(true);
    DocumentBuilder builder = factory.newDocumentBuilder();

    Document doc = builder.parse(loadTestDocument());

    XPathFactory xpf = XPathFactory.newInstance();
    XPath xp = xpf.newXPath();

    NamespaceContext names = loadTestNamespaces();
    //there are 12 namespaces in names.  In this example code, I'm using
    //'samplens' instead of the actual namespaces that my application uses
    //for simplicity.  In my real code, the queries are different text, but
    //precisely the same complexity.

    xp.setNamespaceContext(names);

    NodeList nodes = (NodeList) xp.evaluate("/atom:feed/atom:entry",
                     doc.getDocumentElement(), XPathConstants.NODESET);


    for(int i=0;i<nodes.getLength();i++)
    {
        printTimestamp(1);
        xp.evaluate("atom:id/text()", nodes.item(i));
        printTimestamp(2);
        xp.evaluate("samplens:fieldA/text()", nodes.item(i));
        printTimestamp(3);
        xp.evaluate("atom:author/atom:uri/text()", nodes.item(i));
        printTimestamp(4);
        xp.evaluate("samplens:fieldA/samplens:fieldB/&at;attrC", nodes.item(i));
        printTimestamp(5);

        //etc.  My real example has 10 of these xp.evaluate lines

     }
}

КогдаЯ запускаю на Nexus One (не в отладчике, а с подключенным USB), первый раз через цикл каждое xp.evaluate занимает где-то от 10 мс до 20 мс.К 15-му разу цикла каждый xp.evaluate занимает от 200 до 300 мс.К концу цикла (в nodes содержится 150 элементов) для каждого xp.evaluate требуется около 500 мс-600 мс.

Я пытался использовать xp.compile ().Все компиляции занимают <5 мс.Я сделал xp.reset () (без разницы).Я сделал новый объект XPath для каждой оценки (добавляет около 4 мс). </p>

Использование памяти не выходит из-под контроля во время выполнения.

Я выполняю это на одномнить в тестовом примере JUnit, которая не создает действия или что-либо еще.

Я действительно озадачен.

Кто-нибудь знает, что еще можно попробовать?

Спасибо!

обновление

Если я запускаю цикл for в обратном направлении (for(int i=nodes.getLength()-1;i>=0;i--)), то первые несколько узлов принимают 500-600 мс, а последние - 10 мс-20ms.Таким образом, кажется, что это не имеет никакого отношения к количеству вызовов, но вместо этого выражения, контекст которых находится ближе к концу документа, занимают больше времени, чем выражения, контекст которых находится ближе к началу документа.

у кого-нибудь есть мысли о том, что я могу сделать по этому поводу?

Ответы [ 5 ]

51 голосов
/ 27 января 2012

Попробуйте добавить этот код в цикл сверху;

Node singleNode = nodes.item(i);
singleNode.getParentNode().removeChild(singleNode);

, затем выполните каждую оценку, используя переменную singleNode вместо nodes.item(i); (конечно, вы меняете имя)

Это отсоединяет узел, с которым вы работаете, от большого основного документа.Это значительно увеличит время обработки методов оценки.

EX:

for(int i=0;i<nodes.getLength();i++)
{
    Node singleNode = nodes.item(i);
    singleNode.getParentNode().removeChild(singleNode);

    printTimestamp(1);
    xp.evaluate("atom:id/text()", singleNode );
    printTimestamp(2);
    xp.evaluate("samplens:fieldA/text()", singleNode );
    printTimestamp(3);
    xp.evaluate("atom:author/atom:uri/text()", singleNode );
    printTimestamp(4);
    xp.evaluate("samplens:fieldA/samplens:fieldB/&at;attrC", singleNode );
    printTimestamp(5);

    //etc.  My real example has 10 of these xp.evaluate lines

 }
12 голосов
/ 22 декабря 2011

Это, кажется, еще один случай, когда использование XPath кажется медленным, но вместо XPath причина, вероятно, вызвана методом DOM nodelist.item(i)

Реализация по умолчанию NodeList в Java имеет определенные особенности:

  1. оценивается лениво
  2. Список DOM активен
  3. Он реализован в виде связанного списка
  4. В списке есть некоторое кэширование

Когда вы смотрите на эти функции отдельно, вы можете задаться вопросом, почему объект-результат выражения XPath должен иметь такую ​​функцию, но они имеют больше смысла, когда вы их объединяете.

1) Ленивая оценка может размыть местоположение узкого места производительности. Из-за этого возврат NodeList кажется быстрым, но если задача состоит в том, чтобы всегда перебирать список, он более или менее просто откладывает снижение производительности. Ленивая оценка становится дорогостоящей, если оценка всего списка должна обрабатываться снова каждый раз, когда читается следующий элемент в списке.

2) NodeList «живой» список означает, что он обновляется и ссылается на узлы, которые в данный момент находятся в дереве документа, а не на узлы, которые были в дереве, когда список был изначально создан, или на клоны этих узлов. Это важная функция для начинающих DOM. Например, если вы выберете NodeList элементов-братьев и попытаетесь добавить по одному новому элементу-брату к каждому узлу, шаг к item(i+1) всегда достигнет последнего добавленного узла, и цикл никогда не завершится.

3) Актуальный список также дает некоторое объяснение, почему он реализован как связанный список (или AFAIK фактическая реализация - это дважды связанный список). Эффект этого отчетливо виден в вашем тесте, где доступ к последним элементам всегда самый медленный, независимо от того, повторяете ли вы его вперед или назад.

4) Из-за кэширования зацикливание одного списка без каких-либо изменений в дереве должно быть достаточно эффективным, если кэш остается чистым. В некоторых версиях Java были проблемы с этим кэшированием. Я не исследовал, какие все процедуры делают недействительным кэширование, но, вероятно, самые безопасные ставки - посоветовать сохранить одинаковое вычисленное выражение, не вносить изменений в дерево, выполнять циклический переход по одному списку за раз и всегда переходить к следующему или предыдущему элементу списка.

Реальные выигрыши в производительности, конечно, зависят от варианта использования. Вместо того, чтобы просто настраивать зацикливание списка, вы должны попытаться вообще избавиться от зацикливания живого списка - по крайней мере, для справки. Клонирование делает список не живым. Прямой доступ к узлам может быть достигнут путем копирования узлов в массив. Если структура подходит, вы также можете использовать другие методы DOM, такие как getNextSibling(), которые говорят, что дают более эффективные результаты, чем зацикливание на NodeList.

5 голосов
/ 01 февраля 2016

Попробуйте клонировать узел (чтобы у вас не было ненужных ссылок от его предков)

Node singleNode = nodes.item(i).cloneNode(true);

Если вы удалите дочерние элементы, вы потеряете ссылки и получите только половину узлов, которые хотите обработать.

0 голосов
/ 12 мая 2016

Каждый раз, когда вы берете Node из Nodelist, кажется, что он хранит ссылки на всю структуру xml; по этой причине когда вы перемещаетесь по узлу, процесс xpath запускается каждый раз из корня xml, и по этой причине, когда вы переходите в три это занимает больше времени.

По этой причине, когда вы берете узел, прежде чем перемещаться по нему, вы должны привести строку в следующий метод:

private String nodeToString(Node node) {
          StringWriter sw = new StringWriter();
          try {
            Transformer t = TransformerFactory.newInstance().newTransformer();
            t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
            t.transform(new DOMSource(node), new StreamResult(sw));
          } catch (TransformerException te) {
            System.out.println("nodeToString Transformer Exception");
          }
          return sw.toString();
        }

и затем преобразовать его в элемент / узел:

String xml = nodeToString(node);

Element nodeNew =  DocumentBuilderFactory
        .newInstance()
        .newDocumentBuilder()
        .parse(new ByteArrayInputStream(xml.getBytes()))
        .getDocumentElement();

node = nodeNew;

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

0 голосов
/ 24 марта 2015

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

В конце концов я нашел jaxen . Как только я его использовал, анализ документа, который раньше занимал 15 секунд, занимал всего миллисекунды.

Jaxen, к сожалению, довольно плохо документирован, но работал довольно хорошо:

DOMXPath myXPath = new DOMXPath("atom:id/text()");
String myContent = myXPath.stringValueOf(myDocument);

Документ Java можно найти здесь http://jaxen.codehaus.org/apidocs/org/jaxen/dom/DOMXPath.html

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...