Производительность XPath.evaluate замедляется (нелепо) при нескольких вызовах

Я пытаюсь использовать пакет 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), для каждого xp.evaluate требуется около 500 мс-600 мс.

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

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

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

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

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

Спасибо!

Обновить

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

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

6 ответов

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

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

 }

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

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

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

Это, кажется, еще один случай, когда использование 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.

Это потому чтоnodes.getLength()займет много времени, просто сделайте это из цикла for

      int nodes_len=nodes.getLength();
for(int i=0;i<nodes_len;i++)
{
//your code here
}

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

В конце концов я нашел Джаксен. Как только я его использовал, анализ документа, который раньше занимал 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

Каждый раз, когда вы берете 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;

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

Другие вопросы по тегам