XPath.evaluate性能在多次调用时减慢(荒谬)

And*_*sky 22 java performance xpath android

我正在尝试使用javax.xml.xpath包在具有多个名称空间的文档上运行XPath表达式,并且我遇到了愚蠢的性能问题.

我的测试文档来自一个真实的生产示例.它大约是600k的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

     }
}
Run Code Online (Sandbox Code Playgroud)

当我在Nexus One上运行时(不在调试器中,但连接了USB),第一次通过循环时,每个xp.evaluate需要10ms到20ms.到第15次循环时,每个xp.evaluate需要200ms到300ms.在循环结束时(有150个项目nodes),每个xp.evaluate需要大约500ms-600ms.

我尝试过使用xp.compile().编译全部花费<5ms.我已经完成了xp.reset()(没有任何区别).我为每个评估做了一个新的XPath对象(增加大约4ms).

在执行期间,内存使用似乎不会失控.

我在JUnit测试用例中的单个线程上运行它,它不会创建任何活动.

我真的很困惑.

有没有人知道还有什么可以尝试?

谢谢!

更新

如果我向后运行for循环(for(int i=nodes.getLength()-1;i>=0;i--)),那么前几个节点需要500ms-600ms,最后几个节点快速运行10ms-20ms.因此,这似乎与调用的数量无关,而是上下文接近文档末尾的表达式比上下文接近文档开头的表达式花费更长的时间.

对于我能做些什么,有没有人有任何想法?

小智 52

尝试在顶部的循环中添加此代码;

Node singleNode = nodes.item(i);
singleNode.getParentNode().removeChild(singleNode);
Run Code Online (Sandbox Code Playgroud)

然后使用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

 }
Run Code Online (Sandbox Code Playgroud)

  • 我无法相信它有效,但确实如此.就我而言,不是删除我克隆它的节点,而是仍然看到了二十倍的性能提升. (5认同)
  • 分离小费为+1.将我的代码从几分钟改进到不到10秒! (4认同)
  • 是的,这会带来巨大的变化. (3认同)
  • 你有点让我走上正轨.我做了类似删除节点的事情,我克隆了它.它将处理时间从12分钟减少到10秒.我不是在开玩笑. (3认同)

jas*_*sso 13

这似乎是另一种情况,使用XPath看起来很慢但不是XPath,原因可能是由DOM方法引起的 nodelist.item(i)

NodeListJava中的默认实现具有以下特定功能:

  1. 它被懒惰地评估
  2. DOM列表是实时的
  3. 它作为链表实现
  4. 该列表有一些缓存

当您单独查看这些功能时,您可能想知道为什么XPath表达式的结果对象具有这样的功能,但是当您将它们组合在一起时它们会更有意义.

1) 延迟评估可能会模糊性能瓶颈的位置.因此,返回NodeList似乎很快,但如果任务要总是遍历列表,那么它或多或少只会延迟性能成本.如果每次读取列表中的下一个项目时必须再次处理整个列表的评估,则延迟评估变得昂贵.

2) NodeList作为"实时"列表意味着它被更新并且引用当前在文档树中的节点,而不是指最初构建列表时树中的节点或者那些节点的克隆.这是掌握DOM初学者的重要特征.例如,如果您选择一个NodeList兄弟元素并尝试向每个节点添加一个新的兄弟元素,则采取步骤item(i+1)将始终到达最新添加的节点,并且循环将永远不会完成.

3) 实时列表还给出了为什么它被实现为链表(或AFAIK实际实现是双向链表)的一些解释.在您的测试中可以清楚地看到这种效果,其中访问最后一个元素始终是最慢的,无论您是通过向后还是向前迭代它.

4) 由于缓存,如果缓存保持干净,循环在单个列表上而不对树进行任何更改应该是相当有效的.在某些Java版本中,此缓存存在问题.我没有调查所有程序使缓存无效但可能最安全的赌注是建议保持评估的表达式相同,不对树进行更改,一次循环一个列表,并始终步进到下一个或上一个列表项.

当然,真正的性能取决于用例.而不是仅仅调整列表循环,你应该尝试完全摆脱循环列表 - 至少作为参考.克隆使列表无法生效.可以通过将节点复制到阵列来实现对节点的直接访问.如果结构合适,您还可以使用其他DOM方法getNextSibling(),这些方法可以提供比循环NodeList更有效的结果.

  • 很好的答案.我很想看到一些代码示例 - 如何克隆节点列表,将它转换为节点数组的最快方法是什么? (3认同)

小智 6

尝试克隆节点(这样你的祖先就不会有不必要的引用)

Node singleNode = nodes.item(i).cloneNode(true);
Run Code Online (Sandbox Code Playgroud)

如果删除子项,则会丢失引用,只会获得要处理的一半节点.

  • **这是绝对的修复!!!** 我在 45K 子节点上运行了 xpath,花了 50 分钟!使用clone后只需要5秒。这对于 java 实现来说绝对是奇怪的。 (3认同)
  • 我用它来解析传入的消息,而明显的方法是完全不够的。速度的增加既可笑又出乎意料。 (2认同)