【问题标题】:(Avoid) boxing of is null(避免)装箱为空
【发布时间】:2021-03-19 05:00:40
【问题描述】:

给出以下IsNull 方法:

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

T 是值类型时是否有装箱?如果答案是肯定的,不装箱怎么实现?编辑:它应该正确处理可空类型,例如int?

编辑2: 使用ILDASM 表明value is nullvalue == 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

但是,下面的代码:

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

不装箱会编译成如下:

.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

所以编译器确实特别对待Nullable&lt;&gt;,但不在泛型类上下文中。

【问题讨论】:

  • 我不认为它涉及拳击。而且,int?不是引用类型,所以我相信编译器以一种特殊的方式处理它的可空性,因此,很难说这里是否涉及到装箱。
  • @A.T.请看看我更新的问题。

标签: c# .net


【解决方案1】:

如您更新的问题所示:

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

编译为以下 IL 代码:

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

你确实看到了一些拳击。

然而,这还不是全部。我们需要看看 JITter 做了什么,即它是如何转换为本地代码的。 JITter 将为每个使用的 T 值生成一个单独的方法。

引用类型,例如string

对于string(可空类型),将生成以下本机代码:

test ecx, ecx
sete al
movzx eax, al
ret

对字符串对象的引用在寄存器ECX中,检查它是否为0(空)[test ecx, ecxcmp ecx,0几乎相同],并根据结果0(假)或1( true) 被退回。

值类型,例如int

对于像int 这样的值类型,会生成以下本机代码:

xor eax, eax
ret

它只是返回 0(假)。

重要的部分是,对于像 int 这样的值类型,实际上不会发生装箱或它的本机等效项,因为运行时知道值类型永远不会为空,因此该方法总是只返回 false。

可空值类型,例如int?

为了完整性,对于像 int? 这样的可空值类型,我们得到:

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

Nullable 值类型在内部只是结构,它保存实际值以及如果对象实际具有值(即不为 null)则标记。 因此,在本机代码中,它访问标志(您可以看到 int? 存储在堆栈中,标志位于偏移量 4 处)并与 0(假)进行比较。

结论

在这种情况下您不必担心装箱,因为实际上不会创建装箱的对象。

【讨论】:

  • 干得好,实际上破解了 jitted 代码以证明这不是问题
  • @Charlieface 使用 www.sharplab.io 很容易做到
【解决方案2】:

System.Text.Json的源代码启发,解决方法如下:

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;
    }
}

使用派生的NullableField&lt;T&gt; 类可以避免装箱。

【讨论】:

  • 请看我的回答。这可能会生成比 public static bool IsNull(T value) =&gt; value is null; 更糟糕的代码,因为 JITer 会将幼稚的实现优化为最佳的本机代码。
  • @ckuri,非常感谢。我接受了你的回答。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-06-27
相关资源
最近更新 更多