基于LLVM的Fortran编译器分析

本文详细介绍了基于LLVM的Fortran编译器,包括flang、f18和flang-new。flang作为pgfortran的开源版,而f18是一个全新的Fortran前端,已纳入LLVM子项目。flang-new则是未来的主力,但目前不支持完整的编译流程。编译安装过程涉及定制的LLVM项目和依赖于gfortran的代码生成。在原理上,flang利用PGI IR转换为LLVM IR,而flang-new的编译阶段还未完全实现。

简介

本文内容基于LLVM 13.0.0。

目前基于LLVM的Fortran编译器(或者驱动)有3种,分别是flang、f18和flang-new。

flangpgfortran的开源版本,基于PGI/NVIDIA的商业Fortran 编译器,它并不从属于LLVM项目。NVIDIA团队在2018年宣布了Fortran的新前端——f18,f18是使用现代 C++ 从头开始​​编写的,它将与 LLVM 最佳实践紧密结合,并以 LLVM 和 clang 的风格编写,f18已经被纳入为LLVM子项目。flang-new是一款新的flang驱动,在未来将会取代f18驱动。flang-new目前没有实现Fortran程序从源码生成.out的完整过程,f18可以生成.out,但是是借助外部编译器来完成的,默认外部编译器为gfortran。

以下表格展示了f18和flang-new在编译器驱动和前端驱动的细分,编译器驱动程序将允许您控制所有编译阶段(即预处理、前端代码生成、中间/后端代码优化和降级、链接),前端驱动程序将所有前端库粘​​合在一起,并为前端提供易于使用且直观的接口。

Compiler driver

Frontend driver

f18

f18

f18

flang-new

flang-new

flang-new -fc1

在前端驱动方面,flang-new -fc1和f18完全兼容,在编译器驱动方面,flang-new尚不支持代码生成(code-generate),f18调用一个独立的外部Fortran编译器来生成代码。

编译安装

flang

