LLVM优化入门

发布于 2019-05-28  95 次阅读


PS: Clang为LLVM提供的C语言编译器,默认参数可以生成本机可执行的二进制程序。-S和-c参数与GCC一样,可分别生成.s汇编文件与.o目标文件。

将C文件编译为LLVM bitcode文件

clang -O3 -emit-llvm hello.c -c -o hello.bc

-emit-llvm选项可与-S或-c选项一起使用,以分别为代码生成LLVM .ll.bc文件。两者都是LLVM Bitcode,区别在于前者是可读的文本,后者是不可读的二进制格式。
这两个文件都可以使用标准LLVM的工具来操作它。
bc文件可以使用lli工具执行,lli 可以通过解释器或使用高级选项中的即时 (JIT) 编译器执行此工作。请参阅 参考资料,获取关于 lli 的更多信息的链接。

lli hello.bc

llvm-dis工具可用来反汇编LLVM bitcode文件,它会将bc文件的内容以可读文本的形式进行展示。

llvm-dis < hello.bc

bc文件转换成ll文件

llvm-dis hello.bc

llc将 LLVM 字节代码转换成特定于平台的汇编代码

写第一个pass

理论基础

pass机制是llvm系统的一项非常重要的组成部分,也是整个llvm编译器体系中最为有趣和复杂的部分,通过编写pass来为llvm进行代码的优化、转化,或者进行分析以供为优化和转化提供依据。

所有的pass都是llvm的Pass类的子类,通过重写继承的虚函数来实现特定的功能。根据pass不同的功能分类,继承的类也不同,比如:ModulePass , CallGraphSCCPass, FunctionPass , LoopPass, RegionPass, BasicBlockPass,llvm系统会根据实例的类别来判断pass的功能,然后将其整合到现有的优化体系中去。

接下来我们先从简单的开始,比如写一个最简单的hello world!的pass,全面介绍编写pass的代码结构、编译方法、加载方式和执行结果,由浅入深。

hello world!这个pass仅仅打印代码里的函数,并不会对函数或者代码做任何修改,只是Analyze分析一下。这个hello world!的pass的源码和环境其实已经在lib/Transforms/Hello文件夹里了,因为它是官方的“Hello World!”

:~/llvm-8.0.0.src/lib/Transforms$ tree -NCfhl |grep Hello
├── [4.0K]  ./Hello
│   ├── [ 431]  ./Hello/CMakeLists.txt
│   ├── [1.9K]  ./Hello/Hello.cpp
│   └── [   0]  ./Hello/Hello.exports

准备编译环境

首先在lib/Transforms/目录下创建Hello文件夹,并且在文件夹内创建一个编译文件CMakeLists.txt,内容如下:

# If we don't need RTTI or EH, there's no reason to export anything
# from the hello plugin.
if( NOT LLVM_REQUIRES_RTTI )
  if( NOT LLVM_REQUIRES_EH )
    set(LLVM_EXPORTED_SYMBOL_FILE ${CMAKE_CURRENT_SOURCE_DIR}/Hello.exports)
  endif()
endif()

if(WIN32 OR CYGWIN)
  set(LLVM_LINK_COMPONENTS Core Support)
endif()

add_llvm_library( LLVMHello MODULE BUILDTREE_ONLY
  Hello.cpp

  DEPENDS
  intrinsics_gen
  PLUGIN_TOOL
  opt
  )

该编译文件将会被make命令所加载和使用,将文件夹内的Hello.cpp编译并且链接成为一个库文件$(LEVEL)/lib/LLVMHello.so,该库文件可以被opt命令使用-load参数来动态加载。
接下来要修改一下上层目录中的lib/Transforms/CMakeLists.txt文件,加上一行:

add_subdirectory(Hello)

可以看到文件内其实已经“注册”好了大量的pass,这些都会最终被编译系统所编译。

$ cat CMakeLists.txt
add_subdirectory(Utils)
add_subdirectory(Instrumentation)
add_subdirectory(AggressiveInstCombine)
add_subdirectory(InstCombine)
add_subdirectory(Scalar)
add_subdirectory(IPO)
add_subdirectory(Vectorize)
add_subdirectory(Hello)
add_subdirectory(ObjCARC)
add_subdirectory(Coroutines)

编写代码并编译

接下来就是编写Hello.cpp代码,虽然其实在源码中已经编写好了,我们也稍微注释和解释一下。

开头是包含一些头文件,因为我们写的是Pass,我们操作的是Function,还要做一点输出的工作。

#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"

在llvm命名空间内活动

using namespace llvm;

