WWDC Quick Look 💓 By SwiftGGTeam
Explore numerical computing in Swift

Explore numerical computing in Swift

观看原视频

Highlight

Apple 发布 Swift Numerics 和 Float16,让 Swift 数值算法可以用 Real 协议覆盖 Float、Double、Float80 和 Float16,并用 Complex 类型直接对接 C、C++ 与 Accelerate 的复数内存布局。

核心内容

写数值代码时,最容易先从 Double 开始。一个概率函数、一个对数函数、一个矩阵工具,先能跑起来。问题会在项目变大后出现:有的调用方要 Float,有的平台有 Float80,新的数据管线又想试 Float16。如果每种类型复制一份实现,bug fix 很容易漏掉其中一份。

这场 session 从一个 logit 函数切入。Darwin.logDarwin.log1p 能写出 Double 版本,却不能表达“这个函数适用于所有真正支持对数运算的浮点类型”。Swift 泛型需要一个更准确的约束。Swift Numerics 给出的入口就是 Real 协议(00:5502:32)。

Real 把标准库里的 FloatingPoint 和 Swift Numerics 里的 AlgebraicFieldRealFunctions 组合起来。开发者日常不需要拆开理解每个底层协议,只要把泛型参数约束到 Real,就能调用对数、三角函数、幂函数和常见数学函数。这样写出来的算法会自动覆盖标准浮点类型,也会跟上将来新增的浮点类型(03:2006:08)。

演讲随后把同一套设计落到复数和半精度浮点。Complex 是一个保存 real 与 imaginary 两个分量的普通 Swift struct,但它的内存布局与 C、C++ 复数类型一致,可以把 Swift 数组指针传给 Accelerate 的 BLAS 函数。Float16 则进入 Swift 核心语言和标准库,在 ARM 平台可用,并符合 Real。这让一批已经用 Real 写好的算法无需源码改动就能处理 16 位浮点(07:0311:28)。

详细内容

1. 用 Real 写一次 logit

01:05)最初的实现只接受 Double。它适合演示公式,却把类型写死了。

import Darwin

/// The log-odds function
///
/// https://en.wikipedia.org/wiki/Logit
///
/// - Parameter p:
///   A probability in the range 0...1.
///
/// - Returns:
///   The log of the odds, 'log(p/(1-p))'.
func logit(_ p: Double) -> Double {
    log(p) - log1p(-p)
}

关键点:

  • import Darwin 提供 C 数学库里的 loglog1p
  • func logit(_ p: Double) -> Double 把输入和输出固定为 Double
  • log(p) - log1p(-p) 是 log-odds 的数值稳定写法,避免直接计算 log(p / (1 - p))
  • 这段代码不能直接用于 FloatFloat80 或未来新增的浮点类型。

02:33)引入 Swift Numerics 后,logit 可以改成泛型函数。约束从具体类型变成 NumberType: Real

import Numerics

/// The log-odds function
///
/// https://en.wikipedia.org/wiki/Logit
///
/// - Parameter p:
///   A probability in the range 0...1.
///
/// - Returns:
///   The log of the odds, 'log(p/(1-p))'.
func logit<NumberType: Real>(_ p: NumberType) -> NumberType {
    .log(p) - .log(onePlus: -p)
}

关键点:

  • import Numerics 引入 Swift Numerics package。
  • NumberType: Real 表示这个类型是完整的浮点实数类型,支持标准算术和数学函数。
  • .log(p) 使用 Numerics 暴露的泛型数学函数,不再绑定到 Darwin 的 Double 函数。
  • .log(onePlus: -p) 对应原来的 log1p(-p),保留了接近 0 时更稳定的计算方式。
  • 返回类型仍是 NumberType,调用方传入 Float 就得到 Float,传入 Double 就得到 Double

2. Real 把浮点能力收束成一个约束

03:20Real 的价值在于边界清楚。它的含义很具体:能支持完整浮点数学的一类类型。演讲中提到它结合了标准库和 Numerics 里的多个协议。

func logit<NumberType: Real>(_ p: NumberType) -> NumberType {
    .log(p) - .log(onePlus: -p)
}

