HashMap在Java 9中的性能比Java 8低25%?

9
注意:这不是关于性能问题的内容。我只观察到一种无法解释/理解的性能差异。
在对一些新开发的针对Java 9的代码进行基准测试时,我发现了一些奇怪的事情。使用5个键的HashMap的一个(非常)简单的基准测试表明,Java 9比Java 8慢得多。这可以解释吗?还是我的(基准)代码有问题?
代码:
@Fork(
    jvmArgsAppend = {"-Xmx512M", "-disablesystemassertions"}
)
public class JsonBenchmark {

    @State(Scope.Thread)
    public static class Data {

        final static Locale RUSSIAN = new Locale("ru");
        final static Locale DUTCH = new Locale("nl");

        final Map<Locale, String> hashmap = new HashMap<>();

        public Data() {
            hashmap.put(Locale.ENGLISH, "Flat flashing adjustable for flat angled roof with swivel");
            hashmap.put(Locale.FRENCH, "Solin pour toit plat inclinée");
            hashmap.put(Locale.GERMAN, "Flachdachkragen Flach Schrägdach");
            hashmap.put(DUTCH, "Plakplaat vlak/hellend dak inclusief glijschaal");
            hashmap.put(RUSSIAN, "Проход через плоскую кровлю регулир. для накл. кровли");
        }

    }

    @Benchmark
    public int bmHashMap(JsonBenchmark.Data data) {
        final Map<Locale, String> m = data.hashmap;
        int sum = 0;
        sum += m.get(Data.RUSSIAN).length();
        sum += m.get(Locale.FRENCH).length();
        sum += m.get(Data.DUTCH).length();
        sum += m.get(Locale.ENGLISH).length();
        sum += m.get(Locale.GERMAN).length();
        return sum;
    }    

}

结果:

  • Java 8_151: JsonBenchmark.bmHashMap thrpt 40 47948546.439 ± 560763.711 ops/s
  • Java 9_181: JsonBenchmark.bmHashMap thrpt 40 34962904.479 ± 276045.691 ops/s (-/- 27%!)

更新

感谢答案和好的评论。

  1. @Holger的建议。我的第一反应是:这一定是解释。然而,如果我只对String#length()函数进行基准测试,则性能没有显著差异。当我只对HashMap#get()方法进行基准测试时(正如@Eugene所建议的),仍然存在大约10-12%的差异。

  2. @Eugene的建议。我改变了参数(更多的预热迭代,更多的内存),但我无法重现你的结果。我将堆大小增加到4G。但这不能解释差异,不是吗?

  3. @Alan Bateman的建议。是的,这提高了性能!然而,仍有大约20%的差异。

2个回答

11

你所测试的不仅仅是HashMap。你不仅调用了HashMap.get,还隐式调用了Locale.hashCodeLocale.equals。此外,你还调用了String.length

现在,这四个方法都可能会改变它们的性能特征,因此您需要进行更多的测试来推断哪种方法表现出不同的性能。

但是最热门的候选者是String.length。在Java 9中,String类不再使用char[]数组,而是使用byte[]数组来编码Latin 1字符串,每个字符只使用一个字节,从而显着减少典型应用程序的内存占用。然而,这意味着长度不再总是等于数组长度。因此,此操作的复杂性已经发生了变化。

但请记住,你的结果是微基准测试中差异约为77纳秒。这不足以估计对真实应用程序的影响...


虽然这可能有点过分,如果您拒绝了也没关系。但是(作为jmh的新手),如果可能的话,您能否在两个版本中陈述.length的复杂度比较?在理解时,将其呈现为答案和统计数据将会很好。我在这里查找了一些信息(http://cr.openjdk.java.net/~shade/8085796/notes.txt),但大部分都超出了我的理解范围。 - Naman
4
好的回答。还有一点需要提到的是,可以使用"-XX:-CompactStrings"参数来查看不使用紧凑字符串时的性能差异。 - Alan Bateman
2
@nullpointer:旧实现是像array.length那样的,新实现是像arrays.length>>coder这样的,其中coder是零或一,取决于字符串。在实际应用程序中的影响取决于length()被实际调用的频率和拉丁1字符串与其他字符串之间的比率。对于拉丁1字符串,一些其他的字符串操作可能会更快(少量字节需要移动),这可能会更加补偿差距。 - Holger
2
@Holger,我喜欢Aleksey(这个和jol一起工作的人)说添加coder字段不会使String类变得更大,因为由于8字节对齐而已经有了它的空间。 - Eugene
4
@Eugene:现在的字段数量仍然比以前少,当时它有一个“偏移量”和“长度”字段(在Java7 update6之前)。 - Holger
显示剩余2条评论

6
我有一点猜测这与jmh的设置有关,而不仅仅是HashMap。正如已经注意到的,您在这里衡量的远不止HashMap :: get。但即使如此,我还是怀疑java-9会慢那么多,所以我自己测量了一下(源自最新的jmh构建,java-8和9)。
我没有改变您的代码 - 只是添加了更多的堆(10GB)和更多的预热,从而减少了您在±之后看到的“误差”。
使用java-8:
Benchmark            Mode  Cnt   Score   Error  Units
SOExample.bmHashMap  avgt   25  22.059 ± 0.276  ns/op

使用Java 9:

Benchmark            Mode  Cnt   Score   Error  Units
SOExample.bmHashMap  avgt   25  23.954 ± 0.383  ns/op

结果基本上是相当的,没有明显的差异(毕竟这只是纳秒级别的)。如果你真的想测试一下HashMap::get,那么你的方法可以简单地返回该调用,如下所示:

@Benchmark
@Fork(5)
public int bmHashMap(SOExample.Data data) {
    return data.hashmap.get(data.key); // where key is a random generated possible key
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接