(Avoid) boxing of is null

Viewed 144

Giving the following IsNull method:

public class Field<T>
{
    public static bool IsNull(T value) => value is null; // or: => value == null;
}

Is there any boxing when T is a value type? If the answer is yes, how can it be implemented without boxing? Edit: It should handle nullable type such as int? correctly.

Edit2: Using ILDASM shows there is boxing for value is null and value == null:

.method public hidebysig static bool  IsNull(!T 'value') cil managed
{
  // Code size       15 (0xf)
  .maxstack  2
  .locals init (bool V_0)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  box        !T
  IL_0007:  ldnull
  IL_0008:  ceq
  IL_000a:  stloc.0
  IL_000b:  br.s       IL_000d
  IL_000d:  ldloc.0
  IL_000e:  ret
} // end of method Field`1::IsNull

However, the following code:

public class Field
{
    public static bool IsNull(int? value)
    {
        return value == null;
    }
}

will be compiled to the following without boxing:

.method public hidebysig static bool  IsNull(valuetype [System.Runtime]System.Nullable`1<int32> 'value') cil managed
{
  // Code size       16 (0x10)
  .maxstack  2
  .locals init (bool V_0)
  IL_0000:  nop
  IL_0001:  ldarga.s   'value'
  IL_0003:  call       instance bool valuetype [System.Runtime]System.Nullable`1<int32>::get_HasValue()
  IL_0008:  ldc.i4.0
  IL_0009:  ceq
  IL_000b:  stloc.0
  IL_000c:  br.s       IL_000e
  IL_000e:  ldloc.0
  IL_000f:  ret
} // end of method Field::IsNull

So the compiler does treat Nullable<> specially but not in a generic class context.

2 Answers

As shown in your updated question:

public static bool IsNull(T value) => value is null;

compiles to the following IL code:

IL_0000: ldarg.0
IL_0001: box !T
IL_0006: ldnull
IL_0007: ceq
IL_0009: ret

where you indeed see some boxing.

However, this is not the whole picture. We need to look what the JITter does, i.e. how this gets transformed to native code. The JITter will generate a separate method for each used value of T.

Reference type, e.g. string

For a string (a nullable type) the following native code gets generated:

test ecx, ecx
sete al
movzx eax, al
ret

The reference to the string object is in register ECX, which is checked if it is 0 (null) [test ecx, ecx is virtually the same as cmp ecx,0], and depending on the result 0 (false) or 1 (true) get returned.

Value type, e.g. int

For a value type like int the following native code is generated:

xor eax, eax
ret

It just returns 0 (false).

The important part is that for a value type like int no boxing or a native equivalent of it actually happens as the runtime knows a value type is never null, and therefore the method always just return false.

Nullable value type, e.g. int?

Just for completeness, for a nullable value type like int? we get:

cmp byte ptr [esp+4], 0
sete al
movzx eax, al
ret 8

Nullable value types are internally just structs which hold the actual value as well as flag if the object actually has a value, i.e. is not null. Therefore in the native code it accesses the flag (you can see that the int? is stored on the stack and flag is at offset 4) and it gets compared to 0 (false).

Conclusion

You don't have to worry about boxing in this case, as no boxed object will actually be created.

Inspired by source code of System.Text.Json, here is the solution:

internal static partial class Extensions
{
    private static readonly Type s_nullableType = typeof(Nullable<>);

    /// <summary>
    /// Returns <see langword="true" /> when the given type is of type <see cref="Nullable{T}"/>.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool IsNullableOfT(this Type type) =>
        type.IsGenericType && type.GetGenericTypeDefinition() == s_nullableType;

    /// <summary>
    /// Returns <see langword="true" /> when the given type is either a reference type or of type <see cref="Nullable{T}"/>.
    /// </summary>
    /// <remarks>This calls <see cref="Type.IsValueType"/> which is slow. If knowledge already exists
    /// that the type is a value type, call <see cref="IsNullableOfT"/> instead.</remarks>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool CanBeNull(this Type type) => !type.IsValueType || type.IsNullableOfT();
}

public class Field<T>
{
    private static readonly bool s_canBeNull = typeof(T).CanBeNull();
    public bool CanBeNull => s_canBeNull;

    public virtual bool IsNull(T? value)
    {
        return IsNullValue(value);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    internal static bool IsNullValue(T? value)
    {
        // Nullable<> gets boxing here, use NullableField<T> instead.
        // Code analysis should generate a warning for using nullable data type of this class
        return !s_canBeNull ? false : value == null;
    }
}

public class NullableField<T> : Field<Nullable<T>>
    where T : struct
{
    public sealed override bool IsNull(T? value)
    {
        return !value.HasValue;
    }
}

Boxing can be avoided by using derived NullableField<T> class.

Related