【问题标题】:How does Box CIL work internally in .net?Box CIL 如何在 .net 内部工作?
【发布时间】:2021-05-01 21:03:57
【问题描述】:

假设我们有以下 C# 代码:

public static void Main() 
{
   int v = 5;
   Object o = v;
   v = 123;
   Console.WriteLine(v + (Int32) o); // Displays "1235"
}

而生成的IL代码是:

.locals init ([0]int32 v, [1] object o)

 // Load 5 into v.
 IL_0000: ldc.i4.5
 IL_0001: stloc.0

 // Box v and store the reference pointer in o.    <------first boxing
 IL_0002: ldloc.0
 IL_0003: box [mscorlib]System.Int32
 IL_0008: stloc.1

 // Load 123 into v.
 IL_0009: ldc.i4.s 123
 IL_000b: stloc.0

 // Box v and leave the pointer on the stack for Concat.  <------second boxing
 IL_000c: ldloc.0
 IL_000d: box [mscorlib]System.Int32

 // Unbox o: Get the pointer to the In32's field on the stack.
 IL_0017: ldloc.1
 IL_0018: unbox.any [mscorlib]System.Int32

 // Box the Int32 and leave the pointer on the stack for Concat.   <------third boxing
 IL_001d: box [mscorlib]System.Int32

 // Call Concat.
 IL_0022: call string [mscorlib]System.String::Concat(object, object) 

我们可以看到第一次拳击和第二次拳击的工作原理如下:

  1. 将第一个参数v 推入堆栈。

  2. 致电boxCIL

所以看起来当box被调用时,需要的“参数”是指向v的第一个字段的堆栈指针。

第三拳的作用如下:

  1. 前面的unbox创建了一个值类型指针,这个值类型指针指向堆上装箱实例的第一个字段,然后这个值类型指针被压入栈中。

  2. 致电boxCIL

所以现在看起来当box被调用时,它首先检查堆栈指针以通过取消引用堆栈指针来获取其内容(指向堆的值类型指针)。

所以我的问题是,box CIL 是否被设计成通用的,有时它直接读取堆栈指针,而有时它取消引用堆栈指针以获取另一个指针(在我的情况下是指向堆的指针)?

【问题讨论】:

标签: c# .net clr cil


【解决方案1】:

unbox.any 将值类型拆箱并加载到堆栈中(因此它会复制它):

来自MSDN

生成的对象引用或值类型被压入堆栈。

当应用于值类型的装箱形式时,unbox.any 指令提取包含在 obj(O 类型)中的值,因此相当于 unbox 后跟 ldobj。 p>

你在想的是unbox instruction

unbox 指令将对象引用(类型 O)(值类型的装箱表示)转换为值类型指针(托管指针,类型 &),它的未装箱形式。提供的值类型 (valType) 是元数据标记,指示装箱对象中包含的值类型的类型。

与需要复制值类型以在对象中使用的 Box 不同,unbox 不需要从对象中复制值类型。通常它只是计算已存在于装箱对象内部的值类型的地址。

