人物采访 / T SALON

编辑内容

戴铭谈 LLVM 与持续学习:从工程优化到画图、分享和长期探索

回顾 2022 年 T Chat 第 6 期两段录播,覆盖无用代码分析、LLVM IR 与 Pass、开发提效,以及戴铭对技术成长、知识分享、绘画、团队文化和职业方向的思考。

T Chat 第 6 期戴铭分享 LLVM 与个人成长的访谈封面
T Chat 第 6 期戴铭分享 LLVM 与个人成长的访谈封面

本期视频发布于 2022 年。文中的技术状态、个人经历与观点均为当时情况。

一段从清理无用代码开始的技术分享,为什么最后会讲到绘画和持续学习?在 2022 年 7 月发布的 T Chat 第 6 期中,戴铭给出了一个连贯的回答:围绕同一个问题不断改进解法,会迫使人进入新的知识领域;把理解画出来、讲给别人听,又会暴露自己还没有想清楚的地方。

第一段录播约 33 分钟,从大型移动工程的优化需求,走到 LLVM 中间表示、Pass 和开发反馈速度。第二段约 57 分钟,讨论他在快手的工作、此前的经历、绘画与编程的结合、团队选择,以及对后续技术方向的兴趣。

技术分享:为什么从编译过程寻找优化空间

长期演进的应用,需要重新审视留下来的代码

戴铭先提出两个问题:怎样优化积累多年的移动工程,怎样在工程越来越大时保持开发效率。旧工程可能留下不再需要的功能和代码,而客户端的交付方式决定了这些内容会随安装包来到用户设备上。因此,清理工作不仅影响代码维护,也关系到包体积及用户设备上的资源使用。

他选择从无用代码切入,并逐步比较不同观察位置能够提供什么信息。分享的主线是:先看编译产物,再看源码结构,接着观察实际运行,最后考虑能否在编译过程中定制自己需要的数据。

这个问题在客户端格外直观:服务端保留一段不再走到的代码,通常不会让每位用户为它下载安装;移动应用的旧功能却仍可能留在包里。戴铭也把清理与启动、内存占用联系起来,但没有给出“删掉多少代码就能节省多少资源”的通用换算。要做判断,先得知道自己到底看见了什么。

编译产物里的引用关系,并不等于完整的运行行为

第一种路径从产物入手。戴铭介绍,开发者可以结合链接结果与 Objective-C 相关的引用信息,比较有哪些方法、哪些方法在产物中被引用,以寻找可能不再使用的部分。

但 Objective-C 存在运行时调用,方法是否被执行不能只靠一类静态引用判断。通过选择子、字符串或框架回调触发的行为,未必能被简单的集合相减可靠捕捉。因此,这类方法更适合产生待检查的候选项,不能把所有没有被某种规则识别到的调用都直接删掉。

这也是分享中第一个重要的边界:工具从某个位置没有看到使用证据,与程序永远不会用到它,是两件不同的事。

他还原了一个早期做法:从链接映射取得方法集合,再从产物里的 Objective-C 引用信息提取调用关系,试着将两者相减。集合相减很容易实现,却容易漏掉运行时才确定目标的调用。反过来,有静态引用也不保证用户会真正进入那条路径。这让“无用”逐渐分成两个问题:代码里有没有可能调用,以及真实运行时到底有没有调用。

回到源码,理解调用是怎样写出来的

为补充产物侧的信息,戴铭转向编译前端。他以 Clang 及其处理的语法结构说明,分析工具可以遍历节点,识别特定调用形式,再读取相应参数。与只看最终产物相比,这样能利用源码中保留的更多表达信息。

他用 performSelector 一类调用方式,以及事件、定时器等相关场景解释需求。重点是根据不同调用机制设计对应规则,而不是期待一种扫描方式覆盖所有动态行为。源码层面的分析仍然属于静态观察,它能帮助缩小范围,但不能替代运行验证。

他具体展示了前端插件怎样工作:遍历抽象语法树中的调用节点,识别 performSelector 等写法,再查看传入的参数,找出产物引用表不容易呈现的选择子。事件处理、定时器以及键值相关机制中,字符串也可能参与方法查找;因此规则要按调用方式扩充,不能把一次遍历当成完整的动态调用清单。

从“代码可能调用”走到“运行中观察到了什么”

