Collections.emptyList 和带有 JIT 编译器的空 ArrayList 的性能

Performance of Collections.emptyList and empty ArrayList with JIT compiler

使用 Collections.emptyList() 或空 ArrayList 之间是否存在性能差异,尤其是在使用 JIT 编译器时?

我可以想象 - 例如 - JIT 编译器不执行内联或静态方法调用,因为执行的方法取决于类型。

编辑 我知道 Collections.emptyList() returns 是一个不可变列表,而 ArrayList 是可变对象。

我的意思是,如果我将一个或另一个作为参数传递给方法并且该方法不修改列表,是否会限制 JIT 编译器优化该方法的可能性?

一个简单的例子(只是为了阐明我的意思):

int sum(List<Integer> list)
{
    int sum = 0;

    for(int i=0;i<list.size();++i)
      sum += list.get(i);

    return sum;
}

如果我只用 ArrayList 调用此方法,JIT 编译器可以内联 ArrayList.get()。如果我也用 Collections.empty() 打电话,那就不可能了。

对吗?

Collections.emptyList() 总是 returns 相同的、不可变的空列表对象(单例)。另一方面,创建 ArrayList 实际上会创建一个新对象,分配内存,并且该对象必须稍后进行 GC。

应该没有显着差异,但 Collections.emptyList() 做的工作更少。这些操作在功能上并不等效。一个允许获取一个不可变的空列表,而另一个允许创建一个新的可变列表。根据您想要的功能选择一个或另一个。不是出于性能原因。

Collections.emptyList() 和空 new ArrayList<>() 的工作方式略有不同。由 Collections 编辑的列表 return 不仅是空的,而且是 不可变的 ,因此这个列表可以作为单例存储,并且在每次调用 emptyList( )(由于类型擦除,这对于任何类型限定符都是可能的)。

所以,答案取决于您打算如何处理空列表。如果您要在某些代码中将 return 空列表作为最终值,那么 Collections.emptyList() 肯定更好(它甚至不会创建新对象)。如果你要设置空列表进一步修改,Collections.emptyList()是完全不能接受的

免责声明

以下所有内容仅适用于HotSpot JVM

简答

the JIT compiler doesn't do inlining or static method calls because the executed method depends on the type.

这与事实相反。看我的回答。

Is there a performance difference between using Collections.emptyList() or an empty ArrayList, especially when using a JIT compiler?

在极少数情况下 - 是的。查看微基准测试结果。

If I only called this method with ArrayList the JIT compiler could inline ArrayList.get(). If I also made calls with Collections.empty() it wouldn't be possible. Is that correct?

简短的回答 - 视情况而定。 JIT 编译器足够智能,可以识别单态、双态和多态调用模式并提供适当的实现。

回答

为了获得详细的答案,我建议阅读以下 post 关于方法调度的黑魔法。简而言之

C2 does an interesting profile-guided optimization based on the observed type profile. If there is only a single receiver type (that is, the call site is monomorphic), it can simply check for the predicted type, and inline the target directly. The same optimization can and will be applied if there are two receiver types observed (that is, the call site is bimorphic), at the cost of two branches.

让我们考虑以下 JMH 示例(如果您还没有了解 JMH,那么我建议阅读它 here)。

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(value = 5)
public class ExampleBench {

    @Param("10000")
    private int count;

    List<Integer>[] arrays;
    List<Integer>[] empty;
    List<Integer>[] bimorphic;
    List<Integer>[] polimorphic;

    @Setup
    public void setup(){
        Random r = new Random(0xBAD_BEEF);
        arrays = new List[count];
        empty = new List[count];
        bimorphic = new List[count];
        polimorphic = new List[count];
        for (int i = 0; i < arrays.length; i++) {
            bimorphic[i] = r.nextBoolean() ? new ArrayList<Integer>(0) : Collections.<Integer>emptyList();
            int i1 = r.nextInt(3);
            switch (i1) {
                case 0 : polimorphic[i] = new ArrayList<>(0);
                    break;
                case 1 : polimorphic[i] = new LinkedList<>();
                    break;
                case 2 : polimorphic[i] = Collections.emptyList();
                    break;
            }
            arrays[i] = new ArrayList<>(0);
            empty[i] = Collections.emptyList();
        }
    }

