作者:字节跳动终端技术 —— 刘夏

前言

笔者来自字节跳动终端技术 AppHealth (Client Infrastructure - AppHealth) 团队,在工作中我们会对开源 LLVM 及 Swift 工具链进行维护和定制,推动各项编译器优化在业务场景中的落地。编译器作为一个复杂的软件也会有 bug,也会有各种兼容性和正确性的问题,这里我们分享一则开启 clang 的 -Oz 优化选项时发现的编译器缺陷。

问题

在 Xcode 中我们可以对 clang 编译器设置不同的优化等级,比如在 Debug 模式下默认会使用 -O0,在 Reelase 模式默认使用 -Os(兼顾执行速度和体积),但是在一些性能要求不大的场景,我们可以使用 -Oz级别,开启后编译器会针对代码体积采取更加激进的优化手段。

公司的一个视频组件为了减包开启 clang 的 -Oz 优化级别进行编译,但在开启后的测试中发现,视频组件在导出视频时出现内存暴涨然后发生 OOM 闪退,并且可以稳定重现。通过 Instruments 及 Xcode 的 Memory Graph 功能可以看到大量的 GLFramebuffer 被创建,而每个 GLFramebuffer 中会持有一个 2MB 的 CVPixelBuffer ,导致占用大量内存。

预期中这些 GLFramebuffer 应该被复用而不是重复创建,但通过日志发现每次获取时都没有可用的 buffer,于是就不断创建新的 buffewr。在代码逻辑中, buffer 是否能重用依赖于 -[GLFramebuffer unlock] 是否被调用,但是通过观察发现:这些 buffer 会堆积到导出任务结束后才被 unlock,所以我们需要找到 unlock 被推迟的原因

通过阅读代码发现:GLFramebuffer 会被一个 SampleData 对象持有,并在 -[SampleData dealloc] 被调用时对 GLFramebuffer 进行 unlock ,当 SampleData 对象被放到 autoreleasepool 中堆积起来就会出现内存暴涨,符合前面观察到 buffer 批量 unlock 的现象(在 autoreleasepool 批量释放对象的时候)。

注意到之前不开启 -OzSampleData 对象是不会进入 autorelasepool 的,所以没有问题,于是接下来我们需要找到为什么开启 -OzSampleData 对象会被进入 autorelasepool

在 ARC 下对象是通过诸如 objc_autoreleaseReturnValue / objc_autorelease 的 C 函数来触发 autorelease 操作,我们无法通过符号断点到 -[SampleData autorelease] 来确认释放时机,除非把代码改回 MRC,所以这里得通过特殊的方式:

在工程中添加如下一个类,并在 compiler flag 设置 -fno-objc-arc 关闭 ARC:

// 和 SampleData 一样都是继承自 NSObject
@interface BDRetainTracker : NSObject
@end@implementation BDRetainTracker
- (id)autorelease {return [super autorelease]; // 此处设置断点
}
@end

在重写的 autorelease 方法设置断点,然后在 App 启动后执行:

class_setSuperclass(SampleData.class, (Class)NSClassFromString(@"BDRetainTracker"));

如此一来 SampleDataautorelease 时会在我们设置的断点停下。通过这种方法结合上下文可以发现 SampleDataautorelease 的时机集中在 -[CompileReaderUnit processSampleData:]

