第三次挑战 llc


每次进行 LLVM 开发,都会浪费 3min 的构建时间。

$ time ninja -C build modc
ninja: Entering directory `build'
[2/2] Linking CXX executable modc

real    3m10.387s
user    1m8.422s
sys     0m5.547s

—— 某位不愿透露姓名的垃圾

征服 TargetMachine

llc 代码中难度的巅峰就是关于 TargetMachine 的创建:

std::unique_ptr<TargetMachine> Target = std::unique_ptr<TargetMachine>(TheTarget->createTargetMachine(
  TheTriple.getTriple(), CPUStr, FeatureStr, Options, RM, codegen::getExplicitCodeModel, OLvl
));

来自于 Target 对象 TheTarget 的成员函数 createTargetMachine 来创建。 而 TheTarget 则是从 TargetRegistry::lookupTarget 创建而来。

在我的记忆中,llvm 后来加入了 mlir 的支持,llc 代码又多出了 mlir 的部分,和原来的代码耦合在一起,难度又增加了一些。以前用到的东西现在又放进了 lambda 中,再设置成 parseIRModule 的回调,就很淦。

下面是制作的不规整的数据流图:

TargetMachine 数据流图

优化链接速度

感谢 stackoverflow 上的提示,以及 GCC 放过一马,man pages 里面有写使用其他链接器:

-fuse-ld=lld
    Use the LLVM lld linker instead of the default linker.

CMakeLists.txt 中的写法:

set(CMAKE_CXX_FLAGS "-fuse-ld=lld")

结果就是 GNU ld 要 3min 的事情,lld 10m 就能搞定。

坑 1:LLVM CMake 与 llvm_map_components_to_libnames

LLVM 文档 中这么说:

For a list of available components look at the output of running llvm-config --components.

意思是:可以使用 llvm-config --components 来获取可用组件列表。 也就是这样:

$ llvm-config --components
aggressiveinstcombine all all-target analysis asmparser asmprinter binaryfomat bitreader bitstreamreader bitwriter cfguard ...(就先打这么多)

llc 源码中需要很多“所有目标”限定的东西,我一开始真的将 all-target 传给了 llvm_map_components_to_libnames

llvm_map_components_to_libnames(llvm_libs all-target support core irreader)

然后我会得到:

[build] /usr/bin/ld: 找不到 -lLLVMall-target: 没有那个文件或目录

看来是无脑拼接了。 这个问题的答案要去翻这个函数的实现的源码,在 llvm/cmake/modules/LLVM-Config.cmake 或者安装到了 /usr/local/lib/cmake/llvm/LLVM-Config.cmake。 在函数 llvm_expand_pseudo_components 函数实现中你会看到这些:

elseif( c STREQUAL "AllTargetsCodeGens")
  # Link all the codegens from all the targets
  foreach(t ${LLVM_TARGETS_TO_BUILD})
    if( TARGET LLVM${t}CodeGen)
      list(APPEND expanded_components "${t}CodeGen")
    endif()
  endforeach(t)
elseif( c STREQUAL "AllTargetsAsmParsers" )
...

这些名字与 llc 的 CMakeLists.txtLLVM_LINK_COMPONENTS 变量中的名字相同,于是乎,需要这么写:

llvm_map_components_to_libnames(llvm_libs
  AllTargetsAsmParsers
  AllTargetsCodeGens
  AllTargetsDescs
  AllTargetsInfos
)

坑 2:LLVM CMake 与 -fno-rtti

首先上报错:

[1/1] Linking CXX executable modc
FAILED: modc 
: && /usr/bin/c++ -g  CMakeFiles/modc.dir/modc.cpp.o -o modc  /usr/local/lib/libLLVMX86AsmParser.a  /usr/local/lib/libLLVMRISCVAsmParser.a  /usr/local/lib/libLLVMX86CodeGen.a  /usr/local/lib/libLLVMRISCVCodeGen.a  /usr/local/lib/libLLVMX86Desc.a  /usr/local/lib/libLLVMRISCVDesc.a  /usr/local/lib/libLLVMX86Info.a  /usr/local/lib/libLLVMRISCVInfo.a  /usr/local/lib/libLLVMAnalysis.a  /usr/local/lib/libLLVMAsmParser.a  /usr/local/lib/libLLVMAsmPrinter.a  /usr/local/lib/libLLVMCodeGen.a  /usr/local/lib/libLLVMCore.a  /usr/local/lib/libLLVMIRReader.a  /usr/local/lib/libLLVMMC.a  /usr/local/lib/libLLVMRemarks.a  /usr/local/lib/libLLVMScalarOpts.a  /usr/local/lib/libLLVMSelectionDAG.a  /usr/local/lib/libLLVMSupport.a  /usr/local/lib/libLLVMTarget.a  /usr/local/lib/libLLVMTransformUtils.a  /usr/local/lib/libLLVMVectorize.a  /usr/local/lib/libLLVMMCDisassembler.a  /usr/local/lib/libLLVMInstrumentation.a  /usr/local/lib/libLLVMCFGuard.a  /usr/local/lib/libLLVMDebugInfoMSF.a  /usr/local/lib/libLLVMGlobalISel.a  /usr/local/lib/libLLVMSelectionDAG.a  /usr/local/lib/libLLVMCodeGen.a  /usr/local/lib/libLLVMScalarOpts.a  /usr/local/lib/libLLVMAggressiveInstCombine.a  /usr/local/lib/libLLVMInstCombine.a  /usr/local/lib/libLLVMBitWriter.a  /usr/local/lib/libLLVMTarget.a  /usr/local/lib/libLLVMAsmParser.a  /usr/local/lib/libLLVMTransformUtils.a  /usr/local/lib/libLLVMAnalysis.a  /usr/local/lib/libLLVMProfileData.a  /usr/local/lib/libLLVMDebugInfoDWARF.a  /usr/local/lib/libLLVMObject.a  /usr/local/lib/libLLVMMCParser.a  /usr/local/lib/libLLVMMC.a  /usr/local/lib/libLLVMDebugInfoCodeView.a  /usr/local/lib/libLLVMBitReader.a  /usr/local/lib/libLLVMTextAPI.a  /usr/local/lib/libLLVMCore.a  /usr/local/lib/libLLVMRemarks.a  /usr/local/lib/libLLVMBitstreamReader.a  /usr/local/lib/libLLVMBinaryFormat.a  /usr/local/lib/libLLVMSupport.a  -lrt  -ldl  -lm  /usr/lib/libz.so  /usr/lib/libtinfo.so  /usr/local/lib/libLLVMDemangle.a && :
/usr/bin/ld: CMakeFiles/modc.dir/modc.cpp.o:(.data.rel.ro._ZTIN4llvm2cl15OptionValueCopyINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEEE[_ZTIN4llvm2cl15OptionValueCopyINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEEE]+0x10): undefined reference to `typeinfo for llvm::cl::GenericOptionValue'
collect2: 错误:ld 返回 1
ninja: build stopped: subcommand failed.

