Swift Numerics

我很高兴地宣布 Swift 生态系统的一个新的开源项目:Swift Numerics!Swift Numerics 将以一组细粒度的模块的形式提供 Swift 中数值计算的构建块,这些模块捆绑到一个单独的 Swift 包中。我希望我们可以快速填补标准库现有 API 中的一些重要空白,并为 Swift 语言解锁新的编程领域。

我已经用两个广受欢迎的模块播种了存储库,它们对于计算数学非常有用:Real(提供 SE-0246 的功能)和 Complex(提供复数和算术)。让我们来看看它们的作用

实数

SE-0246 提议了一个“基本数学函数”的 API,这将使诸如正弦和对数之类的运算在通用上下文中可用。它已被接受,但是由于编译器的限制,API 尚无法以源代码稳定的方式添加到标准库中。Real 模块提供该 API 作为一个单独的模块,以便您可以立即在项目中使用它来访问这些操作的改进 API。

该模块定义了三个协议。最通用的是 ElementaryFunctions,它使以下函数可用

RealFunctions 协议改进了 ElementaryFunctions,并添加了在比实数更一般的域上难以定义或实现的操作

您最常用的协议是 Real,它描述了一种配备了全套基本数学函数的浮点类型。这是在编写通用代码时使用的绝佳协议,因为它具有实现大多数数值函数所需的所有基础知识。假设我们正在试验一些基本的机器学习,并且需要一个通用的 sigmoid 函数激活函数

import Numerics

func sigmoid<T: Real>(_ x: T) -> T {
  1 / (1 + .exp(-x))
}

或者假设我们正在实现 DFT,并且想要为变换预先计算权重;DFT 权重是单位根

import Numerics

extension Real {
  // The real and imaginary parts of e^{-2πik/n}
  static func dftWeight(k: Int, n: Int) -> (r: Self, i: Self) {
    precondition(0 <= k && k < n, "k is out of range")
    guard let N = Self(exactly: n) else {
      preconditionFailure("n cannot be represented exactly.")
    }
    let theta = -2 * .pi * (Self(k) / N)
    return (r: .cos(theta), i: .sin(theta))
  }
}

这为我们提供了一个适用于 FloatDoubleFloat80(如果目标支持)的实现。当新的基本浮点类型添加到 Swift 时,例如 Float16Float128,它也适用于它们。这个模块——尤其是 Real 协议——是对 Swift 中通用数值计算的重大改进,我真的很期待看到您用它做什么。

复数

Complex 模块建立在 Real 之上,为 Swift 提供复数类型。

复数对于计算很有用,因为它们是“包含有理数的最小代数闭域”。这在实践中意味着,常见的方程(例如给出多项式根或矩阵特征值的方程)不一定在实数中具有解,但保证在复数中具有解。这看起来像是一个深奥的事实,但是当您开发算法时,保证解的存在通常很有用。

当处理傅里叶变换时,复数自然会在计算中出现:实信号的傅里叶变换是对称复信号。这意味着用于从音频处理到电路模拟等各种应用的许多信号处理算法的自然设置是复数。库通常在例行使用中向您隐藏此细节,但是当开发库时,拥有此工具至关重要。

例如,上面我们展示的 dftWeight 代码可以使用 Complex 更自然地编写为

import Numerics

extension Complex {
  // e^{-2πik/n}
  static func dftWeight(k: Int, n: Int) -> Complex {
    precondition(0 <= k && k < n, "k is out of range")
    guard let N = RealType(exactly: n) else {
      preconditionFailure("n cannot be represented exactly.")
    }
    return Complex(length: 1, phase: -2 * .pi * (RealType(k) / N))!
  }
}

由于这些原因,复数是大多数语言或标准库都提供的重要构建块。C 有 _Complex,C++ 有 std::complex,Fortran 和 Python 在语言核心中内置了复数。我希望一旦 Swift Numerics 模块得到一些使用,并且我们对其功能进行几次迭代构建后,我们也将提议将其一部分包含在 Swift 标准库中。

Complex 类型是通用的,基于符合 Real 的底层 RealType

public struct Complex<RealType> where RealType: Real {
  ...
}

为什么不支持整数类型 TComplex<T> 呢?虽然高斯整数“就像”复数一样——毕竟它们是一个子集——但是您对它们执行的实际操作(以及这些操作的理想实现)却大相径庭,因此将它们强制组合到一个通用类型中是没有意义的。我很乐意看到在未来的某个时候将对高斯整数的支持添加到库中,但这应该是一个与 Complex 分开的类型。

作为回顾,复数有两个组成部分:实部和虚部。有一个特殊的数字,称为 i,它是虚数单位。在数学中,我们将实部为 a,虚部为 b 的复数写为 a + bi。在 Swift 中,它看起来非常相似

import Complex

let z: Complex<Double> = 2 + 3 * .i
print(z.real)      // 2.0
print(z.imaginary) // 3.0

Swift Numerics 以 Fortran 风格打印复数;a + bi(a, b)

print(z) // (2.0, 3.0)

您还可以通过指定复数的实部和虚部来构造复数

let w = Complex<Double>(1, -2) // (1.0, -2.0)

要添加两个复数,我们添加相应的部分

print(z + w) // (2.0 + 1.0, 3.0 + -2.0) = (3.0, 1.0)

乘法和除法只是稍微复杂一点;它们的定义来自恒等式

let u: Complex<Double> = .i * .i // (-1.0, 0.0)