根据github上的介绍,安装flang需要下载flang项目(https://github.com/flang-compiler/flang)和定制的LLVM项目(https://github.com/flang-compiler/classic-flang-llvm-project)。

编译时,先用gcc编译安装定制的LLVM,再用生成的clang编译安装libpgmath库和flang。编译LLVM时,需要把clang和openmp都装上,因为flang需要使用到openmp库。

编译安装成功后,flang相关的可执行文件也放在LLVM的bin目录下,其中flang1负责输出PGI IR,flang2负责输出LLVM IR。

f18&flang-new

宏FLANG_BUILD_NEW_DRIVER控制是否需要安装flang-new,默认为on,flang-new依赖于clang驱动,所以在安装flang-new的时候也需要安装clang,通过LLVM_ENABLE_PROJECTS把clang设置上,如果FLANG_BUILD_NEW_DRIVER设置为off,则不需要安装clang。

在f18被移除之后,宏FLANG_BUILD_NEW_DRIVER也会被一并删除,这意味着接下来LLVM中flang驱动对clang的依赖将是必须和永久的。

cmake -G "Unix Makefiles" 
-DLLVM_ENABLE_PROJECTS='clang;flang' 
-DCMAKE_INSTALL_PREFIX=../x86_tools 
-DCMAKE_BUILD_TYPE=Release 
-DLLVM_TARGETS_TO_BUILD=X86 
../llvm

编译安装成功后在bin目录下生成了以下两个flang驱动——f18和flang-new:

图中的flang并不是独立的可执行文件,不能通过gdb被调试,目前在使用时会被扩展为f18,在未来会根据FLANG_BUILD_NEW_DRIVER的设置被扩展为flang-new,同样地,flang_fc1被扩展为flang-new -fc1。

原理介绍

flang

flang在PGI Fortran编译器的基础之上,新增了将PGI中间表示转换为LLVM中间表示的能力,并提供了PGI Fortran的运行时,有了LLVM中间表示后就可以利用起LLVM的后端功能,从而进一步生成二进制文件。可以说,flang也是借助了外部编译器来完成Fortran的编译。

如前文所述,flang1负责输出PGI IR,flang2负责输出LLVM IR。flang1包含以下阶段:

1.扫描提取文本token

2.创建语法树和符号表

3.转换ASTcanonical为AST

4.将一般AST转换为优化的AST

5.创建AST ILM文件,即PGI IR1

flang2包含以下阶段:

1.ILM扩展为ILI文件,即PGI IR2

2.优化ILI文件

3.将ILI优化为LLVM IR

flang-new

编译阶段说明

官方文档表明flang编译分为以下8个阶段:

1.预扫描和预处理

flang-new -fc1 -E src.f90

这一阶段与一般的编译器的预处理阶段是一致的,操作包括宏替换、删除空格和注释等。

2.解析

flang-new -fc1 -fdebug-dump-parse-tree src.f90

将第1步中的输出转储为解析树

flang-new -fc1 -fdebug-unparse src.f90

将解析树转换为标准的Fortran源码

3.验证标签并规范化Do语句

4.解析名称

flang-new -fc1 -fdebug-dump-symbols src.f90

5.检查DO CONCURRENT约束

6.编写模块文件

7.分析表达式和任务

8.生成中间表示

实际上,由于开发尚未完成,目前的f18和flang-new还不能生成LLVM中间表示。

编译器驱动

flang-new的编译器驱动的主入口点的实现在flang/tools/flang-driver/driver.cpp中,它是基于clang的驱动库来实现的,这样的好处在于以下2点:

1. 受益于clang对各种目标、平台和操作系统的支持

2. 利用clang驱动LLVM中各种后端以及链接器、汇编器能力,所有的flang驱动器选项和clang的选项都定义在clang/include/clang/Driver/Options.td里,对于两者通用的选项,定义是同等共享的。

基于clangDriver的编译器驱动通过创建跟大量编译阶段相关的动作(action)来工作,比如clang::driver::Action::ActionClass枚举里定义的 PreprocessJobClassCompileJobClassBackendJobClass LinkJobClassLinkJobClass以及一些比较特殊的不直接映射到常见编译步骤的动作,比如MigrateJobClassInputClass。具体运行哪个动作,由编译选项决定,比如:

  1. -E表示PreprocessJobClass
  2. -c表示CompileJobClass

在大多数情况下,驱动会创建一个关于动作(action)/任务(job)/阶段(phase)的链(chain)来串起整个流程,可以使用-ccc-print-phases选项打印出驱动器为当前编译所生成的序列:

flang-new -ccc-print-phases -c file.f

         +- 0: input, "file.f", f95-cpp-input

      +- 1: preprocessor, {0}, f95

   +- 2: compiler, {1}, ir

+- 3: backend, {2}, assembler

4: assembler, {3}, object

前端驱动

flang-new的前端驱动程序是用户和flang前端之间的主要接口,主入口点fc1_mainflang/tools/flang-driver/driver.cpp里实现,通过flang-new -fc1访问。前端驱动程序一次只会运行一个动作(action),如果指定多个操作选项,则仅最后一个有效。

源码分析

flang

在编译安装后的bin目录中可以看到flang可执行文件是指向clang的,所以当执行flang时,main函数进的是clang的driver.cpp(clang/tools/driver/driver.cpp),通过以下代码解析当前需要使用flang:

auto TargetAndMode = ToolChain::getTargetAndModeFromProgramName(argv[0]);

此时的TargetAndMode打印出来为:

$1 = {TargetPrefix = "", ModeSuffix = "flang", DriverMode = 0x118ea194 "--driver-mode=flang", TargetIsValid = false}

与常规clang驱动流程的不同之处在于,flang会使用ClassicFlang.cpp中的ConstructJob()函数来组装flang的任务,该函数中指定了一系列调flang1和flang2所需要的参数和宏等内容,然后添加任务。

……

const char *UpperExec = Args.MakeArgString(getToolChain().GetProgramPath("flang1"));

……

C.addCommand(std::make_unique<Command>(JA, *this, UpperExec, UpperCmdArgs, Inputs));

const char *UpperExec = Args.MakeArgString(getToolChain().GetProgramPath("flang2"));

……

C.addCommand(std::make_unique<Command>(JA, *this, UpperExec, UpperCmdArgs, Inputs));

有了上面两个任务,Driver会调到以下函数逐个执行:

void Compilation::ExecuteJobs(const JobList &Jobs,

                              FailingCommandList &FailingCommands) const {

  // According to UNIX standard, driver need to continue compiling all the

  // inputs on the command line even one of them failed.

  // In all but CLMode, execute all the jobs unless the necessary inputs for the

  // job is missing due to previous failures.

  for (const auto &Job : Jobs) {

    if (!InputsOk(Job, FailingCommands))

      continue;

    const Command *FailingCommand = nullptr;

    if (int Res = ExecuteCommand(Job, FailingCommand)) {

      FailingCommands.push_back(std::make_pair(Res, FailingCommand));

      // Bail as soon as one command fails in cl driver mode.

      if (TheDriver.IsCLMode())

        return;

    }

  }

}

最终会传递给llvm::sys::ExecuteAndWait()函数。

return llvm::sys::ExecuteAndWait(Executable, Args, Env, Redirects,

                                   /*secondsToWait*/ 0,

                                   /*memoryLimit*/ 0, ErrMsg, ExecutionFailed);

完整调用栈如下图所示,flang1和flang2是完全一致的。

f18

f18的main函数在flang/tools/f18/f18.cpp中,在main函数中通过F18_FC环境变量确定了外部Fortran编译器,对参数做了一系列准备后,通过Link()->Exec()最终调到llvm/lib/Support/Program.cpp中的llvm::sys::ExecuteAndWait()函数实现程序的执行。未来f18.cpp将会被移除,与之相关的代码也将被更新或删除。

int main(int argc, char *const argv[]) {

  atexit(CleanUpAtExit);



  DriverOptions driver;

  const char *F18_FC{getenv("F18_FC")};

  driver.F18_FCArgs.push_back(F18_FC ? F18_FC : "gfortran");

  bool isPGF90{driver.F18_FCArgs.back().rfind("pgf90") != std::string::npos};

……

  if (!driver.compileOnly && !objlist.empty()) {

    Link(liblist, objlist, driver);

  }

  return exitStatus;

}

flang-new

flang-new没有调用外部Fortran编译器,而是自己组织action,同样也是调到llvm::sys::ExecuteAndWait()函数实现程序的执行,但由于尚未开发完成,在ExecuteAction()的时候直接报错。

void EmitObjAction::ExecuteAction() {

  CompilerInstance &ci = this->instance();

  unsigned DiagID = ci.diagnostics().getCustomDiagID(

      clang::DiagnosticsEngine::Error, "code-generation is not available yet");

  ci.diagnostics().Report(DiagID);

}

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值