I'VE GOT THE BYTE ON MY SIDE

57005 or alive

Null-checking considerations in F# - it's harder than you think

May 18, 2015 C# F# performance

The near-complete obviation of nulls is perhaps the most frequently- (and hilariously-) cited benefit of working in F#, as compared to C#. Nulls certainly still exist in F#, but as a practical matter it really is quite rare that they need to be considered explicitly within an all-F# codebase.

It turns out this cuts both ways. On those infrequent occasions where one does need to check for nulls, F# actually makes it surprisingly difficult to do so safely and efficiently.

In this post I’ve tried to aggregate some best practices and pitfalls, in the form of DOs and DON’Ts, for F# null-checking.

Recall that there are a handful of ways that nulls can be introduced into F# code, even for “non-nullable” F# types:

DON’Ts

Don’t use ‘x = null’ or ‘x <> null’

It’s unfortunate that the most obvious approach is not the best. This form is perfectly functional for nullable types, but the generated IL is unoptimized and ends up being quite slow. Standard F# structural/generic comparison is used here, which is a well-known drag on performance.

F#:

let nullCheck01 x = (x = null)

Codegen (C# equivalent):

public static bool nullCheck01<a>(a x) where a : class
{
    return LanguagePrimitives.HashCompare.GenericEqualityIntrinsic<a>(x, default(a));
}

// --- F# core library ---

public static bool GenericEqualityIntrinsic<T>(T x, T y)
{
    return LanguagePrimitives.HashCompare.GenericEqualityObj(false, LanguagePrimitives.HashCompare.fsEqualityComparerNoHashingPER, );
}

internal static bool GenericEqualityObj(bool er, IEqualityComparer iec, object xobj, object yobj)
{
    if(xobj == null) return yobj == null;
    if(yobj != null) { /* ... core logic, fairly big, prevents JIT inlining ... */ }
    return false;
}

By design, this form is not permitted for non-nullable types, as the compiler will not allow null to represent an instance of such a type. One workaround is to convert to System.Object first, i.e. (box x) = null. This is perfectly functional, even for non-nullable types, but it is still slow.

Don’t use x = Unchecked.defaultof<_>

When the need arises to null-check a non-nullable type, one might be tempted to compare directly against Unchecked.defaultof<_>, rather that converting to System.Object and comparing against a null literal.

Unfortunately, this approach falls on its face when used with functional-style record or discriminated union types. Due to its baked-in notion of immutability and non-nullability for such types, the compiler assumes that it is always safe to codegen their equality comparisons as simply x.Equals(y).  That’s fine until x is null, in which case the null-check itself blows up with a NullReferenceException!

F#:

type Cat = { Name : string; Lives : int }

let nullCheck02 (x : Cat) = (x = Unchecked.defaultof<_>)

Codegen (C# equivalent):

public static bool nullCheck02(Cat x)
{
    Cat obj = null;

    // nullref
    return x.Equals(obj, LanguagePrimitives.GenericEqualityComparer);
}

Don’t expect to catch NullReferenceException

Let’s say you decide that you’ll mostly ignore the possibility of nulls, and but harden your code by catching NullReferenceException.

F#:

type Cat = { Name : string; Lives : int }

let ohNoes x =
    try
        if x.Lives > 1 then Some({ x with Lives = x.Lives - 1})
        else None
    with
    | :? NullReferenceException -> None

ohNoes (Unchecked.defaultof<_>)

Most F# developers will be surprised to find that this still fails with an unhandled NullReferenceException.

How is that possible? NullReferenceException is clearly handled!

Due to Cat’s immutability/non-nullability, the compiler believes the contents of the try block cannot fail. Thus it performs an “optimization” by removing the try/catch altogether. Indeed, if you inspect the generated IL, you will find that the try/catch block simply isn’t generated:

Codegen (C# equivalent):

public static FSharpOption<Cat> ohNoes(Cat x)
{
    if (x.Lives > 1)
    {
        return FSharpOption<Cat>.Some(new Cat(x.Name, x.Lives - 1));
    }
    return FSharpOption<Cat>.None;
}

FWIW this is an altogether bogus optimization anyway, and has been removed in F# 4.0.

DOs

Do pattern match against null

Pattern matching against null results in much better generated IL than comparison with = or <>. Nullable types can be matched directly, non-nullable types must be converted to System.Object first by using box.

F#:

type Cat = { Name : string; Lives : int }

let nullCheck03 x = match x with null -> true | _ -> false
let nullCheck04 (x : Cat) = match box x with null -> true | _ -> false

Codegen:

// nullCheck04 is identical except generic "a" becomes concrete "Cat"
.method public static bool nullCheck03<class a> (!!a x) cil managed 
{
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: box !!a
    IL_0006: brfalse.s IL_000a

    IL_0008: ldc.i4.0
    IL_0009: ret

    IL_000a: ldc.i4.1
    IL_000b: ret
}

The JIT produces exactly the same native code for both of these functions:

          ; if x is null, go to 00D50503
00D504FB  test        ecx,ecx
00D504FD  je          00D50503  

          ; set return value to 0 (false)
00D504FF  xor         eax,eax  
          ; return 

          ; set return value to 1 (true)
00D50503  mov         eax,1  
          ; return

Do use Object.ReferenceEquals(x, null)

Another way to get efficient null-checking is to call the BCL method Object.ReferenceEquals.

F#:

let nullCheck05 (x : 't when 't : null) = System.Object.ReferenceEquals(x, null)

Codegen:

.method public static bool nullCheck05<class t> (!!t x) cil managed
{
    .maxstack 8

    IL_0000: nop
    IL_0001: ldarg.0
    IL_0002: box !!t
    IL_0007: ldnull
    IL_0008: call bool [mscorlib]System.Object::ReferenceEquals(object, object)
    IL_000d: ret
}

 // --- BCL ---

.method public hidebysig static bool ReferenceEquals (object objA, object objB) cil managed 
{
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldarg.1
    IL_0002: ceq
    IL_0004: ret
}

At the IL level this appears to suffer from the overhead of an additional method call, but ReferenceEquals is small enough that it will be inlined by the JIT. The resulting native code winds up being at least as good as our earlier efforts, perhaps even better:

          ; if x is null, set the lowest byte of return value to 1
          ; otherwise set it to 0
012404A3  test        ecx,ecx  
012404A5  sete        al  

          ; fill out the rest of return value with 0s
012404A8  movzx       eax,al
          ; return

By some measurements, Object.ReferenceEquals (or pattern-matching) is 7-8x faster than comparison with = or <>.

Do use isNull

In F# 4.0, a new helper function isNull has been added, which has the same pattern matching implementation as nullCheck03 above. It only applies to nullable types, so you will need to box non-nullable types first. box and isNull are both inline functions, so either way the final JITed code is the same as shown above.

Do use Option.ofObj

In F# 4.0, a new helper function Option.ofObj has been added, which will convert a nullable object into an option instance, based on whether it’s null or not. This adds a bit of perf overhead, but is very handy for when you’d prefer to lean on the type system rather than worry about nulls.


Comments