presto中使用了基于ASM的airlift.bytecode进行代码生成,一个主要的用途是对从数据源捞上来的数据进行表达式过滤,这是代码生成的主要应用场景,主要是为了降低进行表达式评估
中 JVM 的各种开销,如虚函数调用,分支预测,原始类型的对象装箱开销以及内存消耗。
Java编译器编译好Java文件后,产生.class文件存放在磁盘中。这种.class文件是二进制文件,内容是只有JVM虚拟机能够识别的机器码。JVM虚拟机读取字节码文件,取出二进制数据,加
载到内存中,解析.class文件内的信息,生成对应的Class对象。
class字节码文件是根据JVM虚拟机规范中规定的字节码组织规则生成的,具体的class文件可以去参考Java虚拟机规范。
基于Java的字节码规范,我们可以实现例如程序分析、生成以及转换技术手段,可以应用在以下场景:
程序分析:从简单的语法解析到完整的语义分析,也可用来发现程序中潜在的bug,检测未使用的代码,以及反向工程等。
帮助编译器生成代码,包括传统的编译器,用在分布式编程中的内嵌的编译器,以及即时编译器等。
程序转换可以用来优化程序或者对程序进行更改,或者在应用中插入调试代码或者性能监控代码,面向切面编程等。
目前对字节码进行操作的类库有很多,ASM、Javassist、airlift.bytecode等等。由于资料不全,这里暂时只针对ASM进行简单介绍,但它们的底层原理都是相同的。Spark在闭包序列化时
也使用了ASM对闭包进行前期的清理和校验操作。
ASM是一个Java字节码操作框架,它能够以二进制形式修改已有类或者动态生成类。ASM可以直接产生二进制class文件,也可以在类被加载入Java虚拟机之前动态改变类行为。ASM从
类文件中读入信息后,能够改变类行为、分析类信息,甚至能够根据用户要求生成新类。
在这里我们只列出了ASM的一些核心API,通过核心API的调用,可以实现分析类型西、改变类行为、生成新类等操作,具体ASM实现原理之后将具体单独讲解。
类名 |
类型 |
说明 |
依赖关系 |
---|---|---|---|
ClassVisitor |
abstract |
类中具体信息的访问(方法、字段、内部类等等) |
|
ClassReader |
|
解析编译过的class的字节数组,然后调用ClassVisitor实例的visitXXX方法,其中 ClassVisitor实例作为ClassReader.accept方法的参数传递进去。ClassReader可以 被看作是一个事件产生者 |
|
ClassWriter |
|
是ClassVisitor的一个实现,用来以二进制方式构建编译后的类。它产生一个包含编译后的类的字节数组。 可以通过它的toByteArray方法来获得。它可以被看做是一个事件消费者 |
实现ClassVisitor |
ClassAdapter |
|
也是ClassVisitor的一个实现,它将对它的方法调用委托给另一个ClassVisitor。可以被认为是一个事件过滤器 |
类名 |
类型 |
说明 |
依赖关系 |
---|---|---|---|
类名 |
类型 |
说明 |
依赖关系 |
TraceClassVisitor |
final |
跟踪代码生成,构造解析过的类的文本展示 |
实现ClassVisitor |
CheckClassAdapter |
|
检查类方法调用顺序,以及参数是否合理 |
实现ClassVisitor |
ASMifierClassVisitor |
|
|
|
类名 |
类型 |
说明 |
依赖关系 |
---|---|---|---|
MethodVisitor |
abstract |
关于方法的生成和转换 |
由ClassVisitor中的visitMethod方法返回 |
MethodAdapter |
|
方法的转换和修改 |
实现MethodVisitor |
airlift.bytecode是一个基于ASM的,用于生成Java字节码的Java类库。ASM提供了对字节码的底层操作,但当用户需要从无到有来构造一个类时,需要进行的操作较多,且复杂。
airlift.bytecode基于ASM,将Java类中的组成对象进行了抽象,提供了简易的字节码构建功能。
BytecodeNode是airlift.bytecode中的一个最底层的抽象接口,用来描述java操作的基础。BytecodeNode具有两个实现接口FlowControl和InstructionNode。
FlowControl接口对应Java中的流程控制语句,具有6个实现类。
FlowControl只定义了一个方法getComment,获取注释。
InstructionNode指令节点有三个抽象实现类Constant、FieldInstruction、VariableInstruction以及6个直接实现类:Comment、InvokeInstruction、JumpInstruction、LabelNode、OpCode、TypeInstruction。具体功能都比较简单,这里就不再一一描述了。其中InvokeInstruction提供了对方法的调用操作。
Constant对应常量定义
FieldInstruction对应对field的操作,只有get和put两种实现类。
VariableInstruction对应对变量的改动操作,例如自增等等。
BytecodeBlock也是BytecodeNode的一个实现类,但是它们的作用有很大区别。BytecodeBlock中存放了一个BytecodeNode的列表,并且提供了很多方法,这些方法是用来将独立的
BytecodeNode组合成一个带有执行顺序的代码块的。bytecode中的方法定义MethodDefinition中的body就是一个BytecodeBlock。
例如在Presto的CursorProcessorCompiler中,通过BytecodeBlock提供的链式操作可以构建一个全新的method的body。链式构建顺序即method的执行顺序。
MethodDefinition是airlift.bytecode对java方法的抽象,每个MethodDefinition具有一个BytecodeBlock,即它的内部执行逻辑,以及一些入参出参等成员,一个MethodDefinition必须和
一个ClassDefinition绑定。方法不能独立于类单独存在。method可以通过InvokeInstruction被触发执行。
ClassDefinition是airlift.bytecode对类的抽象,内容较少。
类型 |
类名 |
功能 |
引用类 |
生成的方法 |
备注 |
---|---|---|---|---|---|
配置/工具类 |
CompilerConfig |
/ |
|||
CompilerOperations |
提供了简单的逻辑操作 如 and or 等函数 |
/ |
|||
CompilerUtils |
工具类,提供了类名生成功能 和创建类的功能defineClass |
/ |
|||
字节码body生成器 (Method) |
BodyCompiler |
接口 为project和filter提供method生成 由于较复杂,单独抽出一个接口 |
ExpressionCompiler (BodyCompiler是一个接口) |
/ |
|
CursorProcessorCompiler |
BodyCompiler的唯一实现类 |
ExpressionCompiler |
project_i (多个) process filter |
||
完整的类生成器 (Class) |
AccumulatorCompiler |
生成累加器的字节码 也生成一些字节码块(类中的方法) |
注:调用的都是generateAccumulatorFactoryBinder方法 调用方都是SqlAggregationFunction的实现类 AbstractMinMaxAggregationFunction AbstractMinMaxNAggregationFunction ArbitraryAggregationFunction ChecksumAggregationFunction CountColumn DecimalAverageAggregation DecimalSumAggregation LazyAccumulatorFactoryBinder MapAggregationFunction MapUnionAggregation MultimapAggregationFunction ArrayAggregationFunction Histogram AbstractMinMaxBy AbstractMinMaxByNAggregationFunction |
getIntermediateType getFinalType getEstimatedSize addInput addIntermediate evaluateIntermediate evaluateFinal prepareFinal |
AccumulatorFactoryBinder 是什么??? |
ExpressionCompiler |
调用CursorProcessorCompiler 同时自己也单独定义方法 |
LocalExecutionPlanner.visitScanFilterAndProject |
toString 下面为调用CursorProcessorCompiler生成的方法 project_i (多个) process filter |
||
InputReferenceCompiler |
只生成字节码body 字段应用代码块 被RowExpressionCompiler调用 对外提供visitInputReference方法 返回值为BytecodeNode |
RowExpressionCompiler PageFunctionCompiler |
|||
JoinCompiler |
getChannelCount getSizeInBytes appendTo hashPosition hashRow rowEqualsRow positionEqualsRowIgnoreNulls positionEqualsRow positionNotDistinctFromRow positionEqualsPositionIgnoreNulls positionEqualsPosition compareSortChannelPositions isSortChannelPositionNull |
||||
JoinFilterFunctionCompiler |
LocalExecutionPlanner.compileJoinFilterFunction |
toString filter |
|||
OrderingCompiler |
用于对比Page对象 |
PagesIndex |
compareTo |
||
PageFunctionCompiler |
提供Page相关的操作 |
ExpressionCompiler LocalExecutionPlanner |
getResult process evaluate isDeterministic getInputChannels toString filter |
||
RowExpressionCompiler |
不构建类和方法,只生成ByteCode |
BytecodeGeneratorContext |
|||
StateCompiler |
返回数组类,构建序列化类 |
getSerializedType deserialize serialize createSingleState createGroupedState getSingleStateClass getGroupedStateClass getEstimatedSize ensureCapacity getEstimatedSize |
下面,我们通过ScanFilterAndProjectOperator算子中对数据操作过程的代码生成样例来窥探代码生成流程。
首先,ScanFilterAndProjectOperator其中一个分支对数据的处理是通过CursorProcessor.process来完成。
CursorProcessor是一个没有实现类的接口,它的实现类都是由airlift.bytecode动态构建生成的字节码。CursorProcessor是一个比较独立的代码生成结果,它只在
ScanFilterAndProjectOperator中被引用。我们以它为例来窥探代码生成的过程和执行过程。
在ScanFilterAndProjectOperator的getOutput方法中,若pageSource为空,则会转换到processColumnSource方法中。在processColumnSource方法中,会调用CursorProcessor的
process方法来对record进行处理。可以认为CursorProcessor是实际对数据的循环处理类,但由于CursorProcessor是一个没有实现类的接口,首先我们需要搞清楚它的构建过程。
ScanFilterAndProjectOperator的创建是由它的内部工厂类ScanFilterAndProjectOperatorFactory.createOperator创建的,CursorProcessor是工厂类的成员,传递给了创建出的
Operator实例,而ScanFilterAndProjectOperatorFactory是在LocalExecutionPlanner对物理计划节点进行遍历时产生的,ScanFilterAndProjectOperatorFactory即为物理执行计划的工厂类。
LocalExecutionPlanner中的内部类Visitor针对物理执行计划的节点类型实现了不同的visit方法,在遇到FilterNode或是ProjectNode(Presto后续可能会将这两个物理执行计划节点合并为
一个节点)时会调用visitScanFilterAndProject方法。
visitScanFilterAndProject方法的整体处理流程如下:
获取节点的输入类型和输出类型。其中在获取输入类型是,需要判断该节点的下级节点sourceNode,若sourceNode类型为TableScanNode,则直接从TableScanNode的输出Symbol集合中获取本节点的输入类型,否则直接从sourceNode的layout信息中获取。输出类型则不区分sourceNode的类型,统一从节点自身的outputSymbol中获取
由于compiler的入参不是Symbol而是Optional
若下级节点sourceNode类型为TableScan,且scan后的column不为空,则会同时编译生成CursorProcessor和PageProcessor,用这两个Processor来构建一个ScanFilterAndProjectOperatorFactory并封装到PhysicalOperation中返回。否则只会生成PageProcessor,并构建一个FilterAndProjectOperatorFactory封装到PhysicalOperation中返回。
在这个节点的visit函数中,Processor的构建都是在ExpressionCompiler中完成的,ExpressionCompiler提供了两个入口方法compileCursorProcessor和compilePageProcessor。
从2.2.1章节中我们了解到CursorProcessor在ScanFilterAndProjectOperator物理算子中对数据进行真正的执行,且它的初始化过程是在LocalExecutionPlanner.Visitor内部类中的
visitScanFilterAndProject方法中进行编译生成的,且编译时的入参是filter和project的Expression。实际编译动作在ExpressionCompiler类中的compileCursorProcessor和compilePageProcessor方法中进行。本章我们主要针对compileCursorProcessor方法进行解析。
首先我们来看一下ExpressionCompiler的成员变量和构造函数。ExpressionCompiler拥有一个LoadingCache
函数中定义了这个LoadingCache的CacheLoader。
即ExpressionCompiler在内存中对CursorProcessor进行缓存,且当有调用者试图从缓存中获取一个CacheKey对应的CursorProcessor,它会先检查是否存在,若不存在,则使用
CacheLoader中定义的Supplier根据传入的CacheKey进行初始化。且初始化的时候针对CacheKey中的内容调用了它自身的compile方法。
上文提到的,实际编译方法compileCursorProcessor中其实就调用了这个LoadingCache中的getUnchecked(即当CacheLoader没有处理抛出异常时的获取缓存数据的方法)
也就是说,当LocalExecutionPlanner试图调用ExpressionCompiler的compileCursorProcessor方法来编译一个新的CursorProcessor时,它实际调用了ExpressionCompiler的compile方
法,根据compile方法的实际调用链,CursorProcessor的构建方法实际是在compileProcessor方法中完成的。
compileProcessor的入参为已经经过类型转换的过滤表达式filter,以及投影表达式projections,一个用来构建类中方法的服务类BodyCompiler,以及一个在LoadingCache中写死的父类
CursorProcessor。注意,这里也就说明了为什么CursorProcessor在源码中是一个没有实现类的接口,但是在实际数据调用是却调用了这个接口中的方法。因为这个接口的实现类是根据查询语句动态构建出来的。
compileProcessor的构建流程在它自身中看起来比较简单,首先,它会调用airlift.bytecode中的ClassDefinition来创建一个新的类,类名使用makeClassName方法生成了一个带有时间戳
后缀的CursorPorcessor类,并定义了它的父类Object和CursorProcessor。其次,compileProcessor会调用BodyCompiler来生成这个类中的具体字节码内容,主要是类的各种方法,由于这里的方法构建逻辑较为复杂,直接抽出了一个独立的服务类BodyCompiler。BodyCompiler是一个接口,且只有一个唯一的实现类CursorProcessorCompiler。(猜测Presto是期望把所有字节码body都用BodyCompiler的实现类来实现,但实际开发中并没有达成???可能是其他类的方法比较简单???)。最后,生成了一个toString的方法,便于调试。从compileProcessor的处理流程我们可以发现,主要的代码生成集中在类中的method的生成。即CursorProcessorCompiler.generateMethods。
CursorProcessorCompiler专门负责为动态变异的CursorProcessor类来生成字节码body,即方法。CursorProcessorCompiler对外只提供了generateMethods方法,为了实现具体的方法,又新建了几个private 方法:
generateProcessMethod:生成"process"方法,用来处理数据
createProjectIfStatement:生成project方法中的if语句
generateMethodsForLambdaAndTry:生成lambda表达式方法
generateFilterMethod:生成"filter"方法
generateProjectMethod:生成一系列"project"方法
fieldReferenceCompiler
它的整体执行过程如下:
调用generateProcessMethod方法,生成"process"方法,用来处理数据
生成有filter前缀的过滤lambda方法
根据lamdba方法生成filter方法
遍历project表达式,生成多个project前缀方法,后缀为计数
声明构造函数
下面,我们针对每个步骤进行详细的解析
generateProcessMethod方法的入参比较简单,只包含原始的类型一ClassDefinition和project表达式的数量,不涉及具体的表达式内容。
generateProcessMethod的步骤主要分为以下几个步骤
声明参数类型,ConnectorSession、DriverYieldSignal、RecordCursor、PageBuilder
声明方法,使用上面的参数类型,声明方法名为method,限定符为Public,返回类型为CursorProcessorOutput
在方法作用于中声明局部变量completedPositions: int和finished: boolean
变量初始化,调用MethodDefinition.putVariable方法,将completedPositions初始化为0,finished初始化为false
构建方法中的循环体WhileLoop
构建第一个if语句if (pageBuilder.isFull() || yieldSignal.isSet()) return new CursorProcessorOutput(completedPositions, false);
构建第二个if语句if (!cursor.advanceNextPosition()) return new CursorProcessorOutput(completedPositions, true);
构建不满足前面两个if条件下的执行操作,即执行projection,调用CursorProcessorCompiler.createProjectIfStatement
执行完ProjectIfStatement后,completedPositionsVariable加1
组装method
其中,createProjectIfStatement方法中调用了还未声明,但接下来即将声明的方法filter、project_x。虽然createProjectIfStatement看起来是一个条件执行语句if,但是实际上if的
condition都为空或者恒等于true,也就是这个方法等于实际上的顺序调用。
直接调用filter方法
获取block位置
调用project方法
即,process为数据的实际执行过程,实际执行时是先对整体数据进行filter,再依次进行投影。
在定义好process方法后,调用generateMethodsForLambdaAndTry将filter中的lambda表达式提取出来,构建为一个PreGeneratedExpressions。
过程略
generateFilterMethod方法生成了"filter"方法,它主要是依赖于RowExpressionCompiler来生成作用于行的表达式,包括and,or以及上一步生成的lambda表达式。
RowExpressionCompiler接收将cursor包装为filedReferenceCompiler作为参数,对Expression中的每个节点进行遍历,最终返回一个BytecodeNode作为方法的实际内容。
和filter的处理方式一致,只不过filter是一个整体expression,但每个column上的函数可能不一致,例如有些列可能在做投影时加上coalesce函数,因此project需要根据column个数生成多个方法并在process方法中循环调用。
构造函数中没有特殊的逻辑,只是将它父类的构造函数传递进来了。因为CursorProcessor和Object是当前构造类的父类。
airlift.bytecode对ASM的封装比较完整,整体操作较简单。Presto的代码生成中复杂的还是Presto内部定义的一些专用对象,理解Presto的代码生成,必须先将Presto内部的一些对象功
能理解清楚才能正确理解到Presto每一步操作的用意,例如RecordCursor、PageBuilder、BlockBuilder等等。