Java中StringTokenizer类与String.split方法的性能比较

41

在我的软件中,我需要将字符串拆分为单词。目前我有超过19,000,000个文档,每个文档都有30个以上的单词。

以下两种方式哪种是最好的方式(从性能方面考虑)?

StringTokenizer sTokenize = new StringTokenizer(s," ");
while (sTokenize.hasMoreTokens()) {

或者

String[] splitS = s.split(" ");
for(int i =0; i < splitS.length; i++)

9
我猜应该是第一个,但为什么不直接测量呢? - musiKk
1
我可以,但我也对解释感兴趣... - JohnJohnGa
3
如果有人说选项X是最快的,你会选择这个选项吗?还是为了确保,你会测试两个选项?如果是后者,为什么不立即测试呢? :) - Bart Kiers
1
@Damodar "在性能方面做这件事的最佳方式" - JohnJohnGa
我认为你无法给出一个通用的答案。这高度取决于所使用的实现方式。 - musiKk
显示剩余2条评论
9个回答

64

如果您的数据已经在数据库中,您需要解析单词字符串,我建议反复使用indexOf函数。它比另外两种解决方案快得多。

然而,从数据库获取数据仍然很可能更加昂贵。

StringBuilder sb = new StringBuilder();
for (int i = 100000; i < 100000 + 60; i++)
    sb.append(i).append(' ');
String sample = sb.toString();

int runs = 100000;
for (int i = 0; i < 5; i++) {
    {
        long start = System.nanoTime();
        for (int r = 0; r < runs; r++) {
            StringTokenizer st = new StringTokenizer(sample);
            List<String> list = new ArrayList<String>();
            while (st.hasMoreTokens())
                list.add(st.nextToken());
        }
        long time = System.nanoTime() - start;
        System.out.printf("StringTokenizer took an average of %.1f us%n", time / runs / 1000.0);
    }
    {
        long start = System.nanoTime();
        Pattern spacePattern = Pattern.compile(" ");
        for (int r = 0; r < runs; r++) {
            List<String> list = Arrays.asList(spacePattern.split(sample, 0));
        }
        long time = System.nanoTime() - start;
        System.out.printf("Pattern.split took an average of %.1f us%n", time / runs / 1000.0);
    }
    {
        long start = System.nanoTime();
        for (int r = 0; r < runs; r++) {
            List<String> list = new ArrayList<String>();
            int pos = 0, end;
            while ((end = sample.indexOf(' ', pos)) >= 0) {
                list.add(sample.substring(pos, end));
                pos = end + 1;
            }
        }
        long time = System.nanoTime() - start;
        System.out.printf("indexOf loop took an average of %.1f us%n", time / runs / 1000.0);
    }
 }

打印

StringTokenizer took an average of 5.8 us
Pattern.split took an average of 4.8 us
indexOf loop took an average of 1.8 us
StringTokenizer took an average of 4.9 us
Pattern.split took an average of 3.7 us
indexOf loop took an average of 1.7 us
StringTokenizer took an average of 5.2 us
Pattern.split took an average of 3.9 us
indexOf loop took an average of 1.8 us
StringTokenizer took an average of 5.1 us
Pattern.split took an average of 4.1 us
indexOf loop took an average of 1.6 us
StringTokenizer took an average of 5.0 us
Pattern.split took an average of 3.8 us
indexOf loop took an average of 1.6 us
打开一个文件的成本约为8毫秒。由于文件非常小,缓存可以将性能提高2-5倍。即使如此,它仍将花费大约10小时打开文件。相比之下,使用split与StringTokenizer的成本要低得多,每个的成本不到0.01毫秒。解析1900万x 30个单词*每个单词8个字母应该需要大约10秒钟(每2秒约1 GB)。
如果您想提高性能,请尽量减少文件数量。例如,使用数据库。如果不想使用SQL数据库,则建议使用这些之一: http://nosql-database.org/

3
有趣的是,我在我的机器上运行了你的代码,发现split的执行时间大约是StringTokenizer的两倍。而indexOf的执行时间则只有其一半。 - Bill the Lizard
3
@Peter Lawrey: StringTokenizer 不使用正则表达式。 - user207421
1
@tjjjohnson Java 7的split方法执行类似于indexOf系列的操作,但仅限于一些非常常见的操作。 - Peter Lawrey
1
仅供参考,你的indexOf循环实现是不正确的,你漏掉了最后一个分隔符之后的部分。不确定这是否会对性能产生太大影响,但无论如何。 - Julien
1
@TriCore 理论上是可以的,但实际上我还没有看到JIT会在循环周围重新组织代码。例如将某些内容从循环前移动到循环后或者反之。这段话是5年前写的,如果今天我会使用JMH。 - Peter Lawrey
显示剩余14条评论

