为什么String.equals对于非相同(但相等)的String对象要慢得多?

11 java string performance equals

我正在深入研究String.equals()真的那么糟糕的问题,在尝试做一些基准测试时遇到了一些令人惊讶的结果.

使用jmh,我编写了一个简单的测试(代码和pom结尾),它可以看到函数可以在1秒内运行多少次.

Benchmark                                Mode  Samples          Score   Score error  Units
c.s.SimpleBenchmark.testEqualsIntern    thrpt        5  698910949.710  47115846.650  ops/s
c.s.SimpleBenchmark.testEqualsNew       thrpt        5     529118.774     21164.872  ops/s
c.s.SimpleBenchmark.testIsEmpty         thrpt        5  470846539.546  19922172.099  ops/s

这是一个1300倍的因素,坦率地说testEqualsIntern,testEqualsNew这对我来说非常令人惊讶.

String.equals()的代码确实对同一个对象进行了测试,该对象可以很快地将相同的(在本例中为interned)字符串对象踢出.我只是非常难以相信额外的代码看起来相当于两个测试和比较元素的大小为1的数组,这是一个很大的性能影响.

我还在String中使用另一个简单的方法调用进行测试,以确保我没有看到太疯狂的东西.

package com.shagie;

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;

public class SimpleBenchmark {
    public final static int ITERATIONS = 1000;
    public final static String EMPTY = "";
    public final static String NEW_EMPTY = new String("");

    @Benchmark
    public int testEqualsIntern() {
        int count = 0;
        String str = EMPTY;

        for(int i = 0; i < ITERATIONS; i++) {
            if(str.equals(EMPTY)) {
                count++;
            }
        }
        return count;
    }

    @Benchmark
    public int testEqualsNew() {
        int count = 0;
        String str = NEW_EMPTY;

        for(int i = 0; i < ITERATIONS; i++) {
            if(str.equals(EMPTY)) {
                count++;
            }
        }
        return count;
    }

    @Benchmark
    public int testIsEmpty() {
        int count = 0;
        String str = NEW_EMPTY;

        for(int i = 0; i < ITERATIONS; i++) {
            if(str.isEmpty()) {
                count++;
            }
        }
        return count;
    }


    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
          .include(".*" + SimpleBenchmark.class.getSimpleName() + ".*")
          .warmupIterations(5)
          .measurementIterations(5)
          .forks(1)
          .build();

        new Runner(opt).run();
    }
}
Run Code Online (Sandbox Code Playgroud)

