【问题标题】:String: Why is indexOf significantely faster than contains?字符串:为什么 indexOf 比 contains 快得多?
【发布时间】:2016-12-26 10:02:29
【问题描述】:

如果 contains 只是第一个的包装器,为什么 indexOf 比 contains 快得多?

来自 Java API 的代码:

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

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

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

请阅读我的编辑: 几乎每个人都在说微基准测试存在缺陷。奇怪的是,它完全反映了我的用例。

实际上,我并不怀疑 indexOf 比 contains 快(对于我的用例),我只是想知道为什么。

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

【问题讨论】:

  • 选择的答案是错误的。它执行了不正确的微基准测试,使contains() 看起来比实际慢。
  • @Kayaman 你能指定一个正确的基准吗?
  • 查看 CoronA here的答案

标签: java string performance


【解决方案1】:

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));
    }
}

【讨论】:

  • 即使我切换 indexOf 和 contains 的顺序,indexOf 仍然更快。在我链接的基准测试中,传递的值也已经是一个字符串!还是“测试”内部是一个 CharSequence?
  • 我对其进行了一些修改,我可以看到 contains 更快:) 这显然不是真的,因为它在内部调用 indexOf。你不能根据有缺陷的微观基准做出结论。她的要点是:gist.github.com/SvetlinZarev/ff6d64306343501427be5f266f0329d6
  • 基准测试是否存在缺陷......这正是我的应用程序所需的用例:D
  • 然后进行适当的微基准测试。看看 JMH 和我的回答中的基准。
  • 不!基准是我在应用程序中需要的 1:1(反映用例)。我的应用程序不是关于编写基准测试,我的应用程序是关于测试一堆字符串的子字符串!
【解决方案2】:

正如其他人所说,基准测试存在严重缺陷 - Java 代码的性能测试不是那样工作 - 你必须预热它以确保所有类都已加载和解析,所有对象都已加载到内存中,并且任何编译为本机代码,例如通过 HotSpot,已经完成。只在 main 方法中运行一次代码的幼稚基准测试并不会真正奏效。一个更好的选择是使用类似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 中是困难 - 在优化运行代码方面有很多事情要做,一些捷径可能会导致基准测试极其不准确。 p>

【讨论】:

  • 不应在@Benchmark 中使用循环。删除for 循环并再次测试。同样second 对于这样的测试来说是一个太大的时间框架 - 切换到微秒或毫秒。此外,JVM 可能会忽略来自第二个片段的断言,从而优化整个循环,这就是第一个片段中发生的情况
  • 我已经按照您的建议进行了更改。它绝对使它更容易阅读。我很确定 JVM 没有忽略断言。您必须使用 -ea 运行,否则 assert 似乎会强制 HotSpot 不优化整个调用。
  • @ipsi 真的很有趣!但是为什么 indexOf("ja".toString())... "ja" 已经是一个字符串了?
  • 这是 contains 方法在内部工作的方式,但我怀疑它在这里没有做太多。编译器或 HotSpot 将优化调用的那部分。将不得不更多地摆弄代码以防止这种优化。
【解决方案3】:

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

【讨论】:

  • 但包含调用相同的方法:) 所以形成 PoV 应该没有任何区别
  • 但是你在里面还有一个额外的调用和比较操作。
  • 是的,但是如果您查看我的基准测试,您会发现性能下降可以忽略不计,而不是问题标题所暗示的significant。
猜你喜欢
  • 2011-05-01
  • 1970-01-01
  • 1970-01-01
  • 2013-06-03
  • 2011-10-04
  • 1970-01-01
  • 2014-04-21
  • 2015-02-09
  • 1970-01-01
相关资源
最近更新 更多