Presto-Code Generation

一、背景

1.1 场景

presto中使用了基于ASM的airlift.bytecode进行代码生成,一个主要的用途是对从数据源捞上来的数据进行表达式过滤,这是代码生成的主要应用场景,主要是为了降低进行表达式评估

中 JVM 的各种开销,如虚函数调用,分支预测,原始类型的对象装箱开销以及内存消耗。

1.2 字节码

Java编译器编译好Java文件后,产生.class文件存放在磁盘中。这种.class文件是二进制文件,内容是只有JVM虚拟机能够识别的机器码。JVM虚拟机读取字节码文件,取出二进制数据,加

载到内存中,解析.class文件内的信息,生成对应的Class对象。


class字节码文件是根据JVM虚拟机规范中规定的字节码组织规则生成的,具体的class文件可以去参考Java虚拟机规范。

    基于Java的字节码规范,我们可以实现例如程序分析、生成以及转换技术手段,可以应用在以下场景:

  1. 程序分析:从简单的语法解析到完整的语义分析,也可用来发现程序中潜在的bug,检测未使用的代码,以及反向工程等。

  2. 帮助编译器生成代码,包括传统的编译器,用在分布式编程中的内嵌的编译器,以及即时编译器等。

  3. 程序转换可以用来优化程序或者对程序进行更改,或者在应用中插入调试代码或者性能监控代码,面向切面编程等。

目前对字节码进行操作的类库有很多,ASM、Javassist、airlift.bytecode等等。由于资料不全,这里暂时只针对ASM进行简单介绍,但它们的底层原理都是相同的。Spark在闭包序列化时

也使用了ASM对闭包进行前期的清理和校验操作。

1.2.1 ASM

ASM是一个Java字节码操作框架,它能够以二进制形式修改已有类或者动态生成类。ASM可以直接产生二进制class文件,也可以在类被加载入Java虚拟机之前动态改变类行为。ASM从

类文件中读入信息后,能够改变类行为、分析类信息,甚至能够根据用户要求生成新类。

在这里我们只列出了ASM的一些核心API,通过核心API的调用,可以实现分析类型西、改变类行为、生成新类等操作,具体ASM实现原理之后将具体单独讲解。

1.2.1.1 核心类

类名

类型

说明

依赖关系

ClassVisitor

abstract

类中具体信息的访问(方法、字段、内部类等等)

 

ClassReader

 

解析编译过的class的字节数组,然后调用ClassVisitor实例的visitXXX方法,其中

ClassVisitor实例作为ClassReader.accept方法的参数传递进去。ClassReader可以

被看作是一个事件产生者

 

ClassWriter

 

是ClassVisitor的一个实现,用来以二进制方式构建编译后的类。它产生一个包含编译后的类的字节数组。

可以通过它的toByteArray方法来获得。它可以被看做是一个事件消费者

实现ClassVisitor

ClassAdapter

 

也是ClassVisitor的一个实现,它将对它的方法调用委托给另一个ClassVisitor。可以被认为是一个事件过滤器


1.2.1.2 工具类

类名

类型

说明

依赖关系

类名

类型

说明

依赖关系

TraceClassVisitor

final

跟踪代码生成,构造解析过的类的文本展示

实现ClassVisitor

CheckClassAdapter

 

检查类方法调用顺序,以及参数是否合理

实现ClassVisitor

ASMifierClassVisitor

 

 

 

1.2.1.3 接口和组件访问类

类名

类型

说明

依赖关系

MethodVisitor

abstract

关于方法的生成和转换

由ClassVisitor中的visitMethod方法返回

MethodAdapter

 

方法的转换和修改

实现MethodVisitor

1.2.2 airlift.bytecode

airlift.bytecode是一个基于ASM的,用于生成Java字节码的Java类库。ASM提供了对字节码的底层操作,但当用户需要从无到有来构造一个类时,需要进行的操作较多,且复杂。

airlift.bytecode基于ASM,将Java类中的组成对象进行了抽象,提供了简易的字节码构建功能。

1.2.2.1 BytecodeNode

BytecodeNode是airlift.bytecode中的一个最底层的抽象接口,用来描述java操作的基础。BytecodeNode具有两个实现接口FlowControl和InstructionNode。

