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?
对于这样的小结构,尤其是保存在数组中时,出于性能原因没有理由不将其设为结构。即使结构大于 16 字节(这是一般准则),对于这样的情况,它通常会产生更快的性能时间:
如果几个后续的数组处理例程也产生了更好的性能,我不会感到惊讶,因为将一堆值类型分配到一个数组中意味着它通常会全部加载到 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 |
那么教训是什么?
始终仔细测量。在非常特殊的情况下,您可以到达值类型总是更快的地方,当这种情况发生时,这很酷!但是只需进行一些细微的更改,您就会获得非常不同的行为。