不管是被带节奏还是啥,在年初放出方舟编译器的消息后,我真的很期待的,毕竟这是我本科一直很想去的华为编译器部门出品的,并且迫不及待地更新了最新的EMUI,体验一波所谓的方舟编译器。不过目前确实,没看到有啥实质性的、明眼可以看的东西。
跨语言编译的事,有一个比较成熟的graal在做了,其实也不算什么新思想。不过放在移动端,甚至是IoT领域,确实是前无古人。
昨天下载了代码,但是在火车上还没看,今天大致看了一下。
其实该吐槽的别人都吐槽了。
- 文档啥的确实写得不怎么样,看完文档确实没懂应该怎么做才能跑起来;
- 为啥是引用计数呢?
- 后端的东西可能和华为的麒麟关系很大,需要脱敏可以理解,但是Java的前端有啥不好开源的?
- 我对LLVM比较熟悉,说实话,我真的觉得二者好像啊,Phase和Pass的概念(感觉方舟的Phase开发没有Pass开发方便)、IR的设计等;(为什么不基于LLVM呢?小声BB……)
- ……
目前开源的东西实在太少了,我其实很想知道以下是怎么做的(如果有内部人士提前透露一下就好了):
- 静态编译后如何保持动态特性(reflection,Java的invokedynamic等),特别是如果要支持JS的话,元信息等怎么处理;
- 内存管理部分,肯定不是单纯的引用计数吧;
- 多语言联合编译的话,是有一个统一的runtime提供所有语言的功能,还是用到一种语言就链接这个语言的runtime子集呢?还是两种结合的方式(一个提供基本功能的runtime,多个language-specific的runtime);
- 代码用方舟编译后在安卓运行,是私有格式,还是类似Xamarin的方式?(更新:这个可以参阅华为的方舟编译器主要修改了zygote)
链接:https://www.zhihu.com/question/343431810/answer/810141581
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
大概看了一些代码了,头晕。
可能因为我是C++菜鸡,有一些地方我实在看不懂为啥这么写……
源码结构
- bin:编译好的可执行文件和一些脚本,很奇怪为什么要放在这里;
- deplibs:因为有很多东西还没有开源,所以在这里提供了静态库;
- huawei_secure_c:华为自己实现的xxx_s函数,很多只是简单的vxxx_s的封装(不是很理解为什么这么做);
- maple_driver:编译器的驱动,就是把编译->各种Phase的转换和优化->链接->生成可执行文件这个流程串起来,目前好像不完整啊。在该目录下的defs/phases.def有所使用到的Phase的定义,有一些可以在src/mpl2mpl和src/maple_me中可以找到Phase的实现,有一些还没找到,不知道是没开源还是我漏了。
- maple_ipa:ipa的实现,没细看,感觉是各种Phase的组合;这个部分应该不完整;
- maple_ir:这次开源的重点,IR的生成、解析等;有一个手写的Lexer和一个手写的递归下降的Parser(牛批!将近3000行,写这个的兄弟写完估计眼睛就瞎了);primitive type很有意思;具体实现还没细看;
- maple_me:me是啥的简写很迷啊,是maple emiting吗?功能应该和LLVM的emit类似;有几个Phase的实现在这里;一些中端优化;(更新:me是middle end的缩写);
- maple_phase:Phase框架的实现,没有开源,只有三个头文件;
- maple_util:工具类,没有开源;
- mempool:内存池的实现,没有开源;有意思的是,定义了一个MapleString(果然每一个C++项目都会有自己的字符串实现啊hhh);
- mpl2mpl:一些Phase的实现,主要是一些analysis、异常处理啥的,针对Java有一个native方法的stub的生成;有对Reflection分析的Phase,提取类型信息、方法信息、类字段信息(不限于Java,看定义可以支持C++、Python等),然后放在maple的metadata(定义在src/maple_ir/include/metadata_layout.h)中,metadata_layout.h中有一句说“metadata layout is shared between maple compiler and runtime”,所以Reflection需要runtime的支持是肯定的;有一些Phase的缩写太奇怪了,没看懂要干啥;
- third_party:第三方库,只有一个zlib;应该是jbc2mpl用到了,用来解压jar文件;
链接:https://www.zhihu.com/question/343431810/answer/810141581
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
MIR
看完MIR的文档,确实感觉有点不太舒服,和我印象中的IR应该有的样子不太一样,我一直觉得LLVM IR才是IR应该有的样子(太天真了点)。
看到有答主提到了MIR有师出同门的Open64的身影,我特意去了解了一下Open64的Whirl IR(Open64 Compiler Whirl Intermediate Representation),发现确实是的,可能这个就是Fred Chow老爷子的设计风格。
可以看到,MIR有很多和语言特性相关的opcode(例如Java的类声明、virtualcall等),甚至有if、while这种特别high level的opcode。如果参照Whirl IR的设计,其实这个是合理的。在Whirl IR中,IR是分为多种不同的level的,虽然都叫Whirl IR,但是囊括了从高级语言到底层机器码的过程中的所有可能需要的不同level的表示形式,可以方便进行不同层级的优化。high level的IR可以进行和语言特性相关的优化,low level进行和硬件特性相关的优化,类似这样。不过MIR的文档最好说明一下这么设计的原因,给个类似Whirl IR的这种流程图也可以,不然很容易被喷的。
链接:https://www.zhihu.com/question/343431810/answer/810141581
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
如图,每一个转换过程都会将更high level的IR翻译为lower level的IR。
还没细看目前开源的实现中Phase的实现,不过像if、while这种high level、层级化的控制流opcode,经过Phase的转换、优化后,应该会变成goto、brfalse、brtrue等扁平化的控制流opcode(更新:在src/maple_ir/src/mir_lower.cpp有具体实现)。整体结构可以算是Open64的翻版,可能确实是老爷子的怨念吧hhh。
和Whirl IR不太一样的地方,就是MIR中有一些直接和前端语言特性强相关的opcode,目前看到的有类型里包含JS的数据类型、opcode中的Java Call和Java Class and Interface Declaration。这部分可能之后添加对JS等的支持后,还会有更多的opcode。
Java Class and Interface Declaration这个部分没啥好说的,Java Call应该是直接和Java Byte Code的invoke系列字节码(invokedynamic、invokespecial、invokeinterface、invokestatic、invokevirtual)对应了,不过按理来说适用其他语言。invokevirtual对应virtualcall、supercall,invokeinterface对应interfacecall,invokespecial和invokestatic编译之后进行name mangling可以当做普通函数(类似C++),对应的应该是MIR中Call系列。invokedynamic没有对应的,所以目前估计是没有直接解决这个问题,看后续的开源吧。
关于invokedynamic指令,我个人的一种猜测是,可能是runtime提供Callsite的构建,类似于在Java 7之前groovy的做法。不过这种方法效率挺低的,需要一些黑科技了。
更新:
关于invokedynamic指令,我觉得我之前肯定傻逼了,既然IR没有提供直接的支持,那就说明一个问题:在前端把invokedynamic翻译为了IR中有的指令。经过我的测试以及对jbc2mpl的逆向,确实是这样的,不过目前还不怎么清楚具体的机制,这里就不细说了。
上手
很惭愧,这么久了其实还没自己编译一遍,也没有上手试一下,只是大致看了代码。
试了一下,可以明显感觉开源的东西太少了,想要跑起来是不可能的,因为无论是编译期Runtime和执行时的Runtime都没有提供,这次开源的东西,确实只能看一下转换出来的IR(直接从Java字节码转换过来的,而这个jbc2mpl还没有开源)。
很多答主都说无法运行,这是因为没有提供Java的Runtime,所以想要能够生成.mpl,任何涉及到Java基本库中的类的都不要出现(所以不能System.out.println、不能测试异常,甚至不能出现main函数,因为main函数的参数有String,摊手.jpg)。
按照文档的指导,配置好Clang、gn、ninja之后,可以正常编译出来。这里要提一句,有人说开源的代码只有声明没有实现,这是不对的,那部分实现只是还没开源而已,提供了静态库,所以编译是没有问题的。
1.准备测试代码
因为不能涉及任何基本库,所以基本算是残废的Java,这里我们就主要看Java字节码和MIR的对应关系。
以下面的斐波那契为例:
public class HelloWorld {
public static int fib(int n) {
return n <= 2 ? 1 : fib(n - 1) + fib( n - 2);
}
}
编译
javac HelloWorld.java
反编译得到Java字节码
javap -verbose HelloWorld.class > HelloWorld.jbc
2.生成对应的Maple IR
如果没有涉及基本库,是可以生成IR的:
jbc2mpl -inclass HelloWorld.class -o HelloWorld.mpl
这个过程中会打印一个警告信息:
Warn 20: method Ljava_2Flang_2FObject_3B_7C_3Cinit_3E_7C_28_29V is undefined
因为父类Object的构造函数没有定义(还是因为没有Runtime)。
同时生成了两个文件:HelloWorld.mpl和HelloWorld.mplt。
.mpl是IR,.mplt是符号表(?)
3.分析
根据官网的演示界面上的流程,后面还要跑maple优化IR和mplcg进行汇编生成的,但是我无论怎么跑都segmentation fault,遂放弃。
flavor 1 srclang 3 id 65535 numfuncs 2 import "HelloWorld.mplt" fileinfo { @INFO_filename "HelloWorld.class"} srcfileinfo { 1 "HelloWorld.java"} javaclass $LHelloWorld_3B <$LHelloWorld_3B> public func &LHelloWorld_3B_7C_3Cinit_3E_7C_28_29V public constructor (var %_this <* <$LHelloWorld_3B>>) void func &LHelloWorld_3B_7Cfib_7C_28I_29I public static (var %Reg4_I i32) i32 func &Ljava_2Flang_2FObject_3B_7C_3Cinit_3E_7C_28_29V public virtual abstract (var %_this <* <$Ljava_2Flang_2FObject_3B>>) void var $__cinf_Ljava_2Flang_2FString_3B <$__class_meta__> func &MCC_GetOrInsertLiteral () <* <$Ljava_2Flang_2FString_3B>> func &LHelloWorld_3B_7C_3Cinit_3E_7C_28_29V public constructor (var %_this <* <$LHelloWorld_3B>>) void { funcid 1 var %Reg1_R45 <* <$LHelloWorld_3B>> var %Reg1_R50 <* <$Ljava_2Flang_2FObject_3B>> dassign %Reg1_R45 0 (dread ref %_this) #INSTIDX : 0||0000: aload_0 #INSTIDX : 1||0001: invokespecial dassign %Reg1_R50 0 (retype ref <* <$Ljava_2Flang_2FObject_3B>> (dread ref %Reg1_R45)) superclasscallassigned &Ljava_2Flang_2FObject_3B_7C_3Cinit_3E_7C_28_29V (dread ref %Reg1_R50) {} #INSTIDX : 4||0004: return return () } func &LHelloWorld_3B_7Cfib_7C_28I_29I public static (var %Reg4_I i32) i32 { funcid 2 var %Reg0_I i32 var %Reg3_I i32 var %Reg1_I i32 intrinsiccallwithtype <$LHelloWorld_3B> JAVA_CLINIT_CHECK () #INSTIDX : 0||0000: iload_0 #INSTIDX : 1||0001: iconst_2 dassign %Reg0_I 0 (constval i32 2) #INSTIDX : 2||0002: if_icmpgt brtrue @label0 (gt i32 i32 (dread i32 %Reg4_I, dread i32 %Reg0_I)) #INSTIDX : 5||0005: iconst_1 dassign %Reg0_I 0 (constval i32 1) #INSTIDX : 6||0006: goto dassign %Reg3_I 0 (dread i32 %Reg0_I) goto @label1 @label0 #INSTIDX : 9||0009: iload_0 #INSTIDX : 10||000a: iconst_1 dassign %Reg0_I 0 (constval i32 1) #INSTIDX : 11||000b: isub dassign %Reg0_I 0 (sub i32 (dread i32 %Reg4_I, dread i32 %Reg0_I)) #INSTIDX : 12||000c: invokestatic intrinsiccallwithtype <$LHelloWorld_3B> JAVA_CLINIT_CHECK () callassigned &LHelloWorld_3B_7Cfib_7C_28I_29I (dread i32 %Reg0_I) { dassign %Reg0_I 0 } #INSTIDX : 15||000f: iload_0 #INSTIDX : 16||0010: iconst_2 dassign %Reg1_I 0 (constval i32 2) #INSTIDX : 17||0011: isub dassign %Reg1_I 0 (sub i32 (dread i32 %Reg4_I, dread i32 %Reg1_I)) #INSTIDX : 18||0012: invokestatic intrinsiccallwithtype <$LHelloWorld_3B> JAVA_CLINIT_CHECK () callassigned &LHelloWorld_3B_7Cfib_7C_28I_29I (dread i32 %Reg1_I) { dassign %Reg1_I 0 } #INSTIDX : 21||0015: iadd dassign %Reg0_I 0 (add i32 (dread i32 %Reg0_I, dread i32 %Reg1_I)) dassign %Reg3_I 0 (dread i32 %Reg0_I) @label1 dassign %Reg0_I 0 (dread i32 %Reg3_I) #INSTIDX : 22||0016: ireturn return (dread i32 %Reg0_I) }
链接:https://www.zhihu.com/question/343431810/answer/810141581
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
和Java字节码对比一下会发现基本是直译过来了(生成的IR还用注释给出了原始的Java字节码指令是啥,良心了)。
之前警告的方法是name mangling后的Object构造函数,这里将invokespecial指令翻译为了MIR的superclasscallassigned,和上面的猜测基本一致。这个地方要依赖Runtime,可能就是后续的步骤无法继续下去的原因吧。
曲线救国
按照官网的流程来说,需要先用maple进行一些中端优化的,但是maple实在跑不起来,所以想了一下,干脆直接进行汇编生成,测试一下整体的流程。
还是以上面的fib为例,用Maple IR写出来(其实是把上面的.mpl删了很多东西直接得到的),然后进行测试。
- fib.mpl
func &fib (var %Reg4_I i32) i32 {
var %Reg0_I i32
var %Reg3_I i32
var %Reg1_I i32
dassign %Reg0_I 0 (constval i32 2)
brtrue @label0 (gt i32 i32 (dread i32 %Reg4_I, dread i32 %Reg0_I))
dassign %Reg0_I 0 (constval i32 1)
dassign %Reg3_I 0 (dread i32 %Reg0_I)
goto @label1
@label0
dassign %Reg0_I 0 (constval i32 1)
dassign %Reg0_I 0 (sub i32 (dread i32 %Reg4_I, dread i32 %Reg0_I))
callassigned &fib (dread i32 %Reg0_I) { dassign %Reg0_I 0 }
dassign %Reg1_I 0 (constval i32 2)
dassign %Reg1_I 0 (sub i32 (dread i32 %Reg4_I, dread i32 %Reg1_I))
callassigned &fib (dread i32 %Reg1_I) { dassign %Reg1_I 0 }
dassign %Reg0_I 0 (add i32 (dread i32 %Reg0_I, dread i32 %Reg1_I))
dassign %Reg3_I 0 (dread i32 %Reg0_I)
@label1 dassign %Reg0_I 0 (dread i32 %Reg3_I)
return (dread i32 %Reg0_I)
}
2. 使用mplcg生成汇编,直接看命令行参数可以发现mplcg的功能还是挺多的:
mplcg fib.mpl
汇编大概如下:
fib.s
.file "fib.mpl"
.text
.align 2
.globl fib
.hidden fib
.type fib, %function
fib:
.Label.fib3:
.cfi_startproc
stp x29, x30, [sp,#-48]!
.cfi_def_cfa_offset 48
.cfi_offset 29, -48
.cfi_offset 30, -40
mov x29, sp
.cfi_def_cfa_register 29
str w0, [x29,#40]
mov w1, #2
str w1, [x29,#16]
ldr w1, [x29,#40]
ldr w2, [x29,#16]
cmp w1, w2
bgt .Label.fib1
mov w1, #1
str w1, [x29,#16]
ldr w1, [x29,#16]
str w1, [x29,#20]
b .Label.fib2
.Label.fib1:
mov w1, #1
str w1, [x29,#16]
ldr w1, [x29,#40]
ldr w2, [x29,#16]
sub w1, w1, w2
str w1, [x29,#16]
ldr w0, [x29,#16]
mov w0, w0
bl fib
str w0, [x29,#16]
mov w1, #2
str w1, [x29,#24]
ldr w1, [x29,#40]
ldr w2, [x29,#24]
sub w1,