前言

DanceCC (Dance Compiler Collection) 是字节跳动的终端技术团队(Client Infrastructure) 下的编译工具链品牌,编译工具链团队成员由国内和硅谷两地的编译器专家及构建零碎专家组成,提供基于开源的 LLVM/Swift 我的项目深度定制的 clang/swift 编译器、链接器、lldb 调试器和语言根底库等工具及优化计划,笼罩构建性能优化利用性能稳定性优化等场景,本系列将会围绕这些场景中的优化案例,介绍编译工具链技术在字节的优化计划和落地状况。

背景

通常来说,大型Swift我的项目常含有大量混编(Objc/C/C++甚至是Rust)代码,含有超过100个以上的Swift Module,并可能同时蕴含二进制局部和源码局部。而这种大型项目在目前的Xcode 13体验下十分不好,常常存在相似“断点陷入后变量面板卡顿转菊花”、“显示变量生效”等问题。而且始终存在于多个历史Xcode版本。

图1:Xcode变量区显示卡顿转菊花,测试应用Xcode 13.3和下文提到的复现Demo

这部分Apple Team迟迟不优化的起因在于,Apple公司的外部我的项目和内部我的项目开发模式的微小差别。Apple外部产品,如零碎利用,零碎库,会间接内嵌到iOS固件中,并间接受害于dyld shared cache(参考WWDC 2017-App Startup Time: Past, Present, and Future[1])来晋升加载速度。这意味着他们通常会将一个App,拆分为一个薄的主二进制,搭载以相当多的动态链接库(Dynamic Framework),以及插件(PlugIn)的模式来进行开发。

举个例子,咱们以iOS的音讯App(MobileSMS.app)为例子,应用iOS 15.4模拟器测试。能够看到其主二进制大小仅有844KB(x86_64架构)。通过otool -L查问链接,能够看到总计动静链接了22个动态链接库,其中有9个是非公开的,大都是撑持音讯App的性能库,这些库占据了大量存储。

图2:音讯App的动态链接库列表

而iOS平台的第三方开发者的工程,为了谋求更快的冷启动时长,因为没有了dyld shared cache的优化(dyld 3提出的启动闭包只能优化非冷启动),很多我的项目会应用尽量少的动态链接库。加之开源社区的CocoaPods,Carthage,SwiftPM等包管理器的流行导致的Swift Module爆炸增长,预二进制的Framework/XCFramework包装格局的滥用,加之闭源三方公司的SDK的集成,最终造成了一个无论是体积还是符号量都十分微小的主二进制,以及相当长的Search Paths。

以公司内飞书利用的内测版为例子,在应用Debug,Onone模式编译,不剥离(Strip)任何符号状况下,能够看到其主二进制大小为1.1GB,动态链接库数量为105,然而仅蕴含Apple的零碎库和Swift规范库。业务代码以动态链接库集成。

图3:公司飞书利用的动态链接库列表

上述这两种不同的工程构造,带来了十分显著的调试体验的差别,并且Apple公司近年来的Xcode Team和Debugger Team优化,并没有齐全思考局部第三方开发者常应用的,厚主二进制下的工程构造。

PS:实践上能够通过业务的工程构造的革新,在本地开发模式下,应用一个动态链接库包裹根底动态链接库的形式,缩小主二进制大小(也会缩小后续提到的DWARF搜寻的耗时),然而大型项目推动工程构造的革新会是一个十分漫长的过程。

图4:一种缩小主二进制大小的工程结构设计

解决方案:自定义LLDB工具链

通过调研,咱们发现业界常见做法,无外乎这几种思路:

  1. 工程革新:缩减Swift Module/Search Path数量:可行,然而收益较低,且不可能无限度缩减
  2. 通过LLDB一些开关:可行,然而内部测试下仍旧达不到现实的调试状态

咱们致力于在字节跳动的挪动端提供根底能力反对,因而提出了一套解决方案,不依赖业务工程构造的革新,而是从LLDB工具链上动手,提供定向的调试性能优化。

调研期间也确认到,借助自定义LLDB工具链,集成到Xcode IDE是齐全可行的,包含iPhone模拟器、真机以及Mac利用。

图5:自定义LLDB工具链的文件构造,系列后续文章会独自解说,这里不开展

