C# 尾巴。ILAsm中的前缀–;有使用的例子吗?

C# 尾巴。ILAsm中的前缀–;有使用的例子吗?,c#,.net,f#,clr,ilasm,C#,.net,F#,Clr,Ilasm,ECMA-335,III.2.4规定了可在递归函数中使用的tail.前缀。然而,我在C#和F#代码中都找不到它的用法。是否有在中使用的示例?您不会在当前MS C#编译器生成的任何代码中找到它。您可以在F#编译器生成的代码中找到它,但由于几乎相反的原因,它没有您预期的那么多 现在,首先纠正你陈述中的一个错误: ECMA-335,III.2.4规定了尾部。可以在递归函数中使用的前缀 严格来说,这不是事实。tail.前缀可用于tail调用;不是所有递归函数都是尾部递归,也不是所有尾部调用都是递归的一部

ECMA-335,III.2.4规定了可在递归函数中使用的
tail.
前缀。然而,我在C#和F#代码中都找不到它的用法。是否有在中使用的示例?

您不会在当前MS C#编译器生成的任何代码中找到它。您可以在F#编译器生成的代码中找到它,但由于几乎相反的原因,它没有您预期的那么多

现在,首先纠正你陈述中的一个错误:

ECMA-335,III.2.4规定了尾部。可以在递归函数中使用的前缀

严格来说,这不是事实。
tail.
前缀可用于tail调用;不是所有递归函数都是尾部递归,也不是所有尾部调用都是递归的一部分

尾部调用是对函数(包括OOP方法)的任何调用,其中该代码路径中的最后一个操作是进行该调用,然后返回它返回的值,或者如果调用的函数未返回值,则仅返回该值。因此:

int DoSomeCalls(int x)
{
  if(A(x))
    return B(x);
  if(DoSomeCalls(x * 2) > 3)
  {
    int ret = C(x);
    return ret;
  }
  return D(DoSomeCalls(x-1));
}
在这里,对
B
D
的调用是尾部调用,因为调用之后唯一要做的事情就是返回它们返回的值。对
C
的调用不是尾部调用,但是可以通过直接返回来删除对
ret
的冗余分配,从而轻松地将其转换为尾部调用。对
A
的调用不是尾部调用,对
DoSomeCalls
的调用也不是尾部调用,尽管它们是递归的

现在,正常的函数调用机制依赖于实现,但通常涉及将调用后可能需要的充实值保存到堆栈中,将参数与当前指令位置(返回)一起放入堆栈和/或寄存器中,移动指令指针,然后,当指令指针移回调用完成后的位置时,从寄存器或堆栈读取返回值。通过尾部调用,可以跳过很多步骤,因为被调用的into函数可以使用当前堆栈帧,然后直接返回到前面的调用方

tail.
前缀请求通过调用完成此操作

虽然这不一定与递归有关,但您谈论递归是正确的,因为在递归情况下消除尾部调用的好处比其他情况更大;当实际使用函数调用机制时,在堆栈空间中进行O(n)的调用会变成堆栈空间中的O(1),同时降低每项恒定时间成本(因此在这方面仍然是O(n),但O(n)时间意味着需要n×k秒,我们有更小的k)。在许多情况下,这可能是有效的调用与抛出
StackOverflowException
的调用之间的区别

现在,在ECMA-335中,有一些案例说明了
如何跟踪。
可能并不总是被尊重。尤其是§III.2.4中的文本规定:

还可能存在特定于实现的限制,以防止出现尾部。前缀在某些情况下不被遵守

从最宽松的角度来看,我们可以将其解释为在所有情况下都可以防止它

相反,允许抖动应用所有方式的优化,包括执行尾部调用消除,即使
tail没有请求它。

因此,实际上有四种方法可以消除IL中的尾部调用:

  • 在通话前使用
    tail.
    前缀,并将其兑现(不保证)
  • 在调用之前不要使用
    尾。
    前缀,但要让jitter决定以任何方式应用它(甚至更不保证)
  • 使用
    jmp
    IL指令,这实际上是尾部调用消除的一种特殊情况(C#从未使用过,因为它会产生无法验证的代码以获得通常相对较小的增益,尽管由于相对简单,有时手工编码可能是最简单的方法)
  • 重新编写整个方法,使用不同的方法;特别是,可以重新编写从尾部调用消除中获益最多的递归代码,以显式地使用迭代算法,尾部调用消除有效地将递归转化为。*(换句话说,尾部调用消除发生在jitting甚至编译之前)
  • (还有一种情况是调用是内联的,因为它不需要新的堆栈帧,而且总体上确实有更强的改进,然后通常允许执行进一步的优化,但通常不认为是尾部调用消除,因为它是一种不依赖于这是一个尾声)

    现在,抖动的第一个实现在很多情况下都不会消除尾部调用,即使它是被请求的

    与此同时,在C方面,决定不发出
    尾部。
    C有一个通用的方法,即不严重优化生成的代码。已经进行了一些优化(特别是消除死代码),但大部分情况下,因为优化工作可能只是重复抖动所做的工作(甚至会妨碍他们)优化的缺点(更多的复杂性意味着更多可能的bug,而IL会让许多开发人员更加困惑)使用
    tail.
    是一个典型的例子,因为有时坚持使用tail调用的成本实际上比使用.NET节省的成本要高,所以如果抖动已经在尝试解决什么时候是个好主意,那么C#编译器很有可能会让事情变得更糟,其他的都一样

    还值得注意的是,对于C风格语言(如C#)最常见的编码风格:

  • 开发人员往往不会编写特别受益于尾部调用消除的代码
    let rec even n = 
      if n = 0 then 
        true 
      else
        odd (n-1)
    and odd n =
      if n = 1 then 
        true 
      else
        even (n-1)
    
    void ClearAllNodes(Node node)
    {
      if(node != null)
      {
        node.Value = null;
        ClearAllNodes(node.Next)
      }
    }
    
    void ClearAllNodes(Node node)
    {
    start:
      if(node != null)
      {
        node.Value = null;
        node = node.Next;
        goto start;
      }
    }
    
    void ClearAllNodes(Node node)
    {
      while(node != null)
      {
        node.Value = null;
        node = node.Next;
      }
    }