尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Apache Arrow Gandiva 外部函数开发指南:从 C 函数到 LLVM IR 函数的注册与集成

Apache Arrow Gandiva 外部函数开发指南:从 C 函数到 LLVM IR 函数的注册与集成 Apache Arrow Gandiva 外部函数开发指南从 C 函数到 LLVM IR 函数的注册与集成【免费下载链接】arrowApache Arrow is the universal columnar format and multi-language toolbox for fast data interchange and in-memory analytics项目地址: https://gitcode.com/GitHub_Trending/arrow3/arrow本指南基于 Apache Arrow 仓库中的 Gandiva 模块官方文档docs/source/cpp/gandiva/external_func.rst整理而成。Gandiva 是一个基于 LLVM 的表达式编译框架通过外部函数机制开发者可以用 C、C、Rust 甚至 Zig 等语言编写自定义函数并注册进 Gandiva 表达式系统。读完本文你将掌握两类外部函数C 函数与 IR 函数的选型原则、NativeFunction元数据注册方法、C 函数签名映射规则含变长字符串类型的特殊处理以及通过FunctionRegistry完成注册的完整 API 用法。Gandiva 外部 C 函数与 IR 函数集成架构图如上图所示外部函数从多语言编写 → 编译为 LLVM IR / 直接暴露为 C 函数 → 注册进 FunctionRegistry → 由 LLVMGenerator 生成 IR → LLVM JIT 引擎编译为机器码形成了一条完整链路本文即围绕这条链路的每一步展开。外部函数的两大类型C Functions 与 IR FunctionsGandiva 支持两种主要的外部函数类型C 函数C Functions符合 C 调用约定的函数。开发者可以用多种语言如 C、Rust、C、Zig实现逻辑再以 C 函数的形式暴露给 Gandiva。其核心优势是语言灵活、适用范围广几乎适用于所有使用场景集成也相对容易。IR 函数IR Functions以 LLVM IRLLVM 中间表示形式实现的函数。同样可以用多种语言编写然后编译成 LLVM IR 后注册进 Gandiva。从源码结构看Gandiva 对这两类函数的注册、查询和调用路径是分离但统一的gandiva::FunctionRegistry见 function_registry.h内部同时维护了pc_registry_预编译函数签名表、c_functions_C 函数指针列表与bitcode_memory_buffers_bitcode 缓冲区列表三类数据说明两类函数最终都汇聚到同一个注册表体系中只是实现来源不同。如何选择C 函数还是 IR 函数考量维度C 函数IR 函数语言灵活性高任意语言实现后暴露 C ABI需编译为 LLVM IR依赖 LLVM 工具链适用场景绝大多数通用场景逻辑简单的操作如单步算术/比较内联能力不可内联存在调用开销可内联对调用开销敏感的热点操作更优第三方库依赖直接链接即可依赖库也必须一并编译进 LLVM IR复杂库会拖累性能高级特性可使用线程局部变量等不支持线程局部变量受 Gandiva 内部 JIT 引擎限制文档中特别指出IR 函数的可内联优势在简单操作且调用开销占比显著的场景下收益最大而像使用 thread-local 变量这类高级特性由于当前 Gandiva 内部 JIT 引擎的限制IR 函数无法支持此时应选用 C 函数。外部函数注册总览元数据 实现要让一个函数对 Gandiva 可用必须完成两件事注册函数元数据签名与属性和提供函数实现C 函数指针或 LLVM IR 函数。元数据注册使用gandiva::NativeFunction类实现则通过gandiva::FunctionRegistry的三种Register重载接入。下面分别展开。用 NativeFunction 类注册函数元数据gandiva::NativeFunction类负责捕获外部函数的签名与元数据其定义位于 native_function.h构造函数见第 56-68 行NativeFunction(const std::string base_name, const std::vectorstd::string aliases, const DataTypeVector param_types, const DataTypePtr ret_type, const ResultNullableType result_nullable_type, std::string pc_name, int32_t flags 0);各构造参数含义如下base_name函数在表达式中使用的名字例如add、upper。aliases函数的别名列表。例如仓库中upper函数注册时就带有别名ucase见 function_registry_string.cc 第 90 行。从构造函数实现看native_function.hbase_name与每个别名都会各生成一个FunctionSignature存入signatures_即别名与主名在签名查找层面完全等价。param_typesstd::vectorstd::shared_ptrarrow::DataType函数接受的参数类型列表。ret_typestd::shared_ptrarrow::DataType函数返回类型。result_nullable_type结果可空性规则由输入的 nullability 推导取值为取值含义ResultNullableType::kResultNullIfNull结果有效性是各子节点有效性的交集任一输入为 null 则结果为 nullResultNullableType::kResultNullNever结果永远有效不产生 nullResultNullableType::kResultNullInternal结果有效性取决于函数内部逻辑该枚举定义在 native_function.h与文档完全一致。pc_name对应预编译函数precompiled function的名字。通常遵循{base_name}_{param1_type}_{param2_type}...{paramN_type}命名约定例如base_name为add、两个int32参数并返回int32的函数其预编译函数名为add_int32_int32。该约定并非强制只要保证唯一即可因为注册后引擎会以pc_name作为符号进行全局映射查找见下文AddGlobalMappingForFunc。flags可选函数属性标志默认 0。可组合使用以下位标志定义见 native_function.hNativeFunction::kNeedsContext函数需要一个int64_t context参数返回字符串类型时必需见下文NativeFunction::kNeedsFunctionHolder函数需要一个函数持有者function holder参数NativeFunction::kCanReturnErrors函数可以通过 context 返回错误信息。源码中的真实注册示例仓库内置函数的注册代码就是最好的范本。例如 function_registry_string.cc 第 77 行注册base64函数NativeFunction(base64, {}, DataTypeVector{binary()}, utf8(), kResultNullIfNull, base64_binary, NativeFunction::kNeedsContext)可以看到base64接受binary参数、返回utf8可空性为kResultNullIfNull预编译函数名为base64_binary并且由于返回类型是字符串必须带上kNeedsContext标志——这正是文档中返回 utf8 必须设置 kNeedsContext规则的直接体现。类似地upper别名ucasefunction_registry_string.cc也是典型的字符串返回函数。外部 C 函数开发C 函数签名映射表并非所有 Arrow 数据类型都被 Gandiva 外部函数支持且 Gandiva 类型与 C 函数签名类型存在固定映射。文档给出的完整映射表如下Gandiva 类型Arrow 数据类型C 函数类型int8int8_tint16int16_tint32int32_tint64int64_tuint8uint8_tuint16uint16_tuint32uint32_tuint64uint64_tfloat32floatfloat64doublebooleanbooldate32int32_tdate64int64_ttimestampint64_ttime32int32_ttime64int64_tinterval_monthint32_tinterval_day_timeint64_tutf8作为参数const char*,uint32_t见变长类型小节utf8作为返回int64_t context,const char*,uint32_t*见变长类型小节binary作为参数const char*,uint32_t见变长类型小节binary作为返回int64_t context,const char*,uint32_t*见变长类型小节这一映射在源码的 LLVM 签名生成逻辑中得到了印证external_c_functions.cc 的GetNumArgs会为kNeedsContext增加 1 个参数、为字符串类型参数额外增加 1 个长度参数、为字符串返回类型额外增加 1 个输出长度指针参数MapToLLVMSignature 则据此把字符串参数映射为i64context、i8*/指针加i32长度、i32*输出长度指针与上表逐项对应。变长类型utf8 / binary的特殊处理arrow::StringType即utf8类型与arrow::BinaryType都是变长类型在外部函数中的处理方式相同。由于utf8更常用文档以它为例说明变长类型的处理规则作为参数时对应的 C 函数需要接受两个参数const char*指向字符串数据的指针uint32_t字符串数据的长度。作为返回类型时需要满足以下四点元数据标志NativeFunction元数据必须包含NativeFunction::kNeedsContext标志这是保证函数上下文正确管理的关键见上文base64注册示例。函数参数C 函数开头要增加int64_t context参数负责上下文管理末尾要增加uint32_t*输出参数用于写回返回字符串的长度。返回值函数返回const char*指针指向字符串数据本身。实现要点函数体内应使用gdv_fn_context_arena_malloc进行内存分配、使用gdv_fn_context_set_error_msg设置错误信息。这两个辅助函数都以int64_t context作为第一个参数声明位于 gdv_function_stubs.hvoid gdv_fn_context_set_error_msg(int64_t context_ptr, const char* err_msg); uint8_t* gdv_fn_context_arena_malloc(int64_t context_ptr, int32_t data_len);gdv_fn_context_arena_malloc从 context 关联的 arena内存池对应SimpleArena机制中分配临时内存避免了每次调用都向系统申请堆内存的开销也保证了 JIT 生成代码中字符串返回值的生命周期安全。仓库内置的字符串/二进制处理函数如gdv_fn_base64_encode_binary、gdv_fn_base64_decode_utf8、gdv_fn_castVARBINARY_int32_int64见 gdv_function_stubs.h都是int64_t contextconst char*输入 int32_t*输出长度这一范式的现成范例可以直接参考其实现来编写自己的变长返回函数。C 函数注册 API使用gandiva::FunctionRegistry的注册 API 即可将外部 C 函数接入声明见 function_registry.h/// \brief register a C function into the function registry /// param func the registered functions metadata /// param c_function_ptr the function pointer to the /// registered functions implementation /// param function_holder_maker this will be used as the function holder if the /// function requires a function holder arrow::Status Register( NativeFunction func, void* c_function_ptr, std::optionalFunctionHolderMaker function_holder_maker std::nullopt);参数说明NativeFunction func外部 C 函数的元数据void* c_function_ptr指向外部 C 函数实现的函数指针可选的function_holder_maker当外部 C 函数需要函数持有者时用于创建该持有者的工厂函数。FunctionHolderMaker类型定义为std::functionarrow::Resultstd::shared_ptrFunctionHolder(const FunctionNode)可参考gandiva::FunctionHolder基类及其若干子类如InHolder、IntervalHolder、RandomGeneratorHolder、RegexFunctionsHolder、ToDateHolder等均位于 cpp/src/gandiva 目录下。从 function_registry.cc 的实现可以看到注册时如果提供了function_holder_maker它会以第一个签名的base_name为键注册到holder_maker_registry_随后函数指针被存入c_functions_列表并调用Add把NativeFunction的每个签名加入pc_registry_map_签名查找表。运行时external_c_functions.cc 的ExternalCFunctions::AddMappings会遍历c_functions_为每个签名计算 LLVM 签名并调用engine-AddGlobalMappingForFunc把pc_name映射到 C 函数指针从而让 JIT 生成的机器码能够直接调用你的 C 实现。外部 IR 函数开发实现与编译IR 函数允许你用多种语言实现然后编译为 Gandiva 可识别的 LLVM 中间表示使用 C 或 C实现代码编译成 LLVM bitcodeGandiva 理解的中间表示。C 实现可用 clang 的-emit-llvm选项直接编译出 LLVM bitcode例如clang -emit-llvm -c my_funcs.cpp -o my_funcs.bc集成 CMake在配合 CMake 的 C 项目中可以使用 Arrow 仓库提供的GandivaAddBitcode.cmake模块将自定义 bitcode 平滑地接入 Gandiva 构建流程。类型一致性要求IR 函数的参数与返回类型必须严格遵循前文 C 函数一节建立的规则以保证与 Gandiva 类型系统的兼容包括字符串类型的双参数/输出长度指针约定。在 Gandiva 中注册外部 IR 函数实现并编译出 LLVM bitcode 之后通过gandiva::FunctionRegistry提供的两个 API 完成注册声明见 function_registry.h从 bitcode 文件注册// Registers a set of functions from a specified bitcode file arrow::Status Register(const std::vectorNativeFunction funcs, const std::string bitcode_path);从 bitcode 缓冲区注册// Registers a set of functions from a bitcode buffer arrow::Status Register(const std::vectorNativeFunction funcs, std::shared_ptrarrow::Buffer bitcode_buffer);关键点这两个 API 用于一次性注册一组外部 IR 函数来源可以是 bitcode 文件路径也可以是已加载进内存的 bitcode 缓冲区arrow::Buffer必须确保 bitcode 文件/缓冲区中包含正确编译的 IR 函数实现每个NativeFunction实例用于定义被注册 IR 函数的元数据签名、可空性、pc_name 等。从 function_registry.cc 的实现看文件版本会先把 bitcode 读入内存并包装为LLVMMemoryArrowBuffer再统一走缓冲区版本bitcode 缓冲区被存入bitcode_memory_buffers_随后逐个Add到签名表。JIT 编译时LLVMGenerator会从GetBitcodeBuffers()取出这些缓冲区把其中的 IR 函数与签名表中注册的pc_name对应起来。此外MakeDefaultFunctionRegistry()function_registry.cc会把算术、日期时间、哈希、数学运算、字符串、日期时间算术六大内置注册表合并为默认注册表——你的外部函数注册到自定义的FunctionRegistry后同样可以随该注册表一起参与表达式编译。总结在 Gandiva 中扩展自定义函数的核心流程可以归纳为四步选型根据逻辑复杂度、内联需求、第三方库依赖和 LLVM 工具链集成情况在 C 函数与 IR 函数之间做出选择实现按签名映射表编写 C 函数注意变长字符串类型的const char* uint32_t参数约定与返回时的kNeedsContext标志、uint32_t*输出长度参数或将实现编译为 LLVM bitcode注册元数据用gandiva::NativeFunction描述 base_name、别名、参数/返回类型、结果可空性与 pc_name注册实现调用gandiva::FunctionRegistry::Register的三种重载之一C 函数指针、bitcode 文件或 bitcode 缓冲区把实现与元数据关联起来。本文涉及的NativeFunction、FunctionRegistry、FunctionHolder等类定义与内置注册实例均可在 cpp/src/gandiva 目录下的头文件与各function_registry_*.cc文件中找到完整实现需要处理更复杂场景如函数持有者、错误返回、十进制/哈希等特殊类型时直接阅读这些源码与 cpp/src/gandiva/tests 下的测试用例是最快的上手路径。【免费下载链接】arrowApache Arrow is the universal columnar format and multi-language toolbox for fast data interchange and in-memory analytics项目地址: https://gitcode.com/GitHub_Trending/arrow3/arrow创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表