- (BOOL)processSampleData:(SampleData *)sampleData {...SampleData *videoData = [self videoReaderOutput];...

如果改写成以下形式,发现内存暴涨现象就会消失:

- (BOOL)processSampleData:(SampleData *)sampleData {@autoreleasepool {...SampleData *videoData = [self videoReaderOutput];...}

这里[self videoReaderOutput] 返回一个 autoreleased 对象是符合 ARC 的约定的,但是之前没开启 -Oz 时编译器进行了优化,对象并不会进入 autoreleasepool,方法返回后就马上被释放了,查看 LLVM 的相关文档:

When returning from such a function or method, ARC retains the value at the point of evaluation of the return statement, then leaves all local scopes, and then balances out the retain while ensuring that the value lives across the call boundary. In the worst case, this may involve an autorelease, but callers must not assume that the value is actually in the autorelease pool.

ARC performs no extra mandatory work on the caller side, although it may elect to do something to shorten the lifetime of the returned value.

由于 autorelase 是一个有比较大开销的操作,所以 ARC 会尽可能将其优化掉,但是从这个现象我们可以猜测,开启 -Oz 后此处的编译器对应的优化失效了,让我们查看 SampleData *videoData = [self videoReaderOutput] 处的汇编:

adrp       x8, #0x1018b5000
ldr        x1, [x8, #0x1c0]                 ; 加载 @selector(videoReaderOutput)
bl         _OUTLINED_FUNCTION_40_100333828  ; 调用外联函数
bl         _OUTLINED_FUNCTION_0_1003336bc   ; 调用外联函数

其中调用的两个 _OUTLINED_FUNCTION_ 函数的内容如下:

_OUTLINED_FUNCTION_40_100333828:
mov        x0, x20
b          imp_stubsobjc_msgSend_OUTLINED_FUNCTION_0_1003336bc:
mov        x29, x29
b          imp_stubsobjc_retainAutoreleasedReturnValue

所以这里生成的代码逻辑是符合预期的:

  1. 调用 objc_msgSend(self, @selector(videoReaderOutput), ...) 返回一个 autoreleased 对象
  2. 然后对返回的对象调用 objc_retainAutoreleasedReturnValue 进行强引用

我们可以对比之前开启 -Os 生成的代码,此处 LLVM 的 MIR outliner 生效了:

adrp       x8, #0x10190d000
ldr        x1, [x8, #0xf0]
mov        x0, x20
bl         imp_stubsobjc_msgSend
mov        x29, x29
bl         imp_stubsobjc_retainAutoreleasedReturnValue

Machine Outliner

编译器在 -Oz 优化级别下 3~4 行和 5~6 行两段指令因为在多处被使用,于是分别被抽离到独立的函数进行复用,而原来的地方变成了一条函数调用的指令,数量从 4 条变成 2 条,从而达到减包的目的,这便是 LLVM 的 Machine Outliner 所做的事情,在 -Oz 下它会被默认开启来达到更极致的代码体积缩减(在其它优化级别下需要通过 -mllvm -enable-machine-outliner=always 来开启),其大致原理如下:

extern int do_something(int);int calc_1(int a, int b) {return do_something(a * (a - b));
}int calc_2(int a, int b) {return do_something(a * (a + b));
}

这段代码中 calc_1/calc_2 都调用了 do_something,尽管参数都不一样,但是我们能从汇编看到一些重复出现的指令序列(这里用 ARMv7 架构的汇编方便演示)

calc_1(int, int):add r1, r1, r0            ; Amul r0, r1, r0            ; Badd r1, r1, r0            ; Amul r0, r1, r0            ; Bb   do_something(int)     ; Ccalc_2(int, int):add r1, r1, r0            ; Aadd r1, r1, r0            ; Amul r0, r1, r0            ; B      b   do_something(int)     ; C

我们给相同的指令打上相同的标签,所以 calc_1 的指令序列是 ABABC 而 calc_2 是 AABC,编译器通过构造一个后缀树可以找到它们的最长公共子串是 ABC,那么 ABC 这一段就可以被剥离成一个独立的函数:

calc_1(int, int):add r1, r1, r0            ; Amul r0, r1, r0            ; Bb OUTLINED_FUNCTION_0calc_2(int, int):add r1, r1, r0            ; Ab OUTLINED_FUNCTION_0OUTLINED_FUNCTION_0:add r1, r1, r0            ; Amul r0, r1, r0            ; B      b   do_something(int)     ; C

由于在 ARC 代码中编译器插入的内存管理相关指令非常常见,所这些操作多数会被 outlined(读者如果对其实现细节感兴趣可以参考这个演讲)。

ARC 优化

但是为何指令被 outline 后 ARC 的优化会失效呢?留意到 mov x29, x29 这条指令,它实际上并没有做任何有意义的操作(将 x29 寄存器的值又存到 x29),它只是个特殊的标记,是编译器用于辅助运行时进行优化的手段, videoReaderOutput 的实现中返回 autorelease 对象是一个这样的调用:

return objc_autoreleaseReturnValue(ret);

其运行时的实现大致如下:

// Prepare a value at +1 for return through a +0 autoreleasing convention.
id  objc_autoreleaseReturnValue(id obj) {if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;return objc_autorelease(obj);
}// Try to prepare for optimized return with the given disposition (+0 or +1).
// Returns true if the optimized path is successful.
// Otherwise the return value must be retained and/or autoreleased as usual.
static ALWAYS_INLINE bool
prepareOptimizedReturn(ReturnDisposition disposition) {assert(getReturnDisposition() == ReturnAtPlus0);if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) {if (disposition) setReturnDisposition(disposition);return true;}return false;
}static ALWAYS_INLINE bool
callerAcceptsOptimizedReturn(const void *ra){// fd 03 1d aa    mov x29, x29if (*(uint32_t *)ra == 0xaa1d03fd) {return true;}return false;
}static ALWAYS_INLINE void
setReturnDisposition(ReturnDisposition disposition) {tls_set_direct(RETURN_DISPOSITION_KEY, (void*)(uintptr_t)disposition);
}

objc_autoreleaseReturnValue 中会使用 __builtin_return_address 获取返回地址的指令,检查是否存在标记 mov x29 x29,如果有,意味着我返回的这个对象会马上被 retain,所以没必要放到 autoreleasepool 中,此时运行时会在 Thread Local Storage 中记录此处做了优化,然后回计数 +1 的对象即可。

对应地 videoReaderOutput 的调用方会使用 objc_retainAutoreleasedReturnValue 引用住对象,实现如下:

// Accept a value returned through a +0 autoreleasing convention for use at +1.
id objc_retainAutoreleasedReturnValue(id obj) {if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;return objc_retain(obj);
}// Try to accept an optimized return.
// Returns the disposition of the returned object (+0 or +1).
// An un-optimized return is +0.
static ALWAYS_INLINE ReturnDisposition
acceptOptimizedReturn() {ReturnDisposition disposition = getReturnDisposition();setReturnDisposition(ReturnAtPlus0);  // reset to the unoptimized statereturn disposition;
}static ALWAYS_INLINE ReturnDisposition
getReturnDisposition() {return (ReturnDisposition)(uintptr_t)tls_get_direct(RETURN_DISPOSITION_KEY);
}

objc_retainAutoreleasedReturnValue 看到 TLS 中的标记知道无需进行额外 retain,于是两者配合从而优化掉了一次 autoreleaseretain 操作,但这是编译器和运行时的优化细节,不应该假设优化一定会被发生。正是由于开启 -Oz 后,machine outliner 棒打鸳鸯把 objc_msgSendobjc_retainAutoreleasedReturnValue 的调用指令及标记 outline 了,导致这个优化没有触发,对象进入 autoreleasepool

总结

所以本质上这既是一个开发者的疏忽:使用占用大内存的临时对象后没有及时增加 autorelasepool 将其释放,只是 ARC 的优化将这个问题隐藏,最终在开启 -Oz 后被暴露。

同时,这也是一个编译器的 bug,不应该将此处代码进行 outline 导致 ARC 的优化失效,这个 bug 直到最近才在 LLVM 里面被修复。

同样是使用 ARC 的 Swift 也有类似的问题,在某些 ARC 优化(比如 -enable-copy-propagation )没有开启的情况下一些对象的生命周期可能会被延长,然后这个现象被开发者利用,在编译器保证之外的生命周期使用该对象,一开始可能没有问题,但是一旦这些优化由于编译器的升级或者代码的改动突然生效了,那么之前使用对象的地方可能就会访问到一个被释放的对象,更多具体的例子可以参考 WWDC 21 的 Session 10216。

关于字节终端技术团队

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

火山引擎应用开发套件MARS是字节跳动终端技术团队过去九年在抖音、今日头条、西瓜视频、飞书、懂车帝等 App 的研发实践成果,面向移动研发、前端开发、QA、 运维、产品经理、项目经理以及运营角色,提供一站式整体研发解决方案,助力企业研发模式升级,降低企业研发综合成本。

一起来找茬:记一起 clang 开启 -Oz 选项引发的血案相关推荐

  1. AI一眼识别这是什么鸟 “我们来找茬”十级选手诞生

    话说,你能看出这三只鹦鹉有什么不一样吗?脸盲如我,要使出玩"我们来找茬"的十级能力. AWSL,鹦鹉鹦鹉,傻傻分不清楚. 结果,AI一顿操作猛如虎,进行了判断:左边的是桃面牡丹鹦鹉 ...

  2. 一眼识别这是什么鸟,比人类还厉害的“我们来找茬”十级选手诞生!

    点击上方"视学算法",选择加"星标"或"置顶" 重磅干货,第一时间送达 AI科技评论报道 编辑:琰琰 话说,你能看出上面这三只鹦鹉有什么不一 ...

  3. 大家一起来找茬(BUG)

    大家一起来找茬(BUG) ----------目录---------- 一.上手体验 1.主界面 2.功能 二.程序的 BUG 三.必应词典的 BUG 1."每日一句"里的句子不能 ...

  4. 比较原始的QQ大家来找茬的原型

    昨天不用加班,闲来无事,玩了几把大家来找茬.可惜眼力不够,总是找不出来,心想能不能写个外挂,帮忙找出在哪里. 在网上搜了一下,大体上也有一些利用python,计算来找茬的外挂. 本人python基础比 ...

  5. 大家来找茬 Matlab小程序 有趣

    简单的大家来找茬Matlab程序 代码过程: 读取影像 转为灰度 做差 转为二值图 标记连通区域 度量图像区域属性 根据阈值筛选面积较大的区域 勾画矩形框标记 [Filename1,filepath1 ...

  6. 最强找茬微信小程序源码修复版,已更新微信授权

    找茬小程序前后端搭建教程 下载压缩包后解压得到三部分内容:前端代码文件.后端代码文件.过关素材资源. 这个找茬小程序是我修复登录后的版本. 后端:Nginx 1.18.0+PHP-7.2+mysql5 ...

  7. Python实现微信找茬小游戏自动进行

    摘要:这篇文章介绍微信小程序"大家来找茬"怎么使用程序自动"找茬",使用到的工具主要是Python3和adb工具. 作者:yooongchun 微信公众号: y ...

  8. windows编程实践之 QQ找茬

    些年前写的代码了, 纯粹是为了记录下.因为这是大学期间写的唯一一个有其他同学用的程序.... 幸好当时发表在了看雪论坛,要不然什么都记不得了 https://bbs.pediy.com/thread- ...

  9. 社交系统/社群系统“ThinkSNS+”H5及PC端即将内测!一起来“找茬”

    还记得2017年4月20日么?那是我们社交系统TS+的APP开启内测的日子,时间过得太快,已经一个月有多不要伤心,今天我们有好多好多好消息要公布,已经激动得不知道先说哪一个了 好消息一.社交系统Thi ...

  10. 可怕的“我们来找茬”,你能看出哪个是正品logo吗?阿里实验室可以!

    点击上方"迈微AI研习社",选择"星标★"公众号 重磅干货,第一时间送达 AI科技评论报道 话说,你能看出上面这三只鹦鹉有什么不一样吗?脸盲如我,要使出玩&qu ...

最新文章

  1. 《OpenCV3编程入门》学习笔记6 图像处理(六)图像金字塔与图片尺寸缩放
  2. 第 2 章 Editor
  3. 转 linux常用查看硬件设备信息命令
  4. php 超链接新页面打开新页面,Typecho 超链接默认新窗口打开
  5. 开源JVM Sampling Profiler
  6. php put 参数,php – 如何在Guzzle 5中发送PUT请求的参数?
  7. php 不允许外部访问,[日常] 解决mysql不允许外部访问
  8. MySQL-安全对调两个表名
  9. Java进阶:SpringMVC中使用fileupload报错Error creating bean with name ‘multipartResolver‘
  10. 企业微信网页授权初试
  11. 敏感词检测软件-在线敏感词批量检测免费
  12. echarts结合amap (echarts-extension-amap)
  13. Android 仿照美团城市选择,微信小程序仿美团城市选择
  14. jmeter的${__time(,)}和${__timeShift(,,,,)}函数使用
  15. 各操作系统支持图标字体的终端推荐
  16. java-php-python-ssm商超销售系统计算机毕业设计
  17. el-table表格某列添加icon图标
  18. java输入成绩并排序简单_java 成绩排序
  19. IC设计数字工程师技能必备
  20. 实现Torchlight(火炬之光)的背包UI效果

热门文章

  1. DRAM基本单元最为通俗易懂的图文解说
  2. R平方值python实现
  3. IP地址及其分类(A、B、C类)
  4. psutil:系统、进程,信息都在我的掌握之中
  5. 秋姑娘_我爱秋天作文300字
  6. C/C++编程学习 - 第5周 ⑤ 人见人爱A+B
  7. 打包时错误 Entry name ‘classes.dex‘ collided 的解决办法
  8. 亲自动手写爬虫系列三、爬取队列
  9. 卫生保健所短信群发模板:预约挂号、就诊提醒、检查结果通知
  10. SUSE常见问题解决办法