把编译器想象成米其林三星餐厅的总厨。你交给他一本菜谱(源码),他在后厨(编译过程)里把它变成一道道能端上桌的佳肴(机器码)。同一本菜谱,同一个总厨,既可以「随手做」快速出菜,也可以按「国宴标准」精雕细琢——差别只在他肯投入多少功夫去备料、统筹和调火候。这就是编译优化。 C++ 和 Rust 的编译器里藏着一整套「总厨工具箱」:优化级别是火力开关,内联是贴身短打,链接时优化(LTO)是全场统筹,PGO 是看客流备菜。用对一把工具,程序能快 50% 甚至翻倍;用错了,也可能让构建时间翻几倍只换回 1% 的提升。这篇文章把这套工具箱逐层打开,看看每件工具什么时候该用、什么时候该收。

岗位关联

系统开发者掌握编译优化,等于白拿一份性能:调好优化级别、LTO 与 PGO 的组合拳,C++/Rust 程序在生产环境每步都快人一截——这是高性能计算、游戏引擎和高频交易方向的核心竞争力。

目录
  1. 可视化演示
  2. 优化级别:火力从零到国宴
  3. 内联优化:大厨的贴身手艺
  4. 链接时优化:总厨的全场统筹
  5. PGO 引导:跟着客流备菜
  6. 体积与速度:厨房台面的权衡
  7. 调试与优化:玻璃厨房的取舍
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示C++/Rust编译器优化全流程:从预处理到代码生成的6阶段流水线、5种机器无关优化的性能收益、以及LTO跨编译单元全局优化的工作原理。

本节配有交互动效(进入视口后自动加载,约 3KB)。

编译流水线6阶段 Pre→Lex→Par→Sem→IR→Opt 六阶段依次处理 IR = f(frontend(source)) 编译器将源代码经多阶段转换后进入优化器

优化级别:火力从零到国宴

米其林后厨里挂着五块刻着出菜模式的牌子:O0 像「随手做」,完全照菜谱一步步来,不讲究效率,备菜最快但出品平庸;O1 是「家常模式」,顺手整理流程,让常见步骤顺畅一点;O2 是「商务套餐」,火力全开但讲究平衡,是大多数餐厅的主力档位;O3 是「国宴模式」,为了极致口感不惜工本,备料时间最长、台面铺得最开;Os 则是「小厨房模式」,场地有限时优先把操作台压到最小。

对 C++ 和 Rust 编译器来说,这些「模式」就是命令行参数。GCC/Clang 用 -O0、-O1、-O2、-O3、-Os 五档;Rust 则在 Cargo 的 profile 里配置 opt-level,debug 构建默认不优化,release 构建默认接近 O3。档位越高,编译器投入的分析时间和代码变换越多,运行期程序越快,但编译期更慢、二进制更大。

O2 是业界默认:它开启大部分高质量优化,比如公共子表达式消除、函数内联、循环优化,而且不太激进、代码膨胀可控。O3 再叠加更强循环展开与更激进的内联,通常比 O2 快 5%~15%,但可能因指令缓存压力反而更慢,需要基准测试验证。Os 则偏向「少即是多」,在体积敏感场景往往比 O3 更实用。加速比公式如下:

回到餐厅比喻:选 O2 还是 O3,就像商务套餐和国宴模式的选择——不差那口火候就开 O3,台面紧张、客人等不了太久就选 O2 甚至 Os。优化的第一课,是先搞清楚自己身处哪个后厨、招待哪种客人。

Speedup=TO0TOx\text{Speedup} = \frac{T_{O0}}{T_{Ox}}
O0 不优化耗时除以优化后耗时,大于 1 代表优化生效。
  1. 基准耗时$T_{O0}$ 是完全不做优化时程序跑完的总时间,作为对比的基线。
    TO0T_{O0}
  2. 优化后耗时$T_{Ox}$ 是开启某一档优化后程序的运行总时间。
    TOxT_{Ox}
  3. 加速比两者相除得到 Speedup,比值大于 1 说明优化带来了真实提升。
    Speedup=TO0TOx\text{Speedup} = \frac{T_{O0}}{T_{Ox}}

内联优化:大厨的贴身手艺

总厨有个习惯:酱油瓶明明在库房,每做一道菜都要跑一趟。他索性把常用调料全放灶台边,省掉来回跑腿的功夫——这就是内联。函数调用在机器码层面要做压栈、跳转、返回、弹栈一套动作,高频小函数如果每次都走这套流程,浪费大量时钟周期。

