【问题标题】:Why is None represented as null?为什么 None 表示为 null?
【发布时间】:2012-08-11 03:49:05
【问题描述】:

CompilationRepresentationFlags.UseNullAsTrueValue可以用来

允许在可区分联合中使用 null 作为 null 鉴别器的表示

Option.None 是这方面最突出的例子。

为什么这很有用?与传统的联合案例检查机制(生成的Tag 属性)相比,空值检查有何优势?

这可能会导致意想不到的行为:

Some(1).ToString() //"Some(1)"
None.ToString()    //NullReferenceException

编辑

我测试了 Jack 的断言,即与 null 而不是静态只读字段相比更快。

[<CompilationRepresentation(CompilationRepresentationFlags.UseNullAsTrueValue)>]
type T<'T> =
  | Z
  | X of 'T

let t = Z

使用 ILSpy,我可以看到 t 编译为 null(如预期的那样):

public static Test.T<a> t<a>()
{
    return null;
}

测试:

let mutable i = 0
for _ in 1 .. 10000000 do
  match t with
  | Z -> i <- i + 1
  | _ -> ()

结果:

真实:00:00:00.036,CPU:00:00:00.046,GC gen0:0,gen1:0,gen2:0

如果CompilationRepresentation 属性被移除,t 将成为静态只读字段:

public static Test.T<a> t<a>()
{
    return Test.T<a>.Z;
}

public static Test.T<T> Z
{
    [CompilationMapping(SourceConstructFlags.UnionCase, 0)]
    get
    {
        return Test.T<T>._unique_Z;
    }
}

internal static readonly Test.T<T> _unique_Z = new Test.T<T>._Z();

而且结果是一样的:

真实:00:00:00.036,CPU:00:00:00.031,GC gen0:0,gen1:0,gen2:0

模式匹配在前一种情况下编译为t == null,在后一种情况下编译为t is Z

【问题讨论】:

    标签: f# discriminated-union


    【解决方案1】:

    Jack 的回答似乎不错,但稍微扩展一下,在 IL 级别,CLR 提供了用于加载空值 (ldnull) 的特定操作码和测试它们的有效方法(ldnull 后跟 beq /bne.un/ceq/cgt.un)。当 JITted 时,这些应该比取消引用 Tag 属性并相应地分支更有效。虽然每次调用的节省可能很小,但选项类型的使用频率足够高,累积的节省可能很可观。

    当然,正如您注意到的那样,有一个权衡:从obj 继承的方法可能会抛出空引用异常。这是在处理 F# 值时使用 string x/hash x/x=y 而不是 x.ToString()/x.GetHashCode()/x.Equals(y) 的一个很好的理由。遗憾的是,null 表示的值没有(可能的)等效于 x.GetType()

    【讨论】:

    • 它似乎使用类型测试(is)而不是检查Tag,这似乎与空检查一样快。
    • 类型测试通常不如空检查快(尽管在某些情况下可能如此)。此外,在最初实现 F# 的 .NET 2.0 时代,它们的性能可能有所不同。
    • 如果这是真的(我很想有证据)那么这似乎是最能解释它的。
    • Daniel,请这样想:需要发出哪些底层 CPU 指令(由 JIT 编译器),然后在运行时执行空值检查和类型测试。 Null 检查可以编译为单个 CPU 指令,类型测试几乎总是比这更多,因为它需要调用 CLR 来执行类型测试。即使他们做了一些非常高级的优化,它仍然需要不止一条 CPU 指令。
    • @daniel,我知道这是几年前的事了,但是当案例数量为 4 或更少时使用类型测试,Tag 用于 5 或​​更多。这个阈值是在 2005 年的多次性能测试后定义的,但仍然存在,请参阅此 PR 中的讨论:github.com/dotnet/fsharp/pull/762。此外,空值检查的 IL 现在得到了更好的优化,性能差异可能更容易看出。
    【解决方案2】:

    F# 编译器有时使用 null 作为 None 的表示,因为它比实际创建 FSharpOption 的实例并检查 Tag 属性更有效。

    想一想——如果你有一个不允许为空的普通 F# 类型(如记录),那么任何指向该类型实例的指针(CLR 内部使用的指针)永远不会是 @987654323 @。同时,如果T是可以表示n状态的类型,那么T option可以表示n+1状态。因此,使用null 作为 None 的表示只是利用了一个额外的状态值,这是由于 F# 类型不允许为空而可用。

    如果您想尝试关闭此行为(对于普通 F# 类型),您可以将[&lt;AllowNullLiteral(true)&gt;] 应用于它们。

    【讨论】:

    • 这比将无效案例表示为静态只读字段(如String.Empty)有什么好处?
    • 联合类型中的空例编译为静态只读字段。使用null 表示None 是对'T option 类型的特定优化,因为它经常出现,CLR 可以通过这种方式生成更高效的代码。
    • [&lt;AllowNullLiteral&gt;] 允许您将null 分配给使用某些标准(即不可为空)F# 类型键入的变量,例如记录。如果将该属性应用于 F# 类型(记录、联合、类等),然后编写一些使用该类型的 'T option 版本的代码,您将看到编译器使用静态只读字段版本的无( FSharpOption.None)。
    • 这似乎与您的断言相矛盾,即它比 FSharpOption&lt;'T&gt; 的新实例更有效,因为这对于空字段来说是不必要的。
    • 将 [] 应用于您的类型会导致代码效率降低。是的,这是一个小的优化,但只要使用 'T 选项,它可能是一个可衡量的(虽然很小)的性能提升。
    猜你喜欢
    • 2016-12-15
    • 2013-08-14
    • 2018-11-29
    • 2018-09-20
    • 2013-07-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多