字节跳动 Service Mesh 数据面编译优化实践(1),【秋招面试专题解析

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip204888 (备注Android)
img

正文

调研

针对 virtual 函数的优化(即 devirtualization,或 Indirect Call Promotion)大致可分为三类:Link Time Optimization (LTO)、 Whole Program Devirtualization (WPD) 以及 Speculative Devirtualization,它们大致的原理如下:

  • Link Time Optimization (LTO):链接时优化,在编译阶段生成中间编译对象代替传统的二进制对象,并保留了元信息,接着在最终的链接阶段以全局的视角链接所有中间编译对象,执行跨模块的优化手段,并生成二进制代码。LTO 分为 full LTO 和 thin LTO,full LTO 主要串行执行,链接非常耗时,thin LTO 以少量的优化损失作为代价换取并发的执行模型,极大加快链接速度。由于 LTO 在链接阶段具有全局的视角,因此可以进行跨模块的类型推导,进行一定的 devirtualization 优化。

  • Whole Program Devirtualization (WPD):通过分析程序中类的继承结构,得到某个 virtual 函数的所有子类实现,并依据这个结果进行 devirtualization。这个优化需要结合 LTO 才能够实施,且经过实践,该优化效果并不理想(后文阐述)。

  • Speculative Devirtualization:该优化针对某个 virtual callsite,“投机”地假设其运行期的实现是某个或某几个特定的子类,如果命中了,则可以直接显式地调用对应的实现逻辑,否则,再走常规的 indirect call 逻辑。这个优化结合 PGO 才有较好效果。

本文主要关注 Speculative Devirtualization 以及 PGO 优化技术的原理及实践,对 LTO 以及 WPD 的原理不作过多展开。

Speculative Devirtualization 原理介绍

下面以一个例子解释 Speculative Devirtualization 的原理,假设我们编写了一个 Foo 的接口以及一个 FooImpl 的具体实现,如下所示:

struct Foo {

virtual ~Foo() = default;

virtual void do_something() = 0;

};

struct FooImpl : public Foo {

void do_something() override { … }

};

接着,在其他模块使用了 Foo 接口,如下:

void bar(Foo &foo) {

foo.do_something();

}

经过编译后,bar 函数的机器指令伪代码大致如下:

addr = vtable.do_something.addr@foo

call *addr

上述伪代码将传入参数 foo 的 do_something 函数的实际地址进行加载,接着对该地址执行一个 call 指令,即动态多态分发的基本原理。

对于上述例子,在 Speculative Devirtualization 优化中,编译器假设在实际运行中,foo 大概率是 FooImpl 的对象,因而生成的指令中,先判断该假设是否成立,如果成立,则直接调用 FooImpl::do_something(),否则,再走常规的 indirect call,伪代码如下:

addr = vtable.do_something.addr@foo

if (addr == FooImpl::do_something)

FooImpl::do_something()

else

call *addr

可以看到,上面的伪代码中,获取实际的函数地址后,并没有直接执行一个 indirect call,而是先判断它是不是 FooImpl,如果命中,则可以直接调用 FooImpl::do_something()。这个例子只有一个子类实现,如果有多个,也是类似会有 if 判断,等所有 if 判断都失败后,最后 fallback 到 indirect call。

初步看来,这个做法反而增加了指令量,有悖于优化的直觉。然而,假设大部分调用中, foo 参数的类型都是 FooImpl 的话,实际上只是增加一个地址的比较指令。并且,由于 CPU 指令的顺序执行特征,这里不会有分支跳转的开销(尽管有个 if)。进一步地,直接调用 FooImpl::do_something() 与 else 分支中的 call *addr 在高级语言中看起来似乎并没有区别,然而在编译器的视角中是完全不一样的。这是因为FooImpl::do_something()是明确的静态函数,可以直接应用内联优化,不仅能够省去函数跳转的开销,还可以消除函数实现中不必要的计算。考虑一个极端场景,假设FooImpl::do_something()的实现是个空函数,经过内联后,整个过程由最开始的一个 indirect call,优化成了只需比较一次函数地址即可结束的过程,这带来的性能差异是巨大的。

当然,正如这个优化给人的直觉一样。如果上面 foo 的类型不是 FooImpl,那么这就是个负优化,也正因如此,这个优化在默认情况下基本不会生效,而是要在 PGO 优化中才会被触发。由于在 PGO 优化中,编译器具备程序在运行期的 profile 信息,其中就包括 indirect call 调用各个实现函数的概率分布,因此编译器可以根据这个信息,针对高概率的函数实现开启该优化。

PGO 优化实践


