我希望這篇文章能達到幾個目的。首先,我希望它可以讓樣式表作者了解 XSLT 可以實現哪種類型的優化,以及哪些結構當前還沒有優化。當然,這種優化的細節在各個處理器以及各個發行版之間都各不相同,但我希望通過閱讀本文可以讓您更好地了解幕後工作。其次,它描述了我認為當前的 XSLT 技術(我認為在這方面,Saxon 與其它 XSLT 處理器沒有高級與否的區別),而且它還描述了我認為該技術將來進一步發展的領域。我希望本文可以激勵那些有編譯器和數據庫優化經驗的研究人員在這個領域中做進一步的工作。
最後(也是最重要的一點),本文打算成為初學者學習 Saxon 源代碼的起點。它並不是簡單地浏覽代碼,也沒有假設您要深入到那一層。但是如果您不滿足於鑽研 JavaDoc 規范或源代碼本身,這篇文章會對您有所幫助。
一些說明:所描述的版本是 Saxon 6.1,而且還講述了代碼的功能性故障,這些代碼不能始終映射成軟件包和模塊結構。例如,本文將編譯器和解釋器描述成單獨的功能組件。但在實際代碼中,例如處理 <xsl:choose> 指令的模塊既包含編譯時代碼,也包含運行時代碼,以支持該指令。如果想要將本文用作代碼的指南,我已經包括了對軟件包和模塊的特殊引用,這樣您可以知道到哪裡查看。
首先,我將描述 Saxon 產品的設計。Saxon 是一個 XSLT 處理器。即,它是使用 XML 文檔和樣式表作為輸入,然後生成結果文檔作為輸出的程序。(我假設有 XSLT 的知識,如果您是初學者,可以閱讀本文的 姐妹篇作為介紹。)
Saxon 包括了最初由 David Megginson 編寫的開放源碼 AElfred XML 語法分析器的副本,盡管它可以與其它實現 Java SAX 接口的語法分析器一起使用。Saxon 還包括了一個串行化器,用於將結果樹轉換成 XML、Html 或純文本。從技術上看,串行化器並不是 XSLT 處理器的一部分,但它在實際使用中是必不可少的。
Saxon 實現了 TrAX(XML 的 轉換 API)接口,該接口被定義成 JAXP 1.1 Java 擴展的一部分。理解本文無須了解這個接口,但了解 TrAX 的體系結構有助於您理解構造 Saxon 的方法。
Saxon 體系結構
圖 1 顯示了 Saxon 軟件的主要組件。
圖 1. Saxon 體系結構
樹構造器創建源 XML 文檔的樹表示法。它用於處理源文檔和樣式表。其中有兩個部件:
XML 語法分析器 (包 com.icl.saxon.aelfred )讀取源文檔,並通知事件,如元素的開始和結束。
樹構建器 (模塊 com.icl.saxon.Builder )被通知這些事件,並使用它們來構造 XML 文檔的內存表示法。
語法分析器和構建器之間的接口是 Java SAX2 API。盡管 SAX API 僅經過非正式的標准化,但它由六個可免費獲取的 XML 語法分析器實現,允許 Saxon 可交替地與這些語法分析器中的任何一個一起使用。在語法分析器和樹構建器之間有一個組件,我將它稱作 Stripper (我無法抗拒這個名稱): Stripper 根據樣式表(模塊 com.icl.saxon.Stripper )中的 <xsl:preserve-space> 和 <xsl:strip-space> 偽指令,在空白文本節點被添加到樹之前,除去這些節點。 Stripper 是 SAX 過濾器的一個良好示例,這是以 SAX 事件流作為輸入,並生成另一個 SAX 事件流作為輸出的一段代碼。從更宏觀的角度看,整個 Saxon 轉換還可以當作 SAX 過濾器來進行操作。這個方法使將一個復雜的轉換分解成以流水線排列的一系列簡單轉換變得非常容易。
樹浏覽器正如其名,允許應用程序通過浏覽層次結構來從樹中選擇節點。由構建器組件構造的樹表示法是 Saxon 的專利。這就是 Saxon 與其它 XSLT 處理器的不同之處:其它一些 XSLT 處理器使用通用 DOM 模型作為其內部樹。使用 DOM 的優點是可以用第三方軟件生成樹。為其它目的構造的樹可以直接作為轉換的輸入而提供,並且等同地,基於 DOM 的應用程序可以直接使用轉換的輸出。
在 Saxon 中,我注意到通過使用 DOM 而提供的互操作性帶來了極高的成本。首先,DOM 樹模型與 XSLT 處理器所需的 XPath 模型有本質上的差別,這種差異會增加將一個模型映射成其它模型所帶來的運行時成本。例如,DOM 樹可以包含 XPath 模型不需要的信息,如實體節點。其次,DOM 樹可以就地更新,而 XSLT 處理模型意味著只能按順序編寫樹。設計只能按順序寫的樹可以實現效率。例如,每個節點可以包含一個序號,這就易於按連續的文檔順序排序節點,這是一個常見的 XSLT 需求。最後,DOM 實現通常包括許多同步代碼以使多線程化訪問安全。由於 XSLT 處理模型是“寫一次讀多次”的,同步邏輯可以變得更簡單,使浏覽樹變得更快。
實際上,您將見到,Saxon 提供了兩種不同的樹實現,每一個都有其自己的構建器和浏覽類(包 com.icl.saxon.tree 和 com.icl.saxon.tinytree )。這兩種實現提供不同的性能取捨。
樣式表編譯器在執行之前對樣式表進行分析。它並不生成可執行代碼;而是生成樣式表的裝飾樹 表示法 ,在其中確認和分析這個樣式表中的所有 XPath 表達式,並解析所有交叉引用,以及預先分配堆棧框架存儲槽,諸如此類。因此,樣式表編譯器執行構造決策樹的重要功能,以便在執行時查找正確的模板規則以處理每個輸入節點;試圖將每個節點與每個可能的模式進行匹配是非常費時的。然後在轉換時,裝飾樹開始進入角色以驅動樣式表處理。(編譯器分布在 com.icl.saxon.style 軟件包的類之間,特別是方法 prepareAttributes() 、 preprocess() 和 validate() 。)
在某個階段,Saxon 確實包括了生成可執行 Java 代碼的樣式表編譯器。但是,它只處理 XSLT 語言的一個子集,並且隨著開發的發展,通過減少完整編譯來達到性能增長。最後,我放棄了這個方法,因為開發復雜性不斷增加而性能優勢卻在下降。當前市場上還沒有完整的 XSLT 編譯器。Sun 已經制作了一個編譯器的 alpha 發行版,叫做 XSLTC,雖然仍處於開發的早期階段,但看起來很有前途(請參閱 參考資料)。
由 Saxon 的樣式表編譯器(在 com.icl.saxon.style.XSLStyleSheet 類中)生成的裝飾樹不能保存到磁盤,因為將樹讀到內存比重新編譯原始代碼還要費時(很可能是因為已經它增加的大小)。只要樹還保留在內存中,您就可以重用它。樹封裝在一個叫作 PreparedStyleSheet 的對象中,該對象實現了 JAXP 1.1 中的 Javax.XML.transform.Templates 接口。在服務器環境中使用同一個樣式表來轉換許多不同的源文檔是很常見的。要允許這樣做,已編譯的樣式表在執行時應該嚴格是只讀的,以允許在多個執行線程中同時使用它。
Saxon 處理器的核心是 樣式表解釋器 ( com.icl.saxon.Controller 類,它實現 JAXP 1.1 中的 Javax.XML.transform.Transformer 接口)。該解釋器使用裝飾樣式表樹來驅動處理。按照語言的處理模型,它首先查找處理輸入樹根節點的模板規則。然後,它對該模板規則求值(或者按標准行話是“實例化”)。
樣式表樹使用不同的 Java 類來表示每種 XSL 指令類型。例如,考慮以下指令:
<xsl:if test="parent::section">
<h3><xsl:value-of select="../@title"></h3>
</xsl:if>
這段代碼的作用是如果源樹中的當前節點有類型為 <section> 的父代元素,那麼將 <h3> 元素輸出到結果樹中;生成的 <h3> 節點的文本內容是父代 section 的 title 屬性的值。
在裝飾樹上,由圖 2 中顯示的結構表示這段代碼。
圖 2. 裝飾樣式表樹
樣式表中的元素直接映射成樹上的節點,如 表 1 所示。所有表示元素的 Java 對象都是 com.icl.saxon.style.StyleElement 的子類,而它又是 Saxon 樹狀結構中元素節點的缺省實現 com.icl.saxon.tree.ElementImpl 的子類。這兩個 XPath 表達式由樹節點引用的 com.icl.saxon.expr.Expression 對象表示。
表 1. 樣式表元素及其對應的 Java 類
元素或表達式…… ……由此 Java 類中的對象表示( com.icl.saxon.style.StyleElement 的子類)
<xsl:if> com.icl.saxon.style.XSLIf <h3> (輸出,不是指令) com.icl.saxon.style.LiteralResultElement <xsl:value-of> com.icl.saxon.style.XSLValueOf . XPath 表達式 com.icl.saxon.expr.Expression執行 <xsl:if> 指令會執行相應 XSLIf 對象的 process() 方法。該方法訪問測試 Expression 對象,該對象有一個方法 evaluateAsBoolean() 。 evaluateAsBoolean() 用於對表達式求值以返回一個布爾結果。(這是一個優化:可以使用直接的 evaluate() 調用,然後將結果轉換成一個布爾值,如規范中所描述的那樣。布爾值通常會使求值過程變得更迅速。例如,當實際值或表達式是一個節點集時,一旦找到節點集的一個成員時,就可以立即知道最終的布爾值結果。)
如果 evaluateAsBoolean() 的結果是真,則 process() 方法會調用 樣式表樹上 XSLIf 節點的所有子節點的 process() 方法。如果結果不是真,它就直接退出。
同樣, LiteralResultElement 的 process() 方法將元素復制到結果樹中,並處理 LiteralResultElement 的子元素,而 XSLValueOf 對象的 process() 方法將 select 表達式當作字符串進行求值,並將結果復制成結果樹的文本節點。
因此,樣式表解釋器的主要組件是:
用於每個包含那個指令邏輯的樣式表類型的類
一組支持類,用於處理變量的綁定、運行時上下文的管理以及與模板規則匹配的節點
用於對 XPath 表達式求值並返回這些值的 XPath 表達式解釋器
XPath 解釋器(包 com.icl.saxon.expr )非常符合 解釋器 設計模式,該模式是由 Gamma、Helm、Johnson 和 Vlissides 描述的面向對象軟件的 23 種經典模式中的一個。XPath 語法的每個結構都有一個對應的 Java 類。例如, UnionExpr 結構(寫作 A|B ,並表示兩個節點集的聯合)由 com.icl.saxon.expr.UnionExpression 類實現。通常在編譯樣式表時執行的 XPath 表達式語法分析器(模塊 com.icl.saxon.expr.ExpressionParser ),生成直接反映表達式的語法分析樹的數據結構。要對表達式求值,該結構中的每個類都有一個 evaluate() 方法,它負責返回表達式的值。如果是 UnionExpression 類, evaluate() 方法將對兩個操作數求值,檢查在這兩種情況下節點集的結果,然後使用排序合並策略組成聯合。
在 Gamma 等 描述的設計模式中, evaluate() 方法使用 Context 參數。 Context 對象封裝了對表達式求值所需的所有上下文信息。
包括:
關於當前節點和當前節點列表(例如對 XPath 函數 position() 和 last() 求值所必需的)的信息
訪問保存變量值的 com.icl.saxon.Bindery 對象
訪問表達式作用域中 XML 名稱空間列表,在測試名稱的等價性時需要
XPath 解釋器還包括擴展 Gamma 等的基本解釋器模式的優化特性
例如:
每個表達式類都有一個 simplify() 方法,以允許表達式重寫。 這就可以執行上下文獨立的優化。有時,這會導致生成不同的 XPath 表達式(例如 title[2=2] 重寫成 title ,而 count($x)=0 重寫成 not($x) )。表達式重寫通常使用表示特殊情況的內部類。例如,表達式 section[@title] 返回擁有標題屬性的當前元素的所有子 <section> 。由於上下文中出現子表達式 @title ,就可能使用專用類來重寫它,這個專用類用於測試當前節點上是否出現給定屬性,並返回一個布爾值。
每個表達式類都有一個 evaluate() 方法和一個 enumerate() 方法 。這(在表達式表示節點集的情況下)允許遞增檢索節點,而不是立即檢索所有節點。這允許按關系數據庫系統采用的典型方式進行流水線執行。例如,通過合並其兩個操作數的枚舉使在聯合表達式上調用 enumerate() 可行。只要已經按文檔順序排序了這兩個操作數,就不必為中間節點集分配內存。
可以逐漸 減少表達式,以消除它們的相關性。 在函數型語言中已經廣泛使用的表達式減少的概念,而且此概念特別適用於優化語言,如 XPath。每個表達式類都有一個 getDependencies() 方法,該方法返回關於方法依賴的上下文方面的信息。這就使某些優化成為可能。例如,如果表達式不使用 last() 函數,那麼就不必執行先行處理來確定上下文列表中有多少元素。此外,每個表達式都有一個 reduceDependencIEs() 方法,它返回一個等價的表達式,在該表達式中消除了指定的相關性,而其余的都保留下來。這有助於重復使用同一個表達式。例如,在執行排序之前,減少了排序鍵表達式,以消除變量上的相關性(因為這些變量與列表中的每個節點相同),但不消除當前節點上的相關性(它將與列表中的每一項都不同)。
XSLT 語言給了處理器很大的自由,讓它可以按其選擇的順序對表達式求值,因為它沒有副作用。Saxon 中的常規策略是盡早對標量值求值(字符串、數字、布爾值),而盡可能晚對節點集求值。盡早對標量值求值可以通過只執行一次操作來實現優化。延遲對節點集求值可以避免在內存中不必要地保留大的列表,這樣可以節省內存。它還可以節省時間,如果對於節點集的唯一操作就是測試它是否是空的(通常是這種情況),或者對它的第一個元素進行賦值。
最後, 輸出器 組件( com.icl.saxon.output.Outputter 類)用於控制輸出進程。Saxon 的結果樹通常不在內存中具體化 -- 因為它的節點總是按文檔順序編寫,一旦將它們輸出到結果樹中,就可以串行化這些節點。實際上,轉換擁有的不是單個結果樹,而是結果樹的更改堆棧,因為 XSL 指令,如 <xsl:message> 和 <xsl:attribute> ,有效地將輸出重定向到一個內部目的地,而 <xsl:variable> 構造了一個結果樹片段,它實際上是以它自己為標題的獨立樹。這些元素的解釋器代碼調用輸出器切換到新目的地,然後還原到原始目的地。
使用串行化器將外部輸出寫到文件中。邏輯上,這將使用結果樹作為輸入,並將它轉成文本文件文檔。實際上,如您所見,結果樹沒有在內存中具體化,所以串行化器將按文檔順序一次交出樹的一個節點。使用類似與 SAX2 的接口 ( com.icl.saxon.Emitter ) 來顯示這個節點流:它在如何表示名稱和名稱空間等細節方面與 SAX2 略有不同。如在 XSLT 推薦書中定義的,對於 XML、Html 和純文本輸出,有獨立的串行化器。Saxon 還允許將樹提供給用戶以便進一步處理,或者作為輸入提供給另一個樣式表。這允許按順序應用幾個樣式表來完成多階段轉換。
性能
良好的性能當然是驅動 Saxon 設計的一個因素,僅次於符合 XSLT 規范。部分原因是這對於用戶來說很關鍵,而且因為在可免費獲取幾個 XSLT 處理器的情況下,性能是區分優劣的主要因素。
這一節討論了影響 XSLT 處理器性能的一些因素,以及在每種情況下 Saxon 用於提高速度的策略。
Java 語言問題
通常都認為 Java 很慢。這有其合理性,但需要仔細驗證這種說法。
許多人認為造成 Java 速度慢的原因是它生成解釋型字節碼,而不是本機代碼。以前是這樣,但現在有了 JIT 編譯器後情況就不同了。原始代碼執行速度通常幾乎與用編譯型語言(如 C 語言)編寫的等價代碼相同(有時更快速)。
然而 Java 可能有內存分配的問題。與 C 和 C++ 不同,Java 自己管理內存,它使用無用信息收集器來釋放不想使用的對象。這給程序員帶來了很大的便利,但卻易於創建浪費內存的程序:它們會因為過多使用虛擬內存而崩潰,或者因為過分頻繁地分配和釋放對象而使無用信息收集器過度操勞。
某些編碼技巧使內存分配問題減至最少。例如,使用 StringBuffer 而不用 String ,使用可再用對象的池,等等。診斷工具可以幫助程序員確定何時使用那些技巧。使代碼更快需要許多調整,但經論證這仍比使用諸如 C++ 之類的語言更簡便,在這些語言中必須手工管理所有內存分配。
XSLT 處理給 Java 帶來了實現樹狀結構的特殊挑戰。Java 強行在每個對象的大小方面增加了相當大的開銷(多達 32 個字節,取決於所使用的 Java VM)。這通常會在內存中生成一個比源 XML 文件大許多倍的樹狀結構。例如,空元素 <a> (在源文件中是 4 個字節)對於節點可以擴展成 Element 對象,對於其名稱可以擴展成 Name 對象,還可以擴展成 Name 對象引用的 String 對象、空的 AttributeList 節點、空的 NamespaceList 節點,以及許多使這些對象互相鏈接,並且將它們與樹中的父節點、兄弟節點和子節點鏈接的 64 位對象引用。簡單實現可以根據這 4 個源碼字節輕易地生成 200 個字節的樹存儲器。如果某些用戶嘗試處理那些原始大小超過 100 MB 的 XML 文檔,可想而知,其結果是可預測的,並且通常是致命的。
這就是 Saxon 開始構建其樹狀結構的一個原因。通過消除實現完整 DOM 接口的需求,就可以除去樹中的某些數據。消除支持更新的需求特別實用。例如,對於沒有屬性的元素,Saxon 使用另一個類,因為它知道如果元素一開始就沒有屬性,以後它也不會獲得任何屬性。Saxon 使用的另一個技巧是優化包含單一子文本節點(例如 <b>text</b> )的元素的公共狀態存儲器。
如 W3C 規范中所描述的,XPath 樹模型包括屬性和名稱空間的節點。因為在轉換過程中很少訪問這些節點,所以 Saxon 按要求構造這些節點,而不是讓它們永久占據樹的空間。(這是 Gamma 等的 Flyweight設計模式。)
Saxon 的最新發行版更邁進了一步:它使用的樹實現中,根本不使用 Java 對象來表示節點。事實上,樹中的所有信息以整數數組表示。所有節點都創建成臨時(或輕量級)對象,按要求構造成對這些數組的引用,並在不再需要時廢棄。該樹實現(包 com.icl.saxon.tinytree )占用極少的內存,並且構建過程更迅速,但其代價是浏覽樹的速度會稍微慢一點。總而言之,它的性能比標准樹更好,因此我將它作為缺省值提供。
標准實用程序類,如 Hashtable 和 Vector 也影響 Java 程序性能。開發者常常在整個應用程序中使用這些便利類庫。然而,這是要付出代價的。其中一部分是因為類能做的超出了您真正需要的,與只為一種用途設計的類相比,它們會產生更多的開銷。它們還是按多線程設計的,用於處理最壞的情況。如果您知道多個線程不會同時訪問數據結構,那麼可以設計您自己的對象,而不必使用這些現有的類,從而節省同步成本。用數組替換 Vector 通常會得到回報,唯一的缺點是需要手工處理數組的擴充,而 Vectors 是自我管理的。
位置路徑求值
最有特點的 XPath 表達式(XPath 從中獲取其名稱)是位置路徑。位置路徑用於選擇源樹中的節點。位置路徑基本上包括起始地址和許多步驟。它類似於 UNIX 文件名或 URL,除了每個步驟選擇一組節點而不是單一節點。例如, ./chapter/section 選擇當前節點的所有 <chapter> 子節點的所有 <section> 子節點。起始地址標識浏覽樹的起始點:它可能是當前節點、源樹的根節點、另一棵樹的根節點或由鍵定位的一組節點。位置路徑中的每個步驟都從一個節點浏覽到一組相關的節點。每個步驟都是根據浏覽軸(子軸是缺省值)定義的:例如,祖輩軸選擇所有祖輩節點,以下的兄弟軸選擇原始節點下的所有兄弟節點,子軸選擇其子節點。除了指定軸之外,每個步驟還可以指定所需節點的類型(如元素、屬性或文本節點),所需節點的名稱,以及該節點必須滿足的謂詞(例如,子文本節點的值以 B開頭)。
為位置路徑設計執行策略等同於優化關系型查詢的問題,盡管當前理論還不夠先進,而且所使用的大多數技巧與規范中描述的簡單策略的浏覽方式略有不同。例如,盡管在樣式表中可以指定必須構建以支持聯合訪問的鍵(而不是 SQL 中的 CREATE INDEX ),Saxon 目前只是使用這些索引來支持明確引用它們的查詢(通過使用 key() 函數),並且永遠不優化使用直接謂詞的查詢。
當前在 Saxon 中對於位置路徑使用的優化技術包括:
盡可能避免排序。 許多 XSLT 指令要求按文檔順序處理節點,因此需要花精力按文檔順序檢索節點,還要花更大精力來檢測檢索節點的自然順序是文檔順序還是逆向文檔順序,因而無需排序。這樣的一個示例就是表達式 //item (它被定義成 /descendent-or-self::node()/item 的縮寫)可以由 /descendent::item 代替,只要它不使用位置謂詞。後一個表達式將自然地按文檔順序檢索節點,而前一個則不是這樣。
減少謂詞。 有時這可能使謂詞減少到常量值 true 或 false,允許簡化整個位置路徑。通常它只是除去公共子表達式。例如,在過濾器表達式 $x[count(.|$y)=count($y)] 中(它是 XSLT 1.0 中執行一組邏輯乘操作的唯一簡便方法)中,Saxon 只對 count($y) 求值一次。
使用位置謂詞提前終止。 謂詞 para[position() <= 3] 選擇當前節點的前三個 <para> 子節點。沒有必要對每個 <para> 元素應用該謂詞來查看它是否是真,因為處理可能在第三個節點後停止。
優化屬性引用。 XPath 模型對待屬性與對待子元素完全一樣,這大大簡化了 XPath 語言。但是,由於一個元素對於一個給定名稱最多有一個屬性,因此可以優化對屬性的訪問權。該優化還考慮到屬性節點並沒有在樹上真正具體化,除非需要使用它們。這就意味著當 XPath 表達式 child::title 掃描所有子元素來查找名為 title 的子元素時,類似的表達式 attribute::title (通常縮寫成 @title )會直接得到相關屬性。
位置路徑的弱求值。 對特殊上下文中的位置路徑表達式求值不會返回內存中實際節點的列表,事實上它返回另一個表達式(稱作“內包節點集”,類 com.icl.saxon.expr.NodeSetIntent ),在這個表達式中除去了所有上下文相關性。僅當真正使用內包節點集時,才會枚舉其成員:並且根據如何使用它,可能根本無需檢索這些成員。例如,如果該節點集在布爾型上下文中使用,唯一需要的處理就是測試它是否是空的。當第三次使用內包節點集時,將在擴展存儲它,以內存換取處理時間。這就象 SQL 中的具體化視圖。
樣式表編譯
我已經描述了 Saxon 所做的第一件事就是將樣式表“編譯”成裝飾樹以便以後能夠有效地執行。這提供了一個很好的機會,可以只執行一次操作,而不必每次都執行相關指令。
在樣式表編譯階段完成的一些任務如下:
確認。在編譯階段可以檢測出大多數用戶錯誤。這包括第一眼看上去象運行時錯誤的一些錯誤。XPath 表達式使用動態定類型(在對表達式或變量求值之前,不必知道表達式或變量的類型)。但是,對於大多數實際 XPath 表達式,在編譯時類型 是 已知的。因此,例如, <xsl:for-each select="$x+2"> 可以立即識別成一個錯誤,因為 XPath 表達式 $x+2 永遠不會返回節點集。在許多情況下,甚至可以檢測出 <xsl:for-each select="$x"> 是一個錯誤,因為缺少賦值語句就意味著通常可以根據聲明來推斷變量的類型。
簡化表達式。 已經討論過所使用的一些技巧。一個使用表達式的重要環境是在屬性值模板中。例如,文字結果元素 <td width="{$x * 2}"> 輸出 <td> 元素,在運行時計算出它的 width 屬性。一個重要的編譯任務是將屬性值模板轉換成有效的結構以便在運行時求值。
綁定變量和其它名稱。由於所有變量在編譯時都是可見的,因此編譯器可以提前為每個被調用的模板分配堆棧框架上的槽。然後表達式中對變量的引用可以靜態綁定到本地堆棧框架或一列全局變量的特定槽上。同樣,其它對已命名對象(如模板和外部函數)的引用通常可以靜態解析。在某些情況下,XPath 語法允許動態生成名稱(例如,鍵名稱或十進制格式的名稱),但它仍可以檢測一般情況,即以文字形式提供名稱,然後靜態綁定它。
在其它情況下,在編譯時執行操作不太可行,但執行保存可以避免在運行時執行重復操作。一個例子就是 format-number() 函數,它使用一個模式(描述十進制數所需的輸出格式)作為它的一個變量。如果檢測到格式模式與前一次執行相同的一般情況,則可以節省大量操作。這種優化唯一棘手的一面是將前一次執行的內存保留在與當前線程相關的位置:它不能保留在樣式表自身中,因為需要保證線程安全。
模板規則的模式匹配
模式匹配操作可能是非常昂貴的,因此智能地集中搜索是非常重要的。因此樣式表編譯器構造了決策樹,在運行時用它來決定將哪個模板規則應用於給定節點。
在此,我較為隨意地使用術語 決策樹 。本節較為詳細地描述了實際的數據結構和算法。(請參閱源代碼中的模塊 com.icl.saxon.RuleManager 和 com.icl.saxon.Mode )。
當對某個節點應用 <xsl:apply-templates> 指令時,必須為該節點選擇模板規則。如果沒有匹配的規則,Saxon 會使用內置規則。如果有超過一個規則,則處理器會向 XSLT 規范中的算法報告決定哪個規則取得優先權。這個算法基於用戶分配的優先級、系統分配的優先級、當一個樣式表導入另一個樣式表時建立的優先權關系,以及 -- 如果其它所有都失敗 -- 源樣式表中規則的相對順序。
在典型的樣式表中,大多數模板規則匹配樹中的元素節點。匹配其它節點(如文本節點、屬性和注釋)的規則相對較少。此外,大多數模板規則完整提供了它們必須匹配的元素名稱。允許使用未命名節點的規則,但這些規則並不常用(例如, *[string-length(.) > 10] ,它匹配任何文本內容多於 10 個字符的元素)。
因此,Saxon 的策略是將規則分成兩種:特定規則(在模式中明確指定節點類型和名稱)和一般規則(不指定節點類型和名稱)。決策樹的數據結構包含了特定規則的散列表,按節點類型和節點名稱為鍵,其中每一項都是規則列表,按優先級降序排列;另外有一個列表包含所有一般規則,按優先級排序。要查找某特定節點的模式,處理器將進行兩次搜索:一次是為適用於匹配該節點的相關節點和名稱搜索優先級最高的特定規則,另一次是搜索與該節點匹配的優先級最高的一般規則。然後選擇所找到的優先級最高的規則。
對於由多部分組成的模式,如 chapter/title ,所使用的算法是遞歸的:如果正在測試的節點匹配 title ,並且父節點匹配 chapter (模塊 com.icl.saxon.pattern.LocationPathPattern ),則匹配成功。該簡單方法不能用於使用位置謂詞的模式;例如 chapter/para[last()] ,它只匹配 para 元素,如果該元素是某一章的最後一個元素。匹配這些位置模式可能非常耗時,所以值得特別處理類似模式(如 para[1] )的一般情況。
編號
對樹上的節點進行編號(使用 <xsl:number> 指令)帶來了特殊的優化問題。這是因為每次執行 <xsl:number> 時都會單獨將一個號碼分配給當前節點,該號碼由使用 <xsl:number> 指令中各個屬性的復雜算法定義。算法中並沒有指定如果上一個節點的號碼是 19,那麼下一個的編號就是 20,但大多一般情況下都是如此。檢測那些一般連續編號情況很重要。否則,對大的節點集進行編號將有 O(n 2) 性能,這就類似於將 XSLT 推薦書中指定的編號算法單獨應用於每個節點。
在少數一般情況下,Saxon 會完成該優化,其中編號算法的大多數屬性都是缺省值。特別是,它對 <xsl:number> 指令的最新結果進行編號,如果遇到某些復雜但有經常能滿足的條件,它知道可以通過對這個已記住的號碼加 1 來對此節點進行編號。
結束語
在本文中,我已經嘗試給出了 Saxon XSLT 處理器內幕的概述,特別是它用於提高轉換速度的一些技術。在我發布了 Saxon 最早版本後的大約 18 個月中,性能已經提高了 20 個百分點(或者更多,如果是在缺少內存的情況下運行)。
以後的 18 個月中不太可能有類似的改進。但是,仍有很大的空間可以提高,特別是類似於 <xsl:number> 的構造。另舉一例,Saxon 還沒有開始研究並行執行的可行性,這可以使這種語言更具吸引力。
也許最大的研究挑戰是編寫不用在內存中構建源樹就可以操作的 XSLT 處理器。許多人都歡迎這樣的開發,但它並不是一件容易完成的工作。