76

在调查使用整数原语并将整数原语转换为字符串的小辩论时,我编写了这个JMH 微基准测试:"" + nInteger.toString(int)

@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class IntStr {
    protected int counter;


    @GenerateMicroBenchmark
    public String integerToString() {
        return Integer.toString(this.counter++);
    }

    @GenerateMicroBenchmark
    public String stringBuilder0() {
        return new StringBuilder().append(this.counter++).toString();
    }

    @GenerateMicroBenchmark
    public String stringBuilder1() {
        return new StringBuilder().append("").append(this.counter++).toString();
    }

    @GenerateMicroBenchmark
    public String stringBuilder2() {
        return new StringBuilder().append("").append(Integer.toString(this.counter++)).toString();
    }

    @GenerateMicroBenchmark
    public String stringFormat() {
        return String.format("%d", this.counter++);
    }

    @Setup(Level.Iteration)
    public void prepareIteration() {
        this.counter = 0;
    }
}

我使用 Linux 机器上存在的两个 Java VM(最新的 Mageia 4 64 位、Intel i7-3770 CPU、32GB RAM)使用默认的 JMH 选项运行它。第一个 JVM 是随 Oracle JDK 8u5 64 位提供的:

java version "1.8.0_05"
Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode)

有了这个 JVM,我几乎得到了我的预期:

Benchmark                    Mode   Samples         Mean   Mean error    Units
b.IntStr.integerToString    thrpt        20    32317.048      698.703   ops/ms
b.IntStr.stringBuilder0     thrpt        20    28129.499      421.520   ops/ms
b.IntStr.stringBuilder1     thrpt        20    28106.692     1117.958   ops/ms
b.IntStr.stringBuilder2     thrpt        20    20066.939     1052.937   ops/ms
b.IntStr.stringFormat       thrpt        20     2346.452       37.422   ops/ms

即,StringBuilder由于创建StringBuilder对象和附加空字符串的额外开销,使用类更慢。使用String.format(String, ...)速度甚至更慢,大约一个数量级。

另一方面,发行版提供的编译器基于 OpenJDK 1.7:

java version "1.7.0_55"
OpenJDK Runtime Environment (mageia-2.4.7.1.mga4-x86_64 u55-b13)
OpenJDK 64-Bit Server VM (build 24.51-b03, mixed mode)

这里的结果很有趣

Benchmark                    Mode   Samples         Mean   Mean error    Units
b.IntStr.integerToString    thrpt        20    31249.306      881.125   ops/ms
b.IntStr.stringBuilder0     thrpt        20    39486.857      663.766   ops/ms
b.IntStr.stringBuilder1     thrpt        20    41072.058      484.353   ops/ms
b.IntStr.stringBuilder2     thrpt        20    20513.913      466.130   ops/ms
b.IntStr.stringFormat       thrpt        20     2068.471       44.964   ops/ms

为什么StringBuilder.append(int)使用这个 JVM 看起来要快得多?查看StringBuilder类源代码并没有发现什么特别有趣的东西 - 所讨论的方法几乎与Integer#toString(int). 有趣的是,附加Integer.toString(int)stringBuilder2微基准)的结果似乎并没有更快。

这种性能差异是测试工具的问题吗?或者我的 OpenJDK JVM 是否包含会影响此特定代码(反)模式的优化?

编辑:

为了进行更直接的比较,我安装了 Oracle JDK 1.7u55:

java version "1.7.0_55"
Java(TM) SE Runtime Environment (build 1.7.0_55-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.55-b03, mixed mode)

结果与 OpenJDK 类似:

Benchmark                    Mode   Samples         Mean   Mean error    Units
b.IntStr.integerToString    thrpt        20    32502.493      501.928   ops/ms
b.IntStr.stringBuilder0     thrpt        20    39592.174      428.967   ops/ms
b.IntStr.stringBuilder1     thrpt        20    40978.633      544.236   ops/ms

这似乎是一个更普遍的 Java 7 vs Java 8 问题。也许 Java 7 有更积极的字符串优化?

编辑 2

为了完整起见,以下是这两个 JVM 的与字符串相关的 VM 选项:

对于 Oracle JDK 8u5:

$ /usr/java/default/bin/java -XX:+PrintFlagsFinal 2>/dev/null | grep String
     bool OptimizeStringConcat                      = true            {C2 product}
     intx PerfMaxStringConstLength                  = 1024            {product}
     bool PrintStringTableStatistics                = false           {product}
    uintx StringTableSize                           = 60013           {product}

对于 OpenJDK 1.7:

