3

我很难理解这个基准发生了什么。我想测量我的示例类StringBandStringBuilder. with 的想法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()要实际获取Stringchar[] 而无需复制,请参见此处的代码。我们也可以使用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 个字符的字符串时发生的情况的理解。

字符串生成器

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

弦带

  • new String[2]被建造
  • 两者都追加只是将字符串保留在内部缓冲区中,直到toString()被调用
  • length增加了两次
  • new char[40]已创建(结果字符串的总长度)
  • arraycopy20个第一个字符串字符(UnsafeUtil提供字符串的真实char[]缓冲区)
  • arraycopy20 秒的字符串字符
  • 最后,返回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在代码中显式使用 s 时,也会发生同样的情况。

为什么?

4

2 回答 2

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()并为语句生成更有效的代码。

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

于 2013-12-12T22:24:30.160 回答
0

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

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

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

由于无法char[]从 String 中获取 ,因此您必须将其复制到内存中,然后再将其复制回destination.

您可以尝试将其替换为

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

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

于 2013-12-12T10:06:50.427 回答