尾巴。 ILAsm中的前缀 - 任何使用示例?
ECMA-335,III.2.4规定了tail.
可以在递归函数中使用的前缀。 但是,我无法在C#和F#代码中找到它的用法。 有没有使用的例子?
您不会在当前MS C#编译器生成的任何代码中找到它。 您可以从F#编译器生成的代码中找到它,但由于几乎相反的原因,没有您期望的那么多。
现在,首先要纠正你的陈述中的一个错误:
ECMA-335,III.2.4规定了尾巴。 可以在递归函数中使用的前缀。
这不完全正确。 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
的调用DoSomeCalls
,但它们是递归的。
现在,正常的函数调用机制是依赖于实现的,但通常涉及在调用堆栈之后保存可能需要的enrigstered值,将参数与当前指令位置一起放入堆栈和/或寄存器中(返回) ,移动指令指针,然后在指令指针移回到完成调用的位置时,从寄存器或堆栈中读取返回值。 使用尾调用可以跳过很多这种情况,因为被调用函数可以使用当前的堆栈帧,然后直接返回到先前的调用者。
tail.
前缀请求,这是通过调用完成的。
虽然这不一定与递归有关,但在说到递归时您是正确的,因为在递归情况下消除尾部调用的好处比其他情况更大; 在实际使用函数调用机制时,调用堆栈空间中的O(n)在堆栈空间中变为O(1),同时减少每个项目的常量时间成本(因此它仍然是O(n)但是O(n)时间意味着需要n×k秒,并且我们有一个更小的k)。 在许多情况下,这可能是工作调用和抛出StackOverflowException
的调用之间的区别。
现在,在ECMA-335中有几个关于如何tail.
案例tail.
可能并不总是被尊重。 特别是第§3.2.4条的内容规定:
还可以有阻止尾部的特定于实现的限制。 在某些情况下服从的前缀。
最宽松的是,我们可以把它解释为阻止所有情况。
相反,抖动允许应用所有优化方式,包括执行尾部呼叫消除,即使它没有被tail.
请求tail.
因此,实际上有四种方法可以在IL中完成尾部消除:
tail.
在通话之前添加前缀,并将其授予(不保证)。 tail.
在通话之前添加前缀,但让抖动决定以任何方式应用(甚至更少保证)。 jmp
IL指令,它实际上是尾部调用消除的特例(从未被C#使用,因为它为通常相对较小的增益产生了无法验证的代码,尽管由于相对简单,它可能是手动编码时最简单的方法) 。 (有些情况下调用是内联的,因为它不需要一个新的堆栈框架,而且通常整体上通常有更强的改进,并且反过来又经常允许进行更进一步的优化,但是它不是' t通常被认为是尾声消除的情况,因为它是不依赖于尾声呼叫的呼叫消除)。
现在,在很多情况下,即使请求,抖动的第一个实现也不会执行尾呼叫消除。
同时在C#方面,有一个决定不发出tail.
C#没有大量优化代码生成的一般方法。 有一些优化(特别是去除死代码),但大多数情况下,由于优化工作可能会重复那些由抖动完成的工作(甚至可能妨碍他们),所以优化的缺点(更多并发症意味着更多可能的错误,而且IL对许多开发者来说会更加困惑)相对超过了优势。 使用tail.
就是这样一个典型的例子,因为有时坚持使用tail调用的代价实际上比使用.NET节省更多的代价,所以如果当它是一个好主意的时候抖动已经试图解决,那么C#编译器就会变得更大大部分时间都让事情变得更糟,其余的事情都没有改变。
同样值得注意的是,使用像C#这样的C风格语言最常见的编码风格:
现在,F#一起来了。
通过F#鼓励的功能和声明性编程,有很多情况下C#中迭代方式最自然地完成的事情最自然地用递归方法完成。 在那些黑客使用C风格语言的人学习将递归案例转化为迭代代码的过程中,黑客使用F#风格的语言学习将迭代案例转化为递归代码,将非尾调用递归代码转化为尾调用递归代码。
所以F#用tail.
很多。
而且它得到了很多StackOverflowException
,因为抖动并没有遵守它。
这是导致抖动人群增加消除尾部呼叫的情况的原因之一,无论是在一般情况下还是在tail.
用来。
同时,F#人不能只依靠tail.
所以F#的编译器比C#优化得多。 就像我们可以手动地将递归调用重写为脚注中的迭代一样,所以F#编译器在生成IL时会执行相同的操作。
由于这个原因,很多时候当你编写一个F#方法时,你希望看到一些使用tail.
IL tail.
,你实际上得到的是IL反复做同样的事情。
但是,F#仍然会使用tail.
当一个方法以相互递归的方式调用另一个方法时,如:
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)
我完全从这个答案中偷了,因为我只用F#玩过一点点,所以我宁愿依靠比我更熟悉的人。
在这种情况下,由于尾部调用不在单个函数中,因此不能在IL编译点重新编写以消除尾部调用,因此它必须希望抖动能够消除并使用tail.
增加它的机会。
*将递归调用变为迭代的一个例子是以递归调用开始,如:
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;
}
}
既然有充分的理由可以避免goto
,我们通常会将其更改为通过更严格定义的循环机制来做同样的事情:
void ClearAllNodes(Node node)
{
while(node != null)
{
node.Value = null;
node = node.Next;
}
}
链接地址: http://www.djcxy.com/p/80621.html