创建一个匿名的命名空间,C++中的匿名空间就像C代码中的静态块一样,限定了空间内的变量的访问范围仅限于匿名空间内部。这也体现了pass之间是相互独立的,并不需要另外的界面或者接口(当然这些也是可以有的,只是不需要而已)

namespace {

接下来定义我们的pass类——"Hello",继承于FunctionPass类。FunctionPass类一次只操作一个函数。

struct Hello : public FunctionPass {

声明一个pass的ID,llvm将会使用ID来定位这些pass,这样就避免了llvm使用复杂的C++运行时机制。

static char ID;
Hello() : FunctionPass(ID) {}

定义runOnFunction方法来覆写虚函数,在方法内实现我们想要的操作和功能,当然,目前只是打印出函数名就好。

  bool runOnFunction(Function &F) override {
    errs() << "Hello: ";
    errs().write_escaped(F.getName()) << 'n';
    return false;
  }
}; // end of struct Hello
}  // end of anonymous namespace

将ID初始化一下。llvm使用ID的地址来定位pass,所以初始化的值是多少都没关系。

char Hello::ID = 0;

最后将类注册一下,给一个命令行选项为hello,给一个名字“Hello World Pass”。最后两个参数定义了pass的行为,如果只是遍历CFG并不修改的话,则第三个参数为true,反之为false。如果该pass是一个analyze的pass的话,第四个参数为true,反之为false。

static RegisterPass<Hello> X("hello", "Hello World Pass",
                             false /* Only looks at CFG */,
                             false /* Analysis Pass */);

把代码全部组合起来,即官方的示例代码:

//===- Hello.cpp - Example code from "Writing an LLVM Pass" ---------------===//
//
//                     The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//
//
// This file implements two versions of the LLVM "Hello World" pass described
// in docs/WritingAnLLVMPass.html
//
//===----------------------------------------------------------------------===//

#include "llvm/ADT/Statistic.h"
#include "llvm/IR/Function.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;

#define DEBUG_TYPE "hello"

STATISTIC(HelloCounter, "Counts number of functions greeted");

namespace {
  // Hello - The first implementation, without getAnalysisUsage.
  struct Hello : public FunctionPass {
    static char ID; // Pass identification, replacement for typeid
    Hello() : FunctionPass(ID) {}

    bool runOnFunction(Function &F) override {
      ++HelloCounter;
      errs() << "Hello: ";
      errs().write_escaped(F.getName()) << '\n';
      return false;
    }
  };
}

char Hello::ID = 0;
static RegisterPass<Hello> X("hello", "Hello World Pass");

namespace {
  // Hello2 - The second implementation with getAnalysisUsage implemented.
  struct Hello2 : public FunctionPass {
    static char ID; // Pass identification, replacement for typeid
    Hello2() : FunctionPass(ID) {}

    bool runOnFunction(Function &F) override {
      ++HelloCounter;
      errs() << "Hello: ";
      errs().write_escaped(F.getName()) << '\n';
      return false;
    }

    // We don't modify the program, so we preserve all analyses.
    void getAnalysisUsage(AnalysisUsage &AU) const override {
      AU.setPreservesAll();
    }
  };
}

char Hello2::ID = 0;
static RegisterPass<Hello2>
Y("hello2", "Hello World Pass (with getAnalysisUsage implemented)");

在build目录下执行make,生成LLVMHello.so
编译完成之后,可以在lib/LLVMHello.so路径找到我们的共享库文件。

使用opt命令加载pass

既然我们已经编译成功了,接下来就是加载了。使用opt命令的-load选项来加载该pass,并且可是使用该pass的参数-hello,当然前提是已经加载成功了。

clang -S -O3 -emit-llvm sample.c

这时候在目录下就会出现sample.ll的IR中间码。可以用刚刚写的pass来观察下函数名:

./opt -load ../lib/LLVMHello.dylib -hello < hello.ll > /dev/null
Hello: main

-load参数指定共享库的路径并将其加载,加载之后-hello参数才是可以使用的,当然也是我们注册了这个参数的结果。因为-hello并没有修改任何函数,所以输出肯定跟输入是一样的,直接扔到/dev/null里去就行了。
我们上文还注册了一串字符串呢,大家还记得么?也是可以看到的。

./opt -load ../lib/LLVMHello.dylib -help |grep -i hello
    -hello                                          - Hello World Pass

既然我们的pass已经加载起来跑起来了,可以使用PassManager的-time-passes命令行工具来测一下pass的执行时间,该工具会统计生效中的所有pass的执行时间。

./opt -load ../lib/LLVMHello.dylib -hello -time-passes < sample.ll  > /dev/null

可以看到占用时间最多的是Bitcode Writer,其次才是Hello World Pass,说明该pass并没有占用多长时间。