你们研究得挺欢乐的,赞~
引用
我想问这种调用中的 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就消除掉了。