内联优化把函数体直接「抄」到每个调用点,省去调用开销。C++ 的 inline 关键字只是请求,不是命令;编译器会根据成本模型自行决定。Rust 中 #[inline] 用于跨 crate 内联,#[inline(always)] 接近「强制」,但编译器依然保留最终解释权。

成本模型其实就是算账:调用次数多、函数体小,内联就划算;函数体庞大、调用点分散,内联反而让代码爆炸。公式如下:

记住:总厨不是每次都听你的。你标了 inline,他算完账可能觉得跑一趟库房反而更快。内联的本质是信任编译器在成本模型上的判断,而不是靠关键字「硬按头」。

Gain=Ncall×Coverhead−Cinline\text{Gain} = N_{\text{call}} \times C_{\text{overhead}} - C_{\text{inline}}
调用总开销减去内联新增代码成本,收益为正才值得内联。
  1. 调用次数$N_{\text{call}}$ 是该函数在整个程序中被调用的总次数,越高内联越值。
    NcallN_{\text{call}}
  2. 单次调用开销$C_{\text{overhead}}$ 是每次压栈、跳转、返回消耗的时钟周期。
    CoverheadC_{\text{overhead}}
  3. 内联成本$C_{\text{inline}}$ 是把函数体复制到每个调用点后新增的代码量成本。
    CinlineC_{\text{inline}}
  4. 净收益$\text{Gain}$ 由调用次数乘以单次开销再减去内联成本得到,正数代表净赚。
    Gain=Ncall×Coverhead−Cinline\text{Gain} = N_{\text{call}} \times C_{\text{overhead}} - C_{\text{inline}}

链接时优化:总厨的全场统筹

大餐厅有很多副厨,每人管一道菜、各备各的料:宫保鸡丁备了花生,辣子鸡也备了花生,两边各买一大包,浪费。如果总厨统管整个菜单,就能把花生共用一份——这就是「链接时优化」(LTO)的思路。传统编译是分而治之:每个 .cpp 文件单独编译,编译器看不到其他文件里的函数长什么样,跨文件内联和全局分析都做不了。

LTO 打破这种割裂:编译阶段先生成中间表示(IR),链接阶段把所有模块统一分析,再做跨模块内联、常量传播、死代码消除。这样「定义在 math.cpp、调用在 main.cpp」的小函数也能被内联。C++ 用 -flto(GCC/Clang),Rust 在 release profile 里设 lto = true 或 lto = "thin"(ThinLTO,链接更快且效果接近)。

代价是链接时间变长、内存占用升高。LTO 尤其适合头文件里的小函数被多个源文件调用的项目,收益公式可以抽象为:

回到餐厅:每道菜单独备料时花生买两份、蒜末切两份;总厨统一菜单后,一份花生两端菜共用。LTO 就是让编译器当这个总厨,把「看不见的浪费」全部找回来。

TotalLTO=∑i=1nGainicross+Gaininline\text{Total}_{\text{LTO}} = \sum_{i=1}^{n}\text{Gain}_i^{\text{cross}} + \text{Gain}_{\text{inline}}
LTO 总收益 = 各模块跨文件可见性带来的收益之和 + 额外内联收益。
  1. 编译单元数$n$ 是参与链接的源文件数量,文件越多 LTO 的分析范围越广。
    nn
  2. 跨模块收益$\text{Gain}_i^{\text{cross}}$ 是第 $i$ 个模块因全局可见性获得的分析优化收益。
    Gainicross\text{Gain}_i^{\text{cross}}
  3. 内联收益$\text{Gain}_{\text{inline}}$ 是链接期才知道调用关系后额外获得的内联收益。
    Gaininline\text{Gain}_{\text{inline}}
  4. 总收益把所有跨模块收益累加,加上内联收益,得到 LTO 的整体价值。
    TotalLTO=∑i=1nGainicross+Gaininline\text{Total}_{\text{LTO}} = \sum_{i=1}^{n}\text{Gain}_i^{\text{cross}} + \text{Gain}_{\text{inline}}

PGO 引导:跟着客流备菜

总厨发现,每周五晚上点「招牌红烧肉」的客人特别多,「三文鱼刺身」少有人问津。于是他周五提前把红烧肉炖上,刺身的备料则减半。PGO(Profile-Guided Optimization,引导优化)就是这套「看客流备菜」的机制:先记录真实运行行为,再用数据指导编译决策。