其中的重点其实是:

typeinfo for ...

后来才知道,这个问题与 RTTI 有关。

我这里编译的 LLVM 14.0.6,默认关闭 RTTI,也就是传递了参数 -fno-rtti。 并且 g++ 貌似默认开启 RTTI,在链接时就会报这个错。

这还要从开天辟地讲起(不是):

LLVM 的 这篇文档 中给出了使用 CMake 来使用 LLVM 项目的例子:

cmake_minimum_required(VERSION 3.13.4)
project(SimpleProject)

# 声明依赖
find_pageage(LLVM REQUIRED CONFIG)

message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}")
message(STATUS "Using LLVMConfig.cmake in: ${LLVM_DIR}")

# 添加头文件搜索目录
include_directories(${LLVM_INCLUDE_DIRS})
separate_arguments(LLVM_DEFINITIONS_LIST NATIVE_COMMAND ${LLVM_DEFINITIONS})
# 添加定义:后来才知道这块只会添加命令行参数的宏定义
add_definitions(${LLVM_DEFINITIONS_LIST})

# 定义可执行文件目标
add_executable(simple-tool tool.cpp)
# 声明 LLVM 库文件,写入 llvm_libs 变量,后面的参数指定要使用的组件
llvm_map_components_to_libnames(llvm_libs support core irreader)
# 链接 LLVM 库
target_link_libraries(simple-tool ${llvm_libs})

