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

时间:2014-08-17 16:59:53

标签: 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

这是testEqualsInterntestEqualsNew之间的1300倍因素,坦率地说,相当让我感到惊讶。

String.equals()的代码确实对同一个对象进行了测试,该对象可以很快地将相同的(在这种情况下实例化)字符串对象踢出。我只是非常难以相信额外的代码似乎相当于两个测试和比较元素的大小为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();
    }
}

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>

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

$ mvn archetype:generate \
          -DinteractiveMode=false \
          -DarchetypeGroupId=org.openjdk.jmh \
          -DarchetypeArtifactId=jmh-java-benchmark-archetype \
          -DgroupId=org.sample \
          -DartifactId=test \
          -Dversion=1.0

运行测试:

$ mvn clean install
$ java -jar target/benchmarks.jar ".*SimpleBenchmark.*" -wi 5 -i 5 -f 1

因为这将是一个问题,它运行的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)

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

4 个答案:

答案 0 :(得分: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)
....................................................................................................

您可能知道,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;
}

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

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

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

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

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

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

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

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

答案 1 :(得分:4)

测试新String的相等性并没有出现荒谬的性能损失。您所看到的效果只是Hotspot能够在一种情况下优化掉循环,而不是另一种情况。

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

testIsEmpty

当你将一件事的1000次迭代与另一件事的1次迭代进行比较时,结果相差1000倍并不奇怪。

在向ITERATIONS添加四个零之后我运行了相同的测试,并且正如预期的那样,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 花费的时间与以前一样长,而testEqualsIntern太慢而无法等待。

答案 2 :(得分:3)

解释似乎是(在第一种情况下,intern()一个)JVM能够测试参考相等性,这是一种直接的数值比较。

相反,非引用相等性(值相等)的测试必须迭代两个字符串的字符序列。您观察到的结果并不像您认为的那么重要。有JIT和其他优化正在进行,性能可能会在实践中得到改善(因为并非每个String都相同,并且当它们不相同时它可能会短路)。

最后,微观基准众所周知是不可靠的。但是您已经发现了一种通过设计内置于JVM中的性能优化。参考等同性检查 的速度要快得多。

答案 3 :(得分:-2)

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

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

这里str.equals(EMPTY)将首先通过==检查相等性并且它将返回true,因为str和EMPTY都具有相同的引用并且在字符串池中并且操作将变得更快,但是如果

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

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

EMPTY字符串在字符串池中,而NEW_EMPTY不是池的一部分,并且两者都有不同的引用,因为EMPTY一个是文字常量而NEW_EMPTY不是。所以equals()首先尝试通过==来比较等式,因为两者都有不同的引用,它将返回false,并且它将检查内容,所以在这种情况下,equals()将花费更多的时间。

相关问题