技术 | 时间(单位:纳秒) |
---|---|
String |
69,809,420 |
StringBuffer |
251,652,393 |
Rope.charAt |
79,441,772,895 |
Rope.iterator |
1,910,836,551 |
用来得到表 2 中符合预期的结果的 rope,是通过对初始字符串执行一系列复杂的变换之后创建的。但是,如果直接从字符序列创建这个 rope 而不进行变换,那么性能数字会产生较大的变化。表 3 比较了两种方法的性能。这次是在一个包含 182,029 个字符的 rope 上迭代,这次的 rope 是直接用包含 Project Gutenberg 版查理斯·狄更斯 圣诞欢歌的字符数组初始化的。
技术 | 时间(单位:纳秒) |
---|---|
String |
602,162 |
StringBuffer |
2,259,917 |
Rope.charAt |
462,904 |
Rope.iterator |
3,466,047 |
如何用我前面的理论讨论解释这一性能逆转呢? rope 的构建是个关键因素:当直接使用底层的 CharacterSequence或字符数组构建 rope 时,它只有一个简单的结构,里面包含一个扁平 rope。因为这个 rope 不包含连接或子串节点,所以字符查询操作要么直接委托给底层序列的 charAt方法(在 CharacterSequence的情况下),要么包含进行数组查询(在数组的情况下)。扁平 rope 的 Rope.charAt性能与底层表示的 chatAt 性能匹配,通常是 O(1);所以性能是不同的。
聪明的读者可能想知道既然大家都提供 0(1)的访问时间,为什么 charAt会比迭代器快 7 倍。这个区别是因为在 Java 语言中,Iterator必须返回 Object。而 charAt实现直接返回 char原语,迭代器实现必须将每个 char装箱为一个 Character对象。自动装箱可能消除了原语装箱在语法上的麻烦,但是不能消除性能上的损失。
后,非常明显的是:Rope.charAt的性能比 String.charAt的性能好。原因是 Rope使用专门的类来表示延迟子串(lazy substring),因此使普通 rope 的 charAt的实现保持简单。对比之下,Java SE 的 String实现使用同一个类表示常规字符串和延迟子串,因此多多少少将 charAt的逻辑弄得复杂起来,所以在迭代常规字符串期间增加了少量性能开支。
在讨论 rope 上的正则表达式搜索性能的优化时,还会提到 Rope 迭代。
用 Rope.write 对输出进行优化
在某种程度上,多数 rope 实例都必须写入到某些位置,通常写到一个流内。不幸的是,将对象写入流要调用对象的 toString方法。这种序列化方式强行在写入一个字符之前在内存中建立整个对象的字符串表示,对于大型对象来说,这是个非常大的性能拖累。Ropes for Java 设计的时候考虑了大型的字符串对象,所以它提供了更好的方式。
为了提高性能,Rope引入了一个 write 方法,接受一个 Writer和一个范围说明,然后将 rope 的指定范围的字符写到 writer。这样节省了用 rope 构建 String的时间和内存开支,对于大型 rope 来说,这种开支是相当巨大的。清单 4 显示了 rope 输出的标准方式和增强方式。
清单 4. Rope输出的两种方式
out.write(rope);
rope.write(out);
表 4 包含的测评结果是将一个长度为 10,690,488、深度为 65 的 rope 写入一个由内存缓冲区支持的流的结果。请注意,只显示了节省的时间,但是在临时分配的堆空间上的节省也非常巨大。
技术 | 时间(单位:纳秒) |
---|---|
out.write |
75,136,884 |
rope.write |
13,591,023 |
变换性能测评
Rope 的变换从理论上要比使用连续字符的字符串表示方式快得多。但反过来,正如所看到的,rope 的迭代变慢了。在这一节,将看到 Ropes for Java 和 String、StringBuffer进行变换操作的性能测评比较。
全部测试都用 Project Gutenberg 版本的电子书 圣诞欢歌初始化(182,029 个字节),并对其连续应用一系列变换。在多数情况下,变换的数量在 20 到 480 的范围内变动,以便演示数据结构缩放的效果。(图 2、3、4 将变换的数量称为 计划长度(plan length)。)每个测试执行了七次,并使用中间结果。
插入性能测评
在插入测评中,从前一次迭代的输出中随机选择一个子串,然后插入到字符串的随机位置。图 2 显示了测评的结果。
对 String和 StringBuffer来说,完成测评所需要的时间随着计划长度的增加呈指数级增长。相比之下,Rope 则执行得极好。
附加字符串性能评测