一开始我是百思不得其解,病急乱投医,直到我尝试看编译参数的时候,发现没有 -fno-rtti,而 llvm-config 中是有的:

build CMakeFiles/modc.dir/modc.cpp.o: CXX_COMPILER__modc_Debug /xxxxxx/project/modc.cpp || cmake_object_order_depends_target_modc
  DEFINES = -D_DEBUG -D_GNU_SOURCE -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS
  DEP_FILE = CMakeFiles/modc.dir/modc.cpp.o.d
  FLAGS = -g
  OBJET_DIR = CMakeFiles/modc.dir
  OBJECT_FILE_DIR = CmakeFiles/modc.dir
$ llvm-config --cxxflags
-I/usr/local/include -std=c++14    -fno-exceptions -fno-rtti -D_GNU_SOURCE -D_DEBUG -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS

直到我打印出 LLVM_DEFINITIONS 中的内容后才发现这个变量里面只有宏定义。。。

message(STATUS "${LLVM_DEFINITIONS}")
# [cmake] -- -D_GNU_SOURCE -D_DEBUG -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS

解决方法在上面 LLVM 文档中其实提到过说可以检查 LLVM_ENABLE_RTTI 等变量再手动添加编译器参数。 或者:我在 AddLLVM.cmake 中找到了一个调校 cmake 目标的一个函数 llvm_update_compile_flags

# 将 LLVM 安装的 cmake 脚本加入 cmake 脚本搜索路径
list(APPEND CMAKE_MODULE_PATH ${LLVM_CMAKE_DIR})
# 引入 AddLLVM
include(AddLLVM)
# 调校目标
llvm_update_compile_flags(modc)

坑 3:llvm::codegen::getMCPU()

首先上报错:

