Any reason to not make records value types

man*_*ows 4 f#

So I have been experiment with code such as this:

[<Struct>]
type Component = {
    Num : int
}
with 
    static member New = {
        Num = 0
    }

let init n = 
    seq {
        for _ in 0..n do 
            yield Component.New
    }
    |> Seq.toArray

let test (c : Component []) = 
    c
    |> Array.map ( fun c -> { c with Num = c.Num + 1} )

[<EntryPoint>]
let main argv =
    let sw = Stopwatch()
    let c = init 10000000
    sw.Start ()
    let c' = test c
    sw.Stop ()
    printfn "%A" sw.ElapsedMilliseconds
    0
Run Code Online (Sandbox Code Playgroud)

I get these benchmarks when running this test.

Reference type and list: 1450
Reference type and array: 850
Value type and list: 700
Value type and array: 80
Run Code Online (Sandbox Code Playgroud)

The fact that an array would be faster than the list is obviously within expectation, I also expected the value types to be faster but not that much faster. I am wondering however, is there a scenario when I do not want a record to be a value type? It seems like it is almost always preferrable?

Phi*_*ter 5

对于这样的小结构,尤其是保存在数组中时,出于性能原因没有理由不将其设为结构。即使结构大于 16 字节(这是一般准则),对于这样的情况,它通常会产生更快的性能时间:

  • 分配到数组中
  • 该数组的直接处理
  • 不再对该数组进行进一步处理

如果几个后续的数组处理例程也产生了更好的性能,我不会感到惊讶,因为将一堆值类型分配到一个数组中意味着它通常会全部加载到 CPU 缓存行中并以非常快的速度进行处理。

但是,当您没有一直使用值类型(数组、跨度)时,或者如果您改变了做事的方式,事情会变得更有趣:

  • 基于数组的操作会创建新的数组,最终连续重新创建数据会压倒对该数据的实际处理
  • 有时您正在使用 F# 列表或序列或其他一些引用类型,其中数据不会全部加载到 CPU 缓存行中
  • 有时您会混合和匹配值类型和引用类型,而将内容存储在值类型中的额外复制会对性能产生负面影响

简而言之,这个特定场景非常适合使用值类型——包含的数据只是一个原始值类型,它的实例存储在一个数组中,并且每次都重新创建相同的结构。因此,如果性能很重要,您应该在这种情况下使用值类型。

更改代码以使用列表,而是预先分配数据,而不是重新创建结构,只需在基准下返回单个值的列表,如下所示:

open BenchmarkDotNet.Attributes
open BenchmarkDotNet.Running

module ReferenceType =
    type Component = {
        Num0 : int
    }
    with 
        static member New = {
            Num0 = 0
        }

    let init n = 
        seq {
            for _ in 0..n do 
                yield Component.New
        }
        |> Seq.toList

    let test cs = 
        cs
        |> List.map (fun c -> c.Num0 + 1)

module ValueType = 
    [<Struct>]
    type Component = {
        Num0 : int
    }
    with 
        static member New = {
            Num0 = 0
        }

    let init n = 
        seq {
            for _ in 0..n do 
                yield Component.New
        }
        |> Seq.toList

    let test cs = 
        cs
        |> List.map (fun c -> c.Num0 + 1)

[<MemoryDiagnoser>]
type ReferenceVsValueType() =
    let refs = ReferenceType.init 10_000
    let vals = ValueType.init 10_000

    [<Benchmark(Baseline=true)>]
    member _.BuiltIn() = ReferenceType.test refs

    [<Benchmark>]
    member _.ValueType() = ValueType.test vals

[<EntryPoint>]
let main argv =
    BenchmarkRunner.Run<ReferenceVsValueType>() |> ignore
    0 // return an integer exit code
Run Code Online (Sandbox Code Playgroud)

你会得到非常非常接近的结果:

方法 意思 错误 标准差 比率 比率标准差 第 0 代 第一代 第 2 代 已分配
内置 90.99 ?s 1.979 ?s 5.709 ?s 1.00 0.00 51.5137 25.7568 —— 312.53 KB
值类型 87.13 ?s 1.712 ?s 4.600 ?s 0.95 0.08 51.3916 25.6348 —— 312.53 KB

再添加一个字段并对值求和,引用类型获胜:

方法 意思 错误 标准差 中位数 比率 比率标准差 第 0 代 第一代 第 2 代 已分配
内置 86.14 ?s 1.543 ?s 4.145 ?s 84.61 ?s 1.00 0.00 51.5137 25.7568 —— 312.53 KB
值类型 126.18 ?s 2.516 ?s 3.917 ?s 124.75 ?s 1.46 0.08 51.2695 25.6348 —— 312.53 KB

那么教训是什么?

始终仔细测量。在非常特殊的情况下,您可以到达值类型总是更快的地方,当这种情况发生时,这很酷!但是只需进行一些细微的更改,您就会获得非常不同的行为。