1.2.2.2 FlowControl

FlowControl接口对应Java中的流程控制语句,具有6个实现类。



FlowControl只定义了一个方法getComment,获取注释。

1.2.2.3 InstructionNode

InstructionNode指令节点有三个抽象实现类Constant、FieldInstruction、VariableInstruction以及6个直接实现类:Comment、InvokeInstruction、JumpInstruction、LabelNode、OpCode、TypeInstruction。具体功能都比较简单,这里就不再一一描述了。其中InvokeInstruction提供了对方法的调用操作。

Constant对应常量定义

FieldInstruction对应对field的操作,只有get和put两种实现类。

VariableInstruction对应对变量的改动操作,例如自增等等。

1.2.2.4 BytecodeBlock

BytecodeBlock也是BytecodeNode的一个实现类,但是它们的作用有很大区别。BytecodeBlock中存放了一个BytecodeNode的列表,并且提供了很多方法,这些方法是用来将独立的

BytecodeNode组合成一个带有执行顺序的代码块的。bytecode中的方法定义MethodDefinition中的body就是一个BytecodeBlock。

例如在Presto的CursorProcessorCompiler中,通过BytecodeBlock提供的链式操作可以构建一个全新的method的body。链式构建顺序即method的执行顺序。



1.2.2.5 MethodDefinition

MethodDefinition是airlift.bytecode对java方法的抽象,每个MethodDefinition具有一个BytecodeBlock,即它的内部执行逻辑,以及一些入参出参等成员,一个MethodDefinition必须和

一个ClassDefinition绑定。方法不能独立于类单独存在。method可以通过InvokeInstruction被触发执行。

1.2.2.6 ClassDefinition

ClassDefinition是airlift.bytecode对类的抽象,内容较少。


二、Presto应用

2.1 关键类解析

2.1.1 Compiler相关类

类型

类名

功能

引用类

生成的方法

备注

配置/工具类

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
isPositionNull

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



2.1.2 BytecodeGenerator


2.2 代码生成样例解析

下面,我们通过ScanFilterAndProjectOperator算子中对数据操作过程的代码生成样例来窥探代码生成流程。

首先,ScanFilterAndProjectOperator其中一个分支对数据的处理是通过CursorProcessor.process来完成。

2.2.1 CursorProcessor的执行

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方法的整体处理流程如下:

  1. 获取节点的输入类型和输出类型。其中在获取输入类型是,需要判断该节点的下级节点sourceNode,若sourceNode类型为TableScanNode,则直接从TableScanNode的输出Symbol集合中获取本节点的输入类型,否则直接从sourceNode的layout信息中获取。输出类型则不区分sourceNode的类型,统一从节点自身的outputSymbol中获取

  2. 由于compiler的入参不是Symbol而是Optional<RowExpression>,我们需要先进行格式转换,以满足compiler的参数格式。主要是将输出Symbol转换为ProjectExpression,结合已有的FilterExpression传递给compiler。

  3. 若下级节点sourceNode类型为TableScan,且scan后的column不为空,则会同时编译生成CursorProcessor和PageProcessor,用这两个Processor来构建一个ScanFilterAndProjectOperatorFactory并封装到PhysicalOperation中返回。否则只会生成PageProcessor,并构建一个FilterAndProjectOperatorFactory封装到PhysicalOperation中返回。

在这个节点的visit函数中,Processor的构建都是在ExpressionCompiler中完成的,ExpressionCompiler提供了两个入口方法compileCursorProcessor和compilePageProcessor。

2.2.2 ExpressionCompiler编译生成CursorProcessor类

从2.2.1章节中我们了解到CursorProcessor在ScanFilterAndProjectOperator物理算子中对数据进行真正的执行,且它的初始化过程是在LocalExecutionPlanner.Visitor内部类中的

visitScanFilterAndProject方法中进行编译生成的,且编译时的入参是filter和project的Expression。实际编译动作在ExpressionCompiler类中的compileCursorProcessor和compilePageProcessor方法中进行。本章我们主要针对compileCursorProcessor方法进行解析。

首先我们来看一下ExpressionCompiler的成员变量和构造函数。ExpressionCompiler拥有一个LoadingCache<CacheKey, Class<? extends CursorProcessor>>的成员变量,且在构造