即使代码里写着调用关系,真实用户也可能没有走到对应路径。戴铭因此继续讨论动态观察:在页面上增加埋点,能够知道页面是否使用,但粒度较粗;观察类的初始化,也不容易区分一个大类中究竟哪些方法被执行。

代码覆盖率提供了更细的观察方式。他介绍通过构建选项生成运行数据,再用配套工具生成报告。这样的结果可以帮助了解测试或一段运行期间覆盖了哪些代码,但也会带来采集、构建和运行成本。选择观察粒度,必须结合要回答的问题。

对清理代码而言,这里还有一个不能省略的限制:某段代码在一次测试或一批用户样本里没有被执行,不足以单独证明它可以安全删除。样本、功能入口与低频场景仍需另行确认。

戴铭把几种粒度放在一起比较:页面埋点知道用户到过哪里,却不知道页面内部哪个方法运行;类初始化能够证明类被使用,对很大的类仍不够细;逐行覆盖率能看到更多,却为他只想确认方法是否执行的目标收集了过量信息。选粒度的依据不是哪种工具最“全面”,而是需要据此作出什么决策。覆盖率报告适合线下检查,若想观察真实线上使用,还得重新衡量插桩体积、编译时间和运行成本。

插桩的重点,是选择需要的观察位置

随后,戴铭把话题带到 fuzzing 所使用的覆盖反馈及相关插桩能力,介绍了在函数、基本块或控制流边等位置加入记录逻辑的思路,并概览相关工具路线。他真正关心的,是能否用更合适的粒度观察函数执行,而不必承担与目标无关的全部记录工作。

这里需要区分两类概念。Clang 的源码覆盖率和 SanitizerCoverage 是不同机制;后者可以在函数、基本块或边级别插入对自定义函数的调用。不能把所有代码覆盖率工具统称为由 fuzzing 实现,也不能将它们的开销一概而论。Clang 源码覆盖率文档;SanitizerCoverage 文档

分享通过回调说明,程序执行到相应位置时,开发者可以记录所需信息。戴铭同时提出了自己的工程顾虑:现有方式在其场景下的编译成本和定制空间仍不理想,如果只需要少量特定记录,能否进一步减少插入的工作?这个问题引出了自定义 LLVM Pass。

他以函数入口回调为例:在构建时选定插桩位置,在回调中记录函数身份、出现顺序或调用次数,就能得到运行轨迹。SanitizerCoverage 允许选择函数、基本块或边等不同位置,也可结合规则控制范围;但戴铭在自己的工程里仍认为编译耗时偏高,并希望进一步缩减插入的指令。这里是他提出自定义方案的直接原因,不是说现成机制对所有工程都不适用。

LLVM IR 提供了源码与机器码之间的工作层

在讲 Pass 之前,戴铭先解释 LLVM IR,即编译流程中的一种中间表示。它让开发者有机会在程序已经离开原始源码、但尚未完成目标代码生成的阶段分析或改变程序。

他按模块、函数、基本块与指令展开这一结构:模块中包含函数和全局信息;函数的主体组织为基本块;基本块内排列具体指令,并通过控制流与其他块连接。理解这些关系,才能知道分析或插桩应该放在哪里。

他将结构从大到小画出来:一个 module 里有函数声明、定义和全局数据;函数里有若干 basic block;块带着可供跳转的标识,内部排列取值、存储、计算等指令,末尾再决定返回或去往哪个块。循环和条件判断不再以原来语法的样子出现,却仍能通过块之间的连接看出控制流。这个例子让随后“遍历函数、找到第一条指令并插入调用”有了明确落点。

分享也用 Objective-C 运行时帮助听众理解,高级语言特征在更低层会有相应的实现形式。不过,IR 并不完整保留原始源码语义,也不是能直接在处理器上执行的机器码;分析具体指令时,仍需对照所用工具链的 LLVM IR 参考。

Pass 怎样进入编译流程

戴铭接着介绍 Pass:把某项分析或转换组织成可接入编译流程的处理步骤。分析可以收集程序信息,转换则可能改变 IR,例如加入记录函数的调用。他谈到 C++ 实现、C 接口及语言封装的选择,也说明开发者需要关注接口和版本条件。

