从 5 分钟到 30 秒,如何优化clang 工程的增量编译耗时

从 5 分钟到 30 秒,如何优化clang 工程的增量编译耗时

文章目录

前言背景知识Action 详解LaunchAction 关键配置LaunchAction 其它主要配置BuildAction 的关键配置BuildAction 的其它重要配置project.pbxproj 的 `clang-target` 配置

优化原理优化步骤

总结

前言

本文中的编译根据上下文有不同的含义,请注意区分。

编译原始概念是指:将 a.m 编译为 a.out

本文中,也可以用来表示根据项目产出构建产物(可选的附带执行部分脚本、文件复制等操作)

笔者每次进行 clang 工程编译时,都会被编译耗时困扰。 clang 每次编译都在5分钟左右。

首先,先提供一份效果对比图。

图1,如下所示,笔者在只改动 1 行代码时,编译速度耗时 300.1 秒。

图2,经过简单的处理,编译速度被优化到 28.6 秒。

背景知识

当我们执行点击 运行 按钮时,Xcode 会执行以下步骤:

执行 BuildAction ,为后续的 LaunchAction 做准备

parallelizeBuildables = "YES"

buildImplicitDependencies = "YES">

buildForTesting = "NO"

buildForRunning = "YES"

buildForProfiling = "YES"

buildForArchiving = "YES"

buildForAnalyzing = "YES">

BuildableIdentifier = "primary"

BlueprintIdentifier = "A79DD8637E8944CF96F0A620"

BuildableName = "clang"

BlueprintName = "clang"

ReferencedContainer = "container:../../build/Xcode-DebugAssert/llvm-macosx-x86_64/LLVM.xcodeproj">

执行 LaunchAction,运行程序

buildConfiguration = "Debug"

selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"

selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"

launchStyle = "0"

useCustomWorkingDirectory = "NO"

ignoresPersistentStateOnLaunch = "NO"

debugDocumentVersioning = "YES"

debugServiceExtension = "internal"

allowLocationSimulation = "YES">

runnableDebuggingMode = "0">

BuildableIdentifier = "primary"

BlueprintIdentifier = "A79DD8637E8944CF96F0A620"

BuildableName = "clang"

BlueprintName = "clang"

ReferencedContainer = "container:../../build/Xcode-DebugAssert/llvm-macosx-x86_64/LLVM.xcodeproj">

Action 详解 下面会重点讲解两个 Action 的各种配置参数。

阅读本文可以只看LaunchAction 关键配置 和 BuildAction 关键配置。LaunchAction 其它主要配置 和 BuildAction 其它主要配置可以当做扩展阅读。

LaunchAction 关键配置

buildConfiguration ,代表执行 LaunchAction 使用的配置组合名。默认是 Debug

通过 Xcode 新建的项目会有 Debug 和 Release 两种配置组合。

Build Settings 中可以增加新的配置组合。如下,LLVM 共计有 4 种配置组合。

通过配置组合,开发者可以快速调整编译选项。如下,clang 在不同的配置组合下,开启了不同的编译优化级别 BuildableProductRunnable 编译配置。

ReferencedContainer 代表依赖的文件位置BlueprintIdentifier 代表 ReferencedContainer 的节点ID。A79DD8637E8944CF96F0A620 的内容会在下一节重点讲解(后面会用 clang-target 代替)

runnableDebuggingMode = "0">

BuildableIdentifier = "primary"

BlueprintIdentifier = "A79DD8637E8944CF96F0A620"

BuildableName = "clang"

BlueprintName = "clang"

ReferencedContainer = "container:../../build/Xcode-DebugAssert/llvm-macosx-x86_64/LLVM.xcodeproj">

LaunchAction 其它主要配置

selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" 代表使用的调试器。如果不希望调试程序,可以通过置为空,避免启动调试器。

selectedDebuggerIdentifier = ""

selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" 代表使用的启动器。如果不希望调试程序,可以改为 Spawn 模式。 ​ selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"

debugAsWhichUser 代表是否使用 root 用户执行程序。

allowLocationSimulation 是否模拟自己的所在位置。通常用于开发 APP 进行位置模拟时使用。

enableAddressSanitizer = "YES" 是否启用地址杀毒剂。可以用于调试 EXC_BAD_ACCESS 等问题

enableASanStackUseAfterReturn = "YES"

enableUBSanitizer = "YES"

launchStyle = "1" 启动类型,1 代表需要程序员手动执行程序,0 代表自动执行程序

CommandLineArguments 启动APP时传递的参数

