简介
这是2020年的职位,所以新年快乐敬你们所有人!
自从11年前我开始玩LLVM以来,我就是它的忠实粉丝JIT数据结构比如avl,然后再到JIT受限AST树和来自TensorFlow图的JIT本地代码.从那时起,LLVM发展成为最重要的编译器框架生态系统之一,现在许多重要的开源项目都在使用它。
我最近注意到的一个很酷的项目是Gandiva.刚帝娃是由Dremio之后捐赠给阿帕奇箭(向Dremio团队致敬).Gandiva的主要思想是,它提供了一个编译器来生成LLVM IR,可以操作批量的Apache箭头.Gandiva是用c++编写的,它提供了许多不同的函数来构建表达式树,可以使用LLVM进行JIT化。该设计的一个很好的特性是,它可以使用LLVM自动优化复杂表达式,在操作Arrow批处理时添加本机目标平台向量化(如AVX),并执行本机代码来计算表达式。
下图是Gandiva的概述:

在这篇文章中,我将构建一个非常简单的表达式解析器,它亚洲金博宝支持有限的操作集,我将用它来筛选Pandas DataFrame。
用Gandiva构建简单的表达式
在本节中,我将展示如何使用Gandiva中的树构建器手动创建一个简单的表达式。
使用Gandiva Python绑定到JIT和表达式
在为表达式构建解析器和表达式构建器之前,让我们用Gandiva手动构建一个简单的表达式。首先,我们将创建一个简单的Pandas数据帧,从0.0到9.0:
导入熊猫为pd导入pyarrow为pa导入pyarrow。gandivaas gandiva # Create a simple Pandas DataFrame df = pd.DataFrame({"x": [1.0 * i for i in range(10)]}) table = pa.Table.from_pandas(df) schema = pa.Schema.from_pandas(df)
我们将数据帧转换为箭头表需要注意的是,在本例中这是一个零拷贝操作,Arrow并没有从Pandas复制数据并复制DataFrame。之后我们得到模式
从表中,它包含列类型和其他元数据。
之后,我们想用Gandiva构建下面的表达式来过滤数据:
(x > 2.0)和(x < 6.0)
这个表达式将使用Gandiva中的节点来构建:
builder = gandiva.TreeExprBuilder() #引用列"x" node_x = builder.make_field(table.schema.field("x")) #创建两个字面量:2.0和6.0 two = builder.make_literal(2.0, pa.float64()) six = builder.make_literal(6.0, pa.float64()) #为"x > 2.0" gt_five_node = builder.make_field()make_function("greater_than", [node_x, two], pa.bool_()) #为"x < 6.0"创建一个函数lt_ten_node = builder。make_function("less_than", [node_x, six], pa.bool_()) #为"(x > 2.0) and (x < 6.0)" and_node = builder创建一个"and"节点。make_and([gt_five_node, lt_ten_node]) #将表达式作为一个条件,并创建一个过滤条件= builder.make_condition(and_node) filter_ = gandiva.make_filter(table. filter)模式、条件)
这段代码现在看起来有点复杂,但很容易理解。我们基本上是在创建树的节点,这些节点将表示我们前面展示的表达式。下面是它的图形表示:
检查生成的LLVM IR
不幸的是,还没有找到转储使用Arrow的Python绑定生成的LLVM IR的方法,然而,我们可以使用c++ API构建相同的树,然后查看生成的LLVM IR:
Auto field_x = field("x", float32());自动模式=箭头::模式({field_x});autonode_x = TreeExprBuilder::MakeField(field_x);auto two = TreeExprBuilder::MakeLiteral((float_t)2.0);auto six = TreeExprBuilder::MakeLiteral((float_t)6.0);auto gt_five_node = TreeExprBuilder::MakeFunction("greater_than", {node_x, two}, arrow::boolean());auto lt_ten_node = TreeExprBuilder::MakeFunction("less_than", {node_x, six}, arrow::boolean());auto and_node = TreeExprBuilder::MakeAnd({gt_five_node, lt_ten_node});autocondition = TreeExprBuilder::MakeCondition(and_node);std::要查看> <过滤器过滤; auto status = Filter::Make(schema, condition, TestConfiguration(), &filter);
上面的代码与Python代码相同,但使用的是c++ Gandiva API。现在我们已经用c++构建了树,我们可以获得LLVM模块并转储它的IR代码。生成的IR充满了来自Gandiva注册表的样板代码和JIT函数,但是重要的部分如下所示:
;函数Attrs: alwaysinline norecurse nounwind readnone ssp uwtable define internal zeroext i1 @less_than_float32_float32(float, float) local_unnamed_addr #0 {%3 = fcmp olt float %0, %1 ret i1 %3};函数Attrs: alwaysinline norecurse nounwind readnone ssp uwtable定义内部zeroext i1 @greater_than_float32_float32(浮动,浮动)local_unnamed_addr # 0 {% 3 = fcmp油气痕迹浮动% 0,% 1 i1 ret % 3 } (...) % x =负载浮子,浮子* % 11% greater_than_float32_float32 =叫i1 @greater_than_float32_float32 (% x浮动,浮动2.000000 e + 00)(…)% x11 =负载浮子,浮子* % 15% less_than_float32_float32 =叫i1 @less_than_float32_float32 (% x11浮动,浮动6.000000 e + 00)
如你所见,在IR上我们可以看到对函数的调用less_than_float32_float_32
而且greater_than_float32_float32
它们是(在本例中非常简单的)用于进行浮点数比较的Gan亚洲金博宝diva函数。通过查看函数名前缀,注意函数的专门化。
非常有趣的是,LLVM将在这段代码中应用所有优化,它将为目标平台生成高效的本机代码,而Godiva和LLVM将负责确保内存对齐是正确的,以便用于向矢量化的扩展(如AVX)。
我展示的这个IR代码实际上并不是执行的那个,而是优化的那个。在优化后的代码中,我们可以看到LLVM内联了函数,如下面的部分优化代码所示:
% x。我们=加载浮动,浮动* %10,对齐4 %11 = FCMP ogt浮动%x。我们,2.000000e+00 %12 = FCMP olt浮动%x。我们,6.000000e+00 %not.or。Cond =和i1 %12, %11
您可以看到,经过优化之后,表达式现在简单多了,因为LLVM应用了强大的优化并内联了许多Gandiva函数。
用Gandiva构建Pandas过滤器表达式JIT
现在我们希望能够实现一些类似于Pandas的东西DataFrame.query ()
函数使用Gandiva。我们将面临的第一个问题是,我们需要解析一个字符串,例如(x > 2.0)和(x < 6.0)
之后,我们将不得不使用Gandiva中的树构建器构建Gandiva表达式树,然后在箭头数据上计算该表达式。
现在,我不实现表达式字符串的完整解析,而是使用Python AST模块来解析有效的Python代码并构建该表达式的抽象语法树(AST),稍后我将使用它来发出Gandiva/LLVM节点。
解析字符串的繁重工作将委托给Python AST模块,我们的工作将主要是遍历该树并基于该语法树发出Gandiva节点。访问此Python AST树的节点并发出Gandiva节点的代码如下所示:
类LLVMGandivaVisitor(ast.NodeVisitor): def __init__(self, df_table): self。表= df_table self。生成器= gandiva.TreeExprBuilder()自身。columns = {f.name: self.builder.make_field(f) for f in self.table.schema} self.compare_ops = { "Gt": "greater_than", "Lt": "less_than", } self.bin_ops = { "BitAnd": self.builder.make_and, "BitOr": self.builder.make_or, } def visit_Module(self, node): return self.visit(node.body[0]) def visit_BinOp(self, node): left = self.visit(node.left) right = self.visit(node.right) op_name = node.op.__class__.__name__ gandiva_bin_op = self.bin_ops[op_name] return gandiva_bin_op([left, right]) def visit_Compare(self, node): op = node.ops[0] op_name = op.__class__.__name__ gandiva_comp_op = self.compare_ops[op_name] comparators = self.visit(node.comparators[0]) left = self.visit(node.left) return self.builder.make_function(gandiva_comp_op, [left, comparators], pa.bool_()) def visit_Num(self, node): return self.builder.make_literal(node.n, pa.float64()) def visit_Expr(self, node): return self.visit(node.value) def visit_Name(self, node): return self.columns[node.id] def generic_visit(self, node): return node def evaluate_filter(self, llvm_mod): condition = self.builder.make_condition(llvm_mod) filter_ = gandiva.make_filter(self.table.schema, condition) result = filter_.evaluate(self.table.to_batches()[0], pa.default_memory_pool()) arr = result.to_array() pd_result = arr.to_numpy() return pd_result @staticmethod def gandiva_query(df, query): df_table = pa.Table.from_pandas(df) llvm_gandiva_visitor = LLVMGandivaVisitor(df_table) mod_f = ast.parse(query) llvm_mod = llvm_gandiva_visitor.visit(mod_f) results = llvm_gandiva_visitor.evaluate_filter(llvm_mod) return results
正如您所看到的,代码非常简单,因为我并没有支持所有可能的Python表达式,而是支持其中的一个小子集。亚洲金博宝我们在这个类中所做的基本上是将Python AST节点(如Comparators和BinOps(二进制操作))转换为Gandiva节点。我也改变了语义&
和|
运算符分别表示AND和OR,例如Pandas查询()
函数。
注册为Pandas扩展
方法创建一个简单的Pandas扩展gandiva_query ()
我们创建的方法:
@ pdf .api.extensions.register_dataframe_accessor("gandiva")类gandivaaccessor: def __init__(self, pandas_obj): self。pandas_obj = pandas_obj def query(self, query):返回llvmgandivavisator .gandiva_query(self。pandas_obj、查询)
就是这样,现在我们可以使用这个扩展来做事情,如:
Df = pd。DataFrame({"a": [1.0 * i for i in range(nsize)]}) results = df.gandiva.query("a > 10.0")
因为我们已经注册了一个熊猫扩展称为gandiva
它现在是熊猫数据框架的一级公民。
现在让我们创建一个500万浮动的DataFrame,并使用新的查询()
过滤方法:
Df = pd。DataFrame({"a": [1.0 * i for i in range(50000000)]}) df.gandiva.query("a < 4.0") # This will output: # array([0, 1, 2, 3], dtype=uint32)
注意,返回值是满足我们实现的条件的索引,因此它与Pandas不同查询()
它返回已过滤的数据。
我做了一些基准测试,发现Gandiva通常总是比Pandas快,但是我将把适当的基准测试留到下一篇关于Gandiva的文章中,因为这篇文章将展示如何使用它来JIT表达式。
就是这样!我希望你喜欢这篇文章,就像我喜欢探索刚迪瓦一样。看来我们可能会有越来越多的工具使用Gandiva加速,特别是针对SQL解析/投影/JITing。Gandiva比我刚才展示的要复杂得多,但是您现在可以开始了解它的体系结构以及如何构建表达式树。
——克里斯蒂安·s·佩隆