[英]XPath.evaluate performance slows down (absurdly) over multiple calls
我正在嘗試使用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
}
}
當我在Nexus One上運行時(不在調試器中,但連接了USB),第一次通過循環時,每個xp.evaluate需要10ms到20ms。 到第15次循環時,每個xp.evaluate需要200ms到300ms。 在循環結束時( nodes
有150個項目),每個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。 因此,這似乎與調用的數量無關,而是上下文接近文檔末尾的表達式比上下文接近文檔開頭的表達式花費更長的時間。
對於我能做些什么,有沒有人有任何想法?
嘗試在頂部的循環中添加此代碼;
Node singleNode = nodes.item(i);
singleNode.getParentNode().removeChild(singleNode);
然后使用singleNode
變量而不是nodes.item(i);
運行每個評估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
}
這似乎是另一種情況,使用XPath看起來很慢但不是XPath,原因可能是由DOM方法nodelist.item(i)
引起的
Java中NodeList
的默認實現具有以下特征:
當您單獨查看這些功能時,您可能想知道為什么XPath表達式的結果對象具有這樣的功能,但是當您將它們組合在一起時它們會更有意義。
1)延遲評估可能會模糊性能瓶頸的位置。 因此,返回NodeList似乎很快,但如果任務要總是遍歷列表,那么它或多或少只會延遲性能成本。 如果每次讀取列表中的下一個項目時必須再次處理整個列表的評估,則延遲評估變得昂貴。
2) NodeList
是“實時”列表意味着它被更新並且引用當前在文檔樹中的節點,而不是指最初構建列表時樹中的節點或者那些節點的克隆。 這是掌握DOM初學者的重要特征。 例如,如果選擇兄弟元素的NodeList
並嘗試向每個節點添加一個新的兄弟元素,則執行步驟到item(i+1)
將始終到達最新添加的節點,並且循環將永遠不會完成。
3)實時列表還給出了為什么它被實現為鏈表(或AFAIK實際實現是雙向鏈表)的一些解釋。 在您的測試中可以清楚地看到這種效果,其中訪問最后一個元素始終是最慢的,無論您是通過向后還是向前迭代它。
4)由於緩存,如果緩存保持干凈,循環在單個列表上而不對樹進行任何更改應該是相當有效的。 在某些Java版本中,此緩存存在問題。 我沒有調查所有程序使緩存無效但可能最安全的賭注是建議保持評估的表達式相同,不對樹進行更改,一次循環一個列表,並始終步入下一個或上一個列表項。
當然,真正的性能取決於用例。 而不是僅僅調整列表循環,你應該嘗試完全擺脫循環列表 - 至少是為了參考。 克隆使列表無法生效。 可以通過將節點復制到陣列來實現對節點的直接訪問。 如果結構合適,你也可以使用其他DOM方法,比如getNextSibling()
,它表示比循環NodeList更有效。
嘗試克隆節點(這樣你的祖先就不會有不必要的引用)
Node singleNode = nodes.item(i).cloneNode(true);
如果刪除子項,則會丟失引用,只會獲得要處理的一半節點。
這有點晚了,但我遇到了同樣的情況,但看起來我的文件太大了,其他答案都沒有真正解決問題。
最終,我找到了jaxen 。 一旦我使用它,之前需要15秒才能解析的文檔只需幾毫秒。
不幸的是,Jaxen的記錄非常糟糕,但效果很好:
DOMXPath myXPath = new DOMXPath("atom:id/text()");
String myContent = myXPath.stringValueOf(myDocument);
Java Doc可以在這里找到http://jaxen.codehaus.org/apidocs/org/jaxen/dom/DOMXPath.html
每次從Nodelist中獲取Node時,似乎它都會引用xml的整個結構; 因此,當您導航節點時,xpath進程每次都從xml的根開始,因此,當您在trhee中進入時,需要更多時間。
出於這個原因,當您獲取節點時,在導航之前,您必須通過此方法強制轉換為字符串:
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;
通過這種方式,新元素丟失了對其祖先的所有引用,並將用作簡單節點而不是嵌套節點。 顯然,只有在必須深入導入節點時,此方法才有用。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.