$ ./modc
modc: /xxxxxx/llvm-project/llvm/lib/CodeGen/CommandFlags.cpp:49: std::string llvm::codegen::getMCPU(): Assertion `MCPUView && "RegisterCodeGenFlags not created."' failed.
PLEASE submit a bug report to https://github.com/llvm/llvm-project/issues/ and include the crash backtrace.
0.	Program arguments: ./modc
 #0 0x000055bce74448ec llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) /xxxxxx/llvm-project/llvm/lib/Support/Unix/Signals.inc:565:22
 #1 0x000055bce74449af PrintStackTraceSignalHandler(void*) /xxxxxx/llvm-project/llvm/lib/Support/Unix/Signals.inc:632:1
 #2 0x000055bce7442508 llvm::sys::RunSignalHandlers() /xxxxxx/llvm-project/llvm/lib/Support/Signals.cpp:97:20
 #3 0x000055bce7444262 SignalHandler(int) /xxxxxx/llvm-project/llvm/lib/Support/Unix/Signals.inc:407:1
 #4 0x00007fb83db69a40 (/usr/lib/libc.so.6+0x38a40)
 #5 0x00007fb83dbb94dc (/usr/lib/libc.so.6+0x884dc)
 #6 0x00007fb83db69998 raise (/usr/lib/libc.so.6+0x38998)
 #7 0x00007fb83db5353d abort (/usr/lib/libc.so.6+0x2253d)
 #8 0x00007fb83db5345c (/usr/lib/libc.so.6+0x2245c)
 #9 0x00007fb83db624c6 (/usr/lib/libc.so.6+0x314c6)
#10 0x000055bce645851d llvm::codegen::getMCPU[abi:cxx11]() /xxxxxx/llvm-project/llvm/lib/CodeGen/CommandFlags.cpp:49:1
#11 0x000055bce645e706 llvm::codegen::getCPUStr[abi:cxx11]() /xxxxxx/llvm-project/llvm/lib/CodeGen/CommandFlags.cpp:548:17
#12 0x000055bce59255a3 main /xxxxxx/project/modc.cpp:36:61
#13 0x00007fb83db542d0 (/usr/lib/libc.so.6+0x232d0)
#14 0x00007fb83db5438a __libc_start_main (/usr/lib/libc.so.6+0x2338a)
#15 0x000055bce59253c5 _start /build/glibc/src/glibc/csu/../sysdeps/x86_64/start.S:117:0
已放弃 (核心已转储)

解决方法就在报错里:RegisterCodeGenFlags not created.。在 llc 源码中引用头文件后紧接着的一个名字空间使用,接着就是 RegisterCodeGenFlags 类对象的定义。搜索该字符串就能找到。 将该定义加入文件即可:

static llvm::codegen::RegisterCodeGenFlags CGF;

getMCPU() 是被我们代码中的 getCPUStr() 所调用,在头文件 llvm/CodeGen/CommandFlags.h 中。 该头文件是 LLVM 代码生成器的命令行参数相关内容,定义 RegisterCodeGenFlags 类对象相当于注册 LLVM 代码生成器命令行参数。 getMCPU() 作用是获取命令行参数 --mcpu=<cpu-name> 的内容,其帮助如下:

--mcpu=<cpu-name>    - Target a specific cpu type (-mcpu=help for details)

定义在 llvm/lib/CodeGen/CommandFlags.cpp 中:

// 使用 CGOPT 定义 getMCPU 函数
CGOPT(std::string, MCPU)

// 使用 CGBINDOPT 创建命令行参数,并处理 getMCPU 内部实现(绑定内部对象到这个命令行参数对象上)
CGBINDOPT(MCPU)

getFeaturesStr() 会使用的 getMAttrs() 同理。

坑 4:我真的不懂 llc 源码

首先上报错:

$ ./modc
       .text
       .file   "module name"
modc: /xxxxxx/llvm-project/llvm/lib/CodeGen/MachineFunction.cpp:207: void llvm::MachineFunction::init(): Assertion `Target.isCompatibleDataLayout(getDataLayout()) && "Can't create a MachineFunction using a Module with a " "Target-incompatible DataLayout attached\n"' failed.
PLEASE submit a bug report to https://github.com/llvm/llvm-project/issues/ and include the crash backtrace.
Stack dump:
0.	Program arguments: ./modc
1.	Running pass 'Function Pass Manager' on module 'module name'.
2.	Running pass 'X86 DAG->DAG Instruction Selection' on function '@main'
 #0 0x000055ef36259bfc llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) /xxxxxx/llvm-project/llvm/lib/Support/Unix/Signals.inc:565:22
 #1 0x000055ef36259cbf PrintStackTraceSignalHandler(void*) /hxxxxxx/llvm-project/llvm/lib/Support/Unix/Signals.inc:632:1
 #2 0x000055ef36257818 llvm::sys::RunSignalHandlers() /xxxxxx/llvm/llvm-project/llvm/lib/Support/Signals.cpp:97:20
 #3 0x000055ef36259572 SignalHandler(int) /xxxxxx/llvm-project/llvm/lib/Support/Unix/Signals.inc:407:1
 #4 0x00007fe7ecc51a40 (/usr/lib/libc.so.6+0x38a40)
 #5 0x00007fe7ecca14dc (/usr/lib/libc.so.6+0x884dc)
 #6 0x00007fe7ecc51998 raise (/usr/lib/libc.so.6+0x38998)
 #7 0x00007fe7ecc3b53d abort (/usr/lib/libc.so.6+0x2253d)
 #8 0x00007fe7ecc3b45c (/usr/lib/libc.so.6+0x2245c)
 #9 0x00007fe7ecc4a4c6 (/usr/lib/libc.so.6+0x314c6)
