Eri*_*icP 7 f# struct discriminated-union
我刚刚意识到 F# 记录是引用类型以及我进行了多少装箱和拆箱。我有很多这样的小记录:
type InputParam =
| RegionString of string
| RegionFloat of float32
Run Code Online (Sandbox Code Playgroud)
但是如果我尝试用“Struct”属性来标记它,我会收到一个编译器错误,指出“FS3204 如果联合类型有多个案例并且是一个结构,那么联合类型中的所有字段都必须被赋予唯一的名称。” 该语言参考显示了创建结构歧视这样的工会:
[<Struct>]
type InputParamStruct =
| RegionString of RegionString: string
| RegionFloat of RegionFloat: float32
Run Code Online (Sandbox Code Playgroud)
x of string 和 x of x: string 有什么区别?这些字段一开始就不是唯一的?为什么 F# 不默认为记录结构?
Phi*_*ter 11
首先,这些不是记录——它们是受歧视的工会。Record 是具有生成的相等性/散列的命名数据的简单聚合,也可以将其设为结构体,但没有附加要求。
对 struct Discriminated Unions 的更严格要求是:
前两点是值类型所固有的。值和引用类型只是不同。
最后一点很有趣。考虑以下:
type DU1 =
| Case1 of string
| Case2 of float
[<Struct>]
type DU2 =
| Case1 of sval: string
| Case2 of fval: float
Run Code Online (Sandbox Code Playgroud)
在 的情况下DU1,每种情况都有一个内部类,这些内部类包含用于访问基础数据的属性。这些属性被命名为Item1、 等Item2,并且由于它们封装在内部类中,因此在访问时是唯一的。
在 的情况下DU2,sval和fval值被平放;没有包含它们的内部类。这是因为目标是结构的性能/大小。联合大小写 ( Item1/ Item2/etc. ) 中的数据命名策略不适用,因为所有数据都是平面布局的。所以设计决定是要求唯一命名的案例,而不是应用一些技巧来将案例本身的名称和Item1/ Item2/ 等的一些变体混在一起。唯一性问题是编译器中联合本身的设计所固有的,而不仅仅是代码生成设计选择。
最后,这个问题还有一个有趣的答案:
为什么 F# 不默认为记录结构?
F# 中的元组、记录和 DU 都可以标记为,[<Struct>]但默认情况下不是结构。这是因为结构不仅仅是您可以按下的“使其更高效”的按钮。很多时候,由于结构太大,过度复制会导致 CPU 性能变差。在 F# 中,有大元组和非常非常大的记录和有区别的联合是很正常的。默认情况下制作这些结构不是一个好的选择。引用类型非常强大,设计用于在 .NET 上运行良好,默认情况下不应避免,因为在某些情况下结构可能会导致性能稍快。
每当您关心性能时,永远不要仅仅根据假设或直觉来改变事情:使用性能分析工具,如 PerfView、dotTrace 或 dotMemory;并使用 BenchmarkDotNet 等统计工具对微小变化进行基准测试。性能是一个极其复杂的空间,一旦你解决了明显糟糕的严重问题(比如大型数据集上的 O(n^2) 算法或其他东西),就很少是简单的事情了。
毫无疑问,这应该是一个结构体。它是不可变的并且 16 字节。看看反汇编,这个引用类型:
type InputParam =
| RegionString of string
| RegionFloat of float32
Run Code Online (Sandbox Code Playgroud)
这个参考类型:
type InputParam =
| RegionString of RegionString: string
| RegionFloat of RegionFloat: float32
Run Code Online (Sandbox Code Playgroud)
功能相同。唯一的区别在于编译器如何命名事物。它们都创建了一个名为“RegionString”的子类,但具有不同的属性名称——“RegionString.item”与“RegionString.RegionString”。
当您将第一个示例转换为结构时,它会删除子类并尝试在记录上粘贴 2 个“项目”属性,这会导致 FS3204唯一名称错误。
就性能而言,您应该在编写时在每个像这样的小型类型上使用结构。考虑这个示例脚本:
type Name = Name of string
let ReverseName (Name s) =
s.ToCharArray() |> Array.rev |> System.String |> Name
[<Struct>]
type StrName = StrName of string
let StrReverseName (StrName s) =
s.ToCharArray() |> Array.rev |> System.String |> StrName
#time
Array.init 10000000 (fun x -> Name (x.ToString()))
|> Array.map ReverseName
|> ignore
#time
#time
Array.init 10000000 (fun x -> StrName (x.ToString()))
|> Array.map StrReverseName
|> ignore
#time
sizeof<Name>
sizeof<StrName>
Run Code Online (Sandbox Code Playgroud)
第一个将 ref 类型包装在 ref 类型中,这使性能提高了一倍:
Real: 00:00:04.637, CPU: 00:00:04.703, GC gen0: 340, gen1: 104, gen2: 7
...
Real: 00:00:02.620, CPU: 00:00:02.625, GC gen0: 257, gen1: 73, gen2: 1
...
val it : int = 8
val it : int = 8
Run Code Online (Sandbox Code Playgroud)
功能域建模很棒,但您必须记住它们具有相同的性能开销:
let c = CustomerID 5
let i = 5 :> obj
Run Code Online (Sandbox Code Playgroud)
建议任何小于 16 字节的不可变内容都应该是 struct。如果超过 16 字节,则必须查看其行为。如果它被多次传递,您最好传递 64 位引用指针并承受引用开销。但对于组合类型或函数内的内部数据,请坚持使用结构。