通过KCP实现无为转变

通过KCP实现无为转变

技术博客 admin 177 浏览

根据之前的文章刨根问底:为什么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所需的关键接口和抽象类:

  1. CommandLineProcessor:KCP的入口,可以解析并传递参数。gradle插件中的自定义参数,可以通过KotlinCompilerPluginSupportPlugin传递到此。
  2. CompilerPluginRegistrar:注册各种Extension,并且上一步获取到的参数,可以传递到此。
  3. IrGenerationExtension:有很多种Extension,我们这里使用IrGenerationExtension,可以用来更新Ir节点,从而改变编译产物。
  4. IrElementTransformerVoidWithContext:每个Ir节点都是一个IrElement接口,其中有个transform方法,可以进行转换。

注意在CommandLineProcessorCompilerPluginRegistrar的实现类中,使用了@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树时,我发现:

  1. 在变量声明的过程中有“val”和“var”的标识
  2. 变量有初始化赋值
  3. 变量赋值时有常量赋值也有表达式赋值

那么基于这三点,只要判断变量声明的关键字是“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,所以,在实现类中,我们主要重写visitBodyvisitExpression,并且判断参数是否是IrStatementContainer,即可拿到其中的statements了。

接下去就是该考虑,怎么判断statements列表中哪些语句该删除,哪些语句不该删除。

其实根据“保存常量”这一步,我们已经知道了,哪些变量我们已经判断为是“常量”了,那么我们在visitVariable中打上一个标识,或者返回一个自定义类型表示需要删除的语句可以吗?当然可以,statementsIrStatement类型的列表,而visitVariable返回值需要是IrStatement类型,那不就简单了,我们自定义一个类,实现IrStatement就行了。

在这个实现类中,我们什么都不处理,因为肯定要被删除的。

对前一步进行改造下

这也是为什么checkAndGetConst方法我要返回布尔值的原因。这里返回了这个实现类,然后再重写的visitBodyvisitExpression方法进行判断

好了,重新编译,发布插件,再看看此时的编译结构:

可以了,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声明的局部变量,转换成常量。

参考/学习链接

源文:通过KCP实现无为转变

如有侵权请联系站点删除!

技术合作服务热线,欢迎来电咨询!

0.313507s