EnvironmentVariables 环境变量,比如,可以通过以下命令控制动态库的动态加载 (看过 调试 iOS 的 objc 运行时,你可能还需要掌握这些知识 的同学,可以研究一下这个环境变量)

key = "DYLD_INSERT_LIBRARIES"

value = "A_DYLD_INSERT_LIBRARIES"

isEnabled = "YES">

BuildAction 的关键配置

parallelizeBuildables = "YES" 执行编译任务时,是否多任务并行处理

buildImplicitDependencies = "NO" 是否查找隐式依赖

BuildAction 的其它重要配置

BuildActionEntry 执行其它action时,是否触发 BuildAction。

buildForTesting = "NO" 代表,执行单测任务时,不需要触发编译buildForRunning = "YES" 代表,执行 运行任务时,需要触发编译任务

project.pbxproj 的 clang-target 配置

在 LaunchAction 中,我们曾经提到 clang-target (A79DD8637E8944CF96F0A620)。

因这部分的配置是在 project.pbxproj 中维护,所以单独用一个小节讲解一下。

如下所示,project.pbxproj 项目中关于 clang-target )的配置(因为篇幅原因,dependencies 被省略了一部分)

A79DD8637E8944CF96F0A620 /* clang */ = {

isa = PBXNativeTarget;

buildConfigurationList = A7BAD0EC9F7549319D37DAB4 /* Build configuration list for PBXNativeTarget "clang" */;

buildPhases = (

8720B378A9BF485883000F4C /* Sources */,

05FD7D0438324244B5AB2823 /* CMake PostBuild Rules */,

);

buildRules = (

);

dependencies = (

5D887B4D650A443FAD81013A /* PBXTargetDependency */,

........

........

........

);

name = clang;

productName = clang;

productReference = 66B22582EC2946B9B97BCED6 /* clang */;

productType = "com.apple.product-type.tool";

};

isa 就像 OC 中的类有自己的 isa 属性一样,这里同样有isa 标明这是一个 target PBXNativeTarget

对 target 概念比较陌生的同学可以参考 Xcode中的Workspace、Project、Target和Scheme

buildConfigurationList 指向一个列表,该列表负责维护 clang-target 有几种配置组合

A7BAD0EC9F7549319D37DAB4 /* Build configuration list for PBXNativeTarget "clang" */ = {

isa = XCConfigurationList;

buildConfigurations = (

4FF7FFFB5E8D4AFAB08BDF78 /* Debug */,

895C7D31C2AE46899583672D /* Release */,

85E6509B95FE4402A01A4657 /* MinSizeRel */,

1BAC7DB5A2ED41A0A31AB96E /* RelWithDebInfo */,

);

defaultConfigurationIsVisible = 0;

defaultConfigurationName = Debug;

};

buildPhases 负责维护一个编译 phase 列表

PBXSourcesBuildPhase 源码编译:指向 clang-target 的编译代码 注意:这里只有 cpp c m 等文件,没有h 等头文件

8720B378A9BF485883000F4C /* Sources */ = {

isa = PBXSourcesBuildPhase;

buildActionMask = 2147483647;

files = (

F91A7D5B43464D99B6C2B745 /* cc1_main.cpp in Sources */,

77711346965C4D53B41045FE /* cc1as_main.cpp in Sources */,

FCA5E4060CCE4B9FBE948241 /* cc1gen_reproducer_main.cpp in Sources */,

98295C12ADE0429190E290C0 /* driver.cpp in Sources */,

5C039FA62D1A42F2946FBDC0 /* dummy.cpp in Sources */,

);

runOnlyForDeploymentPostprocessing = 0;

};

PBXShellScriptBuildPhase 脚本执行。(执行效果是通过创建链接的方式,将 clang++ clang-cl clang-cpp 指向 clang-target 的构建产物 clang)

05FD7D0438324244B5AB2823 /* CMake PostBuild Rules */ = {

isa = PBXShellScriptBuildPhase;

buildActionMask = 2147483647;

files = (

);

name = "CMake PostBuild Rules";

runOnlyForDeploymentPostprocessing = 0;

shellPath = /bin/sh;

shellScript = "make -C ~/swift-source/build/Xcode-DebugAssert/llvm-macosx-x86_64/tools/clang/tools/driver -f ~/swift-source/build/Xcode-DebugAssert/llvm-macosx-x86_64/tools/clang/tools/driver/CMakeScripts/clang_postBuildPhase.make$CONFIGURATION OBJDIR=$(basename \"$OBJECT_FILE_DIR_normal\") all";

showEnvVarsInLog = 0;

};

