云服务器价格_云数据库_云主机【优惠】最新活动-搜集站云资讯

微软云_腾讯云乐固_哪家好

小七 141 0

apachespark作为编译器:在笔记本上每秒连接十亿行

在数据库里试试这个笔记本当我们在Databricks的团队计划为即将到来的apachespark2.0发布做贡献时,我们提出了一个雄心勃勃的目标:apachespark已经相当快了,但是我们能不能让它快10倍?这个问题让我们从根本上重新思考构建Spark物理执行层的方式。当您查看现代数据引擎(例如Spark或其他MPP数据库)时,大部分CPU周期都花在无用的工作上,例如进行虚拟函数调用或将中间数据读写到CPU缓存或内存中。通过减少在这些无用的工作中浪费的CPU周期来优化性能一直是现代编译器长期关注的焦点。apachespark2.0将与第二代钨丝发动机一起发布。基于现代编译器和MPP数据库的思想,并应用于数据处理查询,Tungsten在运行时发出优化字节码(SPARK-12795),将整个查询压缩为一个函数,消除了虚拟函数调用,并利用CPU寄存器处理中间数据。作为这种被称为"全阶段代码生成"的优化策略的结果,我们显著提高了CPU效率并获得了性能。过去:火山迭代器模型在深入研究整个阶段代码生成的细节之前,让我们回顾一下Spark(以及大多数数据库系统)目前的工作方式。让我们用一个简单的查询来说明这一点,该查询扫描单个表并使用给定的属性值计算元素的数量:为了评估这个查询,旧版本的Spark(1.x)利用了基于迭代器模型(通常称为火山模型)的流行的经典查询评估策略。在这个模型中,一个查询由多个运算符组成,每个运算符都提供一个接口next(),它一次向树中的下一个运算符返回一个元组。例如,上面查询中的Filter操作符大致翻译成以下代码:类筛选器(child:Operator,谓词:(Row=>Boolean))扩展运算符{def next():行={无功电流=孩子。下一个()while(当前!=空&&!谓词(当前){电流=孩子。下一个()}回流电流}}让每个操作符实现一个迭代器接口,可以让查询执行引擎优雅地组合任意的操作符组合,而不必担心每个操作符提供了什么样的不透明数据类型。因此,火山模型在过去二十年成为数据库系统的标准,也是Spark中使用的体系结构。火山与手写代码稍微离题一点,如果我们问一个大学新生,给她10分钟时间用Java实现上面的查询呢?她很可能会想出迭代代码,循环输入,计算谓词并计算行数:变量计数=0针对(店内商品销售){if(ss_item_sk==1000){计数+=1}}上面的代码是专门为回答一个给定的查询而编写的,显然不是"可组合的",但是这两个火山生成的和手工编写的代码在性能上如何比较呢?一方面,我们选择了Spark和大多数数据库系统的可组合性架构。另一方面,我们有一个初学者在10分钟内编写的简单程序。我们运行了一个简单的基准测试,将"大学新生"版本的程序与使用单线程执行上述查询的Spark程序与磁盘上的拼花数据进行比较:如你所见,"大学新生"手写版比火山模型快一个数量级。结果发现,这6行Java代码经过了优化,原因如下:无虚函数分派:在火山模型中,要处理元组,至少需要调用一次next()函数。这些函数调用由编译器作为虚拟函数分派(通过vtable)来实现。另一方面,手写代码没有一个函数调用。虽然虚拟函数调度是现代计算机体系结构中的一个重点优化领域,但它仍然需要多个CPU指令,而且速度非常慢,尤其是当调度数十亿次时。内存中的中间数据vs CPU寄存器:在火山模型中,每当一个操作符将一个元组传递给另一个操作符时,都需要将元组放入内存(函数调用堆栈)。相比之下,在手工编写的版本中,编译器(在本例中是jvmjit)实际上将中间数据放在CPU寄存器中。同样,CPU访问内存中数据所需的周期数比寄存器中的大几个数量级。循环展开和SIMD:现代编译器和cpu在编译和执行simple for循环时非常高效。编译器通常可以自动展开简单的循环,甚至可以生成SIMD指令来处理每个CPU指令的多个元组。CPU包括流水线、预取和指令重新排序等功能,这些功能使简单循环的执行变得高效。然而,这些编译器和cpu并不擅长优化复杂的函数调用图,而火山模型依赖于这些图形。这里的关键是手写代码是专门为运行查询而编写的,因此它可以利用所有已知的信息,从而优化代码,消除虚拟函数调度,将中间数据保存在CPU寄存器中,并可由底层硬件进行优化。未来:全阶段代码生成根据以上观察,我们下一步自然是探索在运行时自动生成这些手写代码的可能性,我们称之为"全阶段代码生成"。这一想法的灵感来自Thomas Neumann 2011年发表的关于高效编译现代硬件查询计划的开创性论文。关于报纸的更多细节,阿德里安·科耶已经与我们协调,今天在晨报博客上发表了一篇评论。目标是利用整个阶段的代码生成,这样引擎就可以实现手写代码的性能,同时提供通用引擎的功能。这些运算符在运行时不依赖运算符来处理数据,而是在运行时一起生成代码,并在可能的情况下将查询的每个片段折叠为单个函数并执行生成的代码。例如,在上面的查询中,整个查询是一个单独的阶段,Spark将生成以下JVM字节码(以这里所示的Java代码的形式)。更复杂的查询将导致多个阶段,从而由Spark生成多个不同的函数。下面表达式中的explain()函数已扩展为整个阶段的代码生成。在explain输出中,当运算符的周围有一个星号(*),则启用整个阶段的代码生成。在下面的例子中,Range、Filter和这两个聚合都在整个阶段代码生成中运行。然而,Exchange并没有实现整个阶段的代码生成,因为它是通过网络发送数据的。火花射程(1000).filter("id>100").selectExpr("sum(id)").explain()==实际计划==*聚合(函数=[sum(id#201L)])+-Exchange SinglePartition,无+-*聚合(函数=[总和(id#201L)])+-*过滤器(id#201L>100)+-*范围0,1,3,1000,[id#201L]那些密切关注Spark开发的人可能会问以下问题:"我在这篇博文中听说过ApacheSpark1.1以来的代码生成。这次有什么不同?"在过去,与其他MPP查询引擎类似,Spark只将代码生成应用于表达式求值,并且仅限于少数运算符(例如Project、Filter)。也就是说,过去的代码生成只会加快对表达式(如"1+a")的计算,而现在整个阶段的代码生成实际上为整个查询计划生成代码。矢量化整个阶段的代码生成技术特别适用于对大型数据集执行简单、可预测操作的大量查询。但是,在某些情况下,生成代码将整个查询合并为单个函数是不可行的。操作可能太复杂(例如,CSV解析或Parquet解码),或者当我们与第三方组件集成时,可能无法将它们的代码集成到生成的代码中(示例可以从调用Python/R到将计算卸载到GPU)。为了提高这些情况下的性能,我们采用了另一种称为"矢量化"的技术。这里的思想是,引擎批处理不是一次处理一行数据,而是以列格式将多行放在一起,每个运算符使用简单的循环来迭代批处理中的数据。因此,每个next()调用都将返回一批元组,分摊虚拟函数分派的成本。这些简单的循环还将使编译器和CPU能够更高效地执行前面提到的好处。作为一个例子,对于一个有三列(id、name、score)的表,下面以面向行和面向列的格式说明内存布局。这种由MonetDB和C-Store等列式数据库系统发明的处理方式可以实现前面提到的三点中的两个(几乎没有虚拟函数分派和自动循环展开/SIMD)。然而,它仍然需要将中间数据放入内存中,而不是保存在CPU寄存器中。因此,我们只在不可能完成整个阶段代码生成的情况下才使用矢量化。例如,我们实现了一个新的矢量化拼花读取器,它可以分批进行解压和解码。当解码整数列(在磁盘上)时,这个新的读卡器大约比未矢量化的快9倍:将来,我们计划在更多的代码路径中使用向量化,比如Python/R中对UDF的支持。绩效基准我们测量了apachespark1.6和apachespark2.0中的一些操作符在一个内核上处理元组所需的时间(以纳秒为单位)