vlambda博客
学习文章列表

了解 Swift 中的数值计算


Sessions: https://developer.apple.com/videos/play/wwdc2020/10217/

Swift Numerics

Numerics 是一个 Apple 开源的 Swift 包,通过范型约束,提供更简单的方式,来使用所有标准库里的浮点型进行数值计算。下面通过一个例子来看下这个包的作用。比如我们要在 Swift 中实现一个 Logit 模型 的函数,在没有 Numerics 的情况下:

import Darwin
/// Logit 模型
///
/// https://en.wikipedia.org/wiki/Logit
///
/// - 参数 p:
///   取值范围 0...1。
///
/// - 返回值:
///   log(p/(1-p))。
func logit(_ p: Double) -> Double {
    log(p) - log1p(-p)
}

为了实现 log(p/(1-p)),我们需要调用 Darwin 里的 loglog1p,这两个函数位于 Darwin.C 中,是 C 标准库所定义的接口,里面用一系列同名函数来支持不同的具体浮点型。当我们用这类函数编写功能时,为了支持所有的浮点型(DoubleFloatFloat80 以及后续标准库可能增加的类型)就需要将重复的代码拷贝多次,大大提高了维护成本。

这时候可能你会想,要是能使用范型来代替这里面具体的浮点型就好了,这时候 Numerics 就派上用场了。

Real 协议

Numerics 里面提供了一个全新的 Real 协议,对这类计算的类型提供支持。通过 Real 协议,上面的例子可以改造成:

import Numerics

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

NumberType 范型增加 Real 协议约束,并将 loglog1p 函数替换成 Numerics 里支持范型的 loglog(one plus:) 版本。所有浮点型都会遵循 Real 协议,这个改写后的 logit 函数,不仅能根据平台支持其对应的浮点型参数,在以后标准库增加新的浮点型时,也无需做额外的适配。

public protocol RealFloatingPointRealFunctionsAlgebraicField {
}

Real 协议是一个协议组合,其中 FloatingPoint 协议是标准库中的协议,其余两个协议是 Numerics 里所提供的新协议。这里需要注意的是,对于开发者而言,只应该使用 Real 协议本身。

先来看看目前 Swift 标准库里已经存在关于数值的协议:

我们这里只关心其中关键的一部分:了解 Swift 中的数值计算

  • AdditiveArithmetic:用于支持加减法的类型,包括了大部分应该属于“数字”的概念,和数学领域的“代数群”几乎吻合。
  • SignedNumeric:拓展了乘法概念。
  • FloatingPoint:拓展了计算机中浮点型实现所需要的各种概念,比如比较、幂运算和有效位数等,还有各种常用的变量 infinity(∞)、 nanpi 等。

而 Numerics 是基于这些核心概念来构建的。了解 Swift 中的数值计算

AlgebraicField 协议

public protocol AlgebraicFieldSignedNumeric {
  static func /(a: Self, b: Self) -> Self
  
  /// 倒数
  var reciprocal: Self? { get }
  
  /// ...
}

SignedNumeric 的基础上拓展了除法概念。这样就支持了全部四则运算,数学领域称为”代数数域“,这也是这个协议名字的由来。

ElementaryFunctions 协议

public protocol ElementaryFunctionsAdditiveArithmetic {
  /// 指数
  static func exp(_ x: Self) -> Self
  
  /// exp(x) - 1
  static func expMinusOne(_ x: Self) -> Self
  
  /// 三角函数
  static func cos(_ x: Self) -> Self
  static func sin(_ x: Self) -> Self
  static func tan(_ x: Self) -> Self
  
  /// 对数
  static func log(_ x: Self) -> Self
  
 /// log(1 + x)
  static func log(onePlus x: Self) -> Self
  
  /// exp(y * log(x)) 
  static func pow(_ x: Self_ y: Self) -> Self
  
  /// 幂
  static func pow(_ x: Self_ n: Int) -> Self
  
  /// 次方根
  static func root(_ x: Self_ n: Int) -> Self
  
  /// ...
}

AdditiveArithmetic 的基础上拓展了大量通用的浮点型函数,包括核心的三角函数、指数、对数、幂和次方根等。

RealFunctions 协议

public protocol RealFunctionsElementaryFunctions {
  /// 误差函数
  static func erf(_ x: Self) -> Self

  /// sqrt(x*x + y*y)
  static func hypot(_ x: Self_ y: Self) -> Self
  
  /// Γ(x)
  static func gamma(_ x: Self) -> Self
  
  /// log(|Γ(x)|)
  static func logGamma(_ x: Self) -> Self
  
  /// ...
}

ElementaryFuctions 的基础上拓展了更多类似但少用的函数,比如伽马函数、误差函数和更多底数的指数和对数等。

组合而成的 Real 协议因此巧妙地定义了标准浮点型所应该有的通用功能。这就是 Numerics 是如何将标准浮点型变得更加有用和优雅的。

虽然 Real 协议的概念很简单,但在实践中却格外强大。

  • 范型支持

  • 解决重复的代码

  • 更低的维护成本

  • 更好的兼容性(支持新的浮点型)

Complex 类型

Complex 类型是 Numerics 中的一部分,为 Swift 提供了复数支持,且是使用 Real 协议作为泛型约束的。

import Numerics