我什至不知道如何强制编译器使用 unbox 指令(从 here 读取,它在 C# 中没有使用,或者至少在 2010 年的编译器中没有使用。 ..我做了一些测试,混合ref,装箱和拆箱,我无法强制编译器使用它)

嗯...通过查看ILSpy(他们反编译C#代码的专家),似乎unbox仅用于某些“私有”实现switchswitch 语句根据数量和条件类型以不同方式编译)。关于unbox 的唯一参考是在一个名为MatchLegacySwitchOnStringWithHashtable 的方法中......我会说这个名字很清楚。另一个参考在Unsafe.il 文件中...该文件“链接”到.NET 的Unsafe 类。请参阅关于Unsafe.Unbox&lt;T&gt; 方法的提案here。该方法已被接受,现在是 .NET 的一部分。 corresponding C# code 无法编译:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ref T Unbox<T>(object box) where T : struct
{
    return ref (T)box;
}

事实上,通过查看.NET code,它可能是作为内在函数实现的。

取消一切.. Charlieface 找到了如何强制使用unbox

public struct MyStruct
{
    public int A;

    public int Test()
    {
        object st2 = new MyStruct();
        int a = ((MyStruct)st2).A;
        return a;
    }
}

方法Test()编译为:

// Methods
.method public hidebysig 
    instance int32 Test () cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 25 (0x19)
    .maxstack 1
    .locals init (
        [0] valuetype MyStruct
    )

    IL_0000: ldloca.s 0
    IL_0002: initobj MyStruct
    IL_0008: ldloc.0
    IL_0009: box MyStruct
    IL_000e: unbox MyStruct
    IL_0013: ldfld int32 MyStruct::A
    IL_0018: ret
} // end of method MyStruct::Test

来自some tests 我会说这个家族中的智能操作码是ldfld:它既可以使用值类型(Test1),也可以使用值类型引用(Test2)和直接取消装箱的值类型(Test3)。

public struct MyStruct
{
    public int A;

    public int Test1(MyStruct st)
    {
        int a = st.A;
        return a;
    }

    public int Test2(ref MyStruct st)
    {
        int a = st.A;
        return a;
    }

    public int Test3(MyStruct st)
    {
        object st2 = st;
        int a = ((MyStruct)st2).A;
        return a;
    }
}

编译成

.method public hidebysig 
    instance int32 Test1 (
        valuetype MyStruct st
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 7 (0x7)
    .maxstack 8

    IL_0000: ldarg.1
    IL_0001: ldfld int32 MyStruct::A
    IL_0006: ret
} // end of method MyStruct::Test1

.method public hidebysig 
    instance int32 Test2 (
        valuetype MyStruct& st
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 7 (0x7)
    .maxstack 8

    IL_0000: ldarg.1
    IL_0001: ldfld int32 MyStruct::A
    IL_0006: ret
} // end of method MyStruct::Test2

.method public hidebysig 
    instance int32 Test3 (
        valuetype MyStruct st
    ) cil managed 
{
    // Method begins at RVA 0x2058
    // Code size 17 (0x11)
    .maxstack 8

    IL_0000: ldarg.1
    IL_0001: box MyStruct
    IL_0006: unbox MyStruct
    IL_000b: ldfld int32 MyStruct::A
    IL_0010: ret
} // end of method MyStruct::Test3

ldfld 操作码始终相同,并且它适用于两(三)种不同的类型:int32 和对int32 的引用(以及对加框的int32 的引用)。

【讨论】:

  • 强制转换 object 来构造和访问一个字段。生成一个unbox
  • 你是对的:sharplab
  • 我在 LinqPad 中都得到了unbox。它背后的逻辑是合理的:字段访问具有 ref(非复制)语义。事实上,不知道为什么sharplab 会打扰unbox.any,然后是pop,反正没有副作用
  • @Charlieface 尝试发布模式...一些 IL 代码被清除
  • 嗯,你是对的,看起来像是逃逸分析的优化工件,例如在下一行执行a.ToString(),你会得到unbox。我敢打赌 JIT 无论如何都会忽略它。
【解决方案2】:

@xanatos 是正确的。你混淆了unboxunbox.any

来自ECMA-335 规范(.NET 和 CIL 的规范)

第 III.4.33 部分:

与 unbox 指令不同,对于值类型,unbox.any 在堆栈中留下一个值,而不是值的地址。

顺便说一句,有些指令采用ref valuetype 或实际值。例如,ldfld(在此here 上查看更多信息)


此外,你说:

所以看起来当 box 被调用时,需要的“参数”是指向 v 的第一个字段的堆栈指针。

这不是真的:ldloc.0 会将实际值加载到堆栈中。

【讨论】:

  • 我知道ldloc.0 会将实际值加载到堆栈中。假设原始堆栈指针是 0x10,结构实例有两个字段,每个字段是 4 个字节,所以当这个结构实例被压入堆栈时,它的两个字段在堆栈上得到副本,堆栈指针递减到 0x08(16- 8 = 8),所以在下一个box CIL 指令中,它使用堆栈指针值 0x08 作为起始地址来读取第一个字段然后第二个字段,所以我的理解是,对于box CIL,它只需要堆栈指针作为它的“参数”,我的理解正确吗?
  • 您将 MSIL 逻辑堆栈与其在 x86/x64 上的实现混为一谈。是的,我认为这就是 JIT 将它翻译成汇编程序的方式。但是box 指令不是汇编程序,它是 MSIL,它的参数只是top-of-stack value。在 ECMA-335 中没有提到堆栈指针,只是一个逻辑堆栈。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-05-31
  • 2014-10-02
  • 2012-10-07
  • 2019-01-13
  • 2010-10-14
  • 2013-09-28
  • 2012-08-01
相关资源
最近更新 更多