Pass 并非彼此毫无关系。它们可以组成执行流程,并通过管理机制协调分析结果、执行顺序及相关行为。分享介绍了 Pass Manager、运行入口与前后回调等概念,帮助听众理解一个处理步骤怎样被调用,而不是要求业务代码直接承担这些控制工作。

官方文档也区分分析 Pass 与转换 Pass,并说明插件加载及管理接口。具体类名、构建参数和加载方式会随 LLVM 版本变化,实践时需要对应版本核对。LLVM Pass 编写文档

为了说明 Pass 不是一个孤立的函数,他还比较了将处理步骤编进工具与加载动态库两种接入方式,并提到可从 LLVM 自带示例开始学。Pass Manager 负责把多个步骤组织起来,执行前后的回调与分析结果会影响下一步能否复用已有信息。讲者同时提到 C 接口相对稳定、用 Swift 封装需额外工作;这只是当时对实现成本的讨论,实际接口仍要以对应 LLVM 版本为准。

一个记录函数,怎样被放进目标程序

示例回到最初的需求:想知道某些函数在运行时有没有执行。戴铭展示的思路是,先在模块中取得准备调用的记录函数,再遍历相应函数与基本块,在选定的位置插入调用指令。记录逻辑由此进入目标程序,而不必要求业务开发者逐处手写日志。

这使插桩范围、位置和内容有了进一步定制的可能,也将实现责任移到了构建工具一侧。示例中的指令规模说明了精简的方向,但不应把演示中的数字写成所有工程都能达到的固定开销。

演示里的顺序是先从模块及编译上下文找到记录函数,再走过函数和基本块,在选定块的入口取得插入点,生成一次对记录函数的调用。这样业务源码没有逐处写日志,生成的程序却在目标位置留下了执行证据。若要把这种证据用于删除代码,仍必须核对插桩是否覆盖所有构建配置和使用场景。

他随后谈到如何把自定义处理接入 Xcode 构建环境,包括使用相应工具链及选项。实际工程仍需要验证生成结果、调试方式及运行表现;一次演示不足以证明它适合所有线上应用。

缩短反馈周期:从代码注入到 IR 执行的探索

技术分享后半段转向开发提效。理想体验是修改代码后尽快看到结果,减少等待整个工程重新构建的时间。戴铭先比较脚本执行与原生开发中的代码注入思路,并用工具示例解释一种流程:监听文件变化,编译变更部分,加载新的代码,再让运行中的应用使用更新后的实现。

这段讨论涉及动态库、符号替换及相关运行机制。其重点是解释反馈为什么能够变快,同时提醒听众,这类工具有适用范围、接入要求和实现限制。开发期能够使用的方式,并不能直接推导出应用发布或线上更新同样可行。

他把代码注入拆成一条可观察的链路:电脑上的辅助程序监听工程文件变化;变更代码被编成动态库;运行中的应用加载这个库,再让新实现接过原方法的调用。提到 dyld 的符号替换及 SwiftUI Preview 时,他要说明的是“看起来一改就生效”背后仍有编译、加载和替换,而不是跳过构建。旧实例状态、代码接入方式和适配新框架,都会限制它能更新的范围。

接下来,他提出从 IR 层继续探索的想法:如果需要加入辅助行为,可以尝试在中间阶段完成,减少对业务源码的侵入;再研究怎样解释执行相应的中间表示。他借 IR 的结构与静态单赋值形式说明研究这一层的吸引力,并介绍生成、连接和读取 bitcode 后组织执行的过程。

这在 2022 年仍是他提出的技术探索方向,并非已经验证的通用热更新方案。能否用于发布构建、能否通过 App Store 审核,还需按实际产物和规则另行判断。

戴铭接着解释为什么会想到 IR:它采用静态单赋值等形式,变量每次赋值都有新名字,且不必直接处理目标机器有限的寄存器数量,这让他觉得解释执行有研究价值。他谈到将各文件生成 bitcode、连接模块、读取并解析后交给解释器的步骤;同时也明确说相关解释器需要自己集成和改造。这是“接下来可以研究什么”的路径,不是现场完成了一套可发布产品。

成长对谈:同一个问题,也可以不断打开新领域

工作方向与长期收益

