<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Presto 原理 | ASM 與 Presto 動態(tài)代碼生成簡介

          共 11735字,需瀏覽 24分鐘

           ·

          2021-10-01 02:11

          代碼生成是很多計算引擎中常用的執(zhí)行優(yōu)化技術(shù),比如我們熟悉的 Apache Spark 和 Presto 在表達(dá)式等地方就使用到代碼生成技術(shù)。這兩個計算引擎雖然都用到了代碼生成技術(shù),但是實現(xiàn)方式完全不一樣。在 Spark 中,代碼生成其實就是在 SQL 運(yùn)行的時候根據(jù)相關(guān)算子動態(tài)拼接 Java 代碼,然后使用 Janino 來動態(tài)編譯生成相關(guān)的 Java 字節(jié)碼并加載到相關(guān) classLoader 中,這個動態(tài)編譯會帶來一定的使用成本,不過這個對于 Spark 來說,這個開銷是完全可以承受住的,更多關(guān)于 Apache Spark 的代碼生成技術(shù)可以參見 《一條 SQL 在 Apache Spark 之旅(下)》。

          而 Presto 是定位于 OLAP 的,需要快速的把執(zhí)行結(jié)果返回給客戶端,所以每條 SQL 的執(zhí)行時間比較短(比如秒級別),而如果采用 Spark 的方式,那么代碼編譯的時間可能會影響到 Presto 的整個查詢時間;所以 Presto 使用 ASM 直接生成 Java 字節(jié)碼的方式來達(dá)到代碼生成的目的。

          Java 代碼和 Java 字節(jié)碼

          Java 代碼其實就是我們平時在 IDE 中開發(fā)的一堆 java 文件,這些 Java 文件是需要經(jīng)過編譯成 class 文件才能被 Java 虛擬機(jī)執(zhí)行,而這些編譯后的 class 文件其實就是這里說的 Java 字節(jié)碼。Java 代碼和 Java 字節(jié)碼還有以下的區(qū)別:

          ?編譯后的一個 Java 字節(jié)碼文件只描述一個類,而一個 Java 文件里面可以包含多個類。例如,具有一個內(nèi)部類的類源文件被編譯為兩個 Java 字節(jié)碼文件:一個用于主類,另一個用于內(nèi)部類。主類文件包含對其內(nèi)部類的引用,而在方法內(nèi)部定義的內(nèi)部類包含對其外圍方法的引用。?編譯后的 Java 字節(jié)碼文件不包含注釋,但可以包含類、字段、方法和代碼屬性。?編譯后的 Java 字節(jié)碼文件不包含 package 和 import 部分,因此所有類名必須是帶包名的。比如 String 編譯成字節(jié)碼后需要用 com.lang.String 表示。

          Java 字節(jié)碼(Java bytecode)是 Java 虛擬機(jī)執(zhí)行的一種指令格式,每個使用 javac 編譯后的 class 文件是遵循 Java Virtual Machine 相關(guān)規(guī)范的,具體可以參見 The class File Format 了解詳情。

          另外,Java 字節(jié)碼的計算模型是面向堆棧結(jié)構(gòu)計算機(jī)的,其和匯編有點類似,比如如果我們實現(xiàn)兩個數(shù)值相加的話,匯編實現(xiàn)如下:

          mov eax, byte [ebp-4]mov edx, byte [ebp-8]add eax, edxmov ecx, eax

          而如果用 Java 實現(xiàn)的話可以如下:

          public int add(int a, int b) {    return a + b;}

          使用 javac 編譯成 Java 字節(jié)碼后我們使用 javap 查看生成的字節(jié)碼如下:

          public int add(int, int);    descriptor: (II)I    flags: ACC_PUBLIC    Code:      stack=2, locals=3, args_size=3         0: iload_1         1: iload_2         2: iadd         3: ireturn      LineNumberTable:        line 9: 0      LocalVariableTable:        Start  Length  Slot  Name   Signature            0       4     0  this   Lcom/iteblog/Test;            0       4     1     a   I            0       4     2     b   I

          其中比較重要的是第6到第9行。iload_1、iload_2、iadd 和 ireturn 都是 Java 虛擬機(jī)支持的指令集(Instruction Set)。iload_1 是將 #1 個 int 型本地變量推送至棧頂;iload_2 是將 #2 個 int 型本地變量推送至棧頂;iadd 是將棧頂?shù)膬蓚€ int 值相加,并把結(jié)果壓入棧頂;ireturn 是返回當(dāng)前的 int 值。

          ASM 簡介

          ASM 是一個通用的 Java 字節(jié)碼操作和分析框架。它可以用來修改現(xiàn)有的類,或者直接以二進(jìn)制形式動態(tài)生成類。ASM 提供了一些常見的字節(jié)碼轉(zhuǎn)換和分析算法,可以基于它構(gòu)建定制的復(fù)雜轉(zhuǎn)換和代碼分析工具。ASM 的設(shè)計非常關(guān)注性能,因此它的設(shè)計和實現(xiàn)盡可能的小和快,它非常適合在動態(tài)系統(tǒng)中使用。

          ASM API 提供了兩種方式來操作 Java 字節(jié)碼:基于事件(event-based)和基于樹節(jié)點(tree-based)。

          Event-based API

          這個 API 很大程度上是基于 Visitor 模式,類似于處理 XML 文檔的 SAX 解析模型。它的核心由以下幾個部分組成:

          ?ClassReader :可以使用它來讀取 java class 文件,并將讀出來的字節(jié)碼存放到字節(jié)數(shù)組中,它的 accept 方法接受一個 ClassVisitor 實現(xiàn)類,并按照順序調(diào)用 ClassVisitor 中的方法;?ClassVisitor :可以對 ClassReader 讀取的 java class 文件進(jìn)行轉(zhuǎn)換(transform),這個過程會訪問類的成員信息;包括標(biāo)記在類上的注解、類的構(gòu)造方法、類的字段、類的方法、靜態(tài)代碼塊等;?ClassWriter :ClassWriter 是一個 ClassVisitor 的子類,和 ClassReader 類正好對應(yīng),其可以將 Java 字節(jié)碼輸出到 class 文件。

          在 ClassVisitor 中,我們擁有所有訪問器方法,我們將使用這些方法訪問給定 Java 類的不同組件(字段、方法等)。我們通過提供 ClassVisitor 的子類來實現(xiàn)給定類中的任何更改。由于需要確保輸出的 class 文件準(zhǔn)守 Java 虛擬機(jī)規(guī)范,這個類需要一個嚴(yán)格的順序來調(diào)用它的方法以生成正確的輸出?;谑录?API 中的 ClassVisitor 方法按以下順序調(diào)用:

          visitvisitSource?visitOuterClass?( visitAnnotation | visitAttribute )*( visitInnerClass | visitField | visitMethod )*visitEnd

          也就是說必須先訪問 visit 方法,接著是 visitSource(最多只有一個),接著是 visitOuterClass(最多只有一個),接著是 visitAnnotation 或者 visitAttribute,接著是 visitInnerClass、visitField 或 visitMethod,最后必須以 visitEnd 結(jié)尾。

          Tree-based API

          這個 API 是一個更加面向?qū)ο蟮?API,類似于處理 XML 文檔的 JAXB 模型。

          Event-based API 占用更少的系統(tǒng)資源。從內(nèi)存的角度看,Tree-based API 由于要把字節(jié)碼抽象成 tree,在內(nèi)存中會占用跟多的空間;不過 Event-based API 比 Tree-based API 更難用,每次只能操作一個指令,需要非常了解字節(jié)碼相關(guān)規(guī)范,寫起來要小心翼翼。

          使用 ASM 進(jìn)行代碼生成

          因為 Presto 里面使用的是 Event-based API ,所以下面只介紹使用 Event-based API 來進(jìn)行代碼生成。在介紹使用代碼生成之前,有必要簡單介紹一下 Java 類和編譯后的字節(jié)碼的一些區(qū)別。

          Java 類型與 class 文件內(nèi)部類型對應(yīng)關(guān)系

          通常我們在編寫 Java 代碼時定義變量類型的時候會使用 int、long、String 這些來表示,但是在 Java 字節(jié)碼里面可不是這么表示的。在 JVM 中對每一種類型都有與之相對應(yīng)的類型 描述符(Type descriptor),對應(yīng)關(guān)系如下:

          基本類型:

          ?'V' - void?'Z' - boolean?'C' - char?'B' - byte?'S' - short?'I' - int?'F' - float?'J' - long?'D' - double

          Class 類型:

          ?Lcom/iteblog/T2; - com.iteblog.T2?Ljava/io/ObjectOutput; - java.io.ObjectOutput?Ljava/lang/String; - String

          上面列表的左邊是 JVM Type 描述,右邊是 Java 里面的類型,也就是我們平時編程使用的。由于 ASM 是直接操縱字節(jié)碼的,所以會用到 JVM Type。另外,如果你不知道 Java 類型怎么轉(zhuǎn)換到 JVM Type 描述,那么可以使用 ASM 中 org.objectweb.asm.Type 類的 getDescriptor(final Class c) 方法來獲取,具體如下:

          String stringDesc = Type.getDescriptor(String.class);String intDesc = Type.getDescriptor(int.class);

          Java 方法聲明與 class 文件內(nèi)部聲明的對應(yīng)關(guān)系

          在 Java 字節(jié)碼文件中,方法的方法名和方法的描述都是存儲在 Constant pool 中的,且在兩個不同的單元里。因此,方法描述中不含有方法名,只含有參數(shù)類型和返回類型。Java 字節(jié)碼里面的方法描述符(method descriptor)和 Java 方法聲明的對應(yīng)關(guān)系:

          Method declaration in source fileMethod descriptor
          void m(int i, float f)(IF)V
          int m(Object o)(Ljava/lang/Object;)I
          int[] m(int i, String s))(ILjava/lang/String;)[I
          Object m(int[] i)([I)Ljava/lang/Object;

          從上面可知,方法描述符是一個類型描述符(type descriptors)列表,用于在單個字符串中描述方法的參數(shù)類型和返回類型。方法描述符用左括號開始,其次是每個參數(shù)的類型描述符,接著是一個右括號,接著是方法的返回類型的類型描述符,如果方法返回 void 則使用 V 表示。一旦你了解了 Java 字節(jié)碼中類型描述符和方法描述符的含義,你就可以很容易理解 Java 字節(jié)碼中對方法的描述。比如你看到 (I)I 方法描述符,你就知道這個函數(shù)接收一個 int 類型的參數(shù),并返回 int 類型的結(jié)果。

          JVM 中關(guān)于類、方法以及字段的訪問標(biāo)記

          在平常寫 Java 代碼的時候,我們會使用 public、private 等修飾符來設(shè)置類、方法以及字段的訪問情況。在 JVM 中每一種修飾符都有一個 flag 來表示(可以參見JVM 類的 Access flags、JVM 字段的 Access flags、JVM 方法的 Access flags),比如下面是類的 Access flags

          Flag NameValueInterpretation
          ACC_PUBLIC0x0001Declared public; may be accessed from outside its package.
          ACC_FINAL0x0010Declared final; no subclasses allowed.
          ACC_SUPER0x0020Treat superclass methods specially when invoked by the invokespecial instruction.
          ACC_INTERFACE0x0200Is an interface, not a class.
          ACC_ABSTRACT0x0400Declared abstract; must not be instantiated.
          ACC_SYNTHETIC0x1000Declared synthetic; not present in the source code.
          ACC_ANNOTATION0x2000Declared as an annotation type.
          ACC_ENUM0x4000Declared as an enum type.
          ACC_MODULE0x8000Is a module, not a class or interface.

          ASM 中的 org.objectweb.asm.Opcodes 類里面定義了這些 Access flags:

          int ACC_PUBLIC = 0x0001; // class, field, methodint ACC_PRIVATE = 0x0002; // class, field, methodint ACC_PROTECTED = 0x0004; // class, field, methodint ACC_STATIC = 0x0008; // field, methodint ACC_FINAL = 0x0010; // class, field, method, parameterint ACC_SUPER = 0x0020; // classint ACC_SYNCHRONIZED = 0x0020; // methodint ACC_OPEN = 0x0020; // moduleint ACC_TRANSITIVE = 0x0020; // module requiresint ACC_VOLATILE = 0x0040; // fieldint ACC_BRIDGE = 0x0040; // methodint ACC_STATIC_PHASE = 0x0040; // module requiresint ACC_VARARGS = 0x0080; // methodint ACC_TRANSIENT = 0x0080; // fieldint ACC_NATIVE = 0x0100; // methodint ACC_INTERFACE = 0x0200; // classint ACC_ABSTRACT = 0x0400; // class, methodint ACC_STRICT = 0x0800; // methodint ACC_SYNTHETIC = 0x1000; // class, field, method, parameter, module *int ACC_ANNOTATION = 0x2000; // classint ACC_ENUM = 0x4000; // class(?) field innerint ACC_MANDATED = 0x8000; // parameter, module, module *int ACC_MODULE = 0x8000; // class

          好了,前面已經(jīng)簡單介紹了一下 JVM 關(guān)于字節(jié)碼的一些規(guī)范以及 ASM 的一些基礎(chǔ)知識,現(xiàn)在我們使用 ASM 實現(xiàn)一個簡單的加法類。我們的 Java 代碼如下:

          package com.iteblog;/** * @author iteblog * @version 9/27/21 11:52 PM */public class Test {    public int add(int a, int b) {        return a + b;    }}

          邏輯很簡單,類名是 Test,包名是 com.iteblog,里面定義了一個 int add(int a, int b) 方法,那如果我們使用 asm 實現(xiàn)的話具體如下:

          public byte[] addByAsm() {    ClassWriter writer = new ClassWriter(0);    MethodVisitor mv;    writer.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, "com/iteblog/Test", null, "java/lang/Object", null);    writer.visitSource("Test.java", null);    mv = writer.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "add", "(II)I", null, null);    mv.visitCode();    Label l0 = new Label();    mv.visitLabel(l0);    mv.visitLineNumber(9, l0);    mv.visitVarInsn(Opcodes.ILOAD, 0);    mv.visitVarInsn(Opcodes.ILOAD, 1);    mv.visitInsn(Opcodes.IADD);    mv.visitInsn(Opcodes.IRETURN);    Label l1 = new Label();    mv.visitLabel(l1);    mv.visitLocalVariable("a", "I", null, l0, l1, 0);    mv.visitLocalVariable("b", "I", null, l0, l1, 1);    mv.visitMaxs(2, 2);    mv.visitEnd();    writer.visitEnd();    return writer.toByteArray();}

          因為我們是要生成 Java 字節(jié)碼,所以直接使用 ClassWriter 就可以。Opcodes.V1_8 代表的是 JVM 1.8,我們這里定義的類為 com/iteblog/Test,它的父類是 java/lang/Object,訪問修飾符是 Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER。然后使用 visitMethod 定義了一個方法,方法的修飾符是 Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC,方法名是 add,方法描述符是 (II)I,根據(jù)前面的知識就是 add 方法輸入有兩個參數(shù),都是 int 類型,返回值也是 int。add 方法的具體內(nèi)容實現(xiàn)是下面四行代碼:

          mv.visitVarInsn(Opcodes.ILOAD, 0);mv.visitVarInsn(Opcodes.ILOAD, 1);mv.visitInsn(Opcodes.IADD);mv.visitInsn(Opcodes.IRETURN);

          這個其實就是本文中最前面Java 代碼和 Java 字節(jié)碼章節(jié)介紹的,其中 ILOAD、IADD 以及 IRETURN 都是 JVM 的指令集,這些指令集都是定義在 ASM 中 org.objectweb.asm.Opcodes 類里面,JVM 指令集規(guī)范可以參見 The Java Virtual Machine Instruction Set。

          ClassWriter 最后生成的就是字節(jié)碼,我們可以如下方式把它加載到當(dāng)前類路徑里面:

          public class Iteblog extends ClassLoader {    public Iteblog(ClassLoader classLoader) {        super(classLoader);    }    public byte[] addByAsm() {             // 實現(xiàn)看上面    }    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {        Iteblog loader = new Iteblog(Thread.currentThread().getContextClassLoader());        byte[] code = loader.addByAsm();        Class<?> clazz = loader.defineClass("com.iteblog.Test", code, 0, code.length);        Method add = clazz.getMethod("add", int.class, int.class);        System.out.println(add.invoke(null, 1, 2));    }}

          編譯運(yùn)行上面的程序就可以輸出 3.

          Presto 代碼生成

          Presto 在表達(dá)式計算方面用到了代碼生成技術(shù),它是基于 ASM 類庫實現(xiàn)的,從前面的 a + b 的例子中可以看到直接操作 ASM 類庫會比較麻煩,所以 Presto 對 ASM 進(jìn)行了更高層次的封裝,使用起來會更加方便。相關(guān)的代碼可以到 presto-bytecode 模塊里面看,presto 為我們抽象出 ClassDefinition、MethodDefinition 以及 FieldDefinition 等類來操縱類、方法以及字段字節(jié)碼。如果我們使用我們使用 Presto 來生成前面的 a+b 的代碼,實現(xiàn)如下:

          package com.iteblog.presto.bytecode;import com.google.common.collect.ImmutableList;import org.testng.annotations.Test;import java.lang.reflect.Method;import static com.facebook.presto.bytecode.Access.FINAL;import static com.facebook.presto.bytecode.Access.PUBLIC;import static com.facebook.presto.bytecode.Access.STATIC;import static com.facebook.presto.bytecode.Access.a;import static com.facebook.presto.bytecode.ClassGenerator.classGenerator;import static com.facebook.presto.bytecode.Parameter.arg;import static com.facebook.presto.bytecode.ParameterizedType.type;import static com.facebook.presto.bytecode.expression.BytecodeExpressions.add;import static org.testng.Assert.assertEquals;public class Iteblog {    public void addGenerator()            throws Exception {        ClassDefinition classDefinition = new ClassDefinition(                a(PUBLIC, FINAL),                "com/iteblog/Test",                type(Object.class));        Parameter argA = arg("a", int.class);        Parameter argB = arg("b", int.class);        MethodDefinition method = classDefinition.declareMethod(                a(PUBLIC, STATIC),                "add",                type(int.class),                ImmutableList.of(argA, argB));        method.getBody()                .append(add(argA, argB))                .retInt();        Class<?> clazz = classGenerator(getClass().getClassLoader())                .fakeLineNumbers(true)                .runAsmVerifier(true)                .dumpRawBytecode(true)                .defineClass(classDefinition, Object.class);        Method add = clazz.getMethod("add", int.class, int.class);        System.out.println(add.invoke(null, 1, 2));    }}

          可以看到,相比 ASM 代碼生成,presto 屏蔽了很多操作指令的細(xì)節(jié),操作起來更加方便。

          Presto 查詢查詢使用代碼生成的例子

          比如我們下面的查詢就會用到代碼生成技術(shù),

          select o_orderstatus, count(*) from orders_1x group by o_orderstatus;

          其中 count 計算內(nèi)部在創(chuàng)建 HashAggregationOperator 的時候會調(diào)用 com.facebook.presto.operator.aggregation.AccumulatorCompiler 類進(jìn)行代碼生成,定義的類名是 com.facebook.presto.$gen.BigintCountAccumulator_20210928_032653_6,實現(xiàn)了 com.facebook.presto.operator.aggregation.Accumulator 接口,并通過 com.facebook.presto.bytecode.DynamicClassLoader 動態(tài)加載到 ClassLoader 里面。

          總結(jié)

          本文主要簡單介紹了一下 ASM 的基礎(chǔ)知識,簡單介紹了一下使用 ASM 和 Presto 動態(tài)生成代碼的方法。限于篇幅和個人能力,本文知識簡單介紹了 Presto 的代碼生成技術(shù),后期有時間可以考慮更體系的介紹一下 Presto 的代碼生成。

          參考

          ?asm guide:https://asm.ow2.io/asm4-guide.pdf?The Java Virtual Machine Instruction Set:https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-6.html

          瀏覽 48
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  天堂五月丁香 | 国产福利无码视频 | 麻豆91久久久久久 | www.久久久久久 | 视频精品一区二区三区 |