备注: 本篇文章是关于先前相同主题文章的最新版本。先前文章主要介绍创建高性能解析器的一些要点,但它吸收了读者的一部分批评建议。原来的文章进行了全面修订,并补充了相对完整的代码。我们希望你喜欢本次更新。 如果你没有指定数据或语言标准的或开源的Java解析器, 可能经常要用Java实现你自己的数据或语言解析器。或者,可能有很多解析器可选,但是要么太慢,要么太耗内存,或者没有你需要的特定功能。或者开源解析器存在缺陷,或者开源解析器项目被取消诸如此类原因。上述原因都没有你将需要实现你自己的解析器的事实<
ints.length; i++) {ints[i] = elements.get(i);} 当知道数组包含的元素数时,我们可以立即创建最终的Java数组,然后将原始值直接放入数组。在插入数值到数组时,这节省了List实例化和构建,原始值自动装箱和对象转换到原始值的时间。如下所示是使用JsonNavigator功能相同的代码: int[] ints = new int[jsonNavigator.countPrimitiveArrayElements()];for (int i = 0, n = ints.length;
i < n; i++) {ints[i] = jsonNavigator.asInt();jsonNavigator.next();} 即使刚刚从JSON数组构建List对象,知道元素的个数可以让你从一开始就能正确的实例化一个ArrayList对象。这样,你就避免了在达到预设阈值时需动态调整ArrayList大小的麻烦。如下是示例代码: List strings = new ArrayList(jsonNavigator.countPrimitiveArrayElements());jsonNavigator.next();
// skip over array start. while (ElementTypes.JSON_ARRAY_END != jsonNavigator.type()) {strings.add(jsonNavigator.asString());jsonNavigator.next();}jsonNavigator.next(); //skip over array end. 第六,当需访问原始数据缓冲区时,可以在很多地方用ropes代替String对象。一个rope是一个含有char数组引用的一个字符串令牌,有起始位置和长度。可以进行字符串比较,就像一个字符串复制rope等。某些操作可能用rope要比字符串对象快。因为不复制原始数据,它们还占用更少的内存。
第七,如果需要做很多来回的数据访问,您可以创建更高级的索引。 VTD-XML中的索引包含元素的缩进层次,以及同一层的下一个元素(下一个同级)的引用,带有更高缩进层的第一个元素(初始元素),等等。这些都是增加到线性解析器元素索引顶部的整型索引。这种额外的索引可以让已解析数据的遍历速度更快。 性能和错误报告 若看看JsonParser和JsonParser2代码,你将看到更快的JsonParser2比JsonParser更糟糕的错误报告。当分析和解析阶段一分为二时,良好的数据验证和错误报告更易于实现。 通常情况下,这种差异将触发争论,在解析器的实现进行取舍时,优先考虑性能还是错误报告。然而,在索引叠加解析器中,这一讨论是没有必要的。
因为原始数据始终以其完整的形式存在于内存中,你可以同时具有快和慢的解析器解析相同的数据。您可以快速启动快的解析器,若解析失败,您可以使用较慢的解析器来检测其中输入数据中的错误位置。当快的解析器失败时,只要将原始数据交给较慢的解析器。基于这种方式,你可以获得两个解析的优点。 基准分析 基于数据(GSON)创建的对象树与仅标识在数据中找到的数据索引进行比较,而没有讨论比较的标的,这是不公平的比较。 在应用程序内部解析文件通常需要如下步骤: 首先是数据从硬盘或者网络上装载。接着,解码数据,例如从UTF-8到UTF-16。第三步,解析数据。第四步,处理数据。
为了只测量原始的解析器速度, 我预装载待解析的文件到内存。 该基准测试的代码没有以任何方式处理数据。尽管该基准化测试只是测试基础的解析速度,在运行的应用程序中,性能差异并没有转化成性能显著提高。如下是原因: 流式解析器总是能在所有数据装载进内存前开始解析数据。我的JSON解析器现在实现版本不能这样做。这意味着即使它在基础解析基准上更快,在现实运行的应用程序中,我的解析器必须等待数据装载,这将减慢整体的处理速度。如下图说明: 为了加速整体解析速度,你很可能修改我的解析器为数据装载时即可以解析数据。但是很可能会减慢基本解析性能。但整体速度仍可能更快。
此外,通过在执行的基准测试之前数据预加载到内存中,我也跳过数据解码步骤。数据从UTF-8转码为UTF-16是也存在消耗。在现实应用程序中,你不可以跳过这一步。每个待解析的文件来必须要解码。这是所有解析器都要支持的一点。流式解析器可以在读数据时进行解码。索引叠加分析器也可以在读取数据到缓冲区时进行解码。 VTD-XML 和Jackson (另一个JSON解析器)使用另一种技术。它们不会解码所有的原始数据。相反,它们直接在原始数据上进行分析,消费各种数据格式,如(ASCII,UTF-8等)。这可以节省昂贵的解码步骤,解码要使用相当复杂分析器。
一般来说,要想知道那个解析器在你的应用程序更快,需要基于你真实需要解析的数据的基准上进行全量测试。 索引叠加解析器一般讨论 我听到的一个反对索引叠加分析器的论点是,要能够指向原始数据,而不是将其抽取到一个对象树,解析时保持所有数据在内存中是必要的。在处理大文件时,这将导致内存消耗暴增。 一般来说,流式分析器(如SAX或StAX)在解析大文件时将整个文件存入内存。然而,只有文件中的数据可以以更小的块进行解析和处理,每个块都是独立进行处理的,这种说法才是对的。例如,一个大的XML文件包含一列元素,其中每一个元素都可以单独被解析和处理(如日志记录列表)。如果数据能以独立的块进行解析,你可以实现一个工作良好的索引叠加解析器。
如果文件不能以独立块进行解析,你仍然需要提取必要的信息到一些结构,这些结构可以为处理后面块的代码进行访问。尽管使用流式解析器可以做到这一点,你也可以使用索引叠加解析器进行处理。 从输入数据中创建对象树的解析器通常会消耗比原数据大小的对象树更多的内存。对象实例相关联的内存开销,加上需要保持对象之间的引用的额外数据,这是主要原因。 此外,因为所有的数据都需要同时在内存中,你需要解析前分配一个数据缓冲区,大到足以容纳所有的数据。但是,当你开始解析它们时,你并不知道文件大小,如何办呢? 假如你有一个网页应用程序(如Web服务,或者服务端应用),用户使用它上传文件。你不可能知道文件大小,所以开始解析前无法分配合适的缓存给它。基于安全考虑,你应该总是设置一个最大允许文件大小。否则,用户可以通过上传超大文件让你的应用崩溃。或者,他们可能甚至写一个程序,伪装成上传文件的浏览器,并让该程序不停地向服务器发送数据。您可以分配一个缓冲区适合所允许的最大文件大小。这样,你的缓冲区不会因有效文件耗光。如果它耗光了空间,那说明你的用户已经上传了过大的文件。
关于作者 Jakob Jenkov是一个企业家,作家和软件开发者,生活在西班牙的巴塞罗那。Jakob在1997年学习了Java,从1999年工作以来就一直使用Java。他目前专注于跨领域(软件,流程,企业等)的效率改善。他拥有哥本哈根信息技术大学的信息技术专业硕士学位。你可以在他的个人网站上获取更多关于他工作的信息。 感谢张龙对本文的审校。实现高性能Java解析器,古老的榕树,5-wow.com