let z = Complex(1.02.0// z = 1 + 2 i,这里默认是 Double

Complex 类型不仅本身很好用,同时也是一个使用 Real 协议进行范型数值编程的好范例。

/// 定义 NumberType 遵循 Real 协议
public struct Complex<NumberTypewhere NumberTypeReal {
    /// 实数部分
    public var real: NumberType
  
    /// 虚数部分
    public var imaginary: NumberType
  
  /// ...
}

然后需要通过 SignedNumeric 协议支持基本运算函数。了解 Swift 中的数值计算了解 Swift 中的数值计算

extension ComplexSignedNumeric {
    public static func +(z: Complex, w: Complex) -> Complex {
        return Complex(z.real + w.real, z.imaginary + w.imaginary)
    }

    public static func -(z: Complex, w: Complex) -> Complex {
        return Complex(z.real - w.real, z.imaginary - w.imaginary)
    }

    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)
    }
}

复数通常使用极坐标表示,所以需要定义长度和相位角。由于 Real 协议的帮助,我们很容易地计算这两个概念的值。同时还能得到一个便捷的构造函数。<img style="zoom:50%;" />

extension Complex {
   /// 长度
    public var length: NumberType {
        return .hypot(real, imaginary)
    }
   
   /// 相位角
    public var phase: NumberType {
        return .atan2(y: imaginary, x: real)
    }
  
    public init(length: NumberType, phase: NumberType) {
        self = Complex(.cos(phase), .sin(phase)).multiplied(by: length)
    }
}

Complex 类型是一个扁平的结构体,包含着两个浮点型的值。这样,和 C(_Complex double) 与 C++ (std::complex<double>)里的复数类型有着精确匹配的内存布局。这使得 Swift 的复数和 C/C++ 有互操作的可能。在 Swift 中创建的复数缓冲区,可以通过指针传递给 C/C++ 的库使用。

来看这个使用 Accelerate 的 BLAS(线性代数计算标准) 的例子:

import Numerics
import Accelerate

/// 100 个随机的复数
let z = (0 ..< 100).map {
    Complex(length: 1.0, phase: Double.random(in: -.pi ... .pi))
}

/// 计算 L2 范数(欧几里得范数)
let norm = cblas_dznrm2(z.count, &z, 1)

要注意的是,Swift 的 Comple 对待 ∞ 和 NaN 值和 C/C++ 不同,在桥接代码的时候需要小心。但 Swift 的处理更加简单和高效。这里有一个只包含复数乘除法的性能测试:了解 Swift 中的数值计算

从图中可以看到,和 C 对比,乘法有 1.3x、除法有 3.8x,常数作为除数时的除法更有 10x 的速度提升。

同时,Numerics 还是一个持续维护的项目。

最近增加了:

  • 改进的整型幂运算
  • 近似相等的新处理工具

正在讨论中的有:

  • 任意精度整型

  • ShapedArray

  • 十进制浮点型

如果你有任何建议,可以在 Github 上参与贡献或者在 Swift 社区中参与讨论。

Float16 类型

Float16 是 Swift 标准库中新增的数据类型,顾名思义占用 16 位(2 字节)。

  • IEEE 754 标准格式
  • 基于 ARM 的平台已经支持,包括 iOS、iPadOS、tvOS、watchOS
  • 基于 x86 的平台正在支持中(和 Intel 在一起修复中)

Float16 是一个完整支持的标准浮点型。

  • 遵循 BinaryFlatingPointSIMDScalar
  • 遵循 Numerics 的 Real
  • 支持所有的标准浮点型函数 了解 Swift 中的数值计算

和其余数值类型一样,Float16 使用时也需要权衡利弊,这些得失大多仅和它的大小有关。

优点:

  • 更好的性能
  • 与 C/Objective-C 里 __fp16 类型的互操性

缺点:

  • 低精度和小范围 了解 Swift 中的数值计算

在硬件支持上:

  • Apple GPU 已经支持(且为偏向选择)
  • Apple CPU 从 A11 Bionic 之后开始已经选择
    • Scalar(标量)性能与 Float/ Double 相同
    • SIMD 性能 2x 于 Float
  • 更老的 CPU 通过(用 Float 操作)模拟支持

这里有一个简单的 BNNS 卷积计算性能测试:

可以看到 Float16 的运算速度相对于 Float 有 2x 还多的提升。

最后

Float16 加入标准库,让 Swift 本身选择余地更多,可以踏足的领域更加丰富。

而 Swift Numerics 这个项目,和 Apple 对 Swift 的态度是高度一致的:

  • 开源开放
  • 多平台支持
  • 性能出众
  • 和 C 良好的互操性

同时,Numerics 作为 Apple 开源的 Swift 包,也是一个给开发者学习如何编写和封装更优雅 Swift 代码的范例。

可见未来 Swift Only 的包/框架会越来越多,Apple 每年都在告诉(国内大厂)开发者,Swift YES!

推荐阅读

关注我们

我们是「老司机技术周报」,每周会发布一份关于 iOS 的周报,也会定期分享一些和 iOS 相关的技术。欢迎关注。

支持作者

WWDC 内参 系列是由老司机周报、知识小集合以及 SwiftGG 几个技术组织发起的。已经做了几年了,口碑一直不错。 主要是针对每年的 WWDC 的内容,做一次精选,并号召一群一线互联网的 iOS 开发者,结合自己的实际开发经验、苹果文档和视频内容做二次创作。