【问题标题】:Java 9 collections' convenience factory methods as an alternative to collection literalsJava 9 集合的便利工厂方法作为集合文字的替代品
【发布时间】:2017-10-06 16:06:07
【问题描述】:

考虑这种方法(仅用于说明):

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 编译器会在每次调用该方法时优化新对象的创建吗?

【问题讨论】:

  • 量一下就行了。
  • 因为它不会逃避该方法,我相当肯定它会。我认为标量替换甚至可能会为此发挥作用......
  • 我希望未来的 JVM 能够针对常量集进行额外的优化。在此之前,可以开发一种工具来在字节码级别检测和处理它们,以确保所需的性能,而无需更改源代码。尽管如此,在一个类内有“千行”的距离听起来像是代码味道……
  • @Holger 通常这是一种代码异味,但它一直在发生。通常是由于编码指南说所有常量都应该放在类的一个部分(例如,在开始或结束时)。例如,参见 PrintStream.java 中的方法 newLine()。它使用if (autoFlush),其中autoFlush 距离大约500 行。
  • 我会创建一个类,它唯一的职责是检查给定的数字字符串是否很小。因此,建议的集合 SMALL_NUMBERS 将位于方法旁边,并且类本身会很小。封装和 SRP 的示例。这听起来像是过早的优化:这种方法是瓶颈吗?

标签: java collections java-9


【解决方案1】:

我制作了一个简单的 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,但没有优化list3list4set3 , set4 [1]

这似乎完全合理,因为 N &gt;= 3 listN/setN 创建的 List/Set 实现比 N &lt;= 2 更复杂。

List 2 个元素的实现:

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

List 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 位服务器 VM(构建 9+181,混合模式)

【讨论】:

  • @Holger 当然。看我的更新。 gc.count 对于N &lt;= 2 为零,对于N &gt;= 3 非零。同样有趣的是Set.containsList.contains
  • 这并不奇怪。散列只为足够大的集合带来回报,但由于您还要衡量构建集合的成本(散列 all 值),所以不太可能看到Set.of(values).contains(…)List.of(values).contains(…) 更快。如果 JVM 为这些工厂方法实现缓存,而不是像任何其他临时对象那样仅进行标量化,那么情况将会改变。
猜你喜欢
  • 2018-01-29
  • 2017-06-14
  • 2012-08-30
  • 2010-11-06
  • 1970-01-01
  • 2017-02-16
  • 1970-01-01
相关资源
最近更新 更多