在 Kyligence 推出的首期 Data & AI Meetup 中,畅销书《深刻了解 Spark》作者、Kyligence 高级性能工程师耿嘉安带来了主题为「Spark Code Generation & Vectorization」的分享,深入浅出地解说了「Spark 为什么须要 CodeGen」、「Spark CodeGen 与向量化原理」、「Spark 向量化的前沿」等多个与 Spark 无关的热门话题,在直播间播种了一众好评。想理解更多,快往下看吧~
以下为耿嘉安在直播间演讲实录
主题背景
Spark 我的项目是在 2010 年左右开源进去的,越来越多的人理解到了 Spark 这个开源大数据我的项目的存在。从其诞生之初到现在 Spark 3.2.0 版本的公布,整个大数据圈中但凡用了 Spark 的公司和集体,都始终致力于对它的性能进行极致的优化。时至今日,国内外的各大厂商都把性能优化的锋芒直指向量化技术,用意可能把 Spark 的性能齐全压迫进去,让它可能更加迫近硬件的性能。
Vectorization(向量化)
明天分享的主题都围绕着一个关键词,即 Vectorization,翻译成中文就是“向量化”的意思。什么叫“向量化”呢?
其实程序员刚开始学习如何申明一个变量的时候,就曾经开始缓缓地接触到“向量化”了。一般来说,须要申明的这个变量是根底类型或是原始类型,这样的类型在向量化的畛域被称为标量,能够认为它是个一维的货色。比方我要去申明一个数组,这个数组中各个元素的值不一样,但代表的都是同一个含意,对这些内容进行操作时,如果采取批量操作,就可能防止独自操作的反复和繁琐。
作为一个向量,如果能用向量的计算形式,甚至是用一些离散数学中向量的计算形式,从数学的角度来看就曾经很简略了。而明天探讨的不光是数学的问题,更多的是计算机的问题。有了向量后,一些 CPU 的厂商也开始意识到了这个问题,研制出了一些专门的 CPU 指令,这些指令专门应答向量数据,可能对向量进行批量的计算解决。
演讲目录
上面咱们开始由浅入深地一步一步来,我明天主题分享的 Agenda 如下:
- Java Vectorization
- Volcano Iterator Model
- Tungsten Project
- Spark Code Generation
- Spark Code Generation & Vectorization
- The frontier of Spark Vectorization
主题分享
1.Java Vectorization
首先给大家介绍一下 Java 的向量化,因为其实在整个大数据畛域中,最次要的语言就是 Java,或者说是 Java 生态的货色。而明天又要讲 Spark,所以不可避免地要说一下 Java 的向量化是怎么做的。
Java 最外围的一点在于,它跟其余语言一样,都是想着法儿的,绕着弯儿的,搜索枯肠地想要利用 CPU 的向量化指令,这就是它的初衷。
在 Java 中到底是如何将标量的代码转化成向量代码,用向量代码来运行向量指令的呢?其实 Java 使用了一种曲线救国的形式。因为 Java 不像 C++ 或者是 C 语言,可能间接调用一些操作系统底层的命令(例如:分配内存或者调用 CPU 指令),它做不到这些,所以它要曲线救国。曲线救国的第一点是什么?当我把代码从方才那种很繁琐的形式改为这种 FOR 循环后,如果在其中持续运行的话,当循环超过几万次后,它会缓缓地被即时编译器捕捉,而后进行即时编译,编译成机器代码,在它编译的过程中有很多优化的点。其中有一个很重要的点,就是它会把它作为向量化去进行解决,而后去调用底层 CPU 的向量化指令。
这其中第一个点就是热点代码追踪。因为这是一个 FOR 循环,这个热点追踪是靠 JVM 自身外部的实现机制去做到的。当你这个代码须要足够多的次数去执行的时候,JVM 会发现本身对这个中央优化很有价值,其中有一个优化就是把它进行向量化。向量化怎么实现?用即时编译器(JIT)把它编译成本地机器代码之后,这个机器代码再接着去调用底层的 SIMD 的指令,就是这么一个过程。说到 SIMD 指令,比如说英特尔有 vmulps 这么一个指令,就是专门对向量进行操作的指令。这就是 Java 向量化的过程。
依据方才我与大家在这部分的剖析,咱们能够理解 Java 向量化的三个特点:
- Automatic:不能手动管制,只能由 JVM 主动解决。它没有方法做到像 C++ 一样间接调一个底层的 CPU 指令,所以这个过程是一个自动化的过程。
- Implicit:代码层面无奈找到向量化的显式调用,整个过程是隐式的。即使刚刚提到的 FOR 循环足够大、循环次数足够多,一个普通用户来看的话也看不到向量化的货色,感觉它跟向量化没有任何关系。即使最初真的进行了向量指令的调用,其实你也不晓得。所以整个过程是一个隐式的过程。
- Unreliable:依赖于 JVM 运行期的热点代码跟踪以及 JIT,所以整个过程是不牢靠的。它依赖于 JVM 运行期的热点追踪,包含即时编译器等。万一代码的循环次数不够,比如说没有达到须要即时编译的域值的话,可能就不会用即时编译去编译,就无奈真正做到向量化。也就是说即使你感觉 FOR 循环次数够多了,也不肯定真的会有向量化。所以说 Java 的向量化是一种不牢靠的优化形式。
2.Volcano Iterator Model
接下来介绍一下 Volcano Iterator Model,也就是火山迭代模型,它是当初大多数大数据系统或者说数据库底层,对 SQL 进行解决时通常会采纳的模型。
为什么大家都喜爱用这个模型呢?因为这个模型具备易形象、易实现、以及可能通过算子组合表白简单查问这三个劣势。
以 Spark 的一些算子为例,比方要在最底层 Scan 一个数据源,可能是一个数据库表也可能是一个文件,Scan 完了之后,可能加了一个 Filter,来过滤一些行,之后我可能还要应用 Project 抉择某些字段,最初这个后果可能再迭代返回,是一个过程。
这样的过程其实有一个对立的形象接口,比如说我这个外面写了一个叫 next,其实像 Spark 外面就有一个 hasNext,就是表白我这一行拿掉之后,上面还有没有行?next 就是拿掉上面的行,这是易于用代码去形象和实现的,通过这些不同算子之间的组合又能表白很丰盛的语义。所以说这个模型在数据库的 SQL 畛域常常被借鉴和应用。
当然这个模型也有它的劣势,这里咱们就得提到虚函数了,虚函数和实函数绝对,次要区别其实是在于调用机制的不同。大量虚函数的调用,就可能会导致 CPU 的中断和耗时等。所以说这并不那么敌对,Code Generation 其实就是想要解决这个问题。
3.Tungsten Project
把方才两个铺垫的常识说完,接下来咱们一起聊聊赫赫有名的 Tungsten 我的项目。这个我的项目最早是从 Spark 1.3 开始发动的,起初在 Spark 1.4 包含 Spark 1.5 中陆陆续续加强了很多的个性和性能。下个局部我要说的 Spark Code Generation,其实就是 Tungsten 我的项目很重要的组成之一。
Tungsten 我的项目有一个愿景,它心愿 Spark 在内存和 CPU 上的效率可能始终晋升,将它迫近到硬件的程度,尽管目前还没有实现,但愿景是美妙的。
这个 Tungsten 我的项目其实跟向量化的主题并没有很强的相关性,其实最次要的还是 Code Generation。然而这个中央我还是想要简略介绍一下 Tungsten 我的项目,把其中对大家感知非常明显的中央大略说一下。
3.1 内存治理与二进制解决
Spark 在 Tungsten 我的项目中的内存治理之前,最早的内存治理依靠于 JVM 本身的内存调配或是 GC 的过程。而因为 Spark 内存中的对象数量十分大,例如:它的一些数据结构、元数据的信息等等在它的内存里最初会通过对象来示意。又因为这是一个大数据我的项目,所以当数据量很大的时候,对象的收缩往往会十分快,它的内存问题就很显著。所以说社区的很多同志就想把这个问题解决一下。
过程中就发现了 Java 的对象不是个 ” 好货色 ”,我得尽量把它给干掉,能不必它就不必它,于是很多人就用到了一个绕弯子的形式,应用很早年前公开的 UnSafe 的 API。用那些 API 间接地去调一些 native 命令,间接可能在操作系统内存去调配一些空间,利用这些空间,Spark 在实现的时候,或者是 Tungsten 我的项目在实现的时候,就能通过一些地址和偏移量的信息来表白一个对象的内容。同时,因为它不像 Java 了,它的内容数据都是用二进制存储在机器内存的,所以说一个很重要的点就是它把 Java 对象的开销升高了。
这外面有一个很重要的点,举一个例子:以计算机根底来说,比如说一个 int 类型往往在很多零碎里用四个字节就够存储它了,然而在 Java 的一些对象类型里,原本四个字节就够的存储空间,实际上可能要用 32 个字节,所以它造成了十分重大的节约。有了这样的优化之后咱们就能节俭大量的内存了。
因为刚刚说的 JVM 是 Java 应用程序的通用的内存治理。尽管说它也十分棒,从当年的 CMS 内存垃圾回收器,到起初的 G1,包含当初更新的、越来越多的内存管理器;从当年的 Sun 到当初的 Oracle,始终在一直地进化它的 GC 和内存治理的技术。然而 Spark 的管理者,或者说 Spark 的开发人员,他们认为你再怎么样也只是一个通用化的形式,永远不能精确晓得我要创立的这些对象什么时候要开释。Spark 的管理者认为他们更加分明外面的对象,它们的生命周期怎么样、什么时候开释,什么时候申请。所以说 Spark 本人开拓了一个内存治理,先从逻辑上申请内存,申请好之后,再理论申请物理内存的过程。
3.2 缓存感知
有了这两个之后,再说一下缓存感知。缓存感知我理解得不多,然而简略说一下,它其实是为了无效地利用一些 L1、L2、L3 不同级别的 CPU 缓存。CPU 缓存一旦可能命中,它的读写的速度要比内存(或者说主存)至多要高一个量级,所以这个也很有吸引力。
3.3 Spark Code Generation
最初还是要回归到 Code Generation。那么 Spark 为什么须要 Code Generation?
其实方才在后面曾经提到了一些起因,比方我方才说的火山迭代模型,它外面的多态或者是虚函数调用的问题,包含想要做 Java 的向量化。除了这些外还有别的,比方它想通过代码编织的形式,通过字符串拼接,拼接出 Java 代码,可能缩小一些根本类型的主动装箱,根本类型的主动装箱原本是 Java 或者是 Scala 语言本身的一个语法糖,然而这样的语法糖其实对于一些编译过程是不太敌对的,对这些货色须要进行一些优化。除此之外,它其实还做了很多其余优化点。比如说有算子交融、缩减栈深等等,这些问题咱们就不一一开展了。
接着简略说一下 Spark 的 Code Generation 的代码架构。
在 Spark 的源码外面有一个 WholeStageCodegen 这么一个类,这个类它标准了 Spark 外面的物理算子进行代码编织的框架。上图其实是 Spark 源代码里的一段正文,然而这个正文对于不怎么看源码的人可能看得比拟头疼。但你其实不必管别的,最外围的就是要晓得有一个 doProduce,你的每一个物理算子须要实现它,每个物理算子本人更加晓得它应该怎么样编织 Java 代码,实现这个办法就能够了。
既然这个架构编出来的字符串外面示意的是 Java,那要用什么货色去编译呢?难道去执行一个 JavaC 吗?这必定是不太优雅的,这个时候引入了一个正好也是开源的 Java 编译器,叫做 Janino。
举一个例子,Spark 的物理算子里有一个比拟驰名,叫做 DataSourceScanExec,它专门对底层的数据源进行扫描或者说是按行读,它的 doProduce 里最外围的就是上图这五行代码,它实际上是依照行把数据读出来,读出来后去做一些映射,可能还有一些更底层的表达式,所以它还能再对这些表达式进行一一代码编织。
这个中央咱们也能看到火山迭代模型的影子。比如说外面用到 Iterator 这个 Scala 的迭代器的类,外面的泛型就是 InternalRow,就是依照行迭代的意思。这个迭代器迭代进去的后果,返回的后果就是 Iterator[InternalRow]。那么这个 Row 返回给下层,咱们回忆方才的火山模型,它是不是就能够一层一层往上传?传到最初拿到后果的那个中央,正好也是一唱一和的。
Spark 的 Code Generation 咱们就简略介绍这些,感兴趣的人能够持续深入研究。
4.Spark Code Generation & Vectorization
咱们明天的主题是向量化,所以就来说一下在 Code Generation 中,向量化是怎么做到的。
说到向量化,Code Generation 框架外面有一个叫列式框架的内容。
Spark 的列式框架外面跟其余的不太一样,它要解决的数据不是 InternalRow,而是 ColumnarBatch,中文叫做列批,它实际上是依照批次把每一列依照向量存储的形式把它一列一列存起来,这就叫列批。
列批框架外面最外围的代码,你能看到,实际上是一个用 FOR 循环一列一列地拜访它的信息的这么一个过程。这个过程是不是跟方才我说的 Java FOR 循环的形式十分像?
那么 Spark 这个列式框架是怎么实现的呢?次要依附这三步:
- 第一,须要有一个列式的存储,不论是 Parquet 还是 ORC,因为这样的一个存储天生就是一个列式存储,特地便于做这样的向量化的解决。
- 第二,你须要有一个列存储的读取器。像 Spark 外面别离实现了 Parquet Reader 和 ORC 的 Reader,它读出来就是一个列批的数据结构。
- 第三,就是通过我方才说的 Code Generation 那个例子,把那个形式通过列批的这种一步一步这三步下来,把它转化成一个 FOR 循环的形式,就刚好吻合了 Java 向量化的处理过程。
这个中央有一个开发点,或者说一个小分支。很多人会问,是不是要用 Spark 向量化就肯定要找一些列式存储呢?我就是存的文本构造怎么办?那也没问题,因为这个读到列批,最初是 Spark 内存式的数据结构。比如说你先读了一个行的构造,你本人写一些插件或者什么别的形式,把行转成列批就能够了,仍然是能够解决的,只不过就可能在效率、优雅上不太好。
最初我和大家简略分享下我所理解的目前在整个 Spark 社区,或者说国内外的各大厂商在向量化上所做的比拟前沿的事件。
最初,依据我对这些内容的了解,包含对各大厂商调研的状况,我感觉当初做向量化有两条路,一条路是把 SparkSQL 的表达式和物理打算齐全改写,改写成另一种引擎或者是语言反对的一种货色。还有一条路就是改成另外的一种 Native 的运行时,比如说阿里、英特尔他们就是这么做的。
这是明天我分享的内容。最初再次感激大家观看我的主题分享。
Data & AI Meetup 第 3 期预报
干货满满的 Data & AI Meetup 精彩持续,第 3 期将于 12 月 2 日 19:00-20:45 与大家在线上相见。本期咱们特地邀请了来自阿里云、小米、腾讯的三位技术专家分享一线大厂研发 Remote Shuffle Service (RSS) 的动机和实在生产实践,感兴趣的同学们快扫描下方二维码进入流动微信群吧~
对于 Kyligence
Kyligence 由 Apache Kylin 开创团队创立,致力于打造下一代智能数据云平台,为企业实现自动化的数据服务和治理。基于机器学习和 AI 技术,Kyligence 从多云的数据存储中辨认和治理最有价值数据,并提供高性能、高并发的数据服务以撑持各种数据分析与利用,同时一直升高 TCO。Kyligence 已服务中国、美国及亚太的多个银行、保险、制作、批发等客户,包含建设银行、浦发银行、招商银行、安全银行、宁波银行、太平洋保险、中国银联、上汽、一汽、安踏、YUMC、Costa、UBS、Metlife、AppZen 等寰球知名企业和行业领导者。公司已通过 ISO9001,ISO27001 及 SOC2 Type1 等各项认证及审计,并在寰球范畴内领有泛滥生态合作伙伴。