对谈一开始,主持人先替直播间处理两个紧接技术分享的问题。问 Objective-C 和 Swift 学什么,戴铭在 2022 年直接选择 Swift,并把回答与当时苹果开发工具和语言方向联系起来;问自行编译的 LLVM 工具链是否保证 App Store 通过,他没有作保证,而是建议可以先用于线下分析,再按实际发布要求决定提交构建怎么做。这两个问答也给前半场划了一道边界:能够用编译技术做实验,不等于发布路径已经得到验证。

第二段先回应观众的问题。面对学习 Objective-C 还是 Swift 的提问,戴铭在当时倾向于 Swift;面对自建工具链能否用于上架的问题,他没有给出审核一定通过的保证,而是区分线下分析、开发验证与实际提交的构建路径。这两段回答都应保留其时间与适用范围。

介绍在快手的工作时,他列出几个方向:iPad 适配、在已有工程中引入 Swift,以及编译相关的长期研究。适配看起来像一项界面任务,真正落入历史工程后,却会遇到旧布局方式、窗口管理和代码组织等困难;刚进入团队时,还要同时熟悉工程与协作者。

谈到 Swift,他重视语言及工具帮助开发者更早发现一部分错误的价值,也关心表达方式和长期维护。这是他解释为什么投入新语言的理由,并不意味着采用一种语言就能消除全部内存或运行时问题。他将自己的角色概括为继续做好技术专家,推动能够长期产生收益的工作。

主持人问 iPad 适配时,他补了一个容易被忽略的工程细节:刚入团队、工程与同事都不熟悉,还要带几位同样刚接触项目的同学一起推进。系统提供的适配回调只是表面,历史手写布局和多窗口问题才占去大量排查时间;相比之下,已有自动布局的部分更容易处理。他由此谈到声明式 UI 可能简化未来适配,但没有说改用新 UI 技术就能自动消除旧工程问题。

不同经历留下的知识,会在后来重新连接

主持人随后请他回顾成长经历。戴铭从大学期间接项目讲起:面对实际需求,学习服务端、脚本和客户端相关技术,先尝试做出来,再逐渐理解背后的概念。他也讲到创业公司中同时承担开发、产品、设计与管理,以及后来不同工作环境中的经历。

他并没有把这段过程包装成一条事先规划好的直线。有些阶段能接触很多事情,却缺少大规模业务带来的深层技术问题。进入用户规模更大的互联网项目后,反馈和故障变得更密集,对工程能力的要求随之提高,他也因此感受到更强的学习压力和成长。

早期看似分散的经历仍有作用:服务端、网页、脚本和客户端的知识,在后来会找到新的连接点。主持人从中提炼出成长不必过早定型的看法;戴铭则继续用具体经历说明,问题和实践怎样改变自己的理解。

他讲大学接项目时很坦率:当时连 Cookie、Session 以及 GET、POST 都没有完全分清,先接下需求,再按例子学习;做过网页、Flash 和 Delphi 数据库相关项目。创业公司又让他兼做产品、设计和管理,但用户规模不足,也意味着缺少推动他深挖性能与稳定性的现场问题。直到进入用户量更大的项目,真实反馈和故障不断出现,他才感到技术能力被更快地逼着向前走。主持人把这概括成“不怕晚”,戴铭马上纠正自己还谈不上“成”;他关心的是不同阶段见到的问题确实不同。

走过弯路之后,还要继续改进解法

谈到对成长帮助最大的事情,戴铭给出三条相互关联的经验:从弯路中学习,围绕一个问题不断迭代方案,以及记录与分享。

他以早期做复杂列表为例。不同单元格的状态在滚动过程中出现问题,自己反复尝试后才逐渐理解相关机制。正是实际困难,使继续研究原理变得有动力。他所说的弯路,是对既有经历的理解,并不是主张在可以借助资料时刻意重复错误。

列表例子里,他最初按数据给多种 cell 设置不同状态,却发现复用和滚动后内容会串。一个周末反复改动、在不同回调中补设置,才让现象暂时稳定。正因为亲手碰到这些边界,他后来更愿意追问 cell 生命周期和复用机制。这个经历解释了“弯路带来理解”的来历,也提醒读者,暂时消除症状还不是理解原理。

但单靠经历困难仍不够。包体积优化就是一个可以持续深挖的问题:一种方法已经被使用后,如果还想取得新的改进,就需要转向新的观察位置,从产物分析到编译前端,再到链接和 IR 插桩。问题看似相同,追求更好解法的过程却扩展了自己的知识范围。

