Performance difference for splitting strings

《Java程序性能优化》的3.1.3小节阐述了字符串分割的三种方法,并且用程序示例和图形说明了三种方法的性能差异。这三种方法分别是:split方法;使用StringTokenizer类;使用最原始的indexOf和substring方法。这三种方法的性能是依次增强的。但首先一个问题是:使用最后一种方法(本来应该是效率最高的)的例子试验时发现,这种方法却是最慢的,这是为什么呢?

书中的例子是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
for (int i = 0; i < 10000; i++) {
while(true){
String splitStr = null;
int j = tmp.indexOf(DELIMETER);
if (j<0) break;
splitStr = tmp.substring(0,j);
tmp = tmp.substring(j+1);
}
tmp = myStr;
}

很遗憾,上面的代码运行速度非常慢,最后只能修改循环次数为100才能勉强测试,最后运行时间是17632 ms。但是,indexOf和substring的方法真的这么慢吗?并非如此,书上的结论是正确的,只是给的例子不好,不能真正反映这个方法的性能。我们采用网上的另一个代码段来实现,结果截然不同:

1
2
3
4
5
6
int pos = 0, end;
while ((end = tmp.indexOf(DELIMETER, pos)) >= 0) {
String splitStr = null;
splitStr = tmp.substring(pos, end);
pos = end + 1;
}

同样循环100次,运行时间是119 ms,这个结果真的是让人惊呆了。仔细对比这两段代码,似乎只是多一个substring的调用而已,性能差距能有这么大吗?运行性能分析工具(如VisualVM),发现substring方法中的数组拷贝确实占用相当多的时间,tmp = tmp.substring(j+1)所产生的拷贝耗时在大字符串的情况下相当明显。

第二个问题,书中关于前两种方法的性能比较一定是正确的吗?按照SO上的例子(见参考资料),在我机器上的运行结果,并不是那么绝对:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
StringTokenizer took an average of 9.6 us
Pattern.split took an average of 7.0 us
indexOf loop took an average of 4.1 us
StringTokenizer took an average of 4.5 us
Pattern.split took an average of 4.3 us
indexOf loop took an average of 2.4 us
StringTokenizer took an average of 4.5 us
Pattern.split took an average of 4.4 us
indexOf loop took an average of 2.3 us
StringTokenizer took an average of 4.5 us
Pattern.split took an average of 4.4 us
indexOf loop took an average of 2.4 us
StringTokenizer took an average of 4.6 us
Pattern.split took an average of 4.5 us
indexOf loop took an average of 2.4 us

所以,你看到了,大部分情况下不是split慢,而是StringTokenizer慢,所以和书中描述的不符。不过另一个方面更有力的证据是在StringTokenizer类中官方的声明:不建议使用这个类,因为其中使用了老的Enumeration接口,建议使用split方法。所以结论是在性能要求不是特别高的情况下,我们使用split就好了。

最后,其实还有另一种方法性能比JDK的split更好,即Apache Commons的StringUtil的split方法,我没有做测试,感兴趣的同学可以自己试验一下。

参考资料