关键点:

  • FloatingPoint 提供浮点比较、指数与有效数字拆分、无穷大、pi 等基础能力。
  • AlgebraicFieldSignedNumeric 之上加入除法,表达四则运算完整的数系。
  • ElementaryFunctions 覆盖三角函数、对数、指数、根和幂。
  • RealFunctions 扩展到 gamma、error functions 以及更多三角函数变体。
  • 日常算法通常直接写 Real,只有在实现新数值类型时才需要深入拆分这些协议。

3. Complex 用 Swift struct 表达复数

07:10)Swift Numerics 提供完整的复数类型。最小使用方式很直接:导入 Numerics,再构造一个 real/imaginary 组合。

import Numerics

let z = Complex(1.0, 2.0) // z = 1 + 2 i

关键点:

  • Complex(1.0, 2.0) 创建实部为 1、虚部为 2 的复数。
  • 两个字面量默认是 Double,所以这里的 z 会被推断为 Complex<Double>
  • Complex 本身是泛型类型,它要求底层数值类型符合 Real

07:38)演讲展示了 Complex 的基本形状:两个字段,一个初始化器。

public struct Complex<NumberType> where NumberType: Real {
    /// The real component
    public var real: NumberType

    /// The imaginary component
    public var imaginary: NumberType

    /// Construct a complex number with specified real and imaginary parts
    public init(_ real: NumberType, _ imaginary: NumberType) {
        self.real = real
        self.imaginary = imaginary
    }
}

关键点:

  • where NumberType: Real 让实部和虚部都具备完整浮点数学能力。
  • real 保存实部,imaginary 保存虚部。
  • 初始化器没有隐藏额外状态,复数就是两个同类型浮点值的组合。
  • 这种布局也为后面的 C、C++ 互操作打基础。

4. 复数运算和极坐标依赖 Real 的数学函数

08:04Complex 可以符合 SignedNumeric,因为它能定义加法、减法和乘法。

extension Complex: SignedNumeric {
    /// The sum of 'z' and 'w'
    public static func +(z: Complex, w: Complex) -> Complex {
        return Complex(z.real + w.real, z.imaginary + w.imaginary)
    }

    /// The difference of 'z' and 'w'
    public static func -(z: Complex, w: Complex) -> Complex {
        return Complex(z.real - w.real, z.imaginary - w.imaginary)
    }

    /// The product of 'z' and 'w'
    public static func *(z: Complex, w: Complex) -> Complex {
        return Complex(z.real * w.real - z.imaginary * w.imaginary,
                       z.real * w.imaginary + z.imaginary * w.real)
    }
}

关键点:

  • + 分别相加两个复数的实部和虚部。
  • - 分别相减两个复数的实部和虚部。
  • * 使用标准复数乘法公式,实部和虚部要交叉相乘。
  • SignedNumericComplex 能进入更多泛型数值算法。

08:19)极坐标接口展示了 Real 带来的函数集合:hypotatan2cossin 都能在泛型 NumberType 上使用。

extension Complex {
    /// The Euclidean norm (a.k.a. 2-norm) of the number.
    public var length: NumberType {
        return .hypot(real, imaginary)
    }

    /// The phase (angle, or "argument").
    ///
    /// Returns the angle (measured above the real axis) in radians.
    public var phase: NumberType {
        return .atan2(y: imaginary, x: real)
    }

    /// A complex value with specified polar coordinates.
    public init(length: NumberType, phase: NumberType) {
        self = Complex(.cos(phase), .sin(phase)).multiplied(by: length)
    }
}

关键点:

  • length.hypot(real, imaginary) 计算欧几里得范数。
  • phase.atan2(y:x:) 计算复数相位角。
  • init(length:phase:) 先用 cossin 得到单位方向,再按长度缩放。
  • 这些函数都来自 Real 的约束,代码不用关心底层是 Float 还是 Double

5. Swift 复数可以直接交给 Accelerate

09:16Complex 是只包含两个浮点值的普通 struct。演讲明确说明,它的内存布局与 C、C++ 复数类型匹配,因此可以把 Swift buffer 传给对应的 C 或 C++ 库。示例使用 Accelerate 的 BLAS 函数计算 2-norm。

import Numerics
import Accelerate

/// Array of 100 random Complex<Double> numbers
let z = (0 ..< 100).map {
    Complex(length: 1.0, phase: Double.random(in: -.pi ... .pi))
}

