@inlinable 和 @usableFromInline 内联

133 阅读10分钟

@inlinable 是 Swift 中用于标记函数、计算属性或方法的修饰符,允许编译器将其 内联 (inline) 到调用的地方,而不是通过函数调用的方式执行。这种优化可以减少函数调用的开销,并且允许在编译时优化函数内部的逻辑。它在性能优化中有着重要作用。

@inlinable 的优化机制

当你在一个 publicopen 的符号上使用 @inlinable 时,Swift 编译器会将该符号的实现暴露给其他模块。这意味着:

  1. 内联 (Inlining): 编译器可以选择将该函数的代码直接插入到调用它的地方,而不是在运行时执行函数调用。这可以减少栈操作和函数调用的开销,尤其是对频繁调用的小型函数,性能影响显著。

  2. 跨模块优化 (Cross-Module Optimization): @inlinable 使得函数的实现可以被调用它的模块看到,并进行额外的编译期优化。这意味着如果你的函数在多个模块之间调用,其他模块也能内联该函数。

  3. 优化传递性 (Transitive Optimization): 当一个 @inlinable 函数内部调用了其他 @inlinable 函数,编译器可以在编译时将这些嵌套的函数调用展开,从而进一步优化代码路径。

使用场景

@inlinable 的主要使用场景是 性能优化,尤其是在以下情况下:

1. 轻量函数

对于体积很小且频繁调用的函数,例如数学计算、getter、setter、或者一些简短的逻辑判断,内联可以避免每次调用都创建栈帧、跳转到函数执行等额外的开销。

@inlinable
public func add(_ a: Int, _ b: Int) -> Int {
    return a + b
}

在这种情况下,add 函数非常简单,可以内联到调用它的地方,避免函数调用的开销。

2. 跨模块调用

当一个模块提供了一些工具函数或常用功能,并且希望这些功能在其他模块中也能被优化时,使用 @inlinable 可以让调用模块获取函数的实现,进而进行内联或其他优化。

// Module A
@inlinable
public func increment(_ number: Int) -> Int {
    return number + 1
}

// Module B
let result = increment(5)  // Module B 可以内联 Module A 的 increment 实现

3. 允许更好的优化器优化

标记为 @inlinable 的函数在调用模块中暴露实现细节,允许编译器在具体的调用上下文中做更多的优化,如常量折叠、死代码消除、循环展开等。例如,如果你的 @inlinable 函数中包含分支逻辑,编译器可以根据调用时的已知信息来优化路径。

@inlinable
public func isPositive(_ number: Int) -> Bool {
    return number > 0
}

let flag = isPositive(10)  // 编译器可以直接优化为 `true`,无需函数调用

4. 泛型函数

对于 泛型函数 或者高度通用的函数,@inlinable 非常有用,因为它允许编译器根据实际类型参数进行优化,而不必处理函数调用的动态性。

@inlinable
public func genericAdd<T: Numeric>(_ a: T, _ b: T) -> T {
    return a + b
}

编译器可以对不同的类型(Int, Double, 等)生成专门优化的代码,而不是通过泛型函数的方式调用。

使用 @inlinable 的注意事项

虽然 @inlinable 有很多好处,但使用时需要注意以下几点:

  1. 暴露实现细节: @inlinable 会将函数的实现暴露给其他模块,这意味着如果你修改了函数的实现,其他依赖此模块的代码也可能需要重新编译。它打破了模块间的实现封装。

  2. 代码膨胀: 过度使用 @inlinable 可能导致编译器内联过多代码,从而引起代码体积膨胀,尤其是在一些大型项目中,编译时间和二进制文件大小可能会显著增加。

  3. 非递归内联: 编译器不会自动内联递归函数,因为这会导致无限循环内联,产生错误。对于递归函数,不建议使用 @inlinable

  4. 正确性优先于性能: 不要为了一些微小的性能提升滥用 @inlinable,保持代码的可维护性和模块化设计是首要目标。性能问题应通过 实际的性能瓶颈分析 来判断是否值得使用 @inlinable

总结

  • @inlinable 允许函数在编译时内联,从而提高性能,尤其是对于跨模块调用和轻量函数。
  • 它使得函数实现可以暴露给其他模块,允许跨模块优化和内联调用,但也牺牲了一些模块化的封装性。
  • 常用于小型、频繁调用的函数,或者需要跨模块优化的通用工具类函数中,但不建议滥用,避免引起代码体积膨胀和过度暴露实现。

@usableFromInline@inlinable 都是 Swift 中用于优化和访问控制的修饰符,但它们有不同的目的和作用范围。

1. @usableFromInline

@usableFromInline 是一种 访问控制 修饰符,用于将符号(函数、属性、方法等)的访问级别设为 internal(仅限于当前模块内可见),但允许它在其他模块中被 间接使用。这个修饰符常用于与 @inlinable 一起,目的是使符号在编译期间可以被内联,但不会暴露符号的实际实现细节给外部模块。

  • 用途: 用于那些你希望 限制符号的可见性,但仍然希望编译器可以内联或优化它。
  • 访问级别: @usableFromInline 符号的可见性是 internal,但是当它被 @inlinable 的符号所使用时,它可以被内联或被其他模块访问。

示例:

@inlinable
public func publicFunction() -> Int {
    return internalHelper()
}

