热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

for循环优化_从Weld论文看执行器的优化技术

Weld是一个用于数据计算分析的高性能Runtime(High-performanceruntimefordataanalyticsapplicationsÿ
452265e3ca32314d8921c46dfb3a3356.png

Weld 是一个用于数据计算分析的高性能 Runtime(High-performance runtime for data analytics applications),使用 Rust 编写,可以很容易地集成到各种大数据计算框架中,比如 Spark SQL、NumPy & Pandas、TensorFlow 等,带来大幅的性能提升。

除了 Weld 本身的贡献,论文中提到的各种用于执行阶段的优化技术也很有意思,其中的大部分都借鉴自关系型数据库或编译器。本文除了介绍 Weld 之外,也是想对这些技术做个梳理。

本文主要内容来自于 Weld 发表在 VLDB'18 的论文。

整体架构

之前说到,Weld 是一个用于数据计算的 Runtime,它的上层通常是一些计算框架,例如 Spark SQL、NumPy 等。用户用这些计算框架编写程序,这些框架将用户需要的计算翻译成 Weld 中间表示(IR),然后 Weld 对其进行一系列的优化,最后生成代码并编译运行。

做个类比,这就像 LLVM 的工作方式一样:各种语言的编译前端将高级语言翻译成 LLVM IR,LLVM 再对 IR 做一系列的优化,最后再编译成二进制。

虽然都是 IR,但实际上 Weld IR 和 LLVM IR 有很大不同:

  • Weld IR 是声明式的:只表达计算流程,不包含具体的实现。比如下面会提到的 Builder,上层不需要指定用什么方式构建数组或是哈希表等数据结构,这些是由 Weld 优化器决定的;
  • Weld IR 是 Lazy 的:只有当需要输出结果时,相应的 DAG 计算才会真正开始运行。
53b9ff206b3bbc6f87d1f777d62b2528.png

上图是 Weld 的整体工作过程:

  1. 上层调用 Weld 的 API 输入需要计算的 IR 程序,它会被解析成 AST;
  2. 当需要执行时,相关的函数 IR 会被拼在一起,方便进行整体优化;
  3. Weld 优化器使用一系列的启发式规则进行优化,注意结果仍然是 AST;
  4. 最后生成代码并借助 LLVM 编译成二进制。

Weld 主要由两个部分组成:IR 和 Runtime,接下来我们依次进行介绍。

Weld IR

Weld IR 支持 intfloat 等基本数据类型、struct 类型,以及两种容器类型:vecdict,顾名思义,分别是(变长)数组和字典。另外还支持他们的各种组合,就像 JSON 那样。

和数据库的执行器不同,Weld 不考虑数据拉取之类的问题。它假设输入数据都在内存中以数组形式存在,例如: int[100]struct{int, float}[100]

Weld IR 的计算都通过 Builder 和 Merger 来完成,由于 Merger 和 Builder 的接口是一样的,Weld 论文中并没有把二者区分开来。下面我们统称为 Builder。

b81babb6cabcaadff35a13eeda086613.png

Builder 提供两个接口方法:

  • merge(b, v):向 Builder b 添加新的元素;
  • result(b):拿到 b 的结果,注意之后不能再添加元素了。

下面是使用 Builder 的例子:

66babb5d3d7f8bafd56d1e52d55c6960.png

代码中还有个 for,它的语法是 for(vector, builders, (builders, index, elem) => builders),用来并行地对数据做处理——也就是往 Builder 里加元素,这是 Weld 中唯一的计算方式。

for 还可以同时处理多个 Builder,这个特性在优化的时候很有用,可以避免同一个数据扫描多次。

Weld IR 还有些别的特性(比如方便编程的 macro),但不是本文的重点,有兴趣的同学自己看原文吧。

Weld Runtime

0821b1e653645c8dcc40be2c4d1bbb59.png

当上层输入 IR 并发出开始计算的指令时,就轮到 Weld Runtime 登场了。在代码生成之前,Weld Runtime 会对 IR 做优化,优化可以分为两种:

  1. Rule-Based Optimizer (RBO):和我们熟悉的 RBO 优化类似,是基于规则匹配的优化;
  2. Adaptive Optimizer:运行时 sample 数据,然后决定用哪种算法执行,勉强可以对应 CBO。

为什么不是 CBO?关系型数据库的 CBO 是需要以统计信息为基础的,但是 Weld 作为一个通用的 Runtime,上层框架不一定能提供统计信息(比如 NumPy)。

Weld 应用规则是依次进行的,每次运行一种优化规则,称为一个 pass。Pass 之间会进行剪枝,去掉无用的代码。以下我们逐条看看 Weld 做了哪些优化。

Pipeline

Pipeline 在 OLAP 系统中很常见,最经典的是 HyPer 团队提出的 consume/produce 代码生成机制,可以在代码生成时尽可能生成 Pipeline 的代码。

53f8183b99df0c33b4532f90872f02cb.png
Hyper 的 Pipeline 代码生成

为什么需要 Pipeline?设想一下使用代码生成、但是不使用 Pipeline 会怎么样,那么 $R_1$ 和 $sigma_{x=7}$ 就会分成独立的两步,$R_1$ (即 TableScan)的结果被物化到内存中,再进行 $sigma_{x=7}$(Filter)。

而 Pipeline 的代码省略了中间的物化,仅仅用了一个 if 就解决了 filter,这个代价要低得多:计算 if 表达式时相关数据基本还在寄存器或 Cache 里,充分利用 Data Locality,这比去内存取数据快 1~2 个数量级。

Pipeline 优化规则会在 AST 中匹配这样的模式:A 的输出就是 B 的输入,对匹配到的节点应用 pipeline 优化,下面是一个简单例子:

96eafd3f1ca8d5d8af67c8a203e20c69.png

Horizontal Fusion

Fusion 意为把两段代码融合成一段更精炼的代码,刚刚说的 Pipeline 也是一种 Fusion。所谓 Horizontal Fusion 是找出被重复处理的数据,然后将几次处理合在一起。

例如下面图中的 IR,v0 原本被 loop over 了两次,如果把两次循环合成一次,能尽可能利用 Data Locality,减少一半的内存读取代价。

abdf1fa84eb6d121b9cd4040e265e743.png

硬要说的话,这个规则与关系代数优化中的 Project Merge 规则最相似。论文中给了一个更好的例子来说明它的用处:像 Pandas 这类的计算框架,由于 API 设计一次只能处理一列,必须借助 Horizontal Fusion 实现一次处理多列。

向量化和 Adaptive 优化

向量化(Vectorization)优化也不是新鲜事,很多编译器(比如 LLVM)都能自动把循环编译成 SIMD 指令,JVM 甚至可以在运行时生成 SIMD 代码。

SIMD 全称是单条指令、多个数据,即用一条指令处理多个数据计算,比如原本计算 4 个整数加法要用 4 次加法指令,用了 SIMD 之后只要 1 次。没错,就这么简单!

5b90df24598007c56b9e61af22b82e20.png
Scalar vs. SIMD

在这个 pass 中仅处理简单的、没有条件分支的 for 循环,如果满足这一条件,优化器会将被循环的数据从 T 转换成 simd[T],最后 code-gen 的时候为其生成 SIMD 代码。

那对于带有条件分支的 for 循环,能否进行向量化呢?答案是,可以,但是不一定有用。

我们先设想一下:对于有条件分支的 for 循环,它向量化之后是什么样的?SIMD 指令本身是没法处理分支的(compare 这种特别简单的除外),如果一定要用 SIMD,可以假设分支条件全都为 true 或 false,最后根据条件表达式的计算结果(true 或 false),利用 select 指令选出相应的结果。

这种方式相比普通的带分支的指令,有得有失:

  • 优势:用 SIMD 指令集可以加速计算;
  • 劣势:原本只要算一个分支,现在两个分支都要算。
注:另一个优势是,SIMD 去掉了条件跳转,不存在打断 CPU 流水线的问题。但是论文中没有提到这一点,我猜测可能是它的影响因素比较小,或是作者没有找到一个合适的代价计算方式。

论文只给出了对 if(cond, merge(b, body), b) 这样单分支条件的代价建模,有兴趣的同学可以看原论文上的式子。这里只说一个粗糙的结论:当选择率(即进入 if-body 的概率)很小时,有分支的代码更优;当选择率比较大时,SIMD 代码更优。

我们之前说过,Weld 假设上层无法提供统计信息,因而在这一步,由于缺乏关键的选择率信息,它只能采取一种 Adaptive 的思路:同时生成有分支的代码和 SIMD 代码,运行时,首先对输入数据做个 Sample 估算一下选择率,再决定走哪个算法。

选择率(selectivity)这个概念在数据库优化器中也很常用,比如估算 Row Count 时就频繁用到了选择率估计。如果能在优化时直接拿到这个信息,想必不需要这么折腾。

Adaptive Hash Table

Weld 的 dictbuildergroupbuilder 中都需要构建哈希表,这里也有个 trade-off:是用 Partitioned Hash Table 还是 Global Hash Table?

  • Partitioned Hash Table 是将 build 过程分成两步,先各个线程本地做 build,最后再 merge 成完整的结果;
  • Global Hash Table 只有一张全局的哈希表,通过加锁等方式做了控制并发写入。

一般而言,如果 Group by 的基数(Cardinality)比较小,Partitioned 方式更有优势,因为并发冲突会很多;相反,如果基数很大,Global 更占优势,因为无需再做多一次 merge。

Weld 的做法很巧妙地实现了二者取折中:各线程先写到本地的哈希表,但如果大小超过阈值,就写到全局的哈希表。最后把本地数据再 merge 进全局哈希表。这个实现被它称为 Adaptive Hash Table。

0eda15022c44342c3d8aec0c00c5a35a.png
Adaptive Hash Table

Misc.

Weld 中还有还有一些优化手段,比较简单:

循环展开(Loop Unrolling)是编译器中很常见的优化,如果编译期已知 for 循环的次数很小(例如,对于一个 N*3 的矩阵,第二维度长度仅为 3),就将循环展开,避免条件跳转指令打断 CPU 流水线。

数组预分配(Preallocation)在矩阵运算中也很有用,例如,默认 vecbuiler 的实现是自动生长的动态数组。如果预先知道数组长度,就能避免数组生长的拷贝代价。

评估和总结

下面是 Weld 官网放出的性能评估,对于文中提到的这几个框架,的确做到了可观的性能提升。

55a66e98e431ce080443f14f86706e0a.png
注:这里 TensorFlow 性能是用 CPU 运行的,而非 GPU。

Weld 的最大贡献是抽象出了一个通用的执行器 Runtime。这个抽象的层级要比“代码生成”中的“代码”(比如 LLVM IR)高级(high-level)不少,但又比关系代数或是线性代数低级(low-level),从而有更好的通用性。更可贵的是,Weld IR 仅仅包含 Builder 以及 for、if 这些最基本的语句,极其之简单。

上文提到的很多优化规则,不少来源于编译器或关系型数据库。例如 Pipeline Fusion 的思想,在编译器中其实也有体现——编译器会尽可能连续的利用寄存器、避免 store/load。但是 Weld IR 独特的抽象层级令它能做层级更高的优化,达到和数据库的 Pipeline 一样的效果。

References

  1. Evaluating End-to-End Optimization for Data Analytics Applications in Weld (VLDB'18)
  2. Efficiently Compiling Efficient Query Plans for Modern Hardware (VLDB'11)
  3. Weld - Official Website
本文作者: @Eric Fu 原文链接: https://ericfu.me/weld-the-query-exeution-engine/版权声明: 本文章采用 BY-NC-SA 4.0 许可协议。转载请注明出处!



推荐阅读
  • 本文比较了eBPF和WebAssembly作为云原生VM的特点和应用领域。eBPF作为运行在Linux内核中的轻量级代码执行沙箱,适用于网络或安全相关的任务;而WebAssembly作为图灵完备的语言,在商业应用中具有优势。同时,介绍了WebAssembly在Linux内核中运行的尝试以及基于LLVM的云原生WebAssembly编译器WasmEdge Runtime的案例,展示了WebAssembly作为原生应用程序的潜力。 ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • Go语言实现堆排序的详细教程
    本文主要介绍了Go语言实现堆排序的详细教程,包括大根堆的定义和完全二叉树的概念。通过图解和算法描述,详细介绍了堆排序的实现过程。堆排序是一种效率很高的排序算法,时间复杂度为O(nlgn)。阅读本文大约需要15分钟。 ... [详细]
  • 本文介绍了使用哈夫曼树实现文件压缩和解压的方法。首先对数据结构课程设计中的代码进行了分析,包括使用时间调用、常量定义和统计文件中各个字符时相关的结构体。然后讨论了哈夫曼树的实现原理和算法。最后介绍了文件压缩和解压的具体步骤,包括字符统计、构建哈夫曼树、生成编码表、编码和解码过程。通过实例演示了文件压缩和解压的效果。本文的内容对于理解哈夫曼树的实现原理和应用具有一定的参考价值。 ... [详细]
  • Java编程实现邻接矩阵表示稠密图的方法及实现类介绍
    本文介绍了Java编程如何实现邻接矩阵表示稠密图的方法,通过一个名为AMWGraph.java的类来构造邻接矩阵表示的图,并提供了插入结点、插入边、获取邻接结点等功能。通过使用二维数组来表示结点之间的关系,并通过元素的值来表示权值的大小,实现了稠密图的表示和操作。对于对稠密图的表示和操作感兴趣的读者可以参考本文。 ... [详细]
  • 深入理解Java虚拟机的并发编程与性能优化
    本文主要介绍了Java内存模型与线程的相关概念,探讨了并发编程在服务端应用中的重要性。同时,介绍了Java语言和虚拟机提供的工具,帮助开发人员处理并发方面的问题,提高程序的并发能力和性能优化。文章指出,充分利用计算机处理器的能力和协调线程之间的并发操作是提高服务端程序性能的关键。 ... [详细]
  • Java编程思想一书中第21章并发中关于线程间协作的一节中有个关于汽车打蜡与抛光的小例子(原书的704页)。这个例子主要展示的是两个线程如何通过wait ... [详细]
  • 初识java关于JDK、JRE、JVM 了解一下 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • LeetCode笔记:剑指Offer 41. 数据流中的中位数(Java、堆、优先队列、知识点)
    本文介绍了LeetCode剑指Offer 41题的解题思路和代码实现,主要涉及了Java中的优先队列和堆排序的知识点。优先队列是Queue接口的实现,可以对其中的元素进行排序,采用小顶堆的方式进行排序。本文还介绍了Java中queue的offer、poll、add、remove、element、peek等方法的区别和用法。 ... [详细]
  • 给定一个二维平面上的一些点,通过计算曼哈顿距离,求连接所有点的最小总费用。只有任意两点之间有且仅有一条简单路径时,才认为所有点都已连接。给出了几个示例并给出了对应的输出。 ... [详细]
  • C# 7.0 新特性:基于Tuple的“多”返回值方法
    本文介绍了C# 7.0中基于Tuple的“多”返回值方法的使用。通过对C# 6.0及更早版本的做法进行回顾,提出了问题:如何使一个方法可返回多个返回值。然后详细介绍了C# 7.0中使用Tuple的写法,并给出了示例代码。最后,总结了该新特性的优点。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • [转载]从零开始学习OpenGL ES之四 – 光效
    继续我们的iPhoneOpenGLES之旅,我们将讨论光效。目前,我们没有加入任何光效。幸运的是,OpenGL在没有设置光效的情况下仍然可 ... [详细]
author-avatar
芬妮诗婚纱厂
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有