maven的.pom(如果你想重现它,可以自己快速设置它):

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.shagie</groupId>
    <artifactId>bench</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>

    <name>String Benchmarks with JMH</name>

    <prerequisites>
        <maven>3.0</maven>
    </prerequisites>

    <dependencies>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-core</artifactId>
            <version>${jmh.version}</version>
        </dependency>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-generator-annprocess</artifactId>
            <version>${jmh.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <jmh.version>0.9.5</jmh.version>
        <javac.target>1.6</javac.target>
        <uberjar.name>benchmarks</uberjar.name>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <compilerVersion>${javac.target}</compilerVersion>
                    <source>${javac.target}</source>
                    <target>${javac.target}</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>2.2</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <finalName>${uberjar.name}</finalName>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>org.openjdk.jmh.Main</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
        <pluginManagement>
            <plugins>
                <plugin>
                    <artifactId>maven-clean-plugin</artifactId>
                    <version>2.5</version>
                </plugin>
                <plugin>
                    <artifactId>maven-deploy-plugin</artifactId>
                    <version>2.8.1</version>
                </plugin>
                <plugin>
                    <artifactId>maven-install-plugin</artifactId>
                    <version>2.5.1</version>
                </plugin>
                <plugin>
                    <artifactId>maven-jar-plugin</artifactId>
                    <version>2.4</version>
                </plugin>
                <plugin>
                    <artifactId>maven-javadoc-plugin</artifactId>
                    <version>2.9.1</version>
                </plugin>
                <plugin>
                    <artifactId>maven-resources-plugin</artifactId>
                    <version>2.6</version>
                </plugin>
                <plugin>
                    <artifactId>maven-site-plugin</artifactId>
                    <version>3.3</version>
                </plugin>
                <plugin>
                    <artifactId>maven-source-plugin</artifactId>
                    <version>2.2.1</version>
                </plugin>
                <plugin>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>2.17</version>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

</project>
Run Code Online (Sandbox Code Playgroud)

这是自动生成的(对组和工件进行了适当的调整):

$ mvn archetype:generate \
          -DinteractiveMode=false \
          -DarchetypeGroupId=org.openjdk.jmh \
          -DarchetypeArtifactId=jmh-java-benchmark-archetype \
          -DgroupId=org.sample \
          -DartifactId=test \
          -Dversion=1.0
Run Code Online (Sandbox Code Playgroud)

要运行测试:

$ mvn clean install
$ java -jar target/benchmarks.jar ".*SimpleBenchmark.*" -wi 5 -i 5 -f 1
Run Code Online (Sandbox Code Playgroud)

因为这将是一个问题,它运行的Java版本:

$ java -version
java version "1.6.0_65"
Java(TM) SE Runtime Environment (build 1.6.0_65-b14-462-11M4609)
Java HotSpot(TM) 64-Bit Server VM (build 20.65-b04-462, mixed mode)
Run Code Online (Sandbox Code Playgroud)

硬件(可能会出现问题)是Intel Xeon处理器上的OS X,10.9.4.

Clé*_*IEU 6

编写有缺陷的微基准很容易......而且你被困了.

知道发生了什么的唯一方法是查看汇编代码.你必须自己检查结果代码是否符合预期,或者是否发生了一些不必要的魔法.让我们一起尝试.您必须使用addProfile(LinuxPerfAsmProfiler.class)查看汇编代码.

什么是汇编代码testEqualsIntern:

....[Hottest Region 1]..............................................................................
[0x7fb9e11acda0:0x7fb9e11acdc8] in org.sample.generated.MyBenchmark_testEqualsIntern::testEqualsIntern_thrpt_jmhLoop

                                                                ; - org.sample.generated.MyBenchmark_testEqualsIntern::testEqualsIntern_thrpt_jmhLoop@19 (line 103)
                  0x00007fb9e11acd82: movzbl 0x94(%rdx),%r11d   ;*getfield isDone
                                                                ; - org.sample.generated.MyBenchmark_testEqualsIntern::testEqualsIntern_thrpt_jmhLoop@29 (line 105)
                  0x00007fb9e11acd8a: mov    $0x2,%ebp
                  0x00007fb9e11acd8f: test   %r11d,%r11d
                  0x00007fb9e11acd92: jne    0x00007fb9e11acdcc  ;*ifeq
                                                                 ; - org.sample.generated.MyBenchmark_testEqualsIntern::testEqualsIntern_thrpt_jmhLoop@32 (line 105)
                  0x00007fb9e11acd94: nopl   0x0(%rax,%rax,1)
                  0x00007fb9e11acd9c: xchg   %ax,%ax            ;*aload
                                                                ; - org.sample.generated.MyBenchmark_testEqualsIntern::testEqualsIntern_thrpt_jmhLoop@13 (line 103)
6.50%    3.37%    0x00007fb9e11acda0: mov    0xb0(%rdi),%r11d   ;*getfield i1
                                                                ; - org.openjdk.jmh.infra.Blackhole::consume@2 (line 350)
                                                                ; - org.sample.generated.MyBenchmark_testEqualsIntern::testEqualsIntern_thrpt_jmhLoop@19 (line 103)
0.06%    0.05%    0x00007fb9e11acda7: mov    0xb4(%rdi),%r10d   ;*getfield i2
                                                                ; - org.openjdk.jmh.infra.Blackhole::consume@15 (line 350)
                                                                ; - org.sample.generated.MyBenchmark_testEqualsIntern::testEqualsIntern_thrpt_jmhLoop@19 (line 103)
0.06%    0.09%    0x00007fb9e11acdae: cmp    $0x3e8,%r10d
0.03%             0x00007fb9e11acdb5: je     0x00007fb9e11acdf1  ;*return
                                                                ; - org.openjdk.jmh.infra.Blackhole::consume@38 (line 354)
                                                                ; - org.sample.generated.MyBenchmark_testEqualsIntern::testEqualsIntern_thrpt_jmhLoop@19 (line 103)
48.85%   44.47%    0x00007fb9e11acdb7: movzbl 0x94(%rdx),%ecx    ;*getfield isDone
                                                                ; - org.sample.generated.MyBenchmark_testEqualsIntern::testEqualsIntern_thrpt_jmhLoop@29 (line 105)
0.33%    0.62%    0x00007fb9e11acdbe: add    $0x1,%rbp          ; OopMap{r9=Oop rbx=Oop rdi=Oop rdx=Oop off=226}
                                                                ;*ifeq
                                                                ; - org.sample.generated.MyBenchmark_testEqualsIntern::testEqualsIntern_thrpt_jmhLoop@32 (line 105)
0.03%    0.05%    0x00007fb9e11acdc2: test   %eax,0x16543238(%rip)        # 0x00007fb9f76f0000
                                                                ;   {poll}
42.31%   49.43%    0x00007fb9e11acdc8: test   %ecx,%ecx
                   0x00007fb9e11acdca: je     0x00007fb9e11acda0  ;*aload_2
                                                                ; - org.sample.generated.MyBenchmark_testEqualsIntern::testEqualsIntern_thrpt_jmhLoop@35 (line 106)
                  0x00007fb9e11acdcc: mov    $0x7fb9f706fe40,%r10
                  0x00007fb9e11acdd6: callq  *%r10              ;*invokestatic nanoTime
                                                                ; - org.sample.generated.MyBenchmark_testEqualsIntern::testEqualsIntern_thrpt_jmhLoop@36 (line 106)
                  0x00007fb9e11acdd9: mov    %rbp,0x10(%rbx)    ;*putfield operations
                                                                ; - org.sample.generated.MyBenchmark_testEqualsIntern::testEqualsIntern_thrpt_jmhLoop@51 (line 108)
                  0x00007fb9e11acddd: mov    %rax,0x28(%rbx)    ;*putfield stopTime
                                                                ; - org.sample.generated.MyBenchmark_testEqualsIntern::testEqualsIntern_thrpt_jmhLoop@39 (line 106)
....................................................................................................
Run Code Online (Sandbox Code Playgroud)

您可能知道,JMH会将您的基准代码插入到自己的测量循环中.您可以通过查看target/generated-sources文件夹轻松查看生成的代码.您必须知道此代码如何能够将其与程序集进行比较.

有趣的部分在这里:

public void testEqualsIntern_avgt_jmhLoop(InfraControl control, RawResults result, MyBenchmark_1_jmh l_mybenchmark0_0, Blackhole_1_jmh l_blackhole1_1) throws Throwable {
    long operations = 0;
    long realTime = 0;
    result.startTime = System.nanoTime();
    do {
        l_blackhole1_1.consume(l_mybenchmark0_0.testEqualsIntern());
        operations++;
    } while(!control.isDone);
    result.stopTime = System.nanoTime();
    result.realTime = realTime;
    result.operations = operations;
}
Run Code Online (Sandbox Code Playgroud)

好的,你看到这个很好的do/while循环做了两件事:

  • 调用你的功能
  • 调用使用以防止Hotspot不必要的优化?

现在让我们回到程序集.尝试在其中找到这三个操作(循环,消耗和您的代码).你能 ?

你可以看到JMH循环,它是0x00007fb9e11acdb7: movzbl 0x94(%rdx),%ecx ;*getfield isDone和下面的跳转.

你可以看到黑洞,它是从0x00007fb9e11acda00x00007fb9e11acdb5:

但是你的代码在哪里?没了.您没有遵循JMH的良好做法,并且您允许Hotspot删除您的代码.您正在对NOOP进行基准测试.BTW你有没有试过基准NOOP?这是一件好事,当你看到一个接近这个的数字时,你知道你必须非常小心.

您可以对第二个基准进行相同的分析.我没有仔细阅读其汇编代码,但您将能够发现您的for循环和对equals的调用.您可以再次读取JMH样本以尝试避免此类问题.

TL; DR编写正确的微/纳米基准测试非常困难,您应该仔细检查您是否知道测量结果.装配是唯一的出路.观看所有演示文稿并阅读Aleksey的所有博客文章以了解更多信息.他做得很好.最后,这些测量在现实生活中几乎总是无用的,但却是一个很好的学习工具.


tha*_*guy 4

针对新字符串测试相等性不会对性能产生荒谬的影响。您看到的效果很简单,Hotspot 能够在一种情况下优化循环,但在另一种情况下则不能。

以下是来自 OpenJDK 7 (IcedTea7 2.1.7) (7u3-2.1.7-1) 64 位服务器的热点程序集转储testEqualsIntern,显示了无循环结果(为 生成了类似的代码testIsEmpty):

Decoding compiled method 0x00007fb360a1a0d0:
Code:
[Entry Point]
[Constants]
  # {method} 'testEqualsIntern' '()I' in 'Test'
  #           [sp+0x20]  (sp of caller)
  0x00007fb360a1a200: mov    0x8(%rsi),%r10d
  0x00007fb360a1a204: cmp    %r10,%rax
  0x00007fb360a1a207: jne    0x00007fb3609f38a0  ;   {runtime_call}
  0x00007fb360a1a20d: data32 xchg %ax,%ax
[Verified Entry Point]
  0x00007fb360a1a210: push   %rbp
  0x00007fb360a1a211: sub    $0x10,%rsp
  0x00007fb360a1a215: nop                       ;*synchronization entry
                                                ; - Test::testEqualsIntern@-1 (line 8)
  0x00007fb360a1a216: mov    $0x3e8,%eax
  0x00007fb360a1a21b: add    $0x10,%rsp
  0x00007fb360a1a21f: pop    %rbp
  0x00007fb360a1a220: test   %eax,0x6232dda(%rip)        # 0x00007fb366c4d000
                                                ;   {poll_return}
  0x00007fb360a1a226: retq
Run Code Online (Sandbox Code Playgroud)

当您将一件事的 1000 次迭代与另一件事的 1 次迭代进行比较时,结果相差 1000 倍也就不足为奇了。

在向迭代添加四个零后,我运行了相同的测试,并且正如预期的那样,testEqualsIntern花费了与之前相同的时间,但testEqualsNew等待的速度太慢。