了 CMake 3.15,在 Windows 上用 GNU 風格的命令行使用 Clang 編譯器成為可能。這也意味著可以用Mingw-w64工具鏈來使用Clang。文末附上下載地址
雖然可以用Mingw-w64(或MinGW)工具鏈來配置Clang,但如果你決定使用LLVM repo的Clang,它將無法正常工作。這是因為LLVM Clang for Windows是使用Microsoft Visual Studio構建的,所有內置的宏和包含的搜索路徑都是為使用Visual Studio而設置的。所以即使與MinGW工具鏈一起使用,它仍然會嘗試包含MSVC標準庫頭。
我們做了一些實驗,發現了一個可能的工作流程,就是使用CLion與MinGW工具鏈和Clang編譯器相結合。
這是我們的發現:
此Clang編譯器使用mingw-w64構建,并具有與該工具鏈相對應的路徑和宏。
現在我們準備建立CLion工具鏈。轉到Settings/Preferences | Build, Execution, Deployment | Toolchains:
配置了新的工具鏈后,就可以開始構建項目了。您可以使用默認的ld連接或設置lld有-DCMAKE_LINKER=lld。
使用Clang編譯器提供的高級工具
從理論上講,所有Clang工具都應該可以正常工作。但是,涉及編譯器-rt可能會出現問題。編譯器-rt是一組運行時庫,在Clang中使用消毒劑和配置文件是必需的,當前的compile_rt軟件包是使用MinGW構建的。但是Clang需要使用Clang和lld構建的編譯器。
在我們的案例中,我們想使用配置文件引導的優化。進行此工作的一種方法是獲取與MSYS2中的Clang版本完全相同的版本的editor-rt源代碼。這可能具有挑戰性,因此另一個解決方案是克隆LLVM monorepo并構建所需的工具。
對于-fprofile-instr-generate,僅構建compile -rt和llvm-profdata來合并探查器結果可能就足夠了。但是,要可靠地使用所有工具,最好也構建Clang和lld。
幸運的是,我們已經具有該構建所需的設置。
最后一步是將<msys2_path>/mingw64/lib\clang/<clang_version>/libwindows中的二進制文件替換為<compiler-rt_path>/cmake-build-release-mingw_clang/libwindows或<llvm_build_path>/lib/clang/<clang_version>/lib/windows中的庫。
使用Clang進行性能分析
有了正確的編譯器-rt庫,現在就可以使用與-fprofile-instr-generate / -fprofile-instr-use標志設置的相同的工具鏈。因為我們已經有了源代碼,所以讓我們為此實驗構建LLVM。我們還將使用-DLLVM_ENABLE_LTO=Thin進行更多優化。轉到Settings/Preferences | Build, Execution, Deployment | CMake:
使用此CMake配置,您可以構建Clang編譯器并使用它,例如,構建自己的項目。這將生成相關的探查器信息,稍后應將其與我們之前構建的工具llvm-profdata合并。使用合并的profile_merged.profdata文件,您最終可以構建Clang編譯器的優化版本:
使用自定義Clang和lld
要讓gcc風格的-fprofile-generate/-fprofile-use標志正確工作,需要改變Clang路徑,并將-DCMAKE_LINKER設置為新構建的ld。你還需要一些額外的LLVM技巧:-femulated-tls和鏈接pthread。
然后,應重復使用-fprofile-instr-generate / -fprofile-instr-use執行的所有步驟。
結論
現在可以在Windows上使用Clang,并且不需要安裝Microsoft Visual Studio!
我們希望在不久的將來,使用高級的clang工具將變得更加容易,并且不再需要手動構建。讓我們知道,如果您發現其他方法可以達到相同的效果!
今天的內容你學會了嗎?前往慧都網免費下 載最新版嘗試一下,并在評論分享你的想法。
LLVM里面的Clang已經可以替換MSVC的cl.exe(MSVC的編譯過程的組織程序-driver),作為Visual Studio的獨立工具鏈,能生成PDB文件支持在Visual Studio里面的源代碼調試。為了支持替換cl.exe,clang構建會生成可執行文件clang-cl.exe,接收cl.exe的大部分參數而在內部轉換成LLVM的參數形式。
雖然看起來是生成了一個單獨的clang-cl.exe,它實際上就是clang.exe的一個副本,如果程序名是clang.exe,還可以在命令行傳遞"--driver-mode=cl"參數啟用cl.exe的參數解析模式。所以clang-cl.exe和clang.exe是一樣的,都接受"--target=i686-pc-windows", 但是為什么clang-cl.exe卻不能解析"-triple i686-pc-windows"而clang.exe卻可以呢?
程序本身通過檢查自身的文件名(argv[0])來檢測是不是要運行在兼容MSVC cl.exe的模式,如果文件名是"clang-cl.exe",則把對應的DriverMode放到main函數開始處的變量TargetAndMode里面(ToolChain::getTargetAndModeFromProgramName)。下面的代碼顯示了對應關系,可以看到把文件名clang.exe改成cl.exe也會有一樣的效果。
// llvm_root\tools\clang\tools\driver\driver.cpp
const DriverSuffix *FindDriverSuffix(StringRef ProgName, size_t &Pos) {
// A list of known driver suffixes. Suffixes are compared against the
// program name in order. If there is a match, the frontend type is updated as
// necessary by applying the ModeFlag.
static const DriverSuffix DriverSuffixes[]={
{"clang", nullptr},
{"clang++", "--driver-mode=g++"},
{"clang-c++", "--driver-mode=g++"},
{"clang-cc", nullptr},
{"clang-cpp", "--driver-mode=cpp"},
{"clang-g++", "--driver-mode=g++"},
{"clang-gcc", nullptr},
{"clang-cl", "--driver-mode=cl"},
{"cc", nullptr},
{"cpp", "--driver-mode=cpp"},
{"cl", "--driver-mode=cl"},
{"++", "--driver-mode=g++"},
};
在上面從程序名解析出target和mode后,main函數里面緊接著的代碼檢查了返回的mode和命令行參數,只要以一個滿足則進入ClangCLMode。不過這里解析出來的ClangCLMode只用來處理命令行參數的分隔和cl.exe特有的環境變量,包括"CL"和"_CL_"。
// llvm_root\tools\clang\tools\driver\driver.cpp
auto TargetAndMode=ToolChain::getTargetAndModeFromProgramName(argv[0]);
bool ClangCLMode=false;
if (StringRef(TargetAndMode.DriverMode).equals("--driver-mode=cl") ||
std::find_if(argv.begin(), argv.end(), [](const char *F) {
return F && strcmp(F, "--driver-mode=cl")==0;
}) !=argv.end()) {
ClangCLMode=true;
}
再從main函數進入Driver類的對象TheDriver的BuildCompilation方法后,會調用 ParseDriverMode方法,里面會根據程序名重新獲得driver mode(ToolChain::getTargetAndModeFromProgramName),然后把driver mode字符串傳給下面的setDriverModeFromOption方法。這個方法根據傳入的driver mode選項設置成員變量Mode.
// llvm_root\tools\clang\lib\driver\driver.cpp
void Driver::setDriverModeFromOption(StringRef Opt) {
const std::string OptName=getOpts().getOption(options::OPT_driver_mode).getPrefixedName();
if (!Opt.startswith(OptName))
return;
StringRef Value=Opt.drop_front(OptName.size());
const unsigned M=llvm::StringSwitch<unsigned>(Value)
.Case("gcc", GCCMode)
.Case("g++", GXXMode)
.Case("cpp", CPPMode)
.Case("cl", CLMode)
.Default(~0U);
if (M !=~0U)
Mode=static_cast<DriverMode>(M);
上面的Driver類的對象已經知道當前Mode,比如CLMode,下面會組織整個編譯過程,包括調用編譯器(clang.exe -cc1)和鏈接器(MSVC的link或者lld-link)。
BuildCompilation緊接著會調用ParseArgStrings。ParseArgStrings調用下面的getIncludeExcludeOptionFlagMask,根據Driver的當前Mode得到include mask和exclude mask兩個掩碼,用于后面(在調用鏈ParseArgString->ParseArg->ParseOneArg的最后的方法ParseOneArg里面)決定是否接受命令行參數。比如在CLMode下就只會接受ClOption和CoreOption。
// llvm_root\tools\clang\lib\driver\driver.cpp
std::pair<unsigned, unsigned> Driver::getIncludeExcludeOptionFlagMasks() const {
unsigned IncludedFlagsBitmask=0;
unsigned ExcludedFlagsBitmask=options::NoDriverOption;
if (Mode==CLMode) {
// Include CL and Core options.
IncludedFlagsBitmask |=options::CLOption;
IncludedFlagsBitmask |=options::CoreOption;
} else {
ExcludedFlagsBitmask |=options::CLOption;
}
return std::make_pair(IncludedFlagsBitmask, ExcludedFlagsBitmask);
}
那么CLOption和CoreOption都有哪些具體參數呢?Clang的所有命令行參數選項都定義在llvm_root\tools\clang\include\clang\driver\Options.td里面,由tablegen轉成C/C++頭文件而被代碼引用。下面是從里面截取的target的定義,看到"--target="選項是同時屬于DriverOption和CoreOption,而CoreOption在CLMode和非CLMode下均能使用。
def target : Joined<["--"], "target=">, Flags<[DriverOption, CoreOption]>,
HelpText<"Generate code for the given target">;
以下是"-target"的定義,沒有定義Flags,所以在CLMode下也就不能解析,這也就解釋了最開始"clang-cl.exe"不接受"-triple i686-pc-windows"參數。
def target_legacy_spelling : Separate<["-"], "target">, Alias<target>;