函数中定义了这个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。

2.2.3 CursorProcessorCompiler编译生成CursorProcessor类中的方法

CursorProcessorCompiler专门负责为动态变异的CursorProcessor类来生成字节码body,即方法。CursorProcessorCompiler对外只提供了generateMethods方法,为了实现具体的方法,又新建了几个private 方法:

  1. generateProcessMethod:生成"process"方法,用来处理数据

  2. createProjectIfStatement:生成project方法中的if语句

  3. generateMethodsForLambdaAndTry:生成lambda表达式方法

  4. generateFilterMethod:生成"filter"方法

  5. generateProjectMethod:生成一系列"project"方法

  6. fieldReferenceCompiler

它的整体执行过程如下:

  1. 调用generateProcessMethod方法,生成"process"方法,用来处理数据

  2. 生成有filter前缀的过滤lambda方法

  3. 根据lamdba方法生成filter方法

  4. 遍历project表达式,生成多个project前缀方法,后缀为计数

  5. 声明构造函数

下面,我们针对每个步骤进行详细的解析

2.2.3.1 generateProcessMethod

generateProcessMethod方法的入参比较简单,只包含原始的类型一ClassDefinition和project表达式的数量,不涉及具体的表达式内容。

generateProcessMethod的步骤主要分为以下几个步骤

  1. 声明参数类型,ConnectorSession、DriverYieldSignal、RecordCursor、PageBuilder

  2. 声明方法,使用上面的参数类型,声明方法名为method,限定符为Public,返回类型为CursorProcessorOutput

  3. 在方法作用于中声明局部变量completedPositions: int和finished: boolean

  4. 变量初始化,调用MethodDefinition.putVariable方法,将completedPositions初始化为0,finished初始化为false

  5. 构建方法中的循环体WhileLoop

    1. 构建第一个if语句if (pageBuilder.isFull() || yieldSignal.isSet()) return new CursorProcessorOutput(completedPositions, false);

    2. 构建第二个if语句if (!cursor.advanceNextPosition()) return new CursorProcessorOutput(completedPositions, true);

    3. 构建不满足前面两个if条件下的执行操作,即执行projection,调用CursorProcessorCompiler.createProjectIfStatement

  6. 执行完ProjectIfStatement后,completedPositionsVariable加1

  7. 组装method

其中,createProjectIfStatement方法中调用了还未声明,但接下来即将声明的方法filter、project_x。虽然createProjectIfStatement看起来是一个条件执行语句if,但是实际上if的

condition都为空或者恒等于true,也就是这个方法等于实际上的顺序调用。

  1. 直接调用filter方法

  2. 获取block位置

  3. 调用project方法

即,process为数据的实际执行过程,实际执行时是先对整体数据进行filter,再依次进行投影。

2.2.3.2 generateMethodsForLambdaAndTry

在定义好process方法后,调用generateMethodsForLambdaAndTry将filter中的lambda表达式提取出来,构建为一个PreGeneratedExpressions。

过程略

2.2.3.3 generateFilterMethod

generateFilterMethod方法生成了"filter"方法,它主要是依赖于RowExpressionCompiler来生成作用于行的表达式,包括and,or以及上一步生成的lambda表达式。

RowExpressionCompiler接收将cursor包装为filedReferenceCompiler作为参数,对Expression中的每个节点进行遍历,最终返回一个BytecodeNode作为方法的实际内容。

2.2.3.4 generateProjectMethod

和filter的处理方式一致,只不过filter是一个整体expression,但每个column上的函数可能不一致,例如有些列可能在做投影时加上coalesce函数,因此project需要根据column个数生成多个方法并在process方法中循环调用。

2.2.3.5 declareConstructor

构造函数中没有特殊的逻辑,只是将它父类的构造函数传递进来了。因为CursorProcessor和Object是当前构造类的父类。


三、总结

airlift.bytecode对ASM的封装比较完整,整体操作较简单。Presto的代码生成中复杂的还是Presto内部定义的一些专用对象,理解Presto的代码生成,必须先将Presto内部的一些对象功

能理解清楚才能正确理解到Presto每一步操作的用意,例如RecordCursor、PageBuilder、BlockBuilder等等。



评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值