而LLVM/LLDB自身的工具链代码,在Apple的开源领域之内(仓库地址:https://github.com/apple/llvm...) 通过严格追踪跟进上游的公布历史,分支模型,可能尽可能地保障工具链的代码和性能的一致性。

理论收益

通过后文提到的一系列优化伎俩,以公司内大型项目飞书测试,编译器采取Swift 5.6,Xcode抉择13.3为例,比照调试性能:

我的项目Xcode 13.3自定义LLDB
v耗时2分钟40秒
po耗时1分钟5秒
p耗时20秒5秒

图6:切换自定义LLDB工具链

图7:调试优化演示,应用Xcode 13.3自定义LLDB,运行文中提到的耗时Demo(原po耗时约1分钟):

简述po/p/v的工作流程

在介绍咱们自定义LLDB工具链的优化之前,首先来简述一下LLDB的外围调试场景的工作流程,不便后续了解优化的技术点。

咱们一期的目标是次要优化外围的调试场景,包含最常见的“断点陷入到Xcode左侧变量区展现结束”(v),“点击Show Description”(po),“勾选Show Types”(p)。这些对应LLDB原生的上面三个交互命令。

图8:LLDB的交互命令

Apple在WWDC 2019-LLDB: Beyond "po"[2]中,进行了较为具体的介绍,这里咱们进一步具体解释其局部工作流程,为后文的具体优化技术点提供参考。倡议能够搭配视频一并学习。

po [expr]

po是命令expression --object-description -- [expr]的alias

图9:po的流程

  1. 应用Swift编译器编译result = expr失去IR
// 精简版,理论较为简单,源代码搜@LLDBDebuggerFunction关键字func __lldb_expr() {  __lldb_result = expr}
  1. 执行IR代码

    1. 在反对JIT的平台上应用JIT,不反对则应用LLVM的IRInterpreter
  2. 获取执行后果
  3. 应用Swift编译器编译result.description

    1. 实际上LLDB调用的是Swift规范库的公有办法:_DebuggerSupport.stringForPrintObject[3]
  4. 执行IR代码
  5. 获取执行后果字符串
  6. 对失去的字符串进行格式化输入

p [expr]

p是命令expression -- [expr]的alias

图10:p的流程

  1. 应用Swift编译器编译result = expr失去IR
  2. 执行IR代码
  3. 获取执行后果
  4. result进行Dynamic Type Resolve

    1. 利用Swift编译器提供的remoteAST,领有源码的AST之后,会依据内存布局间接读取对象细节
    2. 也会利用Swift Reflection,即Mirror来进行读取,和remoteAST二选一
  5. 对失去的对象细节进行格式化输入

比照下来能够看到,po和p的最大不同点,在于表达式执行的后果,如何获取变量的形容这一点上。po会间接利用运行时的object description(反对CustomDebugStringConvertible[4]协定)拿到的字符串间接展现,并不真正理解对象细节。

图11:获取Object Description的实现细节(SwiftLanguageRuntime.cpp)

而p应用了Swift Runtime(Objc的话就是ISA,Method List那些,材料很多不赘述),拿到了对象细节(反对CustomReflectable[5]协定),进行按层遍历打印。不过值得注意的是,Swift Runtime依赖remoteAST(须要源码AST,即swiftmodule)或者Reflection(可能被Strip掉,并不一定有),意味着它强绑定了,编译时的Swift版本和调试时的LLDB的版本(牢记这一点)。并不像Objc那样有一个成熟稳固运行时,不依赖编译器也能动静得悉任意的对象细节。

图12:Swift Dynamic Type Resolve的实现(SwiftLanguageRuntimeDynamicTypeResolution.cpp)

v [expr]

v是命令frame variable [expr]的alias

图13:v的流程

  1. 获取程序运行状态(寄存器/内存等)
  2. 递归开始
  3. 解释expr的每一层拜访(->或者.),得悉以后变量的内存布局
  4. 对以后变量进行Dynamic Type Resolve
  5. 递归完结
  6. 对失去的对象细节格式化输入

v的特点在于全程没有注入任何代码到程序中,也就是它是实践无副作用的。它的expr只反对拜访对象的表达式(->/.等),不反对函数调用,并不是真正的C++/C/OC/Swift语法。

优化v

下述所有阐明基于发稿日的Swift 5.6(优化思路也适配Swift 5.5)阐明优化计划,后续不排除Apple或者LLVM上游进行其余优化代替,具备肯定时效性。

(临时)敞开swift-typeref-system

  • 敞开形式
settings set symbols.use-swift-typeref-typesystem false
  • 开关阐明
Prefer Swift Remote Mirrors over Remote AST

这里的remoteAST和Swift Mirror的概念,上文介绍过,不同计划会影响Swift的Dynamic Type Resolve的性能。

通过实测,敞开之后,外部我的项目的简单场景下,断点陷入耗时从本来的2分20秒,缩减为1分钟。这部分开关,目前曾经通过Xcode自定义的LLDBInit[6]文件,在多个我的项目中设置。

注:和Apple共事沟通后,swift-typeref-typesystem是团队20年提出的新计划,目前有一些已知的性能问题,然而对Swift变量和类型展现有更好的兼容性。敞开当前会导致诸如,typealias的变量在p/v时展示会有差别,比方TimeInterval(alias为__C.Double)等。待Apple后续优化之后,倡议复原开启状态。

修复动态链接库谬误地应用dlopen(Fixed in Swift 5.7)

简述问题:LLDB在SwiftASTContext::LoadOneModule时假如所有framework包装格局都是动态链接库,疏忽了动态链接库的可能性。

在调试测试工程中,咱们追踪日志发现,LLDB会尝试应用dlopen去加载动态链接库(Static Framework),这是很不合乎预期的一点,因为对一个动态链接库进行dlopen是必然失败的,如日志所示(应用下文提到的复现Demo):

SwiftASTContextForExpressions::LoadOneModule() -- Couldn't import module AAStub: Failed to load linked library AAStub of module AAStub - errors:Looking for "@rpath/AAStub.framework/AAStub", error: dlopen failed for unknown reasons.Failed to find framework for "AAStub" looking along paths:// ...

查看代码浏览发现,这里触发的机会是,LLDB在执行Swift变量Dynamic Type Resolve之前,因为须要激活remoteAST,须要加载源码对应的swiftmodule到内存中。

swiftmodule是编译器序列化的蕴含了AST的LLVM Bitcode[7]。除了AST之外,还有很多Metadata,如编译器版本,编译时刻的参数,Search Paths等(通过编译器参数-serialize-debugging-options记录)。另外,对Swift代码中呈现的import语句,也会记录一条加载模块依赖。而主二进制在编译时会记录所有子模块的递归依赖。

LLDB在进行加载模块依赖时,会依据编译器失去的Search Paths,拼接上以后的Module Name,而后遍历进行dlopen。波及较高的工夫开销:N个Module,M个Search Path,复杂度O(NxM)(外部我的项目为400x1000数量级)。而在执行前。并未检测以后被加载的门路是否真正是一个动态链接库,最终产生了这个谬误的开销。

  • 修复计划

咱们的修复计划一期是进行了一次File Signature断定,只对动态链接库进行dlopen,在外部工程测试(约总计1000个Framework Search Path,400个Module)状况下,一举能够缩小大概1分钟的额定开销。

  • 复现Demo

仓库地址:https://github.com/PRESIDENT8...

这个Demo结构了100个Swift Static Framework,每个Module有100个编译单元,以此模仿简单场景。

后文的一些测试数据优化,会重复提及这个Demo比照。

注:和Apple的共事沟通后,发现能够在下层进行起源辨别:只有通过expression import UIKit这种用户交互输出的Module会进行dlopen查看,以反对调试期间注入内部动静库;其余状况对立不执行,因为这些模块的符号必然曾经在以后被调试过程的内存中了。

Apple修复的PR:https://github.com/apple/llvm... 预计在Swift 5.7上车

优化po/p

(临时)敞开swift-dwarfimporter

  • 敞开形式
settings set symbols.use-swift-dwarfimporter false
  • 开关阐明
Reconstruct Clang module dependencies from DWARF when debugging Swift code

这个开关的作用是,在开启状况下,Swift编译器遇到clang type(如C/C++/Objc)导入到Swift时,容许通过一个自定义代理实现,来从DWARF中读取类型信息,而不是借助编译器应用clang precompiled module[8],即pcm,以及ClangImporter导入桥接类型。

切换当前可能局部clang type的类型解析并不会很准确(比方Apple零碎库的那种overlay framework,用原生Swift类型笼罩了同名C类型),然而能略微减速解析速度,这是因为clang pcm和DWARF的解析实现差别。

禁用之后,对外部我的项目测试工程局部场景有正向晋升约10秒,如果遇到问题倡议放弃默认的true。

优化External Module的查找门路逻辑

在混编工程中,Swift Module依赖一个C/OC的clang module是十分常见的事件。在这种状况下,LLDB须要同时应用编译器,加载到对应的clang module到内存中,用于进行C/OC Type到Swift Type的导入逻辑。

然而理论状况下,咱们可能有一些Swift混编产物,是预二进制的产物,在非以后机器中进行的编译。这种状况下,对应编译器记录的的External Module的门路很可能是在以后机器找不到的。

LLDB的原始逻辑,会针对每一个可能的门路,别离由它的4种ObjectFile插件(为了反对不同的二进制格局)顺次进行判断。每个ObjectFile插件会各自通过文件IO读取和解析Header。这是十分大的开销。

  • 优化计划

咱们外部采取的策略比拟激进,除了间接利用fstat进行前置的判断(而不是别离交给4个ObjectFile插件总计判断4次)外,还针对Mac机器的门路进行了一些非凡门路匹配规定,这里举个例子:

比如说,Mac电脑的编译产物绝对路径,肯定是以/Users/${whoami}结尾,所以咱们能够先尝试获取以后调试器过程的uname(十分快且LLDB过程周期内不会变动),如果不匹配,阐明编译产物肯定不是在以后设施进行上产出的,间接跳过。

图14:非凡匹配规定,间接防止文件IO断定存在与否

通过这一项优化,在外部我的项目测试下(1000多个External Module门路,其中800+有效门路),能够缩小首次变量显示v耗时约30秒。

减少共享的symbols缓存

咱们应用外部我的项目进行性能Profile时,发现Module::FindTypesSymbolFile::FindTypes函数耗时调用占了次要的大头。这个函数的性能是通过DWARF(记录于Mach-O构造中),查找一个符号字符串是否蕴含在内。耗时次要是在须要进行一次性DWARF的解析,以及每次查找的section遍历。

LLDB自身是存在一个searched_symbol_files参数用来缓存,然而问题在于,这份缓存并不是存在于一个全局共享池中,而是在每个具体调用处的长期堆栈上。一旦调用方完结了调用,这份缓存会被间接抛弃。

图15:symbols缓存参数

  • 优化计划

咱们在这里引入了一个共享的symbols缓存,保留了这份拜访记录来防止多个不同调用方仍然搜寻到同一个符号,以空间换工夫。实现计划比较简单。

外部工程实测,下来能够缩小10-20秒的第一次拜访开销,而每个symbol缓存占据字节约为8KB,一次调试周期约10万个符号占据800MB,对于Mac设施这种有虚拟内存的设施来说,内存压力不算很大。另外,也提供了敞开的开关。

优化不必要的同名symbols查找

另一项优化Module::FindTypesSymbolFile::FindTypes函数开销的计划是,原始的这两个函数会返回所有匹配到的列表,起因在于C++/Rust/Swift等反对重载的语言,会应用naming mangle来辨别同一个函数名的不同类型的变种。这些符号名称会以同样的demangled name,记录到DWARF中。

然而调用方可能会关怀同名类型的具体的变种(甚至包含是const还是非const),甚至有很多中央只取了第一个符号,搜寻全副的Symbol File其实是一种节约(在Swift 5.6版本中找到累积约10处调用只取了第一个)

  • 优化计划

咱们对上述Module::FindTypesSymbolFile::FindTypes函数,提供了一个新的参数match_callback,用于提前过滤所须要的具体类型。相似于很多语言规范库提供sort函数中的stop参数。这样,如果只须要第一个找到的符号就能够提前终止搜寻,而须要全副符号列表不受影响。

图16:symbols查找筛选参数

外部我的项目测试这项优化当前,能够缩小C++/C/OC类型导入到Swift类型这种场景下,约5-10秒的第一次查找耗时。

其余优化

定向优化Dynamic Type Resolve的一些特例

在理论我的项目测试中,咱们发现,Dynamic Type Resolve是有一些特例能够进行针对性的shortcut优化,剔除无用开销的。这部分优化仅对特定代码场景无效,并不通用。这里仅列举局部思路

  • 优化Core Foundation类型的Dynamic Type Resolve

Core Foundation类型(后文以CF类型指代),是Apple的诸多底层零碎库的撑持。Objc的Founadtion的NS前缀的很多类型,也会Toll-Free Bridging[9]到CF类型上。而Swift也针对局部罕用的CF类型反对了Briding。

CF类型的特点是,它内存布局相似Objc的Class ISA,然而又不是真正的Objc Class或者Swift imported Type,ISA固定是__NSCFType

而目前LLDB遇到在Swift堆栈中呈现的CF类型,仍旧把它当作规范的clang type进行C++/C那一套解析,还会递归寻找父类ivar,比拟费时。咱们能够利用这一特点提前断定而跳过无用的父类查找。

图17:筛选CF类型

这一项优化在特定场景(如应用CoreText和CoreVideo库和Swift混编)下,能够优化10-20秒的每次Dynamic Type Resolve耗时。

接下来

咱们在之后会有一系列的相干话题,包含:

  • Xcode 13.3导致局部我的项目po提醒Couldn't realize type of self,有什么解决办法?
  • 如何极速构建,散发自定义LLVM/LLDB工具链,来让用户无缝部署?
  • 如何进行调试性能指标的监控和建设,包含Xcode原生的LLDB?

另外,这篇文章提到的非定制的优化和性能,均会向Apple或LLVM上游提交Patches,以回馈社区。

总结

这篇文章解说了,大型Swift我的项目如何通过开关,以及自定义LLDB,优化Swift开发同学的调试速度,进步整体的研发效力。其中解说了LLDB的局部工作流程,以及针对性优化的技术细节,以及实际效果。

咱们的优化指标,不仅仅是服务于字节跳动挪动端外部,更心愿能推动业界的Swift和LLVM联合畛域的相干倒退,交换更多工具链方向的优化建设。

鸣谢

感激飞书根底技术团队提供的一系列技术支持,以及最终业务试点提供的帮忙推广。
感激Apple共事Adrian Prantl在GitHub和邮件上进行的交换反馈,帮助定位问题。

对于字节终端技术团队

字节跳动终端技术团队 (Client Infrastructure) 是大前端根底技术的全球化研发团队(别离在北京、上海、杭州、深圳、广州、新加坡和美国山景城设有研发团队),负责整个字节跳动的大前端基础设施建设,晋升公司全产品线的性能、稳定性和工程效率;反对的产品包含但不限于抖音、今日头条、西瓜视频、飞书、瓜瓜龙等,在挪动端、Web、Desktop等各终端都有深入研究。

退出咱们

咱们是字节的 Client Infrastructure 部门下的编译器工具链团队,团队成员由编译器专家及构建零碎专家组成,咱们基于开源的 LLVM/Swift 我的项目提供深度定制的 clang/swift 编译器、链接器、lldb 调试器和语言根底库等工具及优化计划,笼罩构建性能优化利用性能稳定性优化等场景,并在业务研发效率和利用品质晋升方面获得了显著的成果,同时,在实际的过程中咱们也看到了很多令人兴奋的新机会,心愿有更多对编译工具链技术感兴趣的同学退出咱们一起摸索。

工作地点

深圳、北京

职位形容

  1. 设计与实现高效的编译器/链接器/调试器优化
  2. 自定义 LLVM 工具链的保护和开发
  3. 晋升Client Infrastructure编译工具链的性能及稳定性
  4. 协同业务团队推动技术计划的落地

职位要求

  1. 至多熟练掌握 C++/Objective-C/Swift 其中一门语言,相熟语言个性的实现细节
  2. 相熟编程语言的实现技术,如解释器、编译器、内存治理方面的实现
  3. 相熟某个构建零碎 (CMake/Bazel/Gradle/XCBuild 等)
  4. 有编译器、链接器、调试器等工具的开发和优化教训优先,有 LLVM、GCC 等我的项目我的项目开发经验优先
  5. 有挪动端技术栈开发教训优先

职位链接

https://job.toutiao.com/s/FBS...

援用链接

  1. https://developer.apple.com/v...
  2. https://developer.apple.com/v...
  3. https://github.com/apple/swif...
  4. https://developer.apple.com/d...
  5. https://developer.apple.com/d...
  6. https://lldb.llvm.org/man/lld...
  7. https://llvm.org/docs/BitCode...
  8. https://clang.llvm.org/docs/M...
  9. https://developer.apple.com/l...