Impala中 LLVM 的交叉编译、调用过程

2019-11-19  本文已影响0人  GOGOYAO

[TOC]

本文主要介绍通过gen_ir_descriptions.py引入的 LLVM IR 函数(交叉编译)。Impala 还可以通过加载文件(hdfs 或本地文件)引入 IR 函数,本文暂不介绍。

1. 简介

Impala 使用的 LLVM JIT,首先通过 Clang 将源码编译成了 LLVM IR 文件,然后通过脚本将 IR 文件装成可加载的二进制文件,BE 进程在运行过程中,通过 LLVM 的加载接口,把二进制文件加载进来使用。

2. 编译流程

待编译的文件通过codegen/impala-ir.cpp指定

// impala_ir.cpp
#ifdef IR_COMPILE

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wheader-hygiene"

#include "codegen/codegen-anyval-ir.cc"
#include "exec/grouping-aggregator-ir.cc"
#include "exec/hash-table-ir.cc"

......
#pragma clang diagnostic pop

// Unused function to make sure printf declaration is included in IR module. Used by
// LlvmCodegen::CodegenDebugTrace().
void printf_dummy_fn() {
  printf("dummy");
}

#else
#error "This file should only be used for cross compiling to IR."
#endif

impala-ir.cpp文件主要的作用就是把需要产生 LLVM IR 的文件包含进来。
确定了哪些文件需要产生 LLVM IR 之后,就开始生成 IR 的二进制文件了。大致流程如下:
{c源文件 => bc 文件 => 优化后的bc 文件 => 可加载二进制 c 文件}

在 Impala 中,有两个版本的 LLVM 代码生成:SSE和非 SSE。根据环境自动适配,二者的编译方式除了"-msse4.2"编译选项的区别,其他全部一致。后文将以 SSE 版本为例进行解说。

2.1. cpp 文件 => bc 文件

这个阶段生成最初始的bc文件,使用的是 CLang 的编译工具。命令可见codegen/CMakeFiles.txt

set(IR_INPUT_FILES impala-ir.cc)
set(IR_SSE_TMP_OUTPUT_FILE "${LLVM_IR_OUTPUT_DIRECTORY}/impala-sse-tmp.bc")

${LLVM_CLANG_EXECUTABLE} ${CLANG_IR_CXX_FLAGS} "-msse4.2" ${CLANG_INCLUDE_FLAGS} ${IR_INPUT_FILES} -o ${IR_SSE_TMP_OUTPUT_FILE}

生成的结果是impala-sse-tmp.bc文件。

2.2. bc 文件 => 优化后的 bc 文件

使用LLVM 优化工具,对原始的 bc 文件进行优化。命令可见codegen/CMakeFiles.txt

set(IR_SSE_TMP_OUTPUT_FILE "${LLVM_IR_OUTPUT_DIRECTORY}/impala-sse-tmp.bc")
set(IR_SSE_OUTPUT_FILE "${LLVM_IR_OUTPUT_DIRECTORY}/impala-sse.bc")

${LLVM_OPT_EXECUTABLE} ${LLVM_OPT_IR_FLAGS} < ${IR_SSE_TMP_OUTPUT_FILE} > ${IR_SSE_OUTPUT_FILE}

生成的结果就是impala-sse.bc。

2.3. 优化后的 bc 文件 => 可加载的二进制c文件

这一步使用的是Impala 自定义的一个脚本file2array.sh,将优化后的 bc 文件转换为可加载的二进制c 文件。命令可见codegen/CMakeFiles.txt。

set(IR_SSE_C_FILE $ENV{IMPALA_HOME}/be/generated-sources/impala-ir/impala-sse-ir.cc)
set(IR_SSE_TMP_C_FILE ${IR_SSE_C_FILE}.tmp)

$ENV{IMPALA_HOME}/bin/file2array.sh -n -v impala_sse_llvm_ir ${IR_SSE_OUTPUT_FILE}
COMMAND mv ${IR_SSE_TMP_C_FILE} ${IR_SSE_C_FILE}