$ java -XX:+PrintFlagsFinal 2>/dev/null | grep String
     bool OptimizeStringConcat                      = true            {C2 product}        
     intx PerfMaxStringConstLength                  = 1024            {product}           
     bool PrintStringTableStatistics                = false           {product}           
    uintx StringTableSize                           = 60013           {product}           
     bool UseStringCache                            = false           {product}   

UseStringCache选项在 Java 8 中被删除,没有替换,所以我怀疑这有什么不同。其余选项似乎具有相同的设置。

编辑 3:

AbstractStringBuilder文件中的源代码和StringBuilderInteger的并排比较src.zip显示没有什么值得注意的。除了大量的外观和文档更改之外,Integer现在还支持无符号整数,StringBuilder并且经过轻微重构以与StringBuffer. 这些更改似乎都不会影响 使用的代码路径StringBuilder#append(int),尽管我可能遗漏了一些东西。

IntStr#integerToString()为和生成的汇编代码的比较IntStr#stringBuilder0()更有趣。为这两个 JVM生成的代码的基本布局IntStr#integerToString()是相似的,尽管 Oracle JDK 8u5 似乎更积极地在Integer#toString(int)代码中内联了一些调用。与 Java 源代码有明确的对应关系,即使对于装配经验很少的人也是如此。

然而,的汇编代码IntStr#stringBuilder0()完全不同。Oracle JDK 8u5 生成的代码再次与 Java 源代码直接相关——我可以很容易地识别出相同的布局。相反,OpenJDK 7 生成的代码对于未经训练的眼睛(就像我的眼睛)几乎无法识别。该new StringBuilder()调用似乎已被删除,就像在StringBuilder构造函数中创建数组一样。此外,反汇编插件无法像在 JDK 8 中那样提供对源代码的引用。

StringBuilder我认为这要么是 OpenJDK 7 中更积极的优化传递的结果,要么更可能是为某些操作插入手写的低级代码的结果。我不确定为什么在我的 JVM 8 实现中没有发生这种优化,或者为什么Integer#toString(int)在 JVM 7 中没有实现相同的优化。我想熟悉 JRE 源代码相关部分的人必须回答这些问题......

4

2 回答 2

97

TL;DR:append明显破坏 StringConcat 优化的副作用。

在原始问题和更新中很好的分析!

