字符串:为什么indexOf明显比包含更快?

时间:2016-08-19 09:34:05

标签: java string performance

为什么indexOf明显快于contains,如果后者仅仅是第一个的包装?

来自Java API的代码:

public boolean contains(CharSequence s) {
     return indexOf(s.toString()) > -1;
}

thread中选择的答案显示了一个简短的测试,显示了差异。

thread中选择的答案表明附加方法调用的开销无关紧要。那么,为什么差异呢?

请阅读我的编辑:几乎每个人都说微基准是有缺陷的。奇怪的是,它恰好反映了我的用例。

实际上,我并不怀疑indexOf首先比contains(对于我的用例)更快,我只是想知道原因。

我的意图是永远不要写基准!我只是在寻找最有效的方法来测试一个字符串是否包含另一个字符串(对于我的应用程序而言,它与基准测试无关,而是“现实生活中的情况”)。

3 个答案:

答案 0 :(得分:10)

contains方法实现为:

public boolean contains(CharSequence s) {
    return indexOf(s.toString()) > -1;
}

这意味着如果CharSequence s不是java.lang.String,它可能会更慢,因为调用s.toString()将导致新字符串实例的分配和初始化。如果s是一个字符串 - 则不应存在任何可衡量的差异。

PS:这里的测试存在缺陷:https://stackoverflow.com/a/18340277/2588800 Java最初在​​"解释"中执行。模式,这是非常慢的,当它检测到一段代码被执行很多次时,它将它编译为本机代码,以加快它(阅读有关JIT编译)。

正如您所见contains内部调用indexOf,这意味着indexOf最终将被编译为本机。因此,当他测试indexOf时(注意他在contains之后测试它),它可能已经被编译为本机代码。这就是时差的原因。尝试颠倒测试的顺序 - 首先测试indexOf然后contains,我打赌你会看到相反的结果。

JMH救援

Benchmark                            Mode  Cnt   Score   Error   Units
StringSearchBenchmark.testContains  thrpt  500  22,071 ± 0,269  ops/us
StringSearchBenchmark.testIndexOf   thrpt  500  22,654 ± 0,233  ops/us

正如您所看到的,差异可以忽略不计,可能会被其他方法调用(indexOf() + toString())和系统上的负载所淹没。

<强>源代码:

@Fork(1)
@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Measurement(iterations = 500, time = 50, timeUnit = TimeUnit.MILLISECONDS)
@Warmup(iterations = 10)
@BenchmarkMode(Mode.Throughput)
public class StringSearchBenchmark {
    private static final String STATE = "absdefghijklmnopqrstuvwxyzabsdefghijklmnopqrstuvwxyzabsdefghijklmnopqrstuvwxyzabsdefghijklmnopqrstuvwxyz";
    private static final String SEARCH_TERM = "abcd";

    @Benchmark
    public void testContains(Blackhole sink) {
        sink.consume(STATE.contains(SEARCH_TERM));
    }

    @Benchmark
    public void testIndexOf(Blackhole sink) {
        sink.consume(STATE.indexOf(SEARCH_TERM));
    }
}

答案 1 :(得分:2)

正如其他人所说的那样,基准测试存在严重缺陷 - 对Java代码的性能测试就是这样 - 你必须将其加热以确保所有类都有已经加载和解析,所有对象都已加载到内存中,并且任何编译到本机代码,例如通过HotSpot,已经完成。一个天真的基准,你只需要在主方法中运行一次代码就不会真正开始。更好的选择是使用JMH之类的东西。鉴于以下测试:

package com.stackoverflow.example;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Measurement(time = 250, timeUnit = TimeUnit.MILLISECONDS)    
public class MyBenchmark {

    private static final String[] names = new String[]{"jack", "jackson", "jason", "jadifu"};

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(MyBenchmark.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }

    @Benchmark
    public void contains() {
        names[0].contains("ja");
    }

    @Benchmark
    public void containsExplicit() {
        names[0].indexOf("ja".toString());
    }

    @Benchmark
    public void indexOf() {
        names[0].indexOf("ja");
    }

    @Benchmark
    public void matches() {
        names[0].matches(".*ja.*");
    }
}

我得到以下结果:

Benchmark                      Mode  Cnt     Score    Error   Units
MyBenchmark.contains          thrpt   20   219.770 ±  2.032  ops/us
MyBenchmark.containsExplicit  thrpt   20  1820.024 ± 20.583  ops/us
MyBenchmark.indexOf           thrpt   20  1828.234 ± 18.744  ops/us
MyBenchmark.matches           thrpt   20     3.933 ±  0.052  ops/us

现在,这非常有趣,因为它仍然表明contains明显慢于indexOf。但是,如果我将测试稍微改为以下内容:

@Benchmark
public void contains() {
    assert names[0].contains("ja");
}

@Benchmark
public void containsExplicit() {
    assert names[0].indexOf("ja".toString()) == 0;
}

@Benchmark
public void indexOf() {
    assert names[0].indexOf("ja") == 0;
}

@Benchmark
public void matches() {
   assert names[0].matches(".*ja.*");
}

我得到以下结果:

Benchmark                      Mode  Cnt    Score   Error   Units
MyBenchmark.contains          thrpt   20  220.480 ± 1.266  ops/us
MyBenchmark.containsExplicit  thrpt   20  219.962 ± 2.329  ops/us
MyBenchmark.indexOf           thrpt   20  219.706 ± 2.401  ops/us
MyBenchmark.matches           thrpt   20    3.766 ± 0.026  ops/us

在此,我们得到的结果与包含相同,但indexOf已放慢速度以匹配contains。这是一个非常有趣的结果。为什么会这样?

可能由于HotSpot认识到indexOf调用的结果从未被检查过,并且因为它正在使用final类(String),HotSpot可能会保证那里没有副作用。因此,如果我们没有查看结果并且对呼叫没有副作用,我们为什么要这样做呢? HotSpot能够意识到方法调用毫无意义,并将其完全删除,这可能就是这里发生的事情。它肯定会解释数量级的差异。

为什么这对contains不起作用?我只能假设这是因为contains接受CharSequence,而不是String,这是一个抽象类,这足以阻止HotSpot优化方法调用。

这也表明Java中的微基准是 hard - 表面下有很多东西可以优化你的运行代码,而一些快捷方式可能会导致极其不准确的基准测试。

答案 2 :(得分:0)

indexOf是Hotspot JVM中的内部方法示例。这意味着此方法根本不使用java.lang.String中的java代码。这种方法有特殊的原生版本。您可以在此处找到此类方法的列表:do_intrinsic(_indexOf