PGO 分三步走:第一步,用插桩版本的程序(-fprofile-generate)在真实数据上跑一遍,记录哪些函数、哪些分支最常执行;第二步,得到一份 profile 数据文件;第三步,用 -fprofile-use 重新编译,让编译器把热点分支排得更紧凑、把热门函数内联,甚至调整分支预测的权重。

为什么有效?程序的时间分布极不均匀:往往 20% 的代码占 80% 的执行时间。PGO 让编译器知道这 20% 在哪,把力气花在刀刃上。平均耗时的期望公式如下:

火焰图是验证 PGO 效果的最佳放大镜:优化前热点函数高高耸立,优化后尖峰明显变矮。就像总厨查看点单报表后调整备料,让高峰期的出菜速度肉眼可见地提升。

Tavg=∑i=1npi⋅tiT_{\text{avg}} = \sum_{i=1}^{n} p_i \cdot t_i
程序平均时间 = 各路径概率乘以耗时的加权和,压平热点路径就能显著降低均值。
  1. 路径概率$p_i$ 表示第 $i$ 条执行路径被走到的概率,来自采样统计。
    pip_i
  2. 路径耗时$t_i$ 表示第 $i$ 条路径单次执行的耗时。
    tit_i
  3. 加权求和把每条路径按概率加权后累加,得到程序平均运行时间。
    ∑i=1npi⋅ti\sum_{i=1}^{n} p_i \cdot t_i
  4. 优化目标$T_{\text{avg}}$ 越小越好,热点路径被优化后权重占比自然下降。
    Tavg=∑i=1npi⋅tiT_{\text{avg}} = \sum_{i=1}^{n} p_i \cdot t_i

体积与速度:厨房台面的权衡

厨房的操作台就那么大。为了追求「国宴模式」,总厨把各种食材、工具全铺上台面,结果转身都困难,反而拖慢了上菜速度。代码体积和运行速度之间,也有这种拉扯。激进优化(循环展开、大量内联)会让二进制膨胀;而现代 CPU 有指令缓存(I-cache),热代码只有装进缓存才能跑得快。

一旦代码膨胀超出指令缓存容量,CPU 就要反复去内存取指令,得不偿失。这就是 O3 在某些场景反而比 O2 慢的根本原因。Clang 提供 -Os(优化体积)和 -Oz(更激进压缩体积),Rust 在 Cargo profile 里设 opt-level = "s" 或 "z"。加上 strip 去除符号表,二进制通常能瘦身 30%~70%。膨胀率公式:

聪明的做法是「按需铺台面」:热函数交给内联和循环展开,冷函数保持朴素形态,整个程序用 -Os 和 strip 控制体积。就像总厨把常用工具挂上墙、不常用的收进柜子,台面始终清爽,出菜速度反而最快。

Blowup=SoptSbase\text{Blowup} = \frac{S_{\text{opt}}}{S_{\text{base}}}
优化后体积除以基准体积,大于 1 说明代码膨胀了。
  1. 基准体积$S_{\text{base}}$ 是未优化或低优化级别下的二进制体积。
    SbaseS_{\text{base}}
  2. 优化后体积$S_{\text{opt}}$ 是施加激进优化后的二进制体积,常显著大于前者。
    SoptS_{\text{opt}}
  3. 膨胀率$\text{Blowup}$ 是两者相除的比值,越大说明体积代价越明显。
    Blowup=SoptSbase\text{Blowup} = \frac{S_{\text{opt}}}{S_{\text{base}}}

调试与优化:玻璃厨房的取舍

顾客想透过玻璃窗围观主厨做菜,但主厨在国宴模式下动作太快,颠勺、跑位一气呵成,谁也看不清哪一步是哪步。优化后的代码正是如此:机器码和源码行号严重错位。调试符号(debug info)像给每道工序贴溯源标签,让调试器能告诉你,当前正在执行源码第几行。

编译器的 -g 选项控制调试信息生成。GCC/Clang 有 -g0(无信息)、-g1(最少信息)、-g3(最全信息,含宏定义);Rust 在 profile 里设 debug = true 或 debug = 1/2。但开启 -O2 后,优化器会把变量放进寄存器甚至整个消除,你在调试器里看到的变量值可能「消失」,断点位置也「对不上」。

这背后的实质,是优化对调试信息保留率的侵蚀:

成熟团队的解法是「分档构建」:日常开发用 Debug 配置(O0 + 完整 -g),发布用 Release 配置(O2 + -g + strip),线上崩了再用存档的带符号包解析堆栈。就像玻璃厨房分两种模式:对外展示时从容优雅,对内培训时放慢动作,每一步都标得清清楚楚。

ρ=DkeptDdebug\rho = \frac{D_{\text{kept}}}{D_{\text{debug}}}
ρ 越接近 1,调试体验越接近源码级;优化越激进,ρ 越低。
  1. 完整信息量$D_{\text{debug}}$ 是关闭优化时完整的调试信息量,包含所有变量和行号映射。
    DdebugD_{\text{debug}}
  2. 保留信息量$D_{\text{kept}}$ 是优化后仍能有效映射到源码的调试信息量。
    DkeptD_{\text{kept}}
  3. 保留率$\rho$ 是两者相除的比值,越低说明优化对调试的干扰越大。
    ρ=DkeptDdebug\rho = \frac{D_{\text{kept}}}{D_{\text{debug}}}

AI 应用场景

游戏引擎发布构建

阶段:性能优化与发布工程化

解决的问题:游戏引擎源码数百万行,传统单一文件优化看不到全局,角色动画、物理碰撞等热点函数跨模块调用密集,白白损失性能。用 LTO + PGO 组合,让编译器看到整个引擎的调用关系,把热点动画函数内联并合理布局。

具体案例:某 MOBA 游戏在 PC 端发布时,用 CMake 开启 INTERPROCEDURAL_OPTIMIZATION,并用自动回放脚本收集对局热点数据做 PGO,最终帧率提升 12%、加载时间降低 20%。

高频交易系统 Rust 服务

阶段:基准测试与生产发布

解决的问题:撮合引擎延迟敏感,Rust 默认 release 已经很快,但红黑树、哈希表的遍历热点以及跨 crate 小函数调用仍有优化空间。通过调整 Cargo profile 的 lto = "thin"、codegen-units = 1、opt-level = 3,让编译器在链接期做全局内联和布局优化。

具体案例:某量化团队把撮合核心服务的 p50 延迟从 1.4μs 压到 1.1μs,仅靠调整编译配置,未改一行业务代码。

嵌入式端侧体积优化

阶段:体积受限的发布部署

解决的问题:边缘设备存储只有 2MB,传统 -O3 导致二进制膨胀放不下。用 -Os + strip + 关闭 RTTI/异常(C++)或 opt-level = "z"(Rust),在体积受限下保住核心性能。

具体案例:一款工业网关固件从 1.8MB 压到 632KB,仍能跑满 100Mbps 转发;同时用 LTO 避免纯体积优化导致性能崩盘。

Python 演示

python58 行
"""编译优化决策模拟器: 内联与 LTO 收益估算"""CALL_OVERHEAD = 50          # 单次函数调用的机器周期开销INLINE_COST_FACTOR = 0.05   # 内联导致的代码膨胀成本系数  def should_inline(freq, body_cost):    benefit = freq * CALL_OVERHEAD    cost = body_cost * INLINE_COST_FACTOR    return benefit > cost  def build_inline_plan(funcs):    plan = []    total_gain = 0    for name in sorted(funcs, key=lambda k: -funcs[k]["freq"]):        if should_inline(funcs[name]["freq"], funcs[name]["body_cost"]):            plan.append(name)            total_gain += funcs[name]["freq"] * CALL_OVERHEAD    return plan, total_gain  def build_lto_plan(funcs, caller_map, def_map):    cross_gain = 0    cross_funcs = []    for name in funcs:        if caller_map[name] != def_map[name]:            cross_funcs.append(name)            cross_gain += funcs[name]["body_cost"] * 10    return cross_funcs, cross_gain  def main():    funcs = {        "matrix_mul": {"freq": 10 ** 7, "body_cost": 80},        "vector_add": {"freq": 5 * 10 ** 6, "body_cost": 20},        "parse_config": {"freq": 100, "body_cost": 1200},        "error_log": {"freq": 3 * 10 ** 4, "body_cost": 90},    }    caller_map = {        "matrix_mul": "main.cpp", "vector_add": "main.cpp",        "parse_config": "main.cpp", "error_log": "main.cpp",    }    def_map = {        "matrix_mul": "math.cpp", "vector_add": "math.cpp",        "parse_config": "main.cpp", "error_log": "log.cpp",    }     plan, gain = build_inline_plan(funcs)    print("内联计划:", plan)    print("内联总收益(周期):", gain)     cross_funcs, cross_gain = build_lto_plan(funcs, caller_map, def_map)    print("LTO跨模块可内联函数:", cross_funcs)    print("LTO额外收益(周期):", cross_gain)  if __name__ == "__main__":    main()
目的:用成本模型模拟编译器「哪些函数值得内联、哪些跨文件函数能被 LTO 捞回来」的决策过程。
  1. def should_inline(freq, body_cost):把「要不要内联」抽象成一次成本收益比较,对应正文的内联成本模型。
  2. benefit = freq * CALL_OVERHEAD算出内联后总节省的调用开销,频率越高收益越大。
  3. cost = body_cost * INLINE_COST_FACTOR估算内联到每个调用点的代码膨胀成本,统一折算成周期。
  4. for name in sorted(funcs, key=lambda k: -funcs[k]["freq"]):按调用频率从高到低排列,贪心地优先处理最热门的函数。
  5. if caller_map[name] != def_map[name]:定义文件和调用文件不一致,说明单文件编译看不见它,正是 LTO 发挥价值的场景。
  6. cross_gain += funcs[name]["body_cost"] * 10跨模块函数每个调用点都可能获得收益,这里按调用点数粗略放大。

