热门标签 | 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 许可协议。转载请注明出处!



推荐阅读
  • 投融资周报 | Circle 达成 4 亿美元融资协议,唯一艺术平台 A 轮融资超千万美元 ... [详细]
  • 从0到1搭建大数据平台
    从0到1搭建大数据平台 ... [详细]
  • 在什么情况下MySQL的可重复读隔离级别会导致幻读现象? ... [详细]
  • 在Conda环境中高效配置并安装PyTorch和TensorFlow GPU版的方法如下:首先,创建一个新的Conda环境以避免与基础环境发生冲突,例如使用 `conda create -n pytorch_gpu python=3.7` 命令。接着,激活该环境,确保所有依赖项都正确安装。此外,建议在安装过程中指定CUDA版本,以确保与GPU兼容性。通过这些步骤,可以确保PyTorch和TensorFlow GPU版的顺利安装和运行。 ... [详细]
  • 在使用 SQL Server 时,连接故障是用户最常见的问题之一。通常,连接 SQL Server 的方法有两种:一种是通过 SQL Server 自带的客户端工具,例如 SQL Server Management Studio;另一种是通过第三方应用程序或开发工具进行连接。本文将详细分析导致连接故障的常见原因,并提供相应的解决策略,帮助用户有效排除连接问题。 ... [详细]
  • 第二章:Kafka基础入门与核心概念解析
    本章节主要介绍了Kafka的基本概念及其核心特性。Kafka是一种分布式消息发布和订阅系统,以其卓越的性能和高吞吐量而著称。最初,Kafka被设计用于LinkedIn的活动流和运营数据处理,旨在高效地管理和传输大规模的数据流。这些数据主要包括用户活动记录、系统日志和其他实时信息。通过深入解析Kafka的设计原理和应用场景,读者将能够更好地理解其在现代大数据架构中的重要地位。 ... [详细]
  • 如何高效启动大数据应用之旅?
    在前一篇文章中,我探讨了大数据的定义及其与数据挖掘的区别。本文将重点介绍如何高效启动大数据应用项目,涵盖关键步骤和最佳实践,帮助读者快速踏上大数据之旅。 ... [详细]
  • 本文详细介绍了在 Oracle 数据库中使用 MyBatis 实现增删改查操作的方法。针对查询操作,文章解释了如何通过创建字段映射来处理数据库字段风格与 Java 对象之间的差异,确保查询结果能够正确映射到持久层对象。此外,还探讨了插入、更新和删除操作的具体实现及其最佳实践,帮助开发者高效地管理和操作 Oracle 数据库中的数据。 ... [详细]
  • 深入理解排序算法:集合 1(编程语言中的高效排序工具) ... [详细]
  • 如何利用Java 5 Executor框架高效构建和管理线程池
    Java 5 引入了 Executor 框架,为开发人员提供了一种高效管理和构建线程池的方法。该框架通过将任务提交与任务执行分离,简化了多线程编程的复杂性。利用 Executor 框架,开发人员可以更灵活地控制线程的创建、分配和管理,从而提高服务器端应用的性能和响应能力。此外,该框架还提供了多种线程池实现,如固定线程池、缓存线程池和单线程池,以适应不同的应用场景和需求。 ... [详细]
  • 本文详细介绍了Oracle数据库中的表空间及其分区技术。表空间作为Oracle数据库的一个逻辑单元,每个数据库可包含一个或多个表空间,每个表空间则关联一个或多个数据文件。通过合理的表空间管理和分区策略,可以显著提升数据库的性能和管理效率。文章还总结了实际应用中的最佳实践,为读者提供了宝贵的参考。 ... [详细]
  • MySQL索引详解及其优化策略
    本文详细解析了MySQL索引的概念、数据结构及管理方法,并探讨了如何正确使用索引以提升查询性能。文章还深入讲解了联合索引与覆盖索引的应用场景,以及它们在优化数据库性能中的重要作用。此外,通过实例分析,进一步阐述了索引在高读写比系统中的必要性和优势。 ... [详细]
  • 在第二课中,我们将深入探讨Scala的面向对象编程核心概念及其在Spark源码中的应用。首先,通过详细的实战案例,全面解析Scala中的类和对象。作为一门纯面向对象的语言,Scala的类设计和对象使用是理解其面向对象特性的关键。此外,我们还将介绍如何通过阅读Spark源码来进一步巩固对这些概念的理解。这不仅有助于提升编程技能,还能为后续的高级应用开发打下坚实的基础。 ... [详细]
  • 字节跳动深圳研发中心安全业务团队正在火热招募人才! ... [详细]
  • 如何在Spark数据排序过程中有效避免内存溢出(OOM)问题
    本文深入探讨了在使用Spark进行数据排序时如何有效预防内存溢出(OOM)问题。通过具体的代码示例,详细阐述了优化策略和技术手段,为读者在实际工作中遇到类似问题提供了宝贵的参考和指导。 ... [详细]
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社区 版权所有