讲不清楚,有时是还没有理解清楚

这里的“继续学习”也包含一个很现实的动作:把资料存到将来找得到的地方。戴铭介绍自己尝试用 Swift 并发能力做 macOS 工具,将常看的技术链接、书单、网站、开发手册和知识图谱集中起来;编程时不必假装记住所有语法,遇到细节就回手册确认。他也会观察自己欣赏的开发者正在研究什么,把他们提到的新线索留作下一步阅读的入口。主持人请他给直播间推荐这些资料,他并没有把工具说成唯一的学习路线。

为了继续研究,戴铭会查资料、读书,并把有用内容记录下来。分享则是下一轮检验:同一个主题讲过多次,仍然会发现某处说不清。原因可能不仅是表达技巧,也可能是对问题的理解还不够完整。

他回忆参加早期技术交流、研究新的技术思路并向团队分享的经历。通过多次组织和解释,原本模糊的关系逐渐变得清楚。听众也不必很多,两三个人提出的问题,就可能暴露讲者没有意识到的细节。图里颜色代表什么、步骤为什么这样安排,都能成为继续推敲的入口。

他回忆第一次把学来的开源库实践讲给团队听,讲到一半连自己都觉得没有把关系理顺;重复分享、重新画图后,才渐渐能回答同事的问题。主持人把这三条经验概括为“经历问题、深挖解法、记录分享”,戴铭认可这个顺序,并没有把“输出”说成脱离实践的独立任务。

关于学习渠道,他介绍自己把资料、链接、书单、播客与开发手册汇集进一个 macOS 工具的尝试,也提到整理开发知识图谱。对他而言,这些材料既服务分享,也帮助日常查阅,不需要假装所有语法和接口都已记住。

另一个方法是关注自己认可的开发者,观察他们正在研究什么、怎样解决问题。这样的关注提供线索,再结合自己的需求进入具体资料,而不是用追随别人替代实践。

绘画怎样成为技术表达的一部分

主持人称他为“斜杠青年”,问怎样同时做技术和绘画。戴铭先说的是取舍:把大量时间留给这两件事,其他娱乐和兴趣自然就少了;临近已经承诺的分享,他会短时间减少新的信息输入,先把答应的内容做完。他明确把这说成某一段时间的办法,并非长期什么都不看。后来主持人问一页 PPT 要画多久,他答早期一张图可能磨好几天,熟练之后主要时间花在脑中构图,实际下笔快得多。

进入绘画话题后,戴铭先谈取舍。他把较多时间留给技术和绘画,也就减少了其他活动;临近已经承诺的分享时,还会暂时收拢输入,将精力集中在完成内容上。这是他的阶段性工作方式,并非适合所有人的固定时间管理规则。

编程与绘画真正连接起来,来自一次解释自动布局问题的需要。他回忆,为了说明线上问题及相应机制,自己查阅了大量资料,直到进一步理解原理后,仍觉得只靠口头描述难以让人跟上。于是,他把关系画在纸上,再在白板上一边画一边讲。

画图让过程逐步展开,听众可以跟着提问,他也能立即回应。之后,他更愿意用图形解释技术。绘画还帮助他形成形象记忆,将概念与可见的结构连接起来。随着练习增加,构思仍然重要,但动手绘制不再像初学时那样每一步都需要重新摸索。

这个转变源自一次具体事故:早期自动布局在某个系统版本里出现崩溃,他需要向大规模使用它的团队解释原因,否则大家可能因此放弃这项技术。翻查资料之后,他借助当年的技术讲解弄清了问题,却发现只靠口述很难描述其中的循环关系。于是先在纸上画草图,分享时再在白板上按步骤画出来;听众沿着图提问,他也能及时指出关系在哪里。后来他连记英语单词都试过配图,认为图像能帮助自己记住抽象信息。

团队文化,从交流与实际工作中感受

主持人问快手团队氛围,戴铭先讲入职前去做过两次技术分享:第一次到场的人很多,第二次的问题甚至超出他预想,让他觉得讨论技术的意愿很强。入职后,他又参与客户端技术内容整理,看到启动优化、性能诊断和开源项目等方向。这比一句“团队技术氛围好”更能说明他的判断从何而来,也仍只是个人在当时团队的见闻。