PGO(Profile Guided Optimization),也称 FDO(Feedback Directed Optimization),是指利用程序运行过程中采集到的 profile 数据,来重新编译程序以达到优化效果的 post-link 优化技术。其原理认为,对于特征相似的 input,程序运行的特征也相似,因此,我们可以把运行期的 profile 特征数据先采集一遍,再用来指导编译过程进行优化。

PGO 优化依赖程序运行期所采集的 profile 数据,profile 数据的采集有两种方式,一是编译期插桩(例如 clang 的 -fprofile-instr-generate 编译参数);二是运行期使用 linux-perf 工具采集,并将 perf 的数据转换成 LLVM 可识别的 profile 格式。对于第二种方式,AutoFDO 是更通用的叫法。AutoFDO 的整体流程如下图所示:

35ad999dd1f1ebe1e7f35b714a53189b.png

我们的实践采用的是第二种方式:运行期采集 perf 。这是因为,如果采用插桩的方式,就只能采集特定 benchmark 的 profile,而不能采集线上真实流量的 profile,毕竟不可能在线上环境运行一个插桩的版本。PGO 的成功实践极大地促进了 devirtualization 的效果,同时,由于本身也带来了其他的优化机制,获得了 15% 的性能收益,下面介绍我们在 PGO 优化上的重点工作。

基于 Profile 数据的 PGO 优化基本原理介绍

程序运行期采集到的 profile 数据中,记录了该程序的热点函数及指令,这里不做过多展开,以两个简单例子说明它是如何指导编译器做 PGO 优化的。

virtual 函数 PGO 优化示例

第一个例子接着上文中的 Foo 接口。假设程序中除了有 FooImpl 子类外,还存在 BarImpl 以及其他子类,在 Speculative Devirtualization 优化前,程序是直接获取到实际函数地址后执行 call 指令,而 profile 数据则会记录在所有采集到的这个调用样本中,实际调用了 FooImpl、BarImpl 以及其他子类实现的次数。例如,该调用点一共被采样 10000 次,其中有 9000 次都是调用 FooImpl 实现,那么编译器认为这里大概率都是调用 FooImpl,就可以针对 FooImpl 开启 Speculative Devirtualization,从而优化 90% 的 case。可以看出,这个优化对于只有单个实现的 virtual 函数是极佳的,它在保留了未来的 virtual 函数可扩展性的基础上,将其性能优化到与普通直接函数调用无异。

分支判断 PGO 优化示例

第二个例子是一个针对分支判断的优化示例。假设有如下代码片段,该代码片段判断参数 a 是否为 true,若是,则执行 a_staff 的逻辑;否则,执行 b_staff 逻辑。

if (a)

// do a_staff…

else

// do b_staff…

return

在编译时,由于编译器并不能假设 a 为 true 或者 false 的概率,通常按照同样的 block 顺序输出机器指令,伪汇编代码如下。其中,先对参数 a 进行 bool 判断,若为 true ,则紧接着执行 a_staff 的逻辑,再 return;否则,便跳转到 .else 处,再执行 b_staff 的逻辑。

test a, a

je   .else  ; jump if a is false

.if:

; do a staff…

ret

.else:

; do b staff…

ret

在 CPU 的实际执行中,由于指令顺序执行以及 pipeline 预执行等机制,因此,会优先执行当前指令紧接着的下一条指令。上面的指令对 a_staff 是有利的,如果 atrue,那么整个流水线便一气呵成,没有跳转的开销;相反的,指令对 b_staff 不利,如果 afalse,那么 pipeline 中先前预执行的 a_staff 计算则会被作废,转而需要从 .else 处的重新加载指令,并重新执行 b_staff,这些消耗会显著降低指令的执行性能。

从上面的分析可以得出,如果恰好在实际运行中,atrue 的概率比较大,那么该代码片段会比较高效,反之则低效。借助对程序运行期的 profile 数据进行采集,则可以得到上面的分支判断中,实际走 if 分支和走 else 分支的次数。借助该统计数据,在 PGO 编译中,若走 else 分支的概率较大,编译器便可以对输出的机器指令进行调整,类似如下的伪汇编指令,从而对 b_staff 更有利。

test a, a

jne  .if  ; jump if a is true

.else:

; do b staff…

ret

.if:

; do a staff…

ret

最后送福利了,现在关注我可以获取包含源码解析,自定义View,动画实现,架构分享等。
内容难度适中,篇幅精炼,每天只需花上十几分钟阅读即可。
大家可以跟我一起探讨,有flutter—底层开发—性能优化—移动架构—资深UI工程师 —NDK相关专业人员和视频教学资料,还有更多面试题等你来拿

录播视频图.png

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注Android)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

19065895)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-6GudfNxi-1713119065895)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 24
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值