代码之家  ›  专栏  ›  技术社区  ›  Frank

使用F#度量单位和System.Numerics.Vector<T>

  •  5
  • Frank  · 技术社区  · 8 年前

    我很难将F#度量单位与 System.Numerics.Vector<'T> 类型让我们来看一个玩具问题:假设我们有一个数组 xs 类型的 float<m>[] 出于某种原因,我们希望对其所有组件进行平方运算,从而得到一个类型为 float<m^2>[] 。这对于标量代码非常有效:

    xs |> Array.map (fun x -> x * x) // float<m^2>[]
    

    现在假设我们想通过在 System.Numerics.Vector<float>.Count -使用SIMD确定大小的块,例如:

    open System.Numerics
    let simdWidth = Vector<float>.Count
    // fill with dummy data
    let xs = Array.init (simdWidth * 10) (fun i -> float i * 1.0<m>)
    // array to store the results
    let rs: float<m^2> array = Array.zeroCreate (xs |> Array.length)
    // number of SIMD operations required
    let chunks = (xs |> Array.length) / simdWidth
    // for simplicity, assume xs.Length % simdWidth = 0
    for i = 0 to chunks - 1 do
        let v = Vector<_>(xs, i * simdWidth) // Vector<float<m>>, containing xs.[i .. i+simdWidth-1]
        let u = v * v                        // Vector<float<m>>; expected: Vector<float<m^2>>
        u.CopyTo(rs, i * simdWidth)          // units mismatch
    

    我相信我理解 为什么? 发生这种情况:F#编译器怎么可能知道 System.Numerics.Vector<'T>.op_Multiply 什么算术规则适用?它可以是任何操作。那么它应该如何才能推导出正确的单位呢?

    问题是 :最好的方法是什么?我们如何告诉编译器哪些规则适用?

    尝试1 :从中删除所有度量单位信息 xs公司 稍后再添加:

    // remove all UoM from all arrays
    let xsWoM = Array.map (fun x -> x / 1.0<m>) xs
    // ...
    // perform computation using xsWoM etc.
    // ...
    // add back units again
    let xs = Array.map (fun x -> x * 1.0<m>) xsWoM
    

    问题:执行不必要的计算和/或复制操作,由于性能原因,无法实现代码矢量化的目的。此外,这在很大程度上违背了使用计量单位的目的。

    尝试2 :使用内联IL更改的返回类型 Vector<'T>.op_Multiply :

    // reinterpret x to be of type 'b
    let inline retype (x: 'a) : 'b = (# "" x: 'b #)
    let inline (.*.) (u: Vector<float<'m>>) (v: Vector<float<'m>>): Vector<float<'m^2>> = u * v |> retype
    // ...
    let u = v .*. v // asserts type Vector<float<m^2>>
    

    问题:不需要任何额外的操作,但使用了一个不推荐使用的特性(内联IL),并且不是完全通用的(仅针对度量单位)。

    有人对此有更好的解决方案吗* ?

    *请注意,上面的示例实际上是一个玩具问题,用于演示一般问题。实际程序解决了一个更复杂的初值问题,涉及多种物理量。

    2 回复  |  直到 8 年前
        1
  •  1
  •   TheInnerLight    8 年前

    编译器可以很好地理解如何应用乘法的单位规则,这里的问题是你有一个包装类型。在你的第一个例子中,当你写 xs |> Array.map (fun x -> x * x) ,您是根据数组的元素而不是直接在数组上描述乘法。

    当你有一个 Vector<float<m>> ,单元连接到 float 而不是 Vector 所以当你尝试乘法时 矢量 s、 编译器不会将该类型视为具有任何单元。

    鉴于类公开的方法,我认为使用 Vector<'T> 但是有包装类型的选项。

    类似这样的东西可以给你一个单位友好向量:

    type VectorWithUnits<'a, [<Measure>]'b> = 
        |VectorWithUnits of Vector<'a>
    
        static member inline (*) (a : VectorWithUnits<'a0, 'b0>, b : VectorWithUnits<'a0, 'b1>) 
            : VectorWithUnits<'a0, 'b0*'b1> =
            match a, b with
            |VectorWithUnits a, VectorWithUnits b -> VectorWithUnits <| a * b
    

    在这种情况下,单位被附加到向量上,向量相乘可以按照正确的单位行为正常工作。

    问题是,现在我们可以在 矢量<'T> 以及 浮动 它本身

    您可以将具有度量单位的特定类型的数组转换为 矢量 s使用:

    let toFloatVectors (array : float<'m>[]) : VectorWithUnits<float,'m>[]  =
        let arrs = array |> Array.chunkBySize (Vector<float>.Count)
        arrs |> Array.map (Array.map (float) >> Vector >> VectorWithUnits)
    

    然后返回:

    let fromFloatVectors (vectors : VectorWithUnits<float,'m>[]) : float<'m>[] =
        let arr = Array.zeroCreate<float> (Array.length vectors)
        vectors |> Array.iteri (fun i uVec ->
            match uVec with
            |VectorWithUnits vec -> vec.CopyTo arr)
        arr |> Array.map (LanguagePrimitives.FloatWithMeasure<'m>)
    

    一个破旧的替代方案:

    如果放弃泛型类型 'T 你可以做一个 float Vector 通过一些相当可怕的装箱和运行时强制转换,行为正常。这滥用了一个事实,即度量单位是一个编译时构造,在运行时不再存在。

    type FloatVectorWithUnits<[<Measure>]'b> = 
        |FloatVectorWithUnits of Vector<float<'b>>
    
        static member ( * ) (a : FloatVectorWithUnits<'b0>, b : FloatVectorWithUnits<'b1>) =
            match a, b with
            |FloatVectorWithUnits a, FloatVectorWithUnits b ->
                let c, d = box a :?> Vector<float<'b0*'b1>>, box b :?> Vector<float<'b0*'b1>>
                c * d |> FloatVectorWithUnits
    
        2
  •  1
  •   Community Ian Goodfellow    7 年前

    我想出了一个解决方案,它满足了我的大多数要求(看起来)。它的灵感来自 TheInnerLight's ideas (包装 Vector<'T> ),还添加了一个包装器(称为 ScalarField )用于基础数组数据类型。通过这种方式,我们可以跟踪单元,而下面我们只处理原始数据,并且可以使用不知道单元的 System.Numerics.Vector API。

    一个简化的、简单的、快速的、肮脏的实现将如下所示:

    // units-aware wrapper for System.Numerics.Vector<'T>
    type PackedScalars<[<Measure>] 'm> = struct
        val public Data: Vector<float>
        new (d: Vector<float>) = {Data = d}
        static member inline (*) (u: PackedScalars<'m1>, v: PackedScalars<'m2>) = u.Data * v.Data |> PackedScalars<'m1*'m2>
    end
    
    // unit-ware type, wrapping a raw array for easy stream processing
    type ScalarField<[<Measure>] 'm> = struct
        val public Data: float[]
        member self.Item with inline get i                = LanguagePrimitives.FloatWithMeasure<'m> self.Data.[i]
                         and  inline set i (v: float<'m>) = self.Data.[i] <- (float v)
        member self.Packed 
               with inline get i                        = Vector<float>(self.Data, i) |> PackedScalars<'m>
               and  inline set i (v: PackedScalars<'m>) = v.Data.CopyTo(self.Data, i)
        new (d: float[]) = {Data = d}
        new (count: int) = {Data = Array.zeroCreate count}
    end
    

    现在,我们可以使用这两种数据结构在 相当地 优雅、高效的方式:

    let xs = Array.init (simdWidth * 10) float |> ScalarField<m>    
    let mutable rs = Array.zeroCreate (xs.Data |> Array.length) |> ScalarField<m^2>
    let chunks = (xs.Data |> Array.length) / simdWidth
    for i = 0 to chunks - 1 do
        let j = i * simdWidth
        let v = xs.Packed(j) // PackedScalars<m>
        let u = v * v        // PackedScalars<m^2>
        rs.Packed(j) <- u
    

    最重要的是,重新实现单元感知的常规数组操作可能很有用 标量字段 包装,例如。

    [<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
    module ScalarField =
        let map f (sf: ScalarField<_>) =
            let mutable res = Array.zeroCreate sf.Data.Length |> ScalarField
            for i = 0 to sf.Data.Length do
               res.[i] <- f sf.[i]
            res
    

    缺点:对于基础数字类型而言不是泛型的( float ),因为没有通用的替代品 floatWithMeasure 为了使其通用,我们必须实现第三个包装器 Scalar 还包装了基础图元:

    type Scalar<'a, [<Measure>] 'm> = struct
        val public Data: 'a
        new (d: 'a) = {Data = d}
    end
    
    type PackedScalars<'a, [<Measure>] 'm 
                when 'a: (new: unit -> 'a) 
                and  'a: struct 
                and  'a :> System.ValueType> = struct
        val public Data: Vector<'a>
        new (d: Vector<'a>) = {Data = d}
        static member inline (*) (u: PackedScalars<'a, 'm1>, v: PackedScalars<'a, 'm2>) = u.Data * v.Data |> PackedScalars<'a, 'm1*'m2>
    end
    
    type ScalarField<'a, [<Measure>] 'm
                when 'a: (new: unit -> 'a) 
                and  'a: struct 
                and  'a :> System.ValueType> = struct
        val public Data: 'a[]
        member self.Item with inline get i                    = Scalar<'a, 'm>(self.Data.[i])
                         and  inline set i (v: Scalar<'a,'m>) = self.Data.[i] <- v.Data
        member self.Packed 
               with inline get i                          = Vector<'a>(self.Data, i) |> PackedScalars<_,'m>
               and  inline set i (v: PackedScalars<_,'m>) = v.Data.CopyTo(self.Data, i)
        new (d:'a[]) = {Data = d}
        new (count: int) = {Data = Array.zeroCreate count}
    end
    

    …这意味着我们基本上不是通过使用诸如 float<'m> ,但仅通过具有辅助类型/单元参数的包装类型。

    不过,我还是希望有人能想出更好的主意