【问题标题】:How CLR works when invoking a method of a struct调用结构的方法时 CLR 的工作原理
【发布时间】:2011-03-31 02:46:29
【问题描述】:

我想我已经知道了一堂课的答案,只是想确认我的理解是正确的。假设我有一个ClassA 及其实例名为a。当a.MethodA() 被调用时:

(1)CLR通过堆中a类型指针找到ClassA的类型(该类型已加载到堆中)

(2)在类型中找到MethodA,如果没有找到,就去它的基类型,直到object类。

也许我的理解不是很准确,但我认为它基本上是正确的(如果有错请纠正我!)。这里是一个简单的struct的问题。

struct MyStruct
{
   public void MethodA() { }
}

我有var x = new MyStruct();,它的值在栈上,MyStruct的类型已经加载到堆中。执行x.MethodA()时,当然没有装箱。 CLR 如何找到MethodA 并获取 IL 并执行/JIT?我认为答案可能是:(再次,如果我错了,请纠正我)

(1) 我们在堆栈上有x声明类型。 CLR 通过栈上的信息找到它的类型,并在它的类型中找到MethodA。 -- 我们称之为assumptionA

如果你告诉我我的assumptionA 是正确的,我会很高兴。但即使它是错误的,它也说明了一个事实:CLR 有一种方法可以在不装箱的情况下找到结构的类型。

现在x.ToString()x.GetType() 呢?我们知道该值将被装箱,然后它将像一个类一样执行。但是为什么我们在这里需要拳击呢?既然我们可以得到它的类型(假设 A 告诉我们),为什么不去它的基类型并找到方法(就像一个类一样)?为什么这里需要一个昂贵的盒子操作?

【问题讨论】:

  • @Danny Chen,结构并不总是分配在堆栈上。它仅在以下情况下才在堆栈上分配:1) 作为值作为参数传递给方法,2) 不是闭包的一部分,以及 3) 不是大对象。否则,它仍然在堆上分配。
  • @Stephen Chung:感谢您的提示。但在这种情况下,它是,对吧?
  • 好吧,如果它是一个局部变量,它就在堆栈上,不是很大(似乎不是)并且没有在闭包中捕获(所以这取决于你在这个块中的代码是否包含一个 lambda)。
  • @Stephen:你确定大型结构是在堆上分配的吗?我不相信大小会有所不同...
  • @Mehrdad - 对象 > 85KB 始终分配在与常规堆分开的“大对象堆”上。 > 85KB 的 AFAIK 结构也属于这一类,但您需要努力构建如此大的结构......

标签: c# .net clr value-type il


【解决方案1】:

假设 A 是错误的。 C# 编译器的符号表存储类型信息。几乎在所有情况下都使用静态类型信息,存储在对象中的动态类型仅在类型检查(is 运算符)、强制转换(as 运算符和实际强制转换语法)和数组变化期间需要,然后只有当编译器不知道动态类型时。未装箱结构的动态类型始终是静态已知的,类实例的动态类型在实例化附近和执行类型检查的条件块内是静态已知的(例如,在if (x is T) y = (T)x; 中,类型在 then-part 中是已知的,因此演员表不需要另一个动态检查)。

好的,现在因为 C# 编译器静态知道 x 的类型,它可以进行重载解析并找到正在调用的 准确 MethodA。然后它发出 MSIL 以将参数推送到 MSIL 虚拟堆栈并发出包含对该特定方法的元数据引用的调用指令。运行时不需要类型检查。

对于x.ToString(),C# 编译器仍然知道它要调用的确切方法。如果ToString 已被struct 类型覆盖,则它需要一个类型为pointer-to-MyStruct 的参数,编译器无需装箱即可处理该参数。如果ToString 没有被覆盖,编译器会生成一个对Object.ToString 的调用,它需要一个对象作为它的参数。要将 x 推送到 MSIL 虚拟堆栈上,因为正确的类型需要装箱。

GetType 是静态已知类型的特殊情况,编译器不会调用任何方法,它只是从符号表中获取类型信息,并将元数据引用直接填充到 MSIL 中。

