受歧视联合的结构属性

Struct Attribute on Discriminated Unions

我刚刚意识到 F# 记录是引用类型以及我进行了多少装箱和拆箱操作。我有很多这样的小记录:

type InputParam =
    | RegionString of string
    | RegionFloat of float32

但是如果我尝试用 "Struct" 属性标记它,我会收到一个编译器错误,说明 "FS3204 If a union type has more than one case and is a struct, then all fields within the union type must be given unique names." language reference 显示创建结构区分联合,如下所示:

[<Struct>]
type InputParamStruct =
    | RegionString of RegionString: string
    | RegionFloat of RegionFloat: float32

x of string 和 x of x: string 有什么区别?这些字段从一开始就不是唯一的?为什么 F# 不默认为记录结构?

首先,这些不是记录 - 它们是受歧视的工会。 Record 是具有生成的 equality/hashing 的命名数据的简单聚合,也可以将其设为结构,但没有其他要求。

结构可辨联合的更严格要求是:

  • 没有可调用的默认构造函数
  • 无循环引用/无递归定义
  • 多写必须有唯一的名字

前两点是值类型所固有的。值和引用类型只是不同。

最后一点很有意思。考虑以下因素:

type DU1 =
    | Case1 of string
    | Case2 of float

[<Struct>]
type DU2 =
    | Case1 of sval: string
    | Case2 of fval: float

DU1 的情况下,每种情况都有一个内部 class,它们包含用于访问基础数据的属性。这些属性被命名为 Item1Item2 等等,因为它们被封装在内部 class 中,所以在访问时它们是唯一的。

DU2的情况下,svalfval的值是平铺的;没有包含它们的内部 class。这是因为目标是结构的 performance/size。联合案例中的数据命名策略(Item1/Item2/等)不适用,因为所有数据都是平面布局的。因此,设计决策是要求具有唯一命名的案例,而不是应用一些技巧将案例本身的名称和 Item1/Item2/等的一些变体拼凑在一起。唯一性问题是编译器中联合本身设计所固有的,而不仅仅是代码生成设计选择。

最后,这个问题还有一个有趣的答案:

Why doesn't F# default to structs for records?

F# 中的元组、记录和 DU 都可以标记为 [<Struct>],但默认情况下不是结构。这是因为结构不仅仅是一个可以按下的 "make it more efficient" 按钮。通常情况下,由于您的结构太大而导致过度复制,您的 CPU 性能会变得更差。在 F# 中,拥有大型元组和非常非常大的记录以及有区别的联合是很正常的。默认情况下制作这些结构不是一个好的选择。引用类型非常强大,旨在在 .NET 上很好地工作,默认情况下不应避免仅仅因为在某些情况下结构可能会导致性能稍快一些。

每当您担心性能时,切勿仅根据假设或直觉来更改:使用性能分析工具,如 PerfView、dotTrace 或 dotMemory;并使用 BenchmarkDotNet 等统计工具对小的变化进行基准测试。性能是一个极其复杂的问题space,一旦你解决了明显糟糕的严重问题(比如大型数据集上的 O(n^2) 算法等),性能就很少是简单的了。

毫无疑问,这应该是一个结构体。它是不可变的,有 16 个字节。看反汇编,这个引用类型:

type InputParam =
    | RegionString of string
    | RegionFloat of float32

而这个参考类型:

type InputParam =
    | RegionString of RegionString: string
    | RegionFloat of RegionFloat: float32

功能相同。唯一的区别是编译器如何命名事物。他们都创建了一个名为 "RegionString" 的子类,但具有不同的 属性 名称——"RegionString.item" 与 "RegionString.RegionString".

当您将第一个示例转换为结构时,它取消了子类并尝试在记录上粘贴 2 个 "item" 属性,这导致 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>

第一个将 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

功能域建模很棒,但您必须记住它们具有相同的性能开销:

let c = CustomerID 5
let i = 5 :> obj

推荐的是anything immutable under 16 bytes should be a struct。如果超过 16 个字节,则必须查看行为。如果它被大量传递,您最好传递 64 位 ref 指针并接受 ref 开销命中。但对于组合类型或函数内的内部数据,请坚持使用结构。