酷酷的哀殿


  • 首页

  • 技术

  • 笔记

  • 杂记

  • Todo

  • 关于

  • 搜索
close

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

时间: 2020-04-19   |   阅读: 2524 字 ~6分钟

前言

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

  • 编译原始概念是指:将 a.m 编译为 a.out
  • 本文中,也可以用来表示根据项目产出构建产物(含“执行脚本”、“文件复制”等可选操作)

笔者每次进行 clang 工程编译时,都会被编译耗时困扰。 clang 每次编译都在5分钟左右。 首先,先提供一份效果对比图。

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

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

Xcode 的编译构建

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

  • 执行 BuildAction ,为后续的 LaunchAction 做准备
        <BuildAction
          parallelizeBuildables = "YES"
          buildImplicitDependencies = "YES">
          <BuildActionEntries>
             <BuildActionEntry
                buildForTesting = "NO"
                buildForRunning = "YES"
                buildForProfiling = "YES"
                buildForArchiving = "YES"
                buildForAnalyzing = "YES">
                <BuildableReference
                   BuildableIdentifier = "primary"
                   BlueprintIdentifier = "A79DD8637E8944CF96F0A620"
                   BuildableName = "clang"
                   BlueprintName = "clang"
                   ReferencedContainer = "container:../../build/Xcode-DebugAssert/llvm-macosx-x86_64/LLVM.xcodeproj">
                </BuildableReference>
             </BuildActionEntry>
          </BuildActionEntries>
        </BuildAction>
  • 执行 LaunchAction,运行程序

        <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">
          <BuildableProductRunnable
             runnableDebuggingMode = "0">
             <BuildableReference
                BuildableIdentifier = "primary"
                BlueprintIdentifier = "A79DD8637E8944CF96F0A620"
                BuildableName = "clang"
                BlueprintName = "clang"
                ReferencedContainer = "container:../../build/Xcode-DebugAssert/llvm-macosx-x86_64/LLVM.xcodeproj">
           </BuildableReference>
          </BuildableProductRunnable>
        </LaunchAction>
    

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 代替)

      <BuildableProductRunnable
              runnableDebuggingMode = "0">
              <BuildableReference
            BuildableIdentifier = "primary"
            BlueprintIdentifier = "A79DD8637E8944CF96F0A620"
            BuildableName = "clang"
            BlueprintName = "clang"
            ReferencedContainer = "container:../../build/Xcode-DebugAssert/llvm-macosx-x86_64/LLVM.xcodeproj">
          </BuildableReference>
      </BuildableProductRunnable>
    

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 运行时,你可能还需要掌握这些知识 的同学,可以研究一下这个环境变量)

      <EnvironmentVariables>
         <EnvironmentVariable
            key = "DYLD_INSERT_LIBRARIES"
            value = "A_DYLD_INSERT_LIBRARIES"
            isEnabled = "YES">
         </EnvironmentVariable>
      </EnvironmentVariables>
      

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

  • 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 运行构建工作的原理,提供了一种新的思路,可以快速降低大型项目的增量编译的时间消耗。

相关推荐

  • Ruby 与 clang
  • 强制 Xcode 启用 NSAssert
  • Xcode 中的 Workspace、Project、Target和Scheme
  • lldb 入坑指北(2)- 15行代码搞定二进制与源码映射
  • lldb 入坑指北(3)- 打印 c++ 实例的虚函数表
#Xcode# #clang# #编译# #编译优化#
调试工具系列 - size 命令
  • 文章目录
  • 站点概览
酷酷的哀殿

酷酷的哀殿

单身狗

45 日志
54 标签
友情链接
  • 麋鹿
  • 平凡的你我
  • 前言
  • Xcode 的编译构建
    • Action 详解
    • LaunchAction 关键配置
    • LaunchAction 其它主要配置
    • BuildAction 的关键配置
    • BuildAction 的其它重要配置
    • project.pbxproj 的 clang-target 配置
  • 优化原理
  • 优化步骤
  • 总结
© 2020 酷酷的哀殿
Powered by - Hugo v0.79.0
Theme by - NexT
0%