简体   繁体   English

使用F#计量单位与System.Numerics.Vector <T>

[英]Using F# Units of Measure with System.Numerics.Vector<T>

I struggle using F# units of measure in combination with the System.Numerics.Vector<'T> type. 我很难将F#度量单位与System.Numerics.Vector<'T>类型结合使用。 Let's have a look at a toy problem: Assume we have an array xs of type float<m>[] and for some reason we wanted to square all its components, resulting in an array of type float<m^2>[] . 让我们看一下玩具问题:假设我们有一个类型为float<m>[]的数组xs ,由于某种原因我们想要对其所有组件进行平方,得到一个float<m^2>[]类型的数组。 That works perfectly well with scalar code: 这与标量代码完美匹配:

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

Now suppose we wanted to vectorize this operation by performing the multiplication in System.Numerics.Vector<float>.Count -sized chunks using SIMD, for instance like so: 现在假设我们想通过使用SIMD在System.Numerics.Vector<float>.Count块中执行乘法来向量化此操作,例如:

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

I believe I understand why this happens: How could the F# compiler know, what System.Numerics.Vector<'T>.op_Multiply does and what arithmetic rules apply? 我相信我理解为什么会发生这种情况:F#编译器怎么知道System.Numerics.Vector<'T>.op_Multiply做什么以及适用的算术规则是什么? It could literally be any operation. 它实际上可以是任何操作。 So how should it be able to deduce the correct units? 那么它应该如何推断出正确的单位呢?

The question is : What the best way to make this work? 问题是 :最好的方法什么? How can we tell the compiler which rules apply? 我们如何告诉编译器适用哪些规则?

Attempt 1 : Remove all units of measure information from xs and add it back later on: 尝试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

Problems: Performs unnecessary computations and/or copy operations, defeats the purpose of vectorizing the code for performance reasons. 问题:执行不必要的计算和/或复制操作,由于性能原因而无法实现代码矢量化的目的。 Also, largely defeats the purpose of using UoM to begin with. 此外,很大程度上违背了使用UoM的目的。

Attempt 2 : Use inline IL to change the return type of Vector<'T>.op_Multiply : 尝试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>>

Problems: Doesn't require any additional operations, but uses a deprecated feature (inline IL) and isn't fully generic (only with respect to the unit of measure). 问题:不需要任何其他操作,但使用不推荐使用的功能(内联IL)并且不完全通用(仅针对度量单位)。

Does anyone have a better solution for this* ? 有没有人有更好的解决方案*

*Note that the above example really is a toy problem to demonstrate the general issue. *请注意,上述示例确实是一个玩具问题,以证明一般问题。 The real program solves a much more complicate initial value problem, involving many kinds of physical quantities. 真正的程序解决了更复杂的初始值问题,涉及多种物理量。

The compiler is fine with figuring out how to apply unit rules for multiplication, the problem here is that you have a wrapped type. 编译器很好地解决了如何应用乘法的单位规则,这里的问题是你有一个包装类型。 In your first example, when you write xs |> Array.map (fun x -> x * x) , you're describing the multiplication in terms of the elements of the array rather than on the array directly. 在您的第一个示例中,当您编写xs |> Array.map (fun x -> x * x) ,您将根据数组的元素而不是直接在数组上描述乘法。

When you have a Vector<float<m>> , the units are attached to the float rather than the Vector so when you try and multiply Vector s, the compiler won't treat that type as if it has any units. 当你有一个Vector<float<m>> ,单位被附加到float而不是Vector所以当你尝试乘以Vector ,编译器不会将该类型视为具有任何单位。

Given the methods exposed by the class, I don't think there is an easy workaround using the Vector<'T> directly but there are options for wrapping the type. 鉴于该类暴露的方法,我认为没有一个简单的解决方法直接使用Vector<'T>但是有包装类型的选项。

Something like this could give you a unit-friendly vector: 这样的东西可以给你一个单元友好的矢量:

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

In this case, the units are attached to the vector and multiplying vectors works as expected with the correct unit behaviour. 在这种情况下,单元附加到矢量,乘法矢量按预期工作,具有正确的单位行为。

The problem is that now we can have separate and distinct unit of measure annotations on the Vector<'T> and on the float itself. 问题是,现在我们可以在Vector<'T>float本身上拥有单独且不同的度量单位注释。

You can the turn an array of a specific type with units of measure into a set of the Vector s using: 您可以使用以下方法将具有度量单位的特定类型的数组转换为一组Vector

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

and back: 然后回来:

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

A hacky alternative: 一个hacky替代品:

If you give up the generic type 'T you can make a float Vector behave properly through some fairly horrible boxing and runtime casting. 如果你放弃泛型类型'T你可以通过一些相当可怕的拳击和运行时强制转换使float Vector行为正常。 This abuses the fact that units of measure are a compile time construct that no longer exist at runtime. 这滥用了一个事实,即度量单位是运行时不再存在的编译时构造。

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

I came up with a solution, that meets most of my requirements (it seems). 我提出了一个解决方案,满足了我的大多数要求(似乎)。 It is inspired by TheInnerLight's ideas (wrapping Vector<'T> ), but also adds a wrapper (called ScalarField ) for the underlying array data type. 它的灵感来自TheInnerLight的想法 (包装Vector<'T> ),但也为底层数组数据类型添加了一个包装器(称为ScalarField )。 This way we can track the units, while underneath we only handle raw data and can use the not unit-aware System.Numerics.Vector APIs. 这样我们就可以跟踪单元,而在下面我们只处理原始数据,并且可以使用不是单元感知的System.Numerics.Vector API。

A simplified, bare-bones quick and dirty implementation would look like this: 一个简化的,简单快速和脏的实现看起来像这样:

// 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

We can now use both data structures to solve the sample problem in a relatively elegant, efficient way: 我们现在可以使用这两种数据结构以相对优雅,高效的方式解决样本问题:

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

On top, it might be useful to re-implement usual array operations for the unit-aware ScalarField wrapper, eg 最重要的是,为单元感知的ScalarField包装器重新实现常规数组操作可能很有用,例如

[<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

etc. 等等

Drawbacks: Not generic with respect to the underlying numeric type ( float ), because there is no generic replacement for floatWithMeasure . 缺点:对于底层数字类型( float )而言不是通用的,因为floatWithMeasure没有通用替代floatWithMeasure To make it generic, we have to implement a third wrapper Scalar the also wraps the underlying primitive: 为了使它成为通用的,我们必须实现第三个包装器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

... meaning that we basically track the units not by using "refinement" types like float<'m> , but solely through wrapper types with a secondary type/units argument. ...这意味着我们基本上不使用像float<'m>这样的“细化”类型来跟踪单位,而只是通过具有辅助类型/单位参数的包装类型来跟踪单位。

I'm still hoping someone comes up with a better idea, though. 不过,我仍然希望有人提出更好的主意。 :) :)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM