[讨论] [HotSpot VM] HotSpot 能否优化 list.toArray(new Foo[0])?

kennyluck 2014-12-04
这是从阿里内部的 hllvm 技术群(别名:@撒迦 缅怀团)转出来的问题

引用
Hotspot 能否优化 list.toArray(new Foo[0])?

Java 新手,最近学习了 “list.toArray() 不能用” 的这个大坑。JDK 7.0 代码与范例调用如下:

class ArrayList<E> ... {
    ...
    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }
    ...
}

class Main {
  public String[] main(ArrayList<String> list) {
    return list.toArray(new String[0]);
  }
}


我想问这种调用中的 new String[0] 能否被优化掉。

从没详细看过 HotSpot 代码的理论分析来看,首先,逃逸分析好像不能处理这个情形,如果 list 的长度是 0,则参数会逃逸出去。但是,这种情况 Java 可以内联 toArray 成

if (0 < list.size)
  return (T[]) Arrays.copyOf(list.elementData, list.size, String[].class); // 这个路径不 new String[0]
return new String[0];


么?由于 new String[0] 可能会抛出 OutOfMemoryError,如果这样内敛会不会造成 stacktrace 显示错误?也就是当 list 的长度是 0,OutOfMemoryError 可能从 ArrayList.toArray(T[] a) 抛出而不是 Main.main?这样还符合 Java 规范么?

或许更直接的问题是,在 HotSpot 中,new 究竟算不算是有副作用的?

6小时前提问 平寿


引用
一般情况下不会内联吧,jit对内联还是有一定要求的,另外就算内联应该也是内联到main,不是反过来吧,所以抛出的异常也是main抛出的

5小时前 寒泉子


引用
平寿 2014-12-04 14:21:10

我是说内敛到 main。内敛一般不会影响 stacktrace 的,至少我很确定 V8 是这样,上次简单看了下 SA 里面也是有 VFrame(代表虚拟 Java 栈)跟 Frame(物理栈)的多对一关系 ,一般就用来处理内敛之后维持能正常显示的栈用的,所以我才很好奇 OutOfMemoryError 的存在是不是会对能否内敛与内敛方式造成影响,因为在这里如果 OutOfMemoryError 显示发生的异常是在 return a 那行会很奇怪。

我是猜内联还是会有,不过会是内敛成

  public String[] main(ArrayList<String> list) {
    String[] temp = new String[0];
    if (0 < list.size)
      return (T[]) Arrays.copyOf(list.elementData, list.size, String[].class);
    return temp;
  }


的样子,而不是我刚刚写的,那这样 new String[0] 就不能被优化掉了……

pwq1989 2014-12-04

我构造了一个小的例子来验证了下:

 

环境是openjdk1.6 ,ubuntu 14.10  64位

代码如下:

 

    static public String[] foo(String[] a) {
        if (a.length > 10) {
            // should not reach here
            list.add(new String("ooo"));
            return null;
        }
        String[] s = new String[0];
        //ls.add(s);
        return s;
    }

	public static int foo1(int i) {
 line 27		ls.toArray(new String[0]);
		return i + 2;	
	}
    
    static public void foo222() {
        foo(new String[0]);
        foo(new String[0]);
		foo1(2);
    }

    public static void main(String[] args) throws InterruptedException{
        System.out.println("start");
		for (int i = 0; i < 10000; i++) {
        foo222();
		}

        ...
    }

 

 先使用 VM option '+PrintInlining' 

 

有如下的信息:

start

617   3       test.Main::foo222 (22 bytes)

      @ 4   test.Main::foo  inline (hot)

      @ 12   test.Main::foo  inline (hot)

      @ 17   test.Main::foo1  inline (hot)

        @ 7   java.util.ArrayList::toArray  inline (hot)

        test.Main::foo1 -> @ 7   java.util.ArrayList::toArray  >>TypeProfile (6701/6701 counts) = java/util/ArrayList (60 bytes)

Inlining intrinsic _getClass at bci:18 in java.util.ArrayList::toArray (60 bytes)

Inlining intrinsic _copyOf at bci:21 in java.util.ArrayList::toArray (60 bytes)

Inlining intrinsic _arraycopy at bci:39 in java.util.ArrayList::toArray (60 bytes)

 

 

用 VM option -XX:+PrintAssembly 获得jit后的代码:

部分代码如下

 Decoding compiled method 0x00007f9a650f2e90:

Code:

[Disassembling for mach='i386:x86-64']

[Entry Point]

[Verified Entry Point]

[Constants]

  # {method} 'foo222' '()V' in 'test/Main'

  #           [sp+0x40]  (sp of caller)

  0x00007f9a650f3000: mov    %eax,-0x8000(%rsp)

  0x00007f9a650f3007: push   %rbp

  0x00007f9a650f3008: sub    $0x30,%rsp         ;*synchronization entry

                                                ; - test.Main::foo222@-1 (line 32)

  0x00007f9a650f300c: mov    $0xccc6bd00,%r10   ;   {oop('test/Main')}

  0x00007f9a650f3016: mov    0x258(%r10),%ebp   ;*getstatic ls

                                                ; - test.Main::foo1@0 (line 27)

                                                ; - test.Main::foo222@17 (line 34)

  0x00007f9a650f301d: mov    0x70(%r15),%r11

  0x00007f9a650f3021: mov    %r11,%r10

  0x00007f9a650f3024: add    $0x10,%r10

  0x00007f9a650f3028: cmp    0x80(%r15),%r10

  0x00007f9a650f302f: jae    0x00007f9a650f30bb

  0x00007f9a650f3035: mov    %r10,0x70(%r15)

  0x00007f9a650f3039: prefetchnta 0x100(%r10)

  0x00007f9a650f3041: movq   $0x1,(%r11)

  0x00007f9a650f3048: prefetchnta 0x140(%r10)

  0x00007f9a650f3050: movl   $0xccb02030,0x8(%r11)  ;   {oop('java/lang/String'[])}

  0x00007f9a650f3058: prefetchnta 0x180(%r10)

  0x00007f9a650f3060: mov    %r12d,0xc(%r11)    ;*anewarray

                                                ; - test.Main::foo1@4 (line 27)

                                                ; - test.Main::foo222@17 (line 34)

  0x00007f9a650f3064: mov    0x8(%rbp),%r10d    ; implicit exception: dispatches to 0x00007f9a650f32d9

  0x00007f9a650f3068: cmp    $0xccbe7d50,%r10d  ;   {oop('java/util/ArrayList')}

  0x00007f9a650f306f: jne    0x00007f9a650f30f7

  0x00007f9a650f3075: mov    %rbp,%r10          ;*invokeinterface toArray

                                                ; - test.Main::foo1@7 (line 27)

                                                ; - test.Main::foo222@17 (line 34)

  0x00007f9a650f3078: mov    %r10,0x8(%rsp)

  0x00007f9a650f307d: mov    0x10(%r10),%ebx    ;*getfield size

                                                ; - java.util.ArrayList::toArray@3 (line 303)

                                                ; - test.Main::foo1@7 (line 27)

                                                ; - test.Main::foo222@17 (line 34)

  0x00007f9a650f3081: mov    0x14(%r10),%ebp    ;*getfield elementData

                                                ; - java.util.ArrayList::toArray@10 (line 305)

                                                ; - test.Main::foo1@7 (line 27)

                                                ; - test.Main::foo222@17 (line 34)

......

剩下的是其他inline后的代码

.....

 

结论是:

在toArray这个case下,没有优化参数的那个new String[0]  ,而且foo直接被优化没了

