根据之前的文章刨根问底:为什么kotlin中,不对val声明的局部变量做常量优化? - 掘金 (juejin.cn)。今天这篇文章不是为了解答为什么,而是手动实现这样的转换。
成果
话不多说,为勾起欲望,直接看转换前和转换后的对比图:
没有多余的操作,所有使用到val声明的变量的地方,都用常量代替了。
我们知道,kotlin中也有const关键字,如果不用这个编译插件,将a、b、c、d用const替代,看看结果如何:
效果跟“转换后”的是一样的,也就是说,我们这里手动将a、b、c、d转换成了const val。
当然,图片右边已经是反编译字节码的结果了,有时间大家也可以尝试,用const和不用const的情况下,直接查看字节码,进行更进一步的对比。
KCP是什么
KCP既是Kotlin Compiler Plugin,是kotlin编译器插件,简单来说就是可以在编译过程中增加自己的逻辑,从而改变编译产物的目的。kotlin编译器进一步可以区分为编译器前端和编译器后端,而其中有很多个hook点供开发者使用,比如还可以增加插件,自行处理编译时的代码检查工作。
我们这里使用IrGenerationExtension
,可以修改编译后端中的IR,从而达到最后修改编译产物的目的。
更多知识点,请查看参考链接和自行查阅相关资料。
KCP开发步骤
1. 创建gradle插件
因为KCP是基于gradle插件运行的,所以第一步得先创建gradle插件,并实现相关的接口。
关键就是实现KotlinCompilerPluginSupportPlugin
这个接口,并实现里面的抽象方法。
2. 创建kotlin插件
实现KCP所需的关键接口和抽象类:
-
CommandLineProcessor
:KCP的入口,可以解析并传递参数。gradle插件中的自定义参数,可以通过KotlinCompilerPluginSupportPlugin
传递到此。 -
CompilerPluginRegistrar
:注册各种Extension,并且上一步获取到的参数,可以传递到此。 -
IrGenerationExtension
:有很多种Extension,我们这里使用IrGenerationExtension,可以用来更新Ir节点,从而改变编译产物。 -
IrElementTransformerVoidWithContext
:每个Ir节点都是一个IrElement
接口,其中有个transform方法,可以进行转换。
注意在CommandLineProcessor
和CompilerPluginRegistrar
的实现类中,使用了@AutoService
注解,因为这两个实现类都是通过SPI机制加载的,当然不使用也可以,那就得自行在resources目录下进行注册。
整个Ir结构其实就是一颗树形结构,其每一个(每一个)节点,都代表一个操作,比如声明一个变量,赋值(直接赋值字面量或者一个赋值表达式),一个方法调用,调用的每一个参数等等。在MyExtension中,generate方法中有个moduleFragment的Ir节点,这就是整个Ir节点的根节点,我们可以调用其dump()这个扩展函数打印出整个Ir树的信息。
最后这个Transformer,其使用方式和各种“visit”方法其实跟ASM有些类似。所以感觉,要是做些简单的处理,代码生成,代码结构改变,比如方法前后的插装,在了解了这些基本内容后,上手难度不是很高。
具体实现思路
说实话这也是我第一次使用KCP,继上一篇文章中,对这个val的思考,我就想是不是有什么办法可以做到转变,肯定有什么办法可以做到转变。然后我想到在霍老师的视频里听到的一个词“KCP”,鬼使神差,我就直接开始查阅相关文章(真心不多),然后直接开始做了。
熟悉过程也有些许漫长,首先查看分析整个Ir树,然后再疯狂打断点,查看每一个Ir节点对应的visitXXX方法是什么,就可以针对每个visitXXX方法做处理了。
整体步骤有点长,望大家耐心阅读(●ˇ∀ˇ●)。
在上述完成KCP所需的基本框架后,就可以进行实际的逻辑开发了。其大部分都是围绕IrElementTransformerVoidWithContext
展开的。
每一个步骤的成果图,请对比文章最开头的“转换前”的效果图。
查看Ir树
第一步先查看整颗Ir结构树,看看每个节点到底是什么。
这个testVal方法就是最前面的那个。其转换成Ir树如图所示。每个节点的含义其实也很明显了。
找出“常量”
在看Ir树时,我发现:
- 在变量声明的过程中有“val”和“var”的标识
- 变量有初始化赋值
- 变量赋值时有常量赋值也有表达式赋值
那么基于这三点,只要判断变量声明的关键字是“val”,并且有初始化常量赋值,就可以把这个变量当作常量来使用了。
保存“常量”
将上一步找到的常量通过Map<IrValueSymbol, IrConst<*>>
这个结构的哈希表进行保存,注意这里key需要用“IrValueSymbol”,因为可以在不同的块级作用域下声明相同的变量名,从而影响后续的使用时的判断。而每个IrVariable
节点,都有一个symbol
成员,表示唯一符号。
visitVariable
是变量声明时的回调。
使用常量
每个声明的变量我们已经判断完是否可以当作常量,那么现在就可以在使用时进行替换了。
visitGetValue
是获取变量的回调,其中IrGetValue
就是获取到的变量,我们判断其符号symbol是否在我们之前保存的哈希表中。IrConst继承自IrExpression,所以这里获取到后可以直接返回。
做完这一步的处理后,我们看下结果:
可以看到,c=9了,这是因为编译优化中的“常量折叠”,因为a和b都被我们转换成常量了,而“5”是字面量,所以可以对这个加法表达式做编译时优化。
这里还有个a=true,b=true。我怀疑是因为后续没使用到这两个变量,但是又有变量声明导致的。
删除无用的变量声明
在上一步最后,我们发现有冗余的变量声明,这里我们把他处理了。
首先,我看下transform返回的处理结果的Ir是怎么进行替换的,根据断点,我们一步步往下走到这
这个statements,就是有几条语句,我们这个testVal方法中确实有8条。
而这个扩展函数,就是对每一个语句做transform操作后再替换,所以,我们上一步中在visitGetValue
返回的新的表达式就可以被替换了。
那么关键点就在于这个statements,如果我们想要删除一个语句,只要删除这个列表中的这一项即可。
首先看这个statements是在这个接口中的,再看看实现有哪些
我又写了几个if,while,lambada表达式,分析Ir树时,只看到了前面两种,IrBlock和IrBlockBody,那么在Transform的实现类的父接口中直接搜“IrBlock
:
下面那个方法最终指向了visitExpression
,所以,在实现类中,我们主要重写visitBody
和visitExpression
,并且判断参数是否是IrStatementContainer
,即可拿到其中的statements
了。
接下去就是该考虑,怎么判断statements
列表中哪些语句该删除,哪些语句不该删除。
其实根据“保存常量”这一步,我们已经知道了,哪些变量我们已经判断为是“常量”了,那么我们在visitVariable
中打上一个标识,或者返回一个自定义类型表示需要删除的语句可以吗?当然可以,statements
是IrStatement
类型的列表,而visitVariable
返回值需要是IrStatement
类型,那不就简单了,我们自定义一个类,实现IrStatement
就行了。
在这个实现类中,我们什么都不处理,因为肯定要被删除的。
对前一步进行改造下
这也是为什么checkAndGetConst方法我要返回布尔值的原因。这里返回了这个实现类,然后再重写的visitBody
和visitExpression
方法进行判断
好了,重新编译,发布插件,再看看此时的编译结构:
可以了,a=true,b=true已经没了,说明这两个变量声明指令已经不存在了(有兴趣可以查看字节码,更加清晰)。
优化赋值表达式的常量
什么意思呢,虽然a和b不存在了,但c和d还在啊,因为对于a+b+5这个表达式,我们已经知道a和b是常量了,那么a+b+5肯定也是常量,从编译结果c=9也可以进行说明。但是根据Ir树来看,这是个Call节点,会调用plus方法,因为不确定性,我们在Ir处理时并没有把这里当作常量(因为kotlin中赋值表达式有很多种,比如when、if),但对于这个算数运算来说,如果参数都是常量,那么运算的结果肯定是可预见的。
所以针对所有(能想到的)的运算,我找到其对应的方法名,并在visitFunctionAccess
中进行判断。
只需要判断上面那部分就行
而其处理的每一步,其实就是判断参数类型,并调用对应的函数进行计算即可,比如plusOrDefault中
代码冗长,但判断必不可少。
做完这一步,再看看结果:
当然就是文章开头的“转换后”的效果了。
其实这一步相当于后续的编译优化中的“常量折叠”这一步我们手动处理了。
结尾
到此,文章结束了,整个项目的过程确实有一些学习和思考的过程,对自身有很大帮助。当然,希望耐心看完的各位也能有所收获。
也希望大家参与讨论,因为KCP还不太熟,里面有些api调用不一定是对的,对于visitXXX方法的重写,是否有待完善,最后自行处理的运算符调用可不可以进行改造下(真的太长了)。
项目地址KCP项目,在kotlin中,将val声明的局部变量,转换成常量。
参考/学习链接
- 使用 Kotlin 元编程技术提升开发效率_哔哩哔哩_bilibili
- 【Kotlin Compiler】IR Transform Plugin 教程 - 掘金 (juejin.cn)
- 深入浅出 Compose Compiler(1) Kotlin Compiler & KCP - 掘金 (juejin.cn)
- 使用 KCP 打造更安全的 Gson 与更快的 Moshi - 掘金 (juejin.cn)
如有侵权请联系站点删除!
技术合作服务热线,欢迎来电咨询!