字节跳动 Service Mesh 数据面编译优化实践

前言字节跳动在内部大规模落地了 Service Mesh,提供 RPC、HTTP 等多种流量代理能力,以及丰富的服务治理功能 。Service Mesh 架构包含数据面和控制面,其中,字节跳动 Service Mesh 数据面基于开源的 Envoy 项目进行二次开发及改造,并针对主要的流量代理及服务治理功能进行了重写,项目采用 C++ 语言编写 。
我们在优化数据面的历程中,基于 LLVM 编译工具链,围绕 C++ Devirtualization 以及编译优化进行了较多探索,落地了 LTO (Link Time Optimization)、PGO (Profile Guided Optimization) 、C++ Devirtualization 等编译优化技术,获得了 25% 的可观性能收益 。本文将分享我们在字节跳动 Service Mesh 数据面的编译优化方向相关工作 。
背景字节跳动 Service Mesh 数据面以及依赖的 Envoy(下称 mesh proxy)为了提供较好的抽象与可扩展性,较多使用了 C++ 的 virtual 函数,虽然这能为编写程序带来极大的便捷性,但是编译后生成的机器指令中会包含大量 indirect call,每个 indirect call 都不可避免地需要进行一次动态跳转,过多的 indirect call 会带来如下问题:

  • 间接指令跳转开销:由于运行期的实际函数(或接口)代码地址是动态赋值的,机器指令无法做更多优化,只能直接执行 call 指令,这对于 cache 局部性、指令预执行以及分支预测都十分不友好 。
  • 无法内联优化:由于 virtual 函数的实现本身是多态的,编译中无法得出实际运行期会执行的实现,因此也无法进行内联优化 。同时在很多场景下,调用一个函数只是为了得到部分返回值或副作用,但函数实现通常还执行了某些额外计算,这些计算本可以通过内联优化消除,由于无法内联,indirect call 会执行更多无效的计算 。
  • 阻碍进一步的编译优化:indirect call 相当于是指令中的一个屏障,由于其本身是一个运行期才能确定的调用,它在编译期会使各种控制流判断以及代码展开失效,从而限制进一步编译及链接的优化空间 。
虽然 virtual 函数会较大损失性能,但它又是必需的:第一,很多模块本身就需要动态的子类实现;第二,将功能模块声明为 virtual 接口对于测试编写更友好,便于提供 mock 实现;第三,C++ 对于 virtual 函数及接口的支持较为成熟,代码结构简单清晰,即便对于静态多态的接口,如果不使用 virtual 函数而是换做 template 模式来支持(例如 CRTP),代码结构也会异常复杂,且上手成本较高,较难维护 。
考虑到 virtual 函数本身的优势,以及对代码结构的改造成本,我们决定在代码层继续保持 virtual 函数的结构,转而从编译优化的角度对其性能开销进行优化 。
调研针对 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 { ... }};


推荐阅读