JMH难题:StringBuilder与StringBand

时间:2013-12-12 09:35:45

标签: java benchmarking jmh

我很难理解这个基准测试的内容。我想测量我的示例类StringBandStringBuilder相比的工作方式。使用StringBand的想法是在toString()处连接字符串,而不是append()上的字符串。

来源

以下是StringBand来源 - 已删除基准:

public class StringBandSimple {

private String[] array;
private int index;
private int length;

public StringBandSimple(int initialCapacity) {
    array = new String[initialCapacity];
}

public StringBandSimple append(String s) {
    if (s == null) {
        s = StringPool.NULL;
    }
    if (index >= array.length) {
        //expandCapacity();
    }
    array[index++] = s;
    length += s.length();
    return this;
}

public String toString() {
    if (index == 0) {
        return StringPool.EMPTY;
    }

    char[] destination = new char[length];
    int start = 0;
    for (int i = 0; i < index; i++) {
        String s = array[i];
        int len = s.length();
        //char[] chars = UnsafeUtil.getChars(s);
        //System.arraycopy(chars, 0, destination, start, len);
        s.getChars(0, len, destination, start);
        start += len;
    }
    return new String(destination);
}
}

此代码使用:UnsafeUtil.getChars()实际获取String char []而不进行复制,请参阅代码here。我们也可以使用getChars(),它仍然是相同的。

这是JMH测试:

@State
public class StringBandBenchmark {

String string1;
String string2;

@Setup
public void prepare() {
    int len = 20;
    string1 = RandomStringUtil.randomAlphaNumeric(len);
    string2 = RandomStringUtil.randomAlphaNumeric(len);
}

@GenerateMicroBenchmark
public String stringBuilder2() {
    return new StringBuilder(string1).append(string2).toString();
}

@GenerateMicroBenchmark
public String stringBand2() {
    return new StringBandSimple(2).append(string1).append(string2).toString();
}

}

分析

以下是我对添加两个字符串20个字符时发生的事情的理解。

的StringBuilder

    创建
  • new char[20+16](36个字符)
  • arraycopy被调用以将20 string1个字符复制到StringBuilder
  • 在第二次追加之前,StringBuilder扩展了容量,因为40&gt; 36
  • 因此,new char[36*2+2]已创建
  • arraycopy 20个字符到新缓冲区
  • 20个字符中的
  • arraycopy附加了string2
  • 最后,toString()返回new String(buffer, 0, 40)

StringBand

  • new String[2]已创建
  • 只是将字符串保留在内部缓冲区中,直到调用toString()
  • length增加两次
  • 创建
  • new char[40](结果字符串的总长度)
  • 20个第一个字符串字符的
  • arraycopyUnsafeUtil提供字符串的真实char[]缓冲区)
  • arraycopy 20秒字符串字符
  • 最后,返回new String(buffer, 0, 40)

期望

我们拥有StringBand

  • 少一个arraycopy - 这样做的目的是什么
  • 减少分配大小:new String[]new char[]与两个new char[]
  • 此外,我们没有像StringBuilder方法(大小等)
  • 那样的检查

所以我希望StringBand至少与StringBuilder相同,如果不是更快的话。

基准测试结果

我在2013年中期在MacBookPro上运行基准测试。使用JMH v0.2和Java 1.7b45

命令:

java -jar build/libs/microbenchmarks.jar .*StringBand.* -wi 2 -i 10 -f 2 -t 2

预热迭代次数(2)很好,因为我可以看到第二次迭代达到了相同的性能。

Benchmark                                    Mode Thr     Count  Sec         Mean   Mean error    Units
j.b.s.StringBandBenchmark.stringBand2       thrpt   2        20    1    37806.993      174.637   ops/ms
j.b.s.StringBandBenchmark.stringBuilder2    thrpt   2        20    1    76507.744      582.131   ops/ms

结果表明StringBuilder快了两倍。当我将线程数增加到16或在代码中明确使用BlackHole时,也会发生同样的情况。

为什么吗

2 个答案:

答案 0 :(得分:21)

好吧,像往常一样,“猫头鹰不是他们看起来的样子”。通过快速检查Java代码来推理代码性能变得奇怪。通过查看字节码来推理感觉是一样的。生成的代码反汇编应该对此有更多的了解,即使有一些小的情况下,程序集太高而无法解释这种现象。

这是因为平台在各个层面都大量优化了代码。这是你应该看的提示。在i5 2.0 GHz,Linux x86_64,JDK 7u40上运行基准测试。

<强>基线:

Benchmark                                    Mode Thr     Count  Sec         Mean   Mean error    Units
j.b.s.StringBandBenchmark.stringBand2       thrpt   2        20    1    25800.465      297.737   ops/ms
j.b.s.StringBandBenchmark.stringBuilder2    thrpt   2        20    1    55552.936      876.021   ops/ms

是的,令人惊讶。现在,看看这个。除了...之外,我的袖子里什么都没有。

<强> -XX:-OptimizeStringConcat:

Benchmark                                    Mode Thr     Count  Sec         Mean   Mean error    Units
j.b.s.StringBandBenchmark.stringBand2       thrpt   2        20    1    25727.363      207.979   ops/ms
j.b.s.StringBandBenchmark.stringBuilder2    thrpt   2        20    1    17233.953      219.510   ops/ms

禁止VM进行字符串优化会产生“预期”结果,如原始分析中所述。众所周知,HotSpot具有围绕StringBuilders的优化,有效地识别像new StringBuilder().append(...).append(...).toString()这样的惯用语,并为语句生成更有效的代码。

拆解并弄清楚所应用的字符串优化究竟发生了什么,留给感兴趣的读者练习:)

答案 1 :(得分:0)

首先,由于使用这种方法的对象开销,您在内存中存储了更多数据。

性能问题的原因可能是这部分

char[] chars = UnsafeUtil.getChars(s);
System.arraycopy(chars, 0, destination, start, len); 

由于无法从字符串中获取char[],您必须将其复制到内存中,然后将其复制回destination

您可以尝试用

替换它

s.getChars(0,len,destination,start)

这样,您就可以直接在char[]访问String并将其传递给System.arraycopy