(构造例子好难。。

(初学hotspot,望大大们指点 )

(我也是撒加的粉丝!)

kennyluck 2014-12-05
嗯,看来确实是没优化 new String[0] 。没看过 HotSpot C1/C2 代码的注释加上问题:

  0x00007f9a650f301d: mov    0x70(%r15),%r11 ;0x70(%r15) 应该是当前堆指针? r15 是根专用的寄存器么?
  0x00007f9a650f3021: mov    %r11,%r10
  0x00007f9a650f3024: add    $0x10,%r10 ;所以一个 new String[0] 占 16B?
  0x00007f9a650f3028: cmp    0x80(%r15),%r10 ;0x80(%r15) 应该是堆上限。
  0x00007f9a650f302f: jae    0x00007f9a650f30bb ;无法汇编分配内存的跳出路径,应该就是这边可能抛出 OutOfMemoryError
  0x00007f9a650f3035: mov    %r10,0x70(%r15) ;更新当前堆指针。
  0x00007f9a650f3039: prefetchnta 0x100(%r10) ;神秘的 prefetch…… 为什么会出现在这里?
  0x00007f9a650f3041: movq   $0x1,(%r11) ;传说中的 markOop。
  0x00007f9a650f3048: prefetchnta 0x140(%r10) 
  0x00007f9a650f3050: movl   $0xccb02030,0x8(%r11)  ;klassOop,是 4B 代表 CompressOops 又开来着?   {oop('java/lang/String'[])}
  0x00007f9a650f3058: prefetchnta 0x180(%r10) ; 第三次看到这个了……
  0x00007f9a650f3060: mov    %r12d,0xc(%r11)    ;*anewarray,求问这个是什么。


问题:就算 OutOfMemoryError 是硬要求,jae 后面一大串东西在 ls.length > 0 的一般情形都白执行了吧,如果可以手动移动汇编应该移到 ls.length > 0 的判断后?

没什么空细看了,R 大或是其他大神方便指一下 “new 是有副作用的” 相关的 C1/C2 代码么?

(pwq1989 你汇编的紫色高亮是什么情形啊?)
pwq1989 2014-12-05

@kennyluck  我那个是手动把那几行改了颜色。。。。方便看。。。

 

我的进度比较慢,也没有看到C1.C2的部分,我就我的理解说一下,不一定对

 

写道
0x00007f9a650f302f: jae 0x00007f9a650f30bb ;无法汇编分配内存的跳出路径,应该就是这边可能抛出 OutOfMemoryError

 

这个是在TLAB (Thread local allocate buffer)分配不下的时候,跳到一段代码(位于该方法体的后面),那段代码会call一个函数,然后从堆中分配内存的(带多线程同步机制),最后再跳回来,至于OutOfMemoryError 是更以后的事情

 

写道
movl $0xccb02030,0x8(%r11)
mov %r12d,0xc(%r11)

 这两句是在初始化一个 ArrayOopDesc对象,具体见array对象的内存布局(anewarray 应该就是初始化一个array吧,猜

 

prefetchnta指令的使用策略和r15是不是专门做这个的,我也不清楚,我这两天研究下

RednaxelaFX 2014-12-07
你们研究得挺欢乐的,赞~

引用
我想问这种调用中的 new String[0] 能否被优化掉。

从没详细看过 HotSpot 代码的理论分析来看,首先,逃逸分析好像不能处理这个情形,如果 list 的长度是 0,则参数会逃逸出去。但是,这种情况 Java 可以内联 toArray 成

if (0 < list.size)
  return (T[]) Arrays.copyOf(list.elementData, list.size, String[].class); // 这个路径不 new String[0]
return new String[0];


么?由于 new String[0] 可能会抛出 OutOfMemoryError,如果这样内敛会不会造成 stacktrace 显示错误?也就是当 list 的长度是 0,OutOfMemoryError 可能从 ArrayList.toArray(T[] a) 抛出而不是 Main.main?这样还符合 Java 规范么?

或许更直接的问题是,在 HotSpot 中,new 究竟算不算是有副作用的?


new在HotSpot里不会被看作硬性的有副作用的操作,在一定条件下可以优化掉。

HotSpot C2主要靠escape analysis来消除无用的new。不过HotSpot C2实现的escape analysis是flow-insensitive的,简单说就是无论方法里那条执行路径“有可能”让某个变量逃逸,这个变量都会被认为是逃逸的。它无法做到对某些路径认为不逃逸而别的路径认为是逃逸的。

Graal实现了partial escape analysis,这个是flow-sensitive的。这个可以做到楼主最初问的优化。

HotSpot C2确实是有些优化会在不正确的地方抛出OOME、NPE之类的。不过这被认为是“可以接受的不准确性”所以…

为了让你们更混乱,请看我的版本的测试代码:
import java.util.Arrays;
import java.util.List;

public class TestToArray {
  private static final List<String> STRINGS = Arrays.asList("alpha", "beta");

  public static String[] foo() {
    return STRINGS.toArray(new String[0]);
  }

  public static String[] driver() {
    String[] array = null;
    for (int i = 0; i < 2000; i++) {
      array = foo();
    }
    return array;
  }

  public static void main(String[] args) throws Exception {
    for (int i = 0; i < 200; i++) {
      driver();
    }
    System.out.println("done");
    System.in.read();
  }
}


在Oracle JDK8(jdk1.8.0,build 132那个),Linux/x64上用下面的命令来运行:
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+UnlockExperimentalVMOptions -XX:+TrustFinalNonStaticFields -XX:CompileCommand=exclude,TestToArray,driver -XX:CompileCommand=dontinline,java/util/Arrays,copyOf -XX:DisableIntrinsic=_copyOf,_arraycopy -XX:+PrintIntrinsics -XX:-TieredCompilation -XX:+PrintCompilation -XX:+PrintInlining TestToArray

(嗯我作了很多弊来让最终的代码方便看一些。我把Arrays.copyOf()和System.arraycopy()在C2里的intrisic版禁用了,同时禁止C2内联Arrays.copyOf(),这样foo()里的代码就会比较干净;否则copyOf、arraycopy被intrinsic版内联展开之后会多好多代码,不方便演示结果。
-XX:-TieredCompilation是关掉多层编译,免得C1出来浑水摸鱼)

然后这是TestToArray.foo()被C2编译出来的结果:
    554    8             TestToArray::foo (16 bytes)
                            @ 7   java.util.Arrays$ArrayList::toArray (47 bytes)   inline (hot)
                              @ 1   java.util.Arrays$ArrayList::size (6 bytes)   inline (hot)
                              @ 17   java.lang.Object::getClass (0 bytes)   (intrinsic)
                              @ 20   java.util.Arrays::copyOf (46 bytes)   disallowed by CompilerOracle
                            @ 1   java.util.Arrays$ArrayList::size (6 bytes)   inline (hot)
                            @ 17   java.lang.Object::getClass (0 bytes)   (intrinsic)
                            @ 20   java.util.Arrays::copyOf (46 bytes)   disallowed by CompilerOracle
                            @ 32   java.lang.System::arraycopy (0 bytes)   native method
Decoding compiled method 0x00007fa49d070290:
Code:
[Entry Point]
[Verified Entry Point]
[Constants]
  # {method} {0x00007fa4968004a8} 'foo' '()[Ljava/lang/String;' in 'TestToArray'
  #           [sp+0x20]  (sp of caller)
  0x00007fa49d0703e0: mov    %eax,-0x14000(%rsp)
  0x00007fa49d0703e7: push   %rbp
  0x00007fa49d0703e8: sub    $0x10,%rsp         ;*synchronization entry
                                                ; - TestToArray::foo@-1 (line 8)

  0x00007fa49d0703ec: mov    $0x5801810b8,%rcx  ;   {oop(a 'java/lang/Class' = 'java/lang/String'[])}
  0x00007fa49d0703f6: mov    $0x2,%edx
  0x00007fa49d0703fb: mov    $0x5801810a0,%rsi  ;   {oop(a 'java/lang/String'[2] )}
  0x00007fa49d070405: xchg   %ax,%ax
  0x00007fa49d070407: callq  0x00007fa49d046160  ; OopMap{off=44}
                                                ;*invokestatic copyOf
                                                ; - java.util.Arrays$ArrayList::toArray@20 (line 3825)
                                                ; - TestToArray::foo@7 (line 8)
                                                ;   {static_call}
  0x00007fa49d07040c: mov    0x8(%rax),%r11d    ; implicit exception: dispatches to 0x00007fa49d070446
  0x00007fa49d070410: cmp    $0x17420,%r11d     ;   {metadata('java/lang/String'[])}
  0x00007fa49d070417: jne    0x00007fa49d070425
  0x00007fa49d070419: add    $0x10,%rsp
  0x00007fa49d07041d: pop    %rbp
  0x00007fa49d07041e: test   %eax,0xa2b6bdc(%rip)        # 0x00007fa4a7327000
                                                ;   {poll_return}
  0x00007fa49d070424: retq   
;; fast path ends
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; slow path follows
  0x00007fa49d070425: mov    $0xffffffde,%esi
  0x00007fa49d07042a: mov    %rax,%rbp
  0x00007fa49d07042d: xchg   %ax,%ax
  0x00007fa49d07042f: callq  0x00007fa49d047120  ; OopMap{rbp=Oop off=84}
                                                ;*checkcast
                                                ; - TestToArray::foo@12 (line 8)
                                                ;   {runtime_call}
  0x00007fa49d070434: callq  0x00007fa4a6a71140  ;*invokestatic copyOf
                                                ; - java.util.Arrays$ArrayList::toArray@20 (line 3825)
                                                ; - TestToArray::foo@7 (line 8)
                                                ;   {runtime_call}
  0x00007fa49d070439: mov    %rax,%rsi
  0x00007fa49d07043c: add    $0x10,%rsp
  0x00007fa49d070440: pop    %rbp
  0x00007fa49d070441: jmpq   0x00007fa49d06eea0  ;   {runtime_call}
  0x00007fa49d070446: mov    $0xfffffff4,%esi
  0x00007fa49d07044b: callq  0x00007fa49d047120  ; OopMap{off=112}
                                                ;*checkcast
                                                ; - TestToArray::foo@12 (line 8)
                                                ;   {runtime_call}
  0x00007fa49d070450: callq  0x00007fa4a6a71140  ;*checkcast
                                                ; - TestToArray::foo@12 (line 8)
                                                ;   {runtime_call}
  0x00007fa49d070455: hlt    
  0x00007fa49d070456: hlt    
  0x00007fa49d070457: hlt    
  0x00007fa49d070458: hlt    
  0x00007fa49d070459: hlt    
  0x00007fa49d07045a: hlt    
  0x00007fa49d07045b: hlt    
  0x00007fa49d07045c: hlt    
  0x00007fa49d07045d: hlt    
  0x00007fa49d07045e: hlt    
  0x00007fa49d07045f: hlt    
[Stub Code]
  0x00007fa49d070460: mov    $0x0,%rbx          ;   {no_reloc}
  0x00007fa49d07046a: jmpq   0x00007fa49d07046a  ;   {runtime_call}
[Exception Handler]
  0x00007fa49d07046f: jmpq   0x00007fa49d06c4a0  ;   {runtime_call}
[Deopt Handler Code]
  0x00007fa49d070474: callq  0x00007fa49d070479
  0x00007fa49d070479: subq   $0x5,(%rsp)
  0x00007fa49d07047e: jmpq   0x00007fa49d046d00  ;   {runtime_call}
  0x00007fa49d070483: hlt    
  0x00007fa49d070484: hlt    
  0x00007fa49d070485: hlt    
  0x00007fa49d070486: hlt    
  0x00007fa49d070487: hlt  

请只关注在fast path ends那行注释以上的代码:这段才是真正执行的,后面的都其实不会被执行。
可以看到这个版本的foo()里new String[0]被消除掉了。

如果没有被消除掉的话,就会有一串类似这样的代码:
  0x00007fe894c2508c: mov    0x60(%r15),%rbp
  0x00007fe894c25090: mov    %rbp,%r10  ;; r10 = TLAB.top
  0x00007fe894c25093: add    $0x10,%r10 ;; r10 = TLAB.top + sizeof(String[0])
  0x00007fe894c25097: cmp    0x70(%r15),%r10 ;; compare(r10, TLAB.end)
  0x00007fe894c2509b: jae    0x00007fe894c2510c ;; if (TLAB.top + sizeof(String[0]) >= TLAB.end) goto slow_allocation
  0x00007fe894c2509d: mov    %r10,0x60(%r15) ;; TLAB.top = TLAB.top + sizeof(String[0])
  0x00007fe894c250a1: movq   $0x1,0x0(%rbp) ;; array._mark = 0x00000000000001
  0x00007fe894c250a9: movl   $0x17230,0x8(%rbp)  ;  array._klass = {metadata('java/lang/String'[])}
  0x00007fe894c250b0: movl   $0x0,0xc(%rbp)     ; array._length = 0

这是TLAB allocation的快速路径的代码。

我用这个版本的测试是想说明list.toArray(new String[0])里的无用数组是可能被HotSpot C2优化掉的,但需要一些特定的条件。

我这里用了Arrays.asList(),具体返回的List实例是java.util.Arrays$ArrayList,是个特别的List实现。关键在于里面有final的数组。

再来看看java.util.ArrayList.toArray(T[] a)和java.util.Arrays$ArrayList.toArray(T[] a)这俩方法:
ArrayList:
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }

Arrays$ArrayList:
        @Override
        @SuppressWarnings("unchecked")
        public <T> T[] toArray(T[] a) {
            int size = size();
            if (a.length < size)
                return Arrays.copyOf(this.a, size,
                                     (Class<? extends T[]>) a.getClass());
            System.arraycopy(this.a, 0, a, 0, size);
            if (a.length > size)
                a[size] = null;
            return a;
        }

两者的代码结构几乎一模一样。那为啥我要选Arrays$ArrayList呢?
因为这行代码:
if (a.length < size)

当我的foo()可以内联list.toArray(T[] a)的调用时,a.length在这里就是编译时常量,也就是0。问题是size仍然不是常量,所以这个比较无法常量折叠掉,那么System.arraycopy()的调用就会留下,而a作为参数传给了arraycopy()所以必须留下,自然它的分配也就不能优化掉了。

当我使用一个static final常量来持有一个List实例的时候,C2就会知道这是个可以信任的常量而去直接访问那个List实例的内容。
可惜如果这个List是一个java.util.ArrayList的话,size字段可变,所以还是不行。
如果这个List是一个java.util.Arrays$ArrayList的话,它没有size字段;它的size()方法是:
    private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    {
        private final E[] a;

        ArrayList(E[] array) {
            a = Objects.requireNonNull(array);
        }

        @Override
        public int size() {
            return a.length;
        }

        // ...
    }

那么只要我们能让C2相信java.util.Arrays$ArrayList.a也是一个编译时常量,前面说的比较条件就可以被常量折叠,那么就只剩下调用Arrays.copyOf()的分支而不需要使用传入的a数组,于是a数组就可以被干掉了。

C2一般情况下不会把final的成员字段当作编译时常量,因为Java的“final”成员字段并不够“不可变”——像是说别人可以用反射硬是setAccessible(true)之后把final字段的值给改了——虽然这样非常非常不推荐。反正就是一般认为把非static的final当作常量太危险了所以大家通常都不去跳这坑。

但C2在JDK8的版本里有一个实验性功能,-XX:+TrustFinalNonStaticFields,打开了只会就可以让C2把final成员字段也算做编译时常量,只要能从一个常量对象(例如被static final引用的对象)出发,它的final成员字段也会被认为是常量。
这样就彻底解决那个比较条件的常量折叠问题了。

这种条件下,C2在编译foo()时看到的内联了的java.util.Arrays$ArrayList.toArray(T[] a)就变成了:
        public <T> T[] toArray(T[] a) { // a is new String[0]
            int size = size();          // size is constant 2
            if (a.length < size)        // a.length is constant 0
                return Arrays.copyOf(this.a, size,
                                     (Class<? extends T[]>) a.getClass());
            
            // the following part never executes
            System.arraycopy(this.a, 0, a, 0, size);
            if (a.length > size)
                a[size] = null;
            return a;
        }

所以编译结果就是:
  public static String[] foo() {
    return Arrays.copyOf(constant_a, 2, String[].class);
  }

然后因为我禁用了copyOf()的intrinsic,所以前面展示的汇编里就只有一个对copyOf()的调用而没有展开的代码——虽然不影响new String[0]被消除了的结论,但展开了太麻烦不方便解释。

太多黑魔法了(逃

不过如果用Graal来编译的话,就算用普通的ArrayList可能也可以优化掉在实际执行的路径上的new String[0],通过partial escape analysis可以把new的实际推迟到if (a.length < size)之后,然后Arrays.copyOf()的路径上没有用到a这样new就消除掉了。
RednaxelaFX 2014-12-08
用Graal来跑普通的ArrayList的例子,看看Graal的partial escape analysis的威力

测试代码:
import java.util.ArrayList;

public class TestToArray {
  public static String[] foo(ArrayList<String> list) {
    return list.toArray(new String[0]);
  }

  public static String[] driver(ArrayList<String> list) {
    String[] array = null;
    for (int i = 0; i < 2000; i++) {
      array = foo(list);
    }
    return array;
  }

  public static void main(String[] args) throws Exception {
    ArrayList<String> list = new ArrayList<>();
    list.add("alpha");
    list.add("beta");
    for (int i = 0; i < 200; i++) {
      driver(list);
    }
    System.out.println("done");
    System.in.read();
  }
}


运行命令:
java -graal -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:-TieredCompilation -XX:+PrintCompilation -XX:+PrintInlining TestToArray

我用的是Graal VM模式而不是Server VM模式。

编译出来的foo():
(我只贴了走Arrays.copyOf()的路径出来因为实际只走那边;System.arraycopy()的路径有一大块,懒得贴)
TestToArray.foo  [0x000000010a946ac0, 0x000000010a946d20]  608 bytes
[Entry Point]
[Verified Entry Point]
[Constants]
  # {method} {0x000000011ac0d4a0} 'foo' '(Ljava/util/ArrayList;)[Ljava/lang/String;' in 'TestToArray'
  # parm0:    rsi:rsi   = 'java/util/ArrayList'
  #           [sp+0x40]  (sp of caller)
  0x000000010a946ac0: mov    %rax,-0x14000(%rsp)
  0x000000010a946ac8: sub    $0x38,%rsp
  0x000000010a946acc: mov    %rbp,0x30(%rsp)    ; OopMap{rsi=Oop off=17}
                                                ;*aload_0
                                                ; - TestToArray::foo@0 (line 5)

  0x000000010a946ad1: test   %eax,(%rsi)        ;*aload_1
                                                ; - java.util.ArrayList::toArray@0 (line 390)
                                                ; - TestToArray::foo@5 (line 5)
                                                ; - TestToArray::foo@8 (line 5)

  0x000000010a946ad3: mov    0x10(%rsi),%eax
  0x000000010a946ad6: mov    0x14(%rsi),%edi
  0x000000010a946ad9: shl    $0x3,%rdi
  0x000000010a946add: mov    %rdi,0x18(%rsp)
  0x000000010a946ae2: test   %eax,%eax        ; if (a.length < size) // a.length == 0
  0x000000010a946ae4: jg     0x000000010a946bd9 ; 跳到Arrays.copyOf()的路径
;; ...省略掉中间的System.arraycopy()的路径...
  0x000000010a946bd9: movabs $0x6c01d02a0,%rcx  ;   {oop(a 'java/lang/Class' = 'java/lang/String'[])}
  0x000000010a946be3: mov    0x18(%rsp),%rsi
  0x000000010a946be8: mov    %eax,%edx
  0x000000010a946bea: nopl   0x0(%rax,%rax,1)
  0x000000010a946bef: callq  0x000000010a00a160  ; OopMap{off=308}
                                                ;*invokestatic copyOf
                                                ; - java.util.ArrayList::toArray@21 (line 392)
                                                ; - TestToArray::foo@5 (line 5)
                                                ;   {static_call}
  0x000000010a946bf4: nop
  0x000000010a946bf5: test   %rax,%rax
  0x000000010a946bf8: je     0x000000010a946c97
  0x000000010a946bfe: mov    0x8(%rax),%esi
  0x000000010a946c01: cmp    $0xf8002e50,%esi
  0x000000010a946c07: mov    $0x1,%esi
  0x000000010a946c0c: mov    $0x0,%edx
  0x000000010a946c11: cmove  %esi,%edx
  0x000000010a946c14: test   %rax,%rax
  0x000000010a946c17: je     0x000000010a946c26
  0x000000010a946c1d: cmp    $0x1,%edx
  0x000000010a946c20: jne    0x000000010a946cb8  ;*areturn
                                                ; - TestToArray::foo@11 (line 5)

  0x000000010a946c26: mov    0x30(%rsp),%rbp
  0x000000010a946c2b: add    $0x38,%rsp
  0x000000010a946c2f: test   %eax,-0x1b4ac2f(%rip)        # 0x0000000108dfc006
                                                ;   {poll_return}
  0x000000010a946c35: retq   

可以看到这个结果里面也是没有new String[0]的。有趣的是,在我省略掉的System.arraycopy()的路径上,一开始就是那个new String[0]。也就是说foo()内联的ArrayList.toArray(T[] a)的逻辑在这个环境里,new String[0]被推迟到if (a.length < size)的两个分支内了,而在Arrays.copyOf()的分支里这个new就被partial escape analysis消除了。跟楼主最初想要的效果基本上一样。

Graal优化过的foo()的逻辑如下:
public static String[] foo(ArrayList<String> list) {
  // inlined ArrayList.toArray(T[] a)
  if (0 < list.size) {
    return Arrays.copyOf(list.elementData, list.size, String[].class);
  } else {
    String[] array = new String[0]; // new被推迟到分支内
    System.arraycopy(list.elementData, 0, a, 0, list.size);
    return array;
  }
}


顺带一提,JRockit也实现了partial escape analysis,所以这个例子里JRockit也可以做到跟Graal类似的优化效果。
Global site tag (gtag.js) - Google Analytics