/// Compute the Euclidean norm of z
let norm = cblas_dznrm2(z.count, &z, 1)

关键点:

  • import Accelerate 引入 Apple 的高性能数值库。
  • z 是 100 个 Complex<Double> 组成的数组,每个值由长度和随机相位创建。
  • &z 把 Swift 数组作为指针传给 C API。
  • cblas_dznrm2 接收复数数组并返回欧几里得 2-norm。
  • 演讲提醒,移植 C 或 C++ 代码时要留意 infinity 与 NaN 的约定差异;Swift 采用的约定更简单,也带来更好的性能。

6. Float16 用 16 位换内存与吞吐

11:28Float16 是 Swift 新增的 IEEE 754 浮点格式。它符合标准库核心协议,也符合 Real。这点很关键:已经写成 NumberType: Real 的算法,不需要为 Float16 单独复制一份。

func logit<NumberType: Real>(_ p: NumberType) -> NumberType {
    .log(p) - .log(onePlus: -p)
}

关键点:

  • Float16 占 16 bit,也就是 2 bytes。
  • Float 占 4 bytes,Double 占 8 bytes。
  • 更小的元素可以让同一个 SIMD register 或同一页内存容纳更多数值。
  • 代价是精度和范围更小,最大可表示值略高于 65,000,迁移 FloatDouble 代码时要检查溢出风险。
  • Apple GPU 已长期支持半精度,Apple CPU 从 A11 Bionic 起有直接支持;旧 CPU 会用 Float 模拟,结果相同但速度较慢。

14:07)演讲用 BNNS convolution benchmark 给出性能数字:单精度 Float 约 49 giga-flops,Float16 约 119 giga-flops。这个例子适合机器学习、图像处理和信号处理一类可以接受半精度误差的工作负载。

关键点:

  • 先确认算法是否能承受 Float16 的精度和范围。
  • 再把核心泛型约束写成 Real,让 Float16 与其他浮点类型共用同一份源码。
  • 最后用实际设备 benchmark 验证收益,因为旧 CPU 上半精度会被模拟。

核心启发

1. 把项目里的 Double 工具函数改成泛型数值函数

做什么:找出概率、统计、曲线、插值等工具函数,把只接受 Double 的接口改成 NumberType: Real

为什么值得做:session 的 logit 例子展示了复制 DoubleFloatFloat80 版本的维护成本;Real 可以把这些实现合并。

怎么开始:先引入 Swift Numerics,把函数签名改成 func f<NumberType: Real>(_ x: NumberType) -> NumberType,再把 Darwin 数学函数替换为 Numerics 的泛型形式,例如 .log.log(onePlus:).hypot

2. 给音频或信号处理 App 增加复数管线

做什么:用 Complex<Double>Complex<Float> 表示频域数据,再把 buffer 交给 Accelerate 计算范数或线性代数结果。

为什么值得做:演讲说明 Swift Complex 的内存布局匹配 C、C++ 复数类型,可以直接对接 Accelerate 的 BLAS API。

怎么开始:先用 Complex(length:phase:) 构造测试数据,再用 cblas_dznrm2 这类函数验证指针传递和结果;确认后把真实 FFT 或滤波结果接入同一结构。

3. 为图像或机器学习路径试验 Float16

做什么:把可以容忍半精度误差的 convolution、embedding、像素批处理或特征缓存改成支持 Float16

为什么值得做:session 给出的 BNNS benchmark 中,Float16 convolution 从约 49 giga-flops 提升到约 119 giga-flops;半精度还能减少内存占用。

怎么开始:先把核心计算抽成 Real 泛型函数,再用同一组输入分别跑 FloatFloat16;记录误差、溢出次数和目标设备上的耗时。

4. 为自定义数值类型拆分协议实现

做什么:如果项目有定点数、角度、概率或小数类型,按 AdditiveArithmeticSignedNumericAlgebraicFieldRealFunctions 的层级设计能力边界。

为什么值得做:演讲解释了 Real 由多组协议组合而成。拆清楚边界后,调用方可以只依赖自己真正需要的能力。

怎么开始:先实现加减和乘法,再判断是否真的支持除法、三角函数、指数和对数;只有具备完整浮点语义时再考虑符合 Real

关联 Session

评论

GitHub Issues · utterances