常见误区

误区我编译时直接上 `-O3`,优化级别越高肯定跑得越快,反正编译器比我聪明。

正解O3 会开启更激进的循环展开和内联,代码体积变大,可能挤爆指令缓存,反而比 O2 慢 5%~10%。正确做法是以 O2 为基线,O3 必须在目标机器上跑基准测试确认收益,才在生产环境启用。这就像国宴模式虽然精致,但操作台铺太满,热菜反而上得慢。

为何常见:「级别越高越强」的直觉很符合日常经验(辣度、音量),但指令缓存的物理限制打破了这种线性直觉。

误区我在函数前加了 `inline`,它就一定被内联,为什么性能一点没提升?

正解inline 在 C++ 中首先是一种链接语义,允许同一函数在多个编译单元出现而不违反 ODR;它只是对编译器的建议。函数太大、递归、或者调用点太多时,编译器会拒绝内联。Rust 中要近似强制则用 #[inline(always)]。就像你要求总厨必须把酱油放灶台,但他发现灶台已经堆满了,自然选择不听。

为何常见:关键字名字太直白,让人误以为它是命令而非请求;同时很多人分不清「内联优化」和「内联的链接语义」。

误区开启 LTO 链接时间翻倍,收益却看不出来,干脆关掉算了。

正解LTO 尤其擅长跨编译单元的小函数内联和常量传播,在头文件模板繁多的 C++ 项目和跨 crate 的 Rust 项目中收益显著。构建变慢是「一次性」的,运行变快是「每一次」的;ThinLTO 还能把链接开销降到接近无 LTO 的水平。用一次较慢的备菜,换每道菜都省掉重复切配,值。

为何常见:开发者常以「编译时间」而非「运行性能」作为优化选项的评价标准,忽视了线上服务长期运行的成本。

误区线上崩了要定位,`-g` 必须保留,否则没法看堆栈。

正解正确做法是发布时用 -O2 + -g 编译,生成后 strip 剥离符号,同时把带符号的构建产物归档存档(或用 debuginfod 服务)。线上二进制小,崩溃时又能用存档符号解析堆栈。直接带符号发布会让体积大 10~50 倍,等于把整本菜谱都贴在玻璃厨房外墙上。

为何常见:很多人把「调试符号」和「排障能力」混为一谈,其实符号文件可以和运行二进制分离存储,按需调用。

面试题

Q1. 编译器优化级别中,构建最快但运行最慢的是?

单选题 · easy

  • A.不优化构建快运行慢← 正确答案
  • B.平衡体积与速度中间档
  • C.激进优化追求运行快
  • D.压缩体积优先于速度

答案:A。O0 完全不做优化,所以构建最快,但生成的机器码直接照搬源码结构,运行最慢。可以理解为「随手做」模式:备菜最快,但菜品没有摆盘、没有统筹,出菜效率自然最低。

Q2. 为什么 -O3 可能比 -O2 更慢?

单选题 · easy

  • A.代码膨胀挤占指令缓存← 正确答案
  • B.编译器会自动关闭所有循环
  • C.链接优化导致加载变慢
  • D.运行时强制跳转到调试器