dependencies 负责维护的依赖列表(可以近似理解为对 静态库 或 动态库 的依赖)。clang-target 共计有 108 个依赖。

如下所示,这个依赖表明只有LLVMDemangle完成构建后, clang-target 才能执行完毕

5D887B4D650A443FAD81013A /* PBXTargetDependency */ = {

isa = PBXTargetDependency;

target = 4F1F4485C1C44E48A8AF56BB /* LLVMDemangle */;

targetProxy = 2A241593F41943B38ACDA422 /* PBXContainerItemProxy */;

};

name 代表target的名字。

productName 代表该 target 产物的名字。

如下,左侧是 name ,右侧是 productName productReference 代表产物的ID

productType 代表产物的类型。字段的含义可以参考 /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Xcode/Specifications/MacOSX Product Types.xcspec

clang的产物类型是 com.apple.product-type.tool,代表 普通 Unix 命令行可执行文件

// Tool (normal Unix command-line executable)

{ Type = ProductType;

Identifier = com.apple.product-type.tool;

Class = PBXToolProductType;

Name = "Command-line Tool";

Description = "Standalone command-line tool";

IconNamePrefix = "TargetExecutable";

DefaultTargetName = "Command-line Tool";

DefaultBuildProperties = {

FULL_PRODUCT_NAME = "$(EXECUTABLE_NAME)";

MACH_O_TYPE = "mh_execute";

EXECUTABLE_PREFIX = "";

EXECUTABLE_SUFFIX = "";

REZ_EXECUTABLE = YES;

INSTALL_PATH = "/usr/local/bin";

FRAMEWORK_FLAG_PREFIX = "-framework";

LIBRARY_FLAG_PREFIX = "-l";

LIBRARY_FLAG_NOSPACE = YES;

GCC_DYNAMIC_NO_PIC = NO;

GCC_SYMBOLS_PRIVATE_EXTERN = YES;

GCC_INLINES_ARE_PRIVATE_EXTERN = YES;

STRIP_STYLE = "all";

CODE_SIGNING_ALLOWED = YES;

};

PackageTypes = (

com.apple.package-type.mach-o-executable // default

);

WantsSigningEditing = YES;

WantsBundleIdentifierEditing = YES;

},

优化原理

当我们执行点击 运行 按钮时,Xcode 会执行以下步骤:

读取各类配置信息

根据 LaunchAction确定配置组合,本例是 Debug根据 LaunchAction确定编译 target,本例是 clang-target根据 BuildAction确定是否查找隐式依赖根据 BuildAction确定是否并行编译根据 编译 target 确定前置依赖的 dependencies,递归执行本步骤根据 依赖图dependencies确定编译任务 并发或者顺序执行编译步骤

预处理编译链接对产物签名执行脚本

实际上,上面构建 依赖图的根本原因是:编译 clang 需要其它 target 的 些构建产物才能完成。

重点:在增量编译场景下,大部分的产物实际上是保持不变的,只有少数几个静态库会发生变化。

优化步骤

下面,我们看一下如何针对增量编译场景特殊处理:

通过快捷键或者鼠标,复制一份新的 clang target,我们可以称之为 clang-mini

重点:复制的原因是,我们可以在某些场景下(比如不小心删除了某个文件),通过 clang 快速编译所有的依赖产物

更改 Product Name 重点:不是所有的优化都需要此步骤,clang 执行此步骤的原因是之前提到过,编译完成后,会有脚本创建clang++ 等快捷方式,所以,这里特定调整一下

保留会有源文件变更的依赖

比如,我保留 LLVMDemangle 依赖的原因是,我需要加一下日志进行调试。

关闭隐式依赖的查找 重点:对于clang,这步的意义不大,但是,某些项目可能存在大量的隐式依赖,关掉这个开关,并且将依赖放到上一步的Dependencies中,可以节省大量的编译消耗

总结

本文通过分析 Xcode 运行构建工作的原理,提供了一种新的思路,可以快速降低大型项目的增量编译的时间消耗。

首发地址:https://ai-chan.top/%e4%bb%8e-5%e5%88%86%e9%92%9f%e5%88%b0-30-%e7%a7%92%ef%bc%8c%e5%a6%82%e4%bd%95%e4%bc%98%e5%8c%96clang-%e5%b7%a5%e7%a8%8b%e7%9a%84%e5%a2%9e%e9%87%8f%e7%bc%96%e8%af%91%e8%80%97%e6%97%b6/

相关推荐