@usableFromInline
internal func internalHelper() -> Int {
    return 42
}
  • 这里,publicFunction@inlinable 的,意味着它的实现可以暴露给其他模块,允许内联。
  • internalHelper@usableFromInline 的,虽然它只能在当前模块内部使用,但它的实现可以通过 publicFunction 间接暴露,允许编译器优化它,而无需完全暴露其符号。

2. @inlinable

@inlinable 是一种 优化修饰符,用于允许函数或方法的实现被其他模块看到,支持跨模块优化(例如内联)。与 @usableFromInline 不同,@inlinable 不仅是优化,它还明确表示这个符号的实现可以被 暴露给其他模块,以便内联调用。

  • 用途: 用于 暴露符号的实现,让编译器能够在其他模块中直接内联它,以进行更高级的性能优化。
  • 访问级别: @inlinable 符号的访问级别通常是 publicopen,并且它会公开符号的实现。

示例:

@inlinable
public func publicFunction() -> Int {
    return 42
}
  • publicFunction@inlinable 的,这意味着它的实现细节会暴露给任何导入该模块的外部模块,编译器可以选择在调用它的地方直接内联其代码。

区别总结

特性@usableFromInline@inlinable
访问控制内部符号 (internal),只对当前模块可见对其他模块暴露实现,允许跨模块内联
优化作用@inlinable 配合使用,允许编译器内联但不暴露符号允许编译器将函数实现暴露给其他模块并进行内联
使用场景用于保持符号私有或内部,但允许编译器优化用于对外暴露实现,允许编译器进行跨模块优化和内联
暴露的实现细节不暴露实现细节,符号在当前模块内部保持可见暴露实现细节,允许其他模块直接访问和内联
典型用途优化内部逻辑或限制性符号时使用性能优化,特别是跨模块优化时使用

使用示例

@inlinable
public func inlinableFunction() -> Int {
    return helperFunction()
}

@usableFromInline
internal func helperFunction() -> Int {
    return 42
}
  • inlinableFunction 可以被其他模块内联,并且 helperFunction 的调用也可能被内联,但 helperFunction 的符号本身仍然对外部模块不可见(即不能直接调用 helperFunction)。

@inlinable 虽然能带来性能优化,特别是在跨模块调用时能够将函数内联,但它也有一些潜在的缺点和风险,需要在使用时小心权衡:

1. API 设计的封装性降低

  • 缺点: 使用 @inlinable 会将函数或方法的实现暴露给其他模块。这样做意味着模块的实现细节不再是完全私有的,外部模块可以直接看到和依赖你的实现逻辑。
  • 影响: 如果将来需要修改函数的实现,会影响依赖这个实现的外部模块,甚至可能导致外部模块的代码出错或者无法正确编译。
  • 风险: 实现一旦暴露出来,变更该函数的内部逻辑可能引发兼容性问题,特别是对于公开库(public libraries),改变这些暴露的函数实现可能导致外部用户的代码崩溃或行为异常。

2. 代码膨胀(Code Bloat)

  • 缺点: 因为 @inlinable 允许编译器将函数实现内联到调用方代码中,如果有大量的 @inlinable 函数被频繁调用,尤其是在多个地方调用时,会导致编译后的二进制文件大小显著增大。
  • 影响: 内联会增加重复的代码片段,从而增加应用程序的体积,特别是在大规模使用 @inlinable 时,这种膨胀会更加明显。
  • 风险: 增大的二进制文件会占用更多的存储空间,并可能导致性能上的折衷(如增加加载时间)。

3. 优化不一定总是有效

  • 缺点: 内联并不总是能带来性能提升。对于非常大的函数或复杂的逻辑,内联可能反而会降低性能,因为它增加了代码的复杂性,导致更多的指令缓存开销和代码体积增大。
  • 影响: 编译器对内联的决策依赖于函数的复杂度和调用场景。如果函数过大,内联会增加 CPU 指令缓存压力,反而可能导致性能下降。
  • 风险: 在某些情况下,内联可能不会提升性能,甚至可能导致负面影响。因此,不恰当地使用 @inlinable 可能导致不必要的性能问题。

4. 二进制兼容性问题

  • 缺点: 如果你将 @inlinable 用于公共 API,当 API 的实现发生变更时,外部模块(特别是预编译的二进制模块)可能会使用旧版本的实现。
  • 影响: 如果外部模块在没有重新编译的情况下依赖旧的二进制库,当 API 的实现发生变化时,可能会导致行为不一致,甚至引发崩溃或其他未定义的行为。
  • 风险: 这种问题尤其容易出现在 Swift 的 ABI 稳定性引入后,因为 @inlinable 会将实现内联到调用方,调用方可能不需要重新编译,但却依赖于旧的实现。

5. 编译时间增加

  • 缺点: 使用 @inlinable 可能导致编译时间的增加,特别是在有大量 @inlinable 函数或者它们被频繁调用的情况下,因为编译器需要将这些函数的实现内联到多个调用站点中。
  • 影响: 对于较大的项目,内联操作可能显著增加编译器的工作量,从而延长编译时间。
  • 风险: 开发过程中编译时间过长会影响开发效率,尤其是当项目规模较大且使用了很多 @inlinable 函数时。

总结

@inlinable 能带来性能上的潜在优化,但要谨慎使用,因为它会降低封装性,增加代码膨胀,并且可能引入二进制兼容性问题。使用时应优先考虑以下情况:

  • 小型、性能关键的函数。
  • 对于性能有严格要求的代码路径,且该路径涉及跨模块调用。
  • 确保未来不会频繁修改该函数的实现。