答案:A。O3 更激进地做循环展开和内联,代码体积膨胀后超出指令缓存(I-cache)容量,CPU 频繁去内存取指令,反而拖慢执行。就像国宴模式把台面铺太满,转身都困难。

Q3. 想最优先压缩发布二进制体积,应选哪档?

单选题 · easy

  • A.完全关闭所有编译优化
  • B.激进内联而增大代码
  • C.精简代码控制二进制膨胀← 正确答案
  • D.保持默认体积与速度平衡

答案:C。-Os 或 opt-level = "s" 专门面向体积优化,在精简代码的同时保留基本性能。O0 虽然体积小但性能太差,O3 则反过来。就像小厨房模式:台面最小化,出菜依然利索。

Q4. C++ 中 inline 关键字的真正含义是?

单选题 · mid

  • A.强制编译器把函数内联
  • B.允许多编译单元共享定义← 正确答案
  • C.让编译器彻底忽略该函数
  • D.关闭该函数的所有优化

答案:B。inline 的核心语义是链接层面的:允许同一函数在多个编译单元定义而不违反 ODR,同时给编译器一个内联建议。真正是否内联由成本模型决定。就像你跟总厨建议「把酱油放灶台」,但放不放由他判断。

Q5. -g 与 -O2 同时使用会导致什么现象?

单选题 · mid

  • A.变量被优化器消除不可查看← 正确答案
  • B.编译器直接报错拒绝编译
  • C.二进制体积自动缩小一半
  • D.程序运行速度立刻下降一半

答案:A。O2 会把变量放进寄存器、消除临时变量甚至整个短路计算,调试器因此找不到这些变量的当前值。这就是调试信息保留率 ρ 下降的典型表现,并非编译出错或运行变慢。

Q6. PGO 的三阶段流程包括哪些?

多选题 · mid

  • A.插桩采样运行收集数据← 正确答案
  • B.反馈数据驱动重新编译← 正确答案
  • C.运行时动态改写机器码
  • D.热点函数布局与内联加权← 正确答案

答案:ABD。PGO 是编译期的引导优化:先插桩采样收集 profile,再用数据反馈重编,最后让热点路径获得更好的布局与内联权重。C 选项是 JIT 的运行时行为,和 PGO 的「编译前采集」思路完全不同。

Q7. 开启 LTO 后链接时间明显变长,最合理的应对是?

单选题 · hard

  • A.放弃 LTO 改回单文件编译
  • B.采用 ThinLTO 降低链接开销← 正确答案
  • C.删除模板代码加快链接
  • D.强制关闭全部内联来提速

答案:B。ThinLTO 在保持大部分跨模块优化收益的同时,通过并行和只分析相关模块的方式显著降低链接时间和内存占用。为了链接快而放弃 LTO 是捡芝麻丢西瓜,就像为了省备料时间放弃了全场统筹。

Q8. Rust 中希望函数跨 crate 内联,正确做法是?

单选题 · hard

  • A.给函数加标注并配合 LTO← 正确答案
  • B.使用 unsafe 手动嵌入汇编
  • C.关闭优化保证编译通过
  • D.把结构体替换为宏定义

答案:A。Rust 中 #[inline] 标注配合 release 模式的 LTO 配置,让编译器在链接期看到完整函数体并执行内联。unsafe 汇编和宏都不是常规手段。就像总厨需要看到完整菜谱才能决定要不要把配菜步骤并进主流程。

Q9. 关于 LTO 的收益和代价,说法正确的有?

多选题 · mid

  • A.跨模块内联提升运行速度← 正确答案
  • B.链接期做全局常量传播← 正确答案
  • C.编译时间必然大幅缩短
  • D.链接期内存占用明显上升← 正确答案

答案:ABD。LTO 在链接期统一分析所有模块,能跨文件内联和做常量传播,从而提升运行速度;代价是链接时间和内存占用上升。C 说反了,LTO 会延长链接时间而不是缩短。

Q10. 发布生产二进制时,合理的调试与优化配置包括?

多选题 · extreme

  • A.优化构建后再剥离调试符号← 正确答案
  • B.保留符号构建产物存档排障← 正确答案
  • C.关闭优化保证源码可以调试
  • D.通过符号服务按需获取映射← 正确答案

答案:ABD。生产发布的标准姿势是 O2 + -g 构建后 strip,同时把带符号产物存档或接入 debuginfod,既保证运行性能又保留排障能力。C 是典型的 Debug 配置,直接发布到生产既慢又大,属于把培训厨房直接当营业厨房用。