为了完整起见,以下是一些缺失的步骤:

  • 看穿-XX:+PrintInlining7u55 和 8u5。在 7u55 中,您将看到如下内容:

     @ 16   org.sample.IntStr::inlineSideEffect (25 bytes)   force inline by CompilerOracle
       @ 4   java.lang.StringBuilder::<init> (7 bytes)   inline (hot)
       @ 18   java.lang.StringBuilder::append (8 bytes)   already compiled into a big method
       @ 21   java.lang.StringBuilder::toString (17 bytes)   inline (hot)
    

    ...在 8u5 中:

     @ 16   org.sample.IntStr::inlineSideEffect (25 bytes)   force inline by CompilerOracle
       @ 4   java.lang.StringBuilder::<init> (7 bytes)   inline (hot)
         @ 3   java.lang.AbstractStringBuilder::<init> (12 bytes)   inline (hot)
           @ 1   java.lang.Object::<init> (1 bytes)   inline (hot)
       @ 18   java.lang.StringBuilder::append (8 bytes)   inline (hot)
         @ 2   java.lang.AbstractStringBuilder::append (62 bytes)   already compiled into a big method
       @ 21   java.lang.StringBuilder::toString (17 bytes)   inline (hot)
         @ 13   java.lang.String::<init> (62 bytes)   inline (hot)
           @ 1   java.lang.Object::<init> (1 bytes)   inline (hot)
           @ 55   java.util.Arrays::copyOfRange (63 bytes)   inline (hot)
             @ 54   java.lang.Math::min (11 bytes)   (intrinsic)
             @ 57   java.lang.System::arraycopy (0 bytes)   (intrinsic)
    

    您可能会注意到 7u55 版本较浅,并且看起来在StringBuilder方法之后没有调用任何内容——这很好地表明字符串优化已经生效。实际上,如果您使用 7u55 运行-XX:-OptimizeStringConcat,子调用将重新出现,并且性能下降到 8u5 级别。

  • 好的,所以我们需要弄清楚为什么 8u5 没有做同样的优化。Grep http://hg.openjdk.java.net/jdk9/jdk9/hotspot for "StringBuilder" 找出 VM 在哪里处理 StringConcat 优化;这会让你进入src/share/vm/opto/stringopts.cpp

  • hg log src/share/vm/opto/stringopts.cpp找出那里的最新变化。候选人之一是:

    changeset:   5493:90abdd727e64
    user:        iveresov
    date:        Wed Oct 16 11:13:15 2013 -0700
    summary:     8009303: Tiered: incorrect results in VM tests stringconcat...
    
  • 在 OpenJDK 邮件列表中查找评论线程(很容易通过谷歌搜索更改集摘要): http: //mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2013-October/012084.html

  • Spot "String concat 优化优化将模式 [...] 折叠成一个字符串的单个分配并直接形成结果。优化代码中可能发生的所有可能的 deopts 从头开始​​重新启动此模式(从 StringBuffer 分配开始) 。那意味着整个模式必须我没有副作用。 “尤里卡?

  • 写出对比基准:

    @Fork(5)
    @Warmup(iterations = 5)
    @Measurement(iterations = 5)
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @State(Scope.Benchmark)
    public class IntStr {
        private int counter;
    
        @GenerateMicroBenchmark
        public String inlineSideEffect() {
            return new StringBuilder().append(counter++).toString();
        }
    
        @GenerateMicroBenchmark
        public String spliceSideEffect() {
            int cnt = counter++;
            return new StringBuilder().append(cnt).toString();
        }
    }
    
  • 在 JDK 7u55 上测量它,看到内联/拼接副作用的相同性能:

    Benchmark                       Mode   Samples         Mean   Mean error    Units
    o.s.IntStr.inlineSideEffect     avgt        25       65.460        1.747    ns/op
    o.s.IntStr.spliceSideEffect     avgt        25       64.414        1.323    ns/op
    
  • 在 JDK 8u5 上测量它,看到内联效果的性能下降:

    Benchmark                       Mode   Samples         Mean   Mean error    Units
    o.s.IntStr.inlineSideEffect     avgt        25       84.953        2.274    ns/op
    o.s.IntStr.spliceSideEffect     avgt        25       65.386        1.194    ns/op
    
  • 提交错误报告 ( https://bugs.openjdk.java.net/browse/JDK-8043677 ) 与 VM 人员讨论此行为。原始修复的基本原理是坚如磐石,但有趣的是,如果我们可以/应该在像这样的一些微不足道的情况下恢复这种优化。

  • ???

  • 利润。

是的,我应该发布从链中移动增量的基准的结果,StringBuilder在整个链之前进行。此外,切换到平均时间和 ns/op。这是 JDK 7u55:

Benchmark                      Mode   Samples         Mean   Mean error    Units
o.s.IntStr.integerToString     avgt        25      153.805        1.093    ns/op
o.s.IntStr.stringBuilder0      avgt        25      128.284        6.797    ns/op
o.s.IntStr.stringBuilder1      avgt        25      131.524        3.116    ns/op
o.s.IntStr.stringBuilder2      avgt        25      254.384        9.204    ns/op
o.s.IntStr.stringFormat        avgt        25     2302.501      103.032    ns/op

这是 8u5:

Benchmark                      Mode   Samples         Mean   Mean error    Units
o.s.IntStr.integerToString     avgt        25      153.032        3.295    ns/op
o.s.IntStr.stringBuilder0      avgt        25      127.796        1.158    ns/op
o.s.IntStr.stringBuilder1      avgt        25      131.585        1.137    ns/op
o.s.IntStr.stringBuilder2      avgt        25      250.980        2.773    ns/op
o.s.IntStr.stringFormat        avgt        25     2123.706       25.105    ns/op

stringFormat在8u5中其实是快了一点,其他的测试都是一样的。这巩固了假设 SB 链中的副作用破坏是原始问题的主要罪魁祸首。

于 2014-05-21T19:23:19.617 回答
5

我认为这与CompileThreshold控制字节码何时被 JIT 编译成机器码的标志有关。

Oracle JDK 的默认计数为 10,000 作为http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html上的文档。

OpenJDK 在哪里我找不到关于这个标志的最新文档;但一些邮件线程建议一个低得多的阈值:http: //mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2010-November/004239.html

此外,请尝试打开/关闭 Oracle JDK 标志,如-XX:+UseCompressedStrings-XX:+OptimizeStringConcat。我不确定这些标志是否在 OpenJDK 上默认打开。有人可以建议。

您可以做的一个实验是,首先将程序运行很多次,例如 30,000 次循环,执行 System.gc(),然后尝试查看性能。我相信他们会产生同样的结果。

我假设你的 GC 设置也是一样的。否则,您将分配大量对象,而 GC 很可能是您运行时的主要部分。

于 2014-05-20T10:36:16.757 回答