简介
本文内容基于LLVM 13.0.0。
目前基于LLVM的Fortran编译器(或者驱动)有3种,分别是flang、f18和flang-new。
flang是pgfortran的开源版本,基于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枚举里定义的 PreprocessJobClass
, CompileJobClass
, BackendJobClass
和LinkJobClass
和LinkJobClass
,以及一些比较特殊的不直接映射到常见编译步骤的动作,比如MigrateJobClass
和InputClass
。具体运行哪个动作,由编译选项决定,比如:
- -E表示PreprocessJobClass
- -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_main在flang/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);
}