14

在Java 7中,Split仅对此输入调用indexOf函数。查看源代码。 Split应该非常快,接近于重复调用indexOf。


你确定吗?我可以在你提供的链接中的第2361行看到:return Pattern.compile(regex).split(this, limit); - Krzysztof Wolny
1
实现在1770年。 - nes1983
实现将使用(即indexOf),如果正则表达式满足某些条件,则将使用Pattern.compile(regex).split(this, limit);。从源代码中可以看出:如果正则表达式是(1)一个字符的字符串,并且该字符不是RegEx的元字符“.$|()[{^?*+\”之一,或者(2)两个字符的字符串,第一个字符是反斜杠,第二个字符不是ASCII数字或ASCII字母,则采用快速路径。但是正如其他地方指出的那样,这是一个实现细节,因此不应依赖它。 - hendalst

6

1
@downvoters:请明确上述问题,您是需要在Tokenize和split之间选择更好的方法,还是无论Tokenize和split哪个更好,您都在寻找最佳方法。 - developer
3
这个问题很明确,他正在寻找在性能方面做到最好的方法。API建议使用split函数,但没有提到(根据我通过Google搜索得到的所有其他信息)Tokenize函数执行得更好。 - Bill the Lizard
@Bill,抱歉,这是我的错误。那么问题的标题可能会更改。 - developer

5

另一个重要的事情,据我注意到并未记录在案的是,通过使用构造函数 StringTokenizer(String str, String delim, boolean returnDelims) 要求StringTokenizer返回分隔符和已分词字符串也可以减少处理时间。因此,如果您正在寻求性能,我建议使用以下代码:

private static final String DELIM = "#";

public void splitIt(String input) {
    StringTokenizer st = new StringTokenizer(input, DELIM, true);
    while (st.hasMoreTokens()) {
        String next = getNext(st);
        System.out.println(next);
    }
}

private String getNext(StringTokenizer st){  
    String value = st.nextToken();
    if (DELIM.equals(value))  
        value = null;  
    else if (st.hasMoreTokens())  
        st.nextToken();  
    return value;  
}

尽管getNext()方法会引入一些开销,但根据我的基准测试结果,它仍然比原来快50%。


2

使用split方法。

StringTokenizer是一个保留下来的遗留类,为了兼容性而存在,虽然在新代码中不建议使用它。建议任何寻求此功能的人使用split方法代替。


为什么是-1?这确实回答了使用split还是StringTokenizer的问题。规范确实提到Split比StringTokenizer更推荐使用。 - Basanth Roy
4
看看我对Damodar答案的评论。这个规范没有提到性能,这正是这个问题所问的。 - Bill the Lizard
谢谢Bill。- rationalSpring - Basanth Roy
1
我点了踩,因为它没有询问是否使用其中之一,而是询问哪个更快。 - Calvin Taylor

2

这 1900 万份文件有什么作用?您是否需要定期拆分所有文档中的单词?还是这只是一个一次性的问题?

如果您一次只显示/请求一个文档,每个文档只有 30 个单词,那么这是一个非常微小的问题,任何方法都可以解决。

如果您需要一次处理所有文档,每个文档只有 30 个单词,那么这是一个非常微小的问题,您更有可能受到 IO 限制。


2

尽管它是传统的,但我希望StringTokenizer在这个任务中比String.split()快得多,因为它不使用正则表达式:它直接扫描输入,就像你自己通过indexOf()一样。事实上,每次调用String.split()时都必须编译正则表达式,所以它甚至不如直接使用正则表达式本身高效。


从其他地方了解到,String.split 不会每次重新编译,虽然有些情况下可能会重新编译。 - Calvin Taylor

2

在运行微基准测试(包括纳米级别的测试)时,有很多因素会影响您的结果。JIT 优化和垃圾回收只是其中之一。

为了从微基准测试中获得有意义的结果,请查看 jmh 库。它附带了如何运行好基准测试的示例。


1
这可能是使用1.6.0进行合理基准测试。
http://www.javamex.com/tutorials/regular_expressions/splitting_tokenisation_performance.shtml#.V6-CZvnhCM8

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接