    @Benchmark
    public float arrayList() {
        List<Integer>[] l = arrays;
        int c = count;
        float result = 0;
        for (int i = 0; i < c; i++) {
            result += sum(l[i]);
        }
        return result;
    }

    @Benchmark
    public float emptyList() {
        List<Integer>[] l = empty;
        int c = count;
        float result = 0;
        for (int i = 0; i < c; i++) {
            result += sum(l[i]);
        }
        return result;
    }

    @Benchmark
    public float biList() {
        List<Integer>[] l = bimorphic;
        int c = count;
        float result = 0;
        for (int i = 0; i < c; i++) {
            result += sum(l[i]);
        }
        return result;
    }

    @Benchmark
    public float polyList() {
        List<Integer>[] l = polimorphic;
        int c = count;
        float result = 0;
        for (int i = 0; i < c; i++) {
            result += sum(l[i]);
        }
        return result;
    }

    int sum(List<Integer> list) {
        int sum = 0;
        for (int i = 0; i < list.size(); ++i) {
            sum += list.get(i);
        }
        return sum;
    }
}

结果是:

Benchmark               (count)  Mode  Cnt       Score       Error  Units
ExampleBench.arrayList    10000  avgt    5   22902.547 ± 27665.651  ns/op
ExampleBench.biList       10000  avgt    5   50459.552 ±   739.379  ns/op
ExampleBench.emptyList    10000  avgt    5    3745.469 ±   211.794  ns/op
ExampleBench.polyList     10000  avgt    5  164879.943 ±  5830.008  ns/op

在单态和双态调用的情况下,JIT 用具体实现替换虚拟调用。例如,在 arrayList() 的情况下,-XX:+PrintInlining 的输出如下:

@ 27   edu.jvm.runtime.ExampleBench::sum (38 bytes)   inline (hot)
   @ 6   java.util.ArrayList::size (5 bytes)   accessor
    \-> TypeProfile (15648/15648 counts) = java/util/ArrayList

对于 emptyList():

@ 27   edu.jvm.runtime.ExampleBench::sum (38 bytes)   inline (hot)
   @ 6   java.util.Collections$EmptyList::size (2 bytes)   inline (hot)
    \-> TypeProfile (9913/9913 counts) = java/util/Collections$EmptyList

对于 biList():

@ 27   edu.jvm.runtime.ExampleBench::sum (38 bytes)   inline (hot)
   @ 6   java.util.Collections$EmptyList::size (2 bytes)   inline (hot)
   @ 6   java.util.ArrayList::size (5 bytes)   accessor
    \-> TypeProfile (2513/5120 counts) = java/util/ArrayList
    \-> TypeProfile (2607/5120 counts) = java/util/Collections$EmptyList

polyList() 的情况下,JIT 不内联任何实现并使用真正的虚拟调用。

在这些方法中使用内联函数有什么好处?让我们看看编译器为 arrayList():

生成的代码
0x00007ff9e51bce50: cmp [=15=]xf80036dc,%r10d     ;instance of 'java/util/ArrayList'
0x00007ff9e51bce57: jne L0000                 ;if false go to L0000 (invokeinterface size)
0x00007ff9e51bce59: mov 0x10(%rdx),%ebp       ;*getfield size optimization java.util.ArrayList::size@1 

.....

0x00007ff9e51bce6d: retq
             L0000: mov [=15=]xffffffde,%esi      ; true virtual call starts here
0x00007ff9e51bce73: mov %rdx,(%rsp)
0x00007ff9e51bce77: callq 0x00007ff9e50051a0  ; OopMap{[0]=Oop off=92}
                                              ;*invokeinterface size
                                              ; - edu.jvm.runtime.ExampleBench::sum@6 (line 119)
                                              ;   {runtime_call}

如您所见,JIT 将虚拟调用替换为 getfield