XPath.evaluate 性能在多次调用时速度变慢(荒谬地)

2022-09-01 10:09:43

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

我的测试文档是从一个真实的生产示例中提取的。它是大约600k的xml。该文档是一个相当复杂的 Atom 源。

我意识到我用XPath做的事情可以在没有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需要10ms到20ms的时间。到循环的第15次,每个xp.evaluate需要200ms到300ms。到循环结束时(其中有 150 个项目),每个 xp.evaluate 大约需要 500 毫秒-600 毫秒。nodes

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

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

我在 JUnit 测试用例中的单个线程上运行此内容,该测试用例不会创建活动或任何内容。

我真的很困惑。

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

谢谢!

更新

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

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


答案 1

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

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

然后使用变量而不是运行每个计算(当然,您可以更改名称)singleNodenodes.item(i);

这样做会将您正在使用的节点与大型主文档分离。这将大大加快评估方法的处理时间。

前任:

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

 }

答案 2

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

Java 中 NodeList 的默认实现具有某些功能:

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

当您分别查看这些功能时,您可能想知道为什么 XPath 表达式的结果对象应该具有这样的功能,但是当您将它们放在一起时,它们更有意义。

1) 延迟评估可能会模糊性能瓶颈的位置。因此,返回 NodeList 似乎很快,但如果任务是始终循环访问列表,则它或多或少只会推迟性能成本。如果每次读取列表中的下一项时都必须再次处理整个列表的评估,则延迟计算变得代价高昂。

2)作为“实时”列表意味着它被更新并引用当前在文档树中的节点,而不是最初构建列表时树中的节点或这些节点的克隆。对于 DOM 初学者来说,这是一个需要掌握的重要功能。例如,如果选择同级元素的 a 并尝试向每个节点添加一个新的同级元素,则执行步骤将始终到达最新添加的节点,循环将永远不会完成。NodeListNodeListitem(i+1)

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

4)由于缓存,如果缓存保持干净,则在不导致树的任何更改的情况下循环单个列表应该相当有效。在某些版本的Java中,这种缓存存在问题。我还没有调查过为什么所有过程都使缓存无效,但最安全的选择可能是建议保持计算的表达式相同,不对树进行任何更改,一次循环一个列表,并始终单步执行下一个或上一个列表项。

当然,真正的性能胜利取决于用例。与其只是调整列表循环,不如尝试完全摆脱循环列表 - 至少作为参考。克隆使列表无法生效。可以通过将节点复制到数组来实现对节点的直接访问。如果结构合适,您还可以使用其他DOM方法,例如所说的比在NodeList上循环更有效的结果。getNextSibling()


推荐