生成的结果是impala-sse-ir.cc。这个文件内部就是用一个数组存放二进制的值。

const unsigned char impala_sse_llvm_ir[] = {
  0x42, 0x43, 0xc0, 0xde, 0x21, 0x0c, 0x00, 0x00, 0xba, 0x30, 0x02, 0x00,
  0x0b, 0x82, 0x20, 0x00, 0x02, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00,
  0x07, 0x81, 0x23, 0x91, 0x41, 0xc8, 0x04, 0x49, 0x06, 0x10, 0x32, 0x39,
  0x92, 0x01, 0x84, 0x0c, 0x25, 0x05, 0x08, 0x19, 0x1e, 0x04, 0x8b, 0x62,
  0x80, 0x30, 0x45, 0x02, 0x42, 0x92, 0x0b, 0x42, 0x84, 0x11, 0x32, 0x14,
......
};

be 进程就是通过读取impala_sse_llvm_ir数组,把 LLVM IR加载到进程中。
file2array.sh 脚本其实就是使用xxd -i < impala-sse-ir.cc命令把bc 文件内容转成 c 语言的二进制形式。

3. 加载和调用

LlvmCodeGen 类通过CreateImpalaCodegen接口实例化 codegen 对象。CreateImpalaCodegen最终会调用CreateFromMemory,在CreateFromMemory中就是将上文中生成的impala_sse_llvm_ir数组通过 LLVM 接口加载进来。

完成加载后,就可以通过GetFunction获取指定的 IR 函数了。

4. 函数列表和描述

所有的函数名及描述,定义在impala-ir-names.himpala-ir-functions.h,这两个文件是有对应关系的,都是通过gen_ir_descriptions.py生成。

impala-ir-names.h定义了数组FN_MAPPINGS,存储函数名和枚举值的映射关系,如下:

// impala-ir-names.h 片段
static struct {
  std::string fn_name;
  IRFunction::Type fn;
} FN_MAPPINGS[] = {
  { "process_row_batch_with_grouping", IRFunction::AGG_NODE_PROCESS_ROW_BATCH_WITH_GROUPING },
  { "process_row_batch_no_grouping", IRFunction::AGG_NODE_PROCESS_ROW_BATCH_NO_GROUPING },
  { "12HashJoinNode19process_build_batch", IRFunction::HASH_JOIN_PROCESS_BUILD_BATCH },
  { "12HashJoinNode19process_probe_batch", IRFunction::HASH_JOIN_PROCESS_PROBE_BATCH },
  ......
};

impala-ir-functions.h定义了所有函数的枚举值,如下:

class IRFunction {
 public:
  enum Type {
    FN_START = 0,
    AGG_NODE_PROCESS_ROW_BATCH_WITH_GROUPING = 0,
    AGG_NODE_PROCESS_ROW_BATCH_NO_GROUPING = 1,
    HASH_JOIN_PROCESS_BUILD_BATCH = 2,
    HASH_JOIN_PROCESS_PROBE_BATCH = 3,
    ......
  }
};

4.1. 获取函数时使用

通过GetFunction获取函数的时候,因为有了FN_MAPPINGS存储的映射关系,可以通过传入枚举值或者字符串符号查找函数。

4.2. 初始化时使用

InitializeLlvm方法中会使用FN_MAPPINGS,对加载的 llvm 函数进行校验。

  // Validate the module by verifying that functions for all IRFunction::Type
  // can be found.
  for (int i = IRFunction::FN_START; i < IRFunction::FN_END; ++i) {
    DCHECK_EQ(FN_MAPPINGS[i].fn, i);
    const string& fn_name = FN_MAPPINGS[i].fn_name;
    if (init_codegen->module_->getFunction(fn_name) == nullptr) {
      return Status(Substitute("Failed to find function $0", fn_name));
    }
  }

Tips: 学习 codegen 的接口,可以多看下对应的 ut

上一篇 下一篇

猜你喜欢

热点阅读