为何Rust编译器保留已被完全内联的私有类型trait方法?
我发现,为私有类型实现Trait方法时,哪怕编译器已经自动内联了所有对该方法的调用,这个方法的指令还是会留在编译输出里。目前只有给方法加上#[inline(always)]注解,才能让编译器把这个方法移除。我对此感到好奇,以下是具体示例:
// 注意:这个例子里实现的是外部Trait的方法,但本地定义的Trait也会出现同样的行为 use std::ops::Mul; pub fn foo(x: u64, y: u64) -> u64 { let a = Matrix { value: [[x, x], [y, y]] }; let b = Matrix { value: [[x, y], [x, y]] }; let c = a * b; c.value[0][0] } struct Matrix { value: [[u64; 2]; 2] } impl Mul for Matrix { type Output = Self; fn mul(self, rhs: Self) -> Self { let a = self.value; let b = rhs.value; let mut c = [[0; 2]; 2]; c[0][0] = a[0][0] * b[0][0] + a[0][1] * b[1][0]; c[0][1] = a[0][0] * b[0][1] + a[0][1] * b[1][1]; c[1][0] = a[1][0] * b[0][0] + a[1][1] * b[1][0]; c[1][1] = a[1][0] * b[0][1] + a[1][1] * b[1][1]; Self { value: c } } }
上述代码中,私有类型Matrix实现了Mul Trait,公共函数foo通过矩阵运算返回结果的左上元素。在Compiler Explorer中以-C opt-level=[非0值]编译时,编译器已经内联了Mul的调用,还优化掉了多余的矩阵运算,最终foo的汇编代码如下:
example::foo: imul rdi, rdi lea rax, [rdi + rdi] ret
但奇怪的是,编译输出里依然保留了mul方法的指令。既然所有调用都已经内联,保留这个方法纯粹是浪费空间。而给mul方法添加#[inline(always)]注解后,编译输出就只保留foo的指令了。为什么必须加这个注解才能移除该方法?
这本质是Rust编译器(LLVM后端)对Trait方法的符号可见性和优化策略的问题,核心原因有两点:
Trait方法的符号默认可见性
即使你的Matrix是私有类型,但它实现的是公共Trait(比如std::ops::Mul),编译器会默认给这个mul方法标记一个弱外部符号。LLVM在优化时,除非能100%确定这个符号不会被外部引用(比如动态链接场景下的潜在调用),否则不会轻易删除它。哪怕当前 crate 里没有其他调用,编译器也会假设存在外部代码通过Trait对象或其他间接方式调用它的可能性——虽然你的Matrix是私有类型,理论上外部无法构造实例,但LLVM的优化逻辑不会深入分析Rust的私有访问规则,只会基于符号可见性做判断。#[inline(always)]的特殊作用
当你给方法加上#[inline(always)]时,编译器会把这个方法的符号标记为内部链接(internal linkage),同时强制要求所有调用点都内联该方法。这会告诉LLVM:这个方法不需要对外暴露,也不会有外部调用,因此当所有调用都被内联后,就可以安全地删除方法的独立实现体。
相比之下,普通的自动内联(没有#[inline(always)])只是一种优化建议,编译器虽然会内联调用,但不会修改方法的符号可见性,所以LLVM会保留方法的独立实现,以防万一有外部引用。
另外补充一点:如果你的Trait是本地定义的私有Trait,情况会稍微不同,但核心逻辑还是符号可见性和内联标记的优先级问题——#[inline(always)]是最强的内联指令,能彻底消除方法的独立符号。
内容的提问来源于stack exchange,提问作者embradley

