Java 9集合的便利工厂方法作为集合字面量的替代品

10

考虑这个方法(仅供说明):

boolean isSmallNumber(String s) {
    return (n in ["one", "two", "three", "four"]);
}

当然,那不是 Java,但它可能是你喜欢的支持集合字面量的替代语言,如 GroovyKotlin。这个表达式很简洁,就像字符串字面量一样,编译器可以将集合字面量放在某个静态存储区域(甚至可以使用 "intern()" 对其进行优化)。

现在进入 Java 9

boolean isSmallNumber(String s) {
    return Set.of("one", "two", "three", "four").contains(s);
}

这也很简洁,但不幸的是,每次调用它都会在堆上分配一个新的Set,并立即使其可以进行垃圾回收。

当然,您可以定义一个集合常量:

private static final Set<String> SMALL_NUMBERS = Set.of(...);

但是在一个大类中,这个定义可能与方法定义相隔千里,您可能无法为其想到一个好的描述性名称,而直接使用文字可能会更清晰(在这种假设情况下)。

因此,如果我在方法内部使用 Set.of(...)JIT编译器会优化掉每次调用方法时创建新对象的操作吗?


1
只需测量它。 - C-Otto
1
由于它没有逃逸出方法,我相当确定它会这样做。我认为标量替换甚至可能会启动此操作... - Eugene
1
我期望未来的JVM将具有特别针对常量集的额外优化。在那之前,可以开发一个工具,在字节码级别上检测和处理它们,以确保所需的性能,而无需更改源代码。不过,一个类内存在“千行”距离听起来像是代码异味... - Holger
@Holger,一般来说这是代码异味,但它经常发生。通常是由于编码指南规定所有常量都应该放在类的一个部分(例如在开头或结尾)。例如,在PrintStream.java中的方法newLine()中可以看到这种情况。它使用了if (autoFlush),而autoFlush则距离其约500行远。 - DodgyCodeException
我会创建一个类,其唯一的责任是检查给定的数字字符串是否小。因此,建议的集合SMALL_NUMBERS将与该方法相邻,该类本身也将很小。这是封装和SRP的示例。另外,这似乎是一种过早的优化:这个方法是瓶颈吗? - Boris
1个回答

15

我已经编写了一个简单的JMH基准测试:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class Temp {

    private Object value;

    @Setup
    public void setUp() {
        value = 50;
    }

    @Benchmark
    public boolean list1() {
        return List.of("one").contains(value);
    }

    @Benchmark
    public boolean list2() {
        return List.of("one", "two").contains(value);
    }

    @Benchmark
    public boolean list3() {
        return List.of("one", "two", "three").contains(value);
    }

    @Benchmark
    public boolean list4() {
        return List.of("one", "two", "three", "four").contains(value);
    }

    @Benchmark
    public boolean set1() {
        return Set.of("one").contains(value);
    }

    @Benchmark
    public boolean set2() {
        return Set.of("one", "two").contains(value);
    }

    @Benchmark
    public boolean set3() {
        return Set.of("one", "two", "three").contains(value);
    }

    @Benchmark
    public boolean set4() {
        return Set.of("one", "two", "three", "four").contains(value);
    }
}

在使用-prof gc运行基准测试后,我得出以下结论:JIT会优化list1list2set1set2,但不会优化list3list4set3set4 [1]

这似乎完全合理,因为对于N >= 3listN/setN创建的List/Set实现比N <= 2更复杂。

2个元素的List实现:

static final class List2<E> extends AbstractImmutableList<E> {
    private final E e0;
    private final E e1;
    ...
}

3个或更多元素的列表实现:

static final class ListN<E> extends AbstractImmutableList<E> {
    private final E[] elements;
    ...
}

ListN 包含另一层间接引用(一个数组),这显然使得逃逸分析更加困难。


JMH 输出(略作修改以适应页面):

Benchmark                  Mode  Cnt     Score      Error   Units
list1                      avgt    5     3,075 ?    1,165   ns/op
list1:·gc.alloc.rate       avgt    5     0,131 ?    1,117  MB/sec
list1:·gc.alloc.rate.norm  avgt    5    ? 10??               B/op
list1:·gc.count            avgt    5       ? 0             counts

list2                      avgt    5     3,161 ?    0,543   ns/op
list2:·gc.alloc.rate       avgt    5     0,494 ?    3,065  MB/sec
list2:·gc.alloc.rate.norm  avgt    5     0,001 ?    0,003    B/op
list2:·gc.count            avgt    5       ? 0             counts

list3                      avgt    5    33,094 ?    4,402   ns/op
list3:·gc.alloc.rate       avgt    5  6316,970 ?  750,240  MB/sec
list3:·gc.alloc.rate.norm  avgt    5    64,016 ?    0,089    B/op
list3:·gc.count            avgt    5   169,000             counts
list3:·gc.time             avgt    5   154,000                 ms

list4                      avgt    5    32,718 ?    3,657   ns/op
list4:·gc.alloc.rate       avgt    5  6403,487 ?  729,235  MB/sec
list4:·gc.alloc.rate.norm  avgt    5    64,004 ?    0,017    B/op
list4:·gc.count            avgt    5   165,000             counts
list4:·gc.time             avgt    5   146,000                 ms

set1                       avgt    5     3,218 ?    0,822   ns/op
set1:·gc.alloc.rate        avgt    5     0,237 ?    1,973  MB/sec
set1:·gc.alloc.rate.norm   avgt    5    ? 10??               B/op
set1:·gc.count             avgt    5       ? 0             counts

set2                       avgt    5     7,087 ?    2,029   ns/op
set2:·gc.alloc.rate        avgt    5     0,647 ?    4,755  MB/sec
set2:·gc.alloc.rate.norm   avgt    5     0,001 ?    0,010    B/op
set2:·gc.count             avgt    5       ? 0             counts

set3                       avgt    5    88,460 ?   16,834   ns/op
set3:·gc.alloc.rate        avgt    5  3565,506 ?  687,900  MB/sec
set3:·gc.alloc.rate.norm   avgt    5    96,000 ?    0,001    B/op
set3:·gc.count             avgt    5   143,000             counts
set3:·gc.time              avgt    5   108,000                 ms

set4                       avgt    5   118,652 ?   41,035   ns/op
set4:·gc.alloc.rate        avgt    5  2887,359 ?  920,180  MB/sec
set4:·gc.alloc.rate.norm   avgt    5   104,000 ?    0,001    B/op
set4:·gc.count             avgt    5   136,000             counts
set4:·gc.time              avgt    5    94,000                 ms

[1] Java HotSpot(TM) 64位服务器虚拟机(版本号9+181,混合模式)



@Holger 当然。请看我的更新。当 N <= 2 时,gc.count 为零,当 N >= 3 时,它为非零值。另外有趣的是,Set.containsList.contains 更慢。 - ZhekaKozlov
3
这并不太令人惊讶。哈希只有在集合足够大时才会产生好处,但由于您还要测量构建集合的成本(哈希所有值),很难看到 Set.of(values).contains(…)List.of(values).contains(…) 更快。如果JVM实现了这些工厂方法的缓存而不仅仅是像其他临时对象一样进行标量化,则情况将发生改变。 - Holger

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