(即 i 是 -1 的平方根)。Complex 类型符合 Numeric 协议,并使用常用的除法运算符 /,因此复数的算术运算看起来就像任何其他数字类型一样。例如,这是一个实现乘以 2i 的函数,它是在复平面中进行 2 倍缩放和 90 度旋转

import Complex

func scaleAndRotate<T>(_ z: Complex<T>) -> Complex<T> {
  z * Complex(0, 2)
}

let z = scaleAndRotate(Complex(1.0, 1.0)) // (-2.0, 2.0)

在这一点上,值得稍微谈论一下无穷大和 NaN 及其对乘法和除法的影响。C 和 C++ 复数数学库尝试对不同的零、无穷大和 NaN 进行细粒度的区分。这有时很有用,但这表示乘法不能使用明显的算术表达式。

Swift 不尝试进行这种区分。任何实部和虚部均为零的复数均为零,所有实部或虚部为非有限值的复数都折叠为单个“无穷远点”。

[ 1> import Complex
[ 2> Complex(.infinity, 0.0) == Complex(0.0, -.nan)
$R0: Bool = true

这会丢失少量信息,但是很少有程序有效地利用这种区分,并且所有程序都会因尝试保留这种区分的决定而对其性能产生不利影响。为了使性能影响具体化,让我们比较一下我的 2015 MacBook Pro 上双精度复数乘法的吞吐量

数据分布 C Swift 加速
良好缩放 1 / 1.4ns 1 / 1.1ns 1.3 倍
不良缩放 1 / 4.5ns 1 / 4.1ns 1.1 倍

关于基准测试测量和方法的说明:这些表中的吞吐量以倒数时间单位报告。1/1.1ns 表示“每 1.1 纳秒产生一个结果”。较小的分母比更大的分母更好;1/1.5ns1/3ns 快两倍。这些基准测试不是孤立地执行乘法(或除法);相反,它们测量的是计算和求和一组乘法(或除法)结果的时间。这为测量引入了一些开销,但是该开销不成比例地落在更快的操作上,因此这使得 Swift 性能看起来比实际更差。您可以在 ArithmeticBenchmarkTests.swift 中看到(非常简单的)基准测试代码。欢迎提出拉取请求以添加更复杂的基准测试!

由于 Swift Numerics 不需要特别关注无穷大,因此在值良好缩放的常见情况下,它速度快约 30%,即使在存在许多不良缩放值的不寻常情况下,速度也稍快一些。在病态情况下,数据集中存在大量无穷大或 NaN,则差异会更大。

除法的影响甚至更大

数据分布 C Swift 加速
良好缩放 1 / 19 ns 1 / 5ns 3.8 倍
不良缩放 1 / 22 ns 1 / 22ns 不适用

由于 Swift Numerics 的复数除法运算对编译器公开,因此当将许多值除以单个除数(非常常见的运算)时,它可以提供更大的优化机会。这是同一个表格,如果我们用单个常见的良好缩放除数除以整个值数组

数据分布 C Swift 加速
良好缩放 1 / 19 ns 1 / 1.8ns 10.6 倍

我还有最后一招:如果数据良好缩放,我们可以使用 reciprocal 属性并改为乘法,这会将性能提高到 1/1.1ns——速度提高 17 倍!这在 C 中也是可能的,当然,但是 Swift 的可选语义提供了一种使其安全的简便机制

// If divisor is well-scaled, use multiply by reciprocal instead of division.
if let recip = divisor.reciprocal {
  return data.map { $0 * recip }
}
// Otherwise, fallback on using division.
return data.map { $0 / divisor }

在某些特别困难的情况下,Swift Numerics 也为复数除法提供了更好的答案——考虑以下测试问题,来自 Baudin & Smith 的论文“Scilab 中稳健的复数除法”

Complex(0x1p-1074, 0x1p-1074) / Complex(0x1p-1073, 0x1p-1074)

这看起来足够简单;如果我们同时将分子和分母缩放 0x1p1074 倍,则问题变为 (1 + i)/(2 + i),我们可以手动计算结果

(1+i)/(2+i) = (1+i)(2-i)/5 = ((2+1) + (2-1)i)/5 = (3+i)/5 = (0.6, 0.2)

Clang(使用 compiler-rt)在 C 和 C++ 中都产生 (0.5, 0.5) 用于此除法。Python 的复数给出的结果为 (1.0, 0.0)Complex 模块给出的答案与您应得的一样准确:(0.6, 0.2),并且在不牺牲任何性能的情况下完成。

我目前正在开发一个补丁,以使 Complex 符合 ElementaryFunctions,这使得常用的超越运算集可用,并将 Complex 提升到与大多数其他语言的功能对等水平。预计这将在未来几周内可用。

为什么是包?

为什么我要以包的形式而不是在标准库中进行这项工作?原因有几个,但主要原因仅仅是并非所有内容都应该进入标准库。Swift Numerics 的某些部分可能会随着时间的推移进入标准库,但是某些模块需要有一个不默认成为每个项目一部分的家。我对 Swift Numerics 的目标是,它为以数值计算为中心的此类模块提供一个共同的家,就像 SwiftNIO 对网络一样。

制作包还有其他一些好处

未来计划

在接下来的几个月中,我将致力于为该软件包添加重要的附加功能。特别是,一些重点领域将是

所有这些项目(和其他项目)都在 Swift Numerics 的 issues 页面上进行跟踪。

参与进来!

我喜欢在 Swift Numerics 上工作,但也希望您参与进来。

有问题吗?

请随时在 Swift 论坛上的相关主题上发布有关此帖子的疑问。