Highlight
Apple 发布 Swift Numerics 和 Float16,让 Swift 数值算法可以用
Real协议覆盖 Float、Double、Float80 和 Float16,并用Complex类型直接对接 C、C++ 与 Accelerate 的复数内存布局。
核心内容
写数值代码时,最容易先从 Double 开始。一个概率函数、一个对数函数、一个矩阵工具,先能跑起来。问题会在项目变大后出现:有的调用方要 Float,有的平台有 Float80,新的数据管线又想试 Float16。如果每种类型复制一份实现,bug fix 很容易漏掉其中一份。
这场 session 从一个 logit 函数切入。Darwin.log 和 Darwin.log1p 能写出 Double 版本,却不能表达“这个函数适用于所有真正支持对数运算的浮点类型”。Swift 泛型需要一个更准确的约束。Swift Numerics 给出的入口就是 Real 协议(00:55、02:32)。
Real 把标准库里的 FloatingPoint 和 Swift Numerics 里的 AlgebraicField、RealFunctions 组合起来。开发者日常不需要拆开理解每个底层协议,只要把泛型参数约束到 Real,就能调用对数、三角函数、幂函数和常见数学函数。这样写出来的算法会自动覆盖标准浮点类型,也会跟上将来新增的浮点类型(03:20、06:08)。
演讲随后把同一套设计落到复数和半精度浮点。Complex 是一个保存 real 与 imaginary 两个分量的普通 Swift struct,但它的内存布局与 C、C++ 复数类型一致,可以把 Swift 数组指针传给 Accelerate 的 BLAS 函数。Float16 则进入 Swift 核心语言和标准库,在 ARM 平台可用,并符合 Real。这让一批已经用 Real 写好的算法无需源码改动就能处理 16 位浮点(07:03、11: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 数学库里的log和log1p。func logit(_ p: Double) -> Double把输入和输出固定为Double。log(p) - log1p(-p)是 log-odds 的数值稳定写法,避免直接计算log(p / (1 - p))。- 这段代码不能直接用于
Float、Float80或未来新增的浮点类型。
(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:20)Real 的价值在于边界清楚。它的含义很具体:能支持完整浮点数学的一类类型。演讲中提到它结合了标准库和 Numerics 里的多个协议。
func logit<NumberType: Real>(_ p: NumberType) -> NumberType {
.log(p) - .log(onePlus: -p)
}
关键点:
FloatingPoint提供浮点比较、指数与有效数字拆分、无穷大、pi等基础能力。AlgebraicField在SignedNumeric之上加入除法,表达四则运算完整的数系。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:04)Complex 可以符合 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)
}
}
关键点:
+分别相加两个复数的实部和虚部。-分别相减两个复数的实部和虚部。*使用标准复数乘法公式,实部和虚部要交叉相乘。SignedNumeric让Complex能进入更多泛型数值算法。
(08:19)极坐标接口展示了 Real 带来的函数集合:hypot、atan2、cos、sin 都能在泛型 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:)先用cos和sin得到单位方向,再按长度缩放。- 这些函数都来自
Real的约束,代码不用关心底层是Float还是Double。
5. Swift 复数可以直接交给 Accelerate
(09:16)Complex 是只包含两个浮点值的普通 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:28)Float16 是 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,迁移
Float或Double代码时要检查溢出风险。 - 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 例子展示了复制 Double、Float、Float80 版本的维护成本;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 泛型函数,再用同一组输入分别跑 Float 和 Float16;记录误差、溢出次数和目标设备上的耗时。
4. 为自定义数值类型拆分协议实现
做什么:如果项目有定点数、角度、概率或小数类型,按 AdditiveArithmetic、SignedNumeric、AlgebraicField、RealFunctions 的层级设计能力边界。
为什么值得做:演讲解释了 Real 由多组协议组合而成。拆清楚边界后,调用方可以只依赖自己真正需要的能力。
怎么开始:先实现加减和乘法,再判断是否真的支持除法、三角函数、指数和对数;只有具备完整浮点语义时再考虑符合 Real。
关联 Session
- What’s new in Swift — 查看 Swift 5.3 对 Float16、Package 生态、运行时和语言特性的整体介绍。
- Embrace Swift type inference — 理解 Swift 如何从泛型约束和调用点线索推断
NumberType这类类型参数。 - Safely manage pointers in Swift — 补充把 Swift 数组指针传给 C API 时需要掌握的内存安全规则。
- Distribute binary frameworks as Swift packages — 了解 Swift Package 在依赖分发中的角色,配合 Swift Numerics 这类 package 使用。
评论
GitHub Issues · utterances