【讨论】:

  • 看起来 GetType 的处理方式不像你描述的那样至少在 IL 级别......
【解决方案2】:

嗯,这里发生了一些不同的事情:

  • 对于在结构中定义的方法,CLR 只在加载程序集元数据时查看它的类型定义,以确定方法是什么,当方法Foo 调用MethodA 时,CLR 只是绑定到正确的方法MethodA 是JIT 时。编译完成后实际上没有其他任何事情发生;直接调用该方法,因为所需的任何信息都已存在。

  • 对于 virtual inherited struct 方法,如 ToString必须 进行装箱,因为虚拟调用只能按照设计在Objects 上调用——没有装箱,就没有要查看的 v-table 来找出结果方法。 (事实上​​,方法调用可能在装箱之后立即可能允许潜在的优化,但这是一个很长的机会——我怀疑 JIT 编译器会这样做。)显然没有装箱;我错了,因为我没有注意到这些方法被覆盖了。事实上,对于被覆盖的方法,编译器确实通过直接调用方法来执行优化,因为没有理由不这样做。 (对于未覆盖的值类型,没有虚方法,所以这实际上不是问题。)

  • 对于继承非虚拟结构方法,对象需要装箱,因为根据定义,该方法是在引用上调用的类型,而不是值类型;没有必要在编译器中对此进行特殊处理,因为我相信 JIT 编译器实际上可以在 JIT'ing 像GetType 这样的方法时进行优化(如避免装箱)(尽管如果我错了,请有人纠正我这个优化的东西)。

【讨论】:

  • 这是错误的,当变量类型是结构时,永远不需要 v-table 查找。如果一个结构体之前已经被装箱(调用是通过object 类型的变量或接口进行的),则可以使用 v-table。
  • @Ben:没关系,你是对的;我很抱歉,感谢您指出这一点。 :) 我还错过了这样一个事实,即这些从 Object 继承的方法稍后会覆盖,是的,不需要 v-table 查找。
【解决方案3】:

编辑:感谢 cmets。我以为我明白它是如何工作的......不再是了。所以我将把它作为调查的起点,而不是答案。

在结构上调用 ToString 或其他虚函数可能需要装箱,因为不需要 v-table 查找。结构是密封的,因此知道确切的方法和编译时间。

另一方面,基类中的 cmets 虚函数需要 Object 作为“this”参数。

在查看生成的 IL 时,不清楚 ToString 和 GetHashCode 是否真的进行了装箱(很可能它被隐藏在某处,因为这里有关于这些情况下装箱的评论 http://blogs.msdn.com/b/lucabol/archive/2007/12/24/creating-an-immutable-value-object-in-c-part-iii-using-a-struct.aspx)。 GetType 肯定需要显式装箱。

查看ILDasm的输出看是否有装箱或直接调用:

       int v = 42;

        string s = v.ToString();

        object a = v;
        s = a.ToString();

编译(调试)为以下 IL。 int.ToString() 没有装箱,但绝对是用于转换为对象的装箱...

  IL_0001:  ldc.i4.s   42
  IL_0003:  stloc.0
  IL_0004:  ldloca.s   v
  IL_0006:  call       instance string [mscorlib]System.Int32::ToString()
  IL_000b:  stloc.1
  IL_0013:  ldloc.0
  IL_0014:  box        [mscorlib]System.Int32
  IL_0019:  stloc.2
  IL_001a:  ldloc.2
  IL_001b:  callvirt   instance string [mscorlib]System.Object::ToString()
  IL_0020:  stloc.1

【讨论】:

  • 是和不是。没有 v-table 查找,因为类型是静态已知的。但是,如果该方法是从Object 继承的并且没有在结构中被覆盖,则它需要一个Object 类型的this 参数。这意味着结构将被装箱。
  • 我以为我也理解了这一点,但我错了(感谢您的回答,这真的很有帮助!)现在我想我明白了:non -virtual 方法需要装箱(因为它们需要Object),但 virtual 方法不需要,因为碰巧它们都已经在层次结构中被覆盖了,所以它们' 真的直接在值类型上调用。 (希望我在这里没有遗漏任何东西......)
猜你喜欢
  • 2015-12-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多