谈到快手的团队氛围,戴铭用自己加入前去做分享的经历举例:参与者多,提问也多,让他感受到同事对技术的兴趣。加入之后,他也参与团队技术内容的整理与分享,并谈到启动优化、性能工具和开源等工作。

这些例子呈现的是他在当时所观察到的团队,不代表对整个公司作统一评价。更具体的判断依据是,技术问题是否有人愿意认真讨论,实践能否沉淀为可共享的材料,以及团队是否愿意投入长期建设。

招聘问答中,他区分了校招与有经验候选人的观察重点。对校招,他重视学习能力,并结合带新人的经验说明其影响;对有工作经历的人,基础与技术能力之外,还要考虑协作方式和团队是否合适。录播中也提到了当时的招聘信息,这些时效性信息不在此延用。

主持人进一步追问选择工作时看什么,戴铭补充了对公司发展方向的认同。技术兴趣、团队相处方式和业务方向共同影响选择,不能只根据一个抽象的公司标签判断自己是否适合。

困惑不会因为擅长某件事就消失

当话题转向困难时,戴铭选了一个持续多年的绘画问题:为什么掌握了一些线条、颜色、光影和结构知识,作品仍然不够好看?他尝试过从不同要素寻找答案,也不断观察自己喜欢的作品,想理解作者为什么这样安排。

他坦言,投入过深也会影响自己的生活节奏;这不是对透支身体的鼓励。真正值得保留的是他面对问题的方式:没有因为暂时解释不了就停止观察,也没有把某一条技巧当成能解决全部问题的公式。

后来,阅读中关于秩序与混乱关系的一种理解,使他重新连接了此前的经验。他认为,如果没有前面那些尝试,同样一句话未必会给自己带来如此具体的体会。主持人由此总结,较平稳的心态和向有经验的人学习,都能帮助人在困惑中继续推进。这是一段个人学习经验,不是对美学问题的唯一答案。

在听到那个关于秩序的说法前,他先后怀疑过线条、配色、光影、结构甚至对比度,每次练习都只能解释一部分。他还会对着喜欢的画反复想:画家为什么在这里安排这个形状和颜色?后来读到一句概括时,他再看影视画面的构图与服装,才把此前散落的观察接起来。主持人强调从前人的经验里寻找线索,戴铭则强调,若没有长期试错,他很可能只记住一句话而没有真正体会。

对未来的兴趣,仍围绕工具与底层能力展开

最后,戴铭介绍当时关注的方向。他谈到一个本地发布网站与订阅内容的工具,以及 IPFS 等去中心化技术带来的想象空间。这里反映的是他对客户端能力扩展的兴趣;访谈中的未来判断不能当成服务端必然消失、内容自动永久可用或无需维护的技术结论。

另一个更贴近日常工程的方向,是编译、IDE 和文本编辑器。他希望分析结果能够更自然地回到开发者正在使用的工具里,减少在不同平台之间切换的割裂感。前半场的编译分析与开发提效,也在这里回到了完整的开发体验。

他还提到对游戏模拟器和虚拟机实现的兴趣:旧软件怎样在新环境中运行,本身就能引出许多值得研究的系统问题。谈到职业方向,他当时仍愿意围绕苹果生态继续投入,因为语言、工具、客户端产品与自己的兴趣存在交集;同时也给未来出现更合适的技术环境留下空间。

主持人最后提醒,这里的客户端应作更广义的理解,不只是编写某一种应用界面,也包括工具、执行环境及系统能力。这是两人当时对探索方向的讨论,不能推导为某类岗位的前景保证。

这段对话还有一次值得保留的观点转折:戴铭因本地发布网站、IPFS 与订阅工具,推想未来会有更多能力回到用户设备;主持人随即把“客户端”解释为更广的终端计算,而不是给当时写 App 的岗位作保证。去中心化内容依然需要可达性、维护和治理,访谈没有证明服务端会消失。与其把他的预测当结论,更能借鉴的是他怎样从一个具体工具,继续想到编辑器、编译器和模拟器的实现问题。

完整原片、分段信息可从第 6 期访谈页继续查看。

查看本期原视频与分段信息:戴铭:如何使用 LLVM →