Saxon在XPath基准测试中初始慢后续快的原因及冷执行方案
Saxon XPath冷运行基准测试问题
问题背景
在Groovy/Java环境中对Saxon的XPath表达式做基准测试,每次重新创建Processor、DocumentBuilder和XPathCompiler对象,期望获取稳定的冷运行性能数据。测试代码如下:
static def measureSaxon(String xml, String xpath) { Processor processor = new Processor(false); processor.setConfigurationProperty(FeatureKeys.OPTIMIZATION_LEVEL, "0") XdmNode xdm = processor.newDocumentBuilder().build(new StreamSource(new StringReader(xml))); def compiler = processor.newXPathCompiler() return measureExecutionTime( "Saxon", xpath, { def evaluate = compiler.evaluate(xpath, xdm) if (evaluate instanceof XdmValue && evaluate.size() > 0) { return evaluate.itemAt(0).getStringValue() } else { return "No node found" } } ) }
已尝试设置processor.setConfigurationProperty(FeatureKeys.OPTIMIZATION_LEVEL, "0"),但首次调用执行速度仍远慢于后续调用,基准测试结果如下:
# Benchmark Results for Saxon | XPath Expression | Execution Time (ns) | Execution Time (ms) | Result | |---------------------------------|---------------------|---------------------|----------------------| | `/program/@name` | 151562270 | 151.56227 | j$Collections | | `/program/objects/o/@base` | 522288 | 0.522288 | some.class | | `/program/objects/o/o/o/o/@base`| 12328695 | 12.328695 | org.lang.bytes |
疑问:Saxon内部是否存在无法通过重置Processor或设置OPTIMIZATION_LEVEL为0来禁用的深层缓存机制或优化?如何强制Saxon始终以“首次调用”的状态运行,以测量真实的冷执行性能?
问题解答
1. Saxon的深层缓存/初始化机制
是的,Saxon存在一些不受OPTIMIZATION_LEVEL=0控制的底层初始化和缓存逻辑,主要包括:
- JVM类加载缓存:首次调用时会加载Saxon核心类(如XPath解析器、字节码生成器相关类),后续调用直接复用已加载的类,这是JVM层面的机制,和Saxon自身配置无关。
- 类级静态缓存:Saxon内部有静态缓存(如命名空间URI映射、内置函数元数据),这些缓存属于类级别,不会随
Processor实例销毁而清除。 - 字节码片段缓存:即使关闭表达式优化,Saxon仍可能缓存生成的小型字节码片段,这类缓存绑定到类加载器而非
Processor实例。 - 文档模型隐式优化:
XdmNode构建时会生成节点索引等底层结构优化,属于文档实例本身,不受全局优化级别控制。
2. 强制冷运行的解决方案
要模拟真实的首次调用状态,需要从JVM和Saxon配置两个层面入手:
(1)使用隔离类加载器
每次测试用新的类加载器加载Saxon相关类,彻底规避JVM类加载缓存:
static ClassLoader createIsolatedClassLoader() throws Exception { URL[] saxonUrls = [ new File("path/to/saxon-he.jar").toURI().toURL(), // 若有依赖jar需一并加入 ] return new URLClassLoader(saxonUrls, null) // 不继承父类加载器的缓存 } static def measureSaxonColdRun(String xml, String xpath) throws Exception { ClassLoader cl = createIsolatedClassLoader() // 反射创建Processor实例 Class processorClass = cl.loadClass("net.sf.saxon.s9api.Processor") Object processor = processorClass.getConstructor(boolean.class).newInstance(false) // 设置优化级别 Class featureKeysClass = cl.loadClass("net.sf.saxon.lib.FeatureKeys") processor.getClass().getMethod("setConfigurationProperty", String.class, Object.class) .invoke(processor, featureKeysClass.getField("OPTIMIZATION_LEVEL").get(null), "0") // 构建XdmNode Class documentBuilderClass = cl.loadClass("net.sf.saxon.s9api.DocumentBuilder") Object builder = processor.getClass().getMethod("newDocumentBuilder").invoke(processor) Class streamSourceClass = cl.loadClass("javax.xml.transform.stream.StreamSource") Class stringReaderClass = cl.loadClass("java.io.StringReader") Object source = streamSourceClass.getConstructor(Reader.class) .newInstance(stringReaderClass.getConstructor(String.class).newInstance(xml)) Object xdm = builder.getClass().getMethod("build", streamSourceClass).invoke(builder, source) // 创建XPathCompiler并执行 Class xpathCompilerClass = cl.loadClass("net.sf.saxon.s9api.XPathCompiler") Object compiler = processor.getClass().getMethod("newXPathCompiler").invoke(processor) return measureExecutionTime( "Saxon Cold Run", xpath, { Object evaluate = compiler.getClass().getMethod("evaluate", String.class, Object.class) .invoke(compiler, xpath, xdm) Class xdmValueClass = cl.loadClass("net.sf.saxon.s9api.XdmValue") if (xdmValueClass.isInstance(evaluate)) { int size = (int) xdmValueClass.getMethod("size").invoke(evaluate) if (size > 0) { Object item = xdmValueClass.getMethod("itemAt", int.class).invoke(evaluate, 0) return (String) item.getClass().getMethod("getStringValue").invoke(item) } } return "No node found" } ) }
(2)禁用Saxon静态缓存
测试前设置系统属性关闭Saxon内部的静态元数据缓存:
System.setProperty("net.sf.saxon.static-cache", "off");
(3)关闭文档索引优化
创建DocumentBuilder时禁用节点索引,避免文档构建时的隐式优化:
DocumentBuilder builder = processor.newDocumentBuilder(); builder.setConfigurationProperty(FeatureKeys.BUILD_TREE_INDEXES, "false");
(4)使用JMH的fork模式
如果用JMH做基准测试,设置每次测试启动新JVM进程,彻底隔离缓存:
@Fork(value = 1, jvmArgs = {"-Dnet.sf.saxon.static-cache=off"}) @Benchmark public String saxonColdRun() { // 测试逻辑 }
3. 额外建议
- 可将XML文档解析时间从测试中分离,单独测量XPath编译+执行的冷性能。
- 多次重复冷运行测试并取平均值,消除单次JVM启动的偶然波动。
内容的提问来源于stack exchange,提问作者Volodya Lombrozo
相关产品推荐
相关产品推荐