#10 0x000055ef3512acd9 llvm::MachineFunction::init() /xxxxxx/llvm-project/llvm/lib/CodeGen/MachineFunction.cpp:212:62
#11 0x000055ef3512a7d5 llvm::MachineFunction::MachineFunction(llvm::Function&, llvm::LLVMTargetMachine const&, llvm::TargetSubtargetInfo const&, unsigned int, llvm::MachineModuleInfo&) /xxxxxx/llvm-project/llvm/lib/CodeGen/MachineFunction.cpp:149:1
#12 0x000055ef35180105 llvm::MachineModuleInfo::getOrCreateMachineFunction(llvm::Function&) /xxxxxx/llvm-project/llvm/lib/CodeGen/MachineModuleInfo.cpp:303:8
#13 0x000055ef3514973c llvm::MachineFunctionPass::runOnFunction(llvm::Function&) /xxxxxx/llvm-project/llvm/lib/CodeGen/MachineFunctionPass.cpp:44:55
#14 0x000055ef35807bdd llvm::FPPassManager::runOnFunction(llvm::Function&) /xxxxxx/llvm-project/llvm/lib/IR/LegacyPassManager.cpp:1434:20
#15 0x000055ef35807eaa llvm::FPPassManager::runOnModule(llvm::Module&) /xxxxxx/llvm-project/llvm/lib/IR/LegacyPassManager.cpp:1480:13
#16 0x000055ef35808309 (anonymous namespace)::MPPassManager::runOnModule(llvm::Module&) /xxxxxx/llvm-project/llvm/lib/IR/LegacyPassManager.cpp:1549:20
#17 0x000055ef35803042 llvm::legacy::PassManagerImpl::run(llvm::Module&) /xxxxxx/llvm-project/llvm/lib/IR/LegacyPassManager.cpp:539:13
#18 0x000055ef35808bf5 llvm::legacy::PassManager::run(llvm::Module&) /xxxxxx/llvm-project/llvm/lib/IR/LegacyPassManager.cpp:1677:1
#19 0x000055ef343c5fa0 compileModule(llvm::LLVMContext&) /xxxxxx/llvm-test/modc.cpp:188:12
#20 0x000055ef343c5854 main /xxxxxx/llvm-test/modc.cpp:105:25
#21 0x00007fe7ecc3c2d0 (/usr/lib/libc.so.6+0x232d0)
#22 0x00007fe7ecc3c38a __libc_start_main (/usr/lib/libc.so.6+0x2338a)
#23 0x000055ef343c53b5 _start /build/glibc/src/glibc/csu/../sysdeps/x86_64/start.S:117:0
已放弃 (核心已转储)

问题的核心是那块失败的断言,llc 代码中定义了一个 lambda SetDataLayout 作为 parseIRModule() 函数的回调函数。在其中创建 TargetMachine 并返回 datalayout 字符串。

auto SetDataLayout = [&](StringRef DataLayoutTargetTriple) -> Optional<std::string> {
  ...
  return Target->createDataLayout().getStringRepresentation();
}

M = parseIRFile(InputFilename, Err, Context, SetDataLayout);

我猜测我生成的 Module 根本没考虑这个 datalayout,于是按照自己的理解使用默认 Target 创建了一个 datlayout 并设置给 Module

std::unique_ptr<Module> module;
Triple TheTriple;
std::string CPUStr = codegen::getCPUStr();
std::string FeaturesStr = codegen::getFeaturesStr();

CodeGenOpt::Level OLvl = CodeGenOpt::Default;
// ...

const Target *TheTarget = nullptr;
std::unique_ptr<TargetMachine> Target;
TargetOptions Options;
Optional<Reloc::Model> RM = codegen::getExplicitRelocModel();

// 准备 module
TheTriple.setTriple(sys::getDefaultTargetTriple()); // 默认 target
std::string Error;
TheTarget = TargetRegistry::lookupTarget(codegen::getMArch(), TheTriple, Error);
if (!TheTarget)
{
  errs() << Error;
  exit(1);
}

auto InitializeOptions = [&](const Triple &TheTriple) {
  Options = codegen::InitTargetOptionsFromCodeGenFlags(TheTriple);
  Options.BinutilsVersion = TargetMachine::parseBinutilsVersion("");
}
InitializeOptions(TheTriple);

Target = std::unique_ptr<TargetMachine>(TheTarget->createTargetMachine(
  TheTriple.getTriple(), CPUStr, FeatureStr, Options, RM, codegen::getExplicitCodeModel, OLvl
));
assert(Target && "Could not allocate target machine!");

// 设置 target 和 datalayout
module->setTargetTriple(TheTriple.getTriple());
module->setDataLayout(Target->createDataLayout().getStringRepresentation());

运行结果

生成的 LLVM IR 是这样:

; ModuleID = 'module name'
source_filename = "module name"

define i32 @main() {
EntryBasicBlock:
  %addop = add i32 42, 42
  ret i32 %addop
}

打印出经过优化的机器码(但是汇编):

$ ./modc
        .text
        .file   "module name"
        .globl  main
        .p2align        4, 0x90
        .type   main,@function
main:
        .cfi_startproc
        movl    $84, %eax
        retq
.Lfunc_end0:
        .size   main, .Lfunc_end0-main
        .cfi_endproc

        .section        ".note.GNU-stack","",@progbits