对每个原始值都有一个包装器很好,这样就没有办法滥用它。我怀疑这种便利是有代价的。有没有性能下降?如果性能是一个问题,我是否应该使用裸原始值?
3 回答
是的,当使用单例联合类型来包装原始值时,性能会下降。联合案例被编译到类中,因此您将支付分配(以及随后收集)该类的代价,并且每次获取联合案例中保存的值时,您还将有一个额外的间接性。
根据您的应用程序的具体情况,以及您多久会产生这些额外的开销,如果它使您的代码更安全和更模块化,它可能仍然值得做。
我用 F# 编写了很多对性能敏感的代码,我个人的偏好是尽可能使用 F# 度量单位类型来“标记”原始类型(例如,整数)。这可以防止它们被滥用(感谢 F# 编译器的类型检查器),但也避免了任何额外的运行时开销,因为在编译代码时会删除度量类型。如果你想要一些这样的例子,我已经在我的fsharp-tools项目中广泛使用了这种设计模式。
Jack 在编写高性能 F# 代码方面比我有更多的经验,所以我认为他的回答是绝对正确的(我也认为使用度量单位的想法很有趣)。
只是为了把事情放在上下文中,我写了一个非常基本的测试(只使用 F# Interactive - 所以在发布模式下事情可能会有所不同)来比较性能。它分配一个包装(与非包装)int
值的数组。这可能是非包装类型确实是一个不错的选择的场景,因为数组将只是一个连续的内存块。
#time
// Using a simple wrapped `int` type
type Foo = Foo of int
let foos = Array.init 1000000 Foo
// Add all 'foos' 1k times and ignore the results
for i in 0 .. 1000 do
let mutable total = 0
for Foo f in foos do total <- total + f
在我的机器上,for
循环平均需要大约 1050 毫秒。现在,展开的版本:
let bars = Array.init 1000000 id
for i in 0 .. 1000 do
let mutable total = 0
for b in bars do total <- total + b
在我的机器上,这大约需要 700 毫秒。
因此,肯定会有一些性能损失,但可能比预期的要小(约 33%)。这是一个测试,除了在循环中展开值之外几乎什么都不做。在做一些有用的代码中,开销会小得多。
如果您正在编写高性能代码、处理大量数据或需要一些时间并且用户会频繁运行它(如编译器和工具),这可能是一个问题。另一方面,如果您的应用程序不是性能关键,那么这不太可能是一个问题。
从 F# 4.1 开始,将[<Struct>]
属性添加到合适的单案例区分联合将提高性能并减少执行的内存分配数量。