我在F#中编写了一些小字符串解析函数 - 以便更好地了解F#并了解如何使用它来解决此类任务。我尝试遍历字符串并通过递归搜索特定字符。
逻辑确实有效但是我发现生成的版本构建的IL代码(启用了优化)确实看起来很奇怪。所以我想有一种更好的方法可以在F#中以高效的方式编写这些东西。
这是解析函数的一部分:
let eatTag (input : string) index =
let len = input.Length
let nothing = 0, null, TagType.Open
// more functions used in the same way
// ...
let rec findName i =
if i >= len then nothing
else
let chr = input.[i]
if isWhitespace chr then
findName (i+1)
elif chr = '/' then
getName (i+1) (i+1) true
else getName (i+1) i false
let rec findStart i =
if i >= len then nothing
elif input.[i] = '<' then findName (i+1)
else findStart (i+1)
findStart index
这是生成的findStart函数的IL代码:
// loop start
IL_0000: nop
IL_0001: ldarg.2
IL_0002: ldarg.1
IL_0003: blt.s IL_000e
IL_0005: ldc.i4.0
IL_0006: ldnull
IL_0007: ldc.i4.0
IL_0008: newobj instance void class [mscorlib]System.Tuple`3<int32, string, valuetype TagType>::.ctor(!0, !1, !2)
IL_000d: ret
IL_000e: ldarg.0
IL_000f: ldarg.2
IL_0010: call instance char [mscorlib]System.String::get_Chars(int32)
IL_0015: ldc.i4.s 60
IL_0017: bne.un.s IL_0024
IL_0019: ldarg.0
IL_001a: ldarg.1
IL_001b: ldarg.2
IL_001c: ldc.i4.1
IL_001d: add
IL_001e: call class [mscorlib]System.Tuple`3<int32, string, valuetype TagType> findName@70(string, int32, int32)
IL_0023: ret
IL_0024: ldarg.0
IL_0025: ldarg.1
IL_0026: ldarg.2
IL_0027: ldc.i4.1
IL_0028: add
IL_0029: starg.s i
IL_002b: starg.s len
IL_002d: starg.s input
IL_002f: br.s IL_0000
// end loop
此函数的C#视图(ILSpy)显示以下代码 - 这也是我认为我在这里做错的原因。显然函数参数以某种方式分配给它自己......?!
internal static Tuple<int, string, TagType> findStart@80(string input, int len, int i)
{
while (i < len)
{
if (input[i] == '<')
{
return findName@70(input, len, i + 1);
}
string arg_2D_0 = input;
int arg_2B_0 = len;
i++;
len = arg_2B_0;
input = arg_2D_0;
}
return new Tuple<int, string, TagType>(0, null, TagType.Open);
}
在以延续式处理的其他函数中可以看到同样的问题。任何指向我正在做或假设错误的指示都非常感激: - )
答案 0 :(得分:7)
这是尾部呼叫消除。
删除尾调用并将尾调用转到'跳转'到函数开头的过程。 (低while(true) { }
构造)。
您看到“相同”赋值的原因是保持语义与正常调用函数时的语义相同。几乎不可能确定1个赋值是否可以有效地影响另一个,因此使用临时变量,然后将赋值转回给它们。
答案 1 :(得分:5)
如前所述,在这种情况下,编译器将递归函数转换为非递归函数。这仅在递归调用出现在“尾调用”位置并且函数调用自身时才可能。通常,编译器具有以下选项:
将函数编译为循环 - 当函数调用自身并且调用处于尾调用位置时。这是最有效的替代方案,因为它消除了创建新的堆栈帧并使用标准循环。
使用.tail call
IL指令编译功能 - 当呼叫出现在尾部呼叫位置时,但您正在呼叫其他功能(例如,如果您有两个使用let rec foo () = ... and bar () = ...
语法声明的相互递归函数。这样可以避免创建一个新的堆栈帧(您不会获得堆栈溢出),但效率较低,因为.NET中的.tail call
指令没有那么多优化。
使用正常递归进行编译 - 当函数递归调用自身并且然后进行更多计算时,代码使用标准递归调用而不是< em> tail-call (并且需要为每个调用分配一个新的堆栈帧,因此可能会出现堆栈溢出)
在第一种情况下(在您的示例中)完成的优化如下所示:通常,尾递归函数看起来像这样:
let rec foo x =
if condition then
let x' = calculateNewArgument x // Run some computation
foo x' // (Tail-)recursively calls itself
else
calculateResult x // Final calculation in the branch that returns
代码被转换为以下循环,该参数将参数存储在可变变量中:
let foo x =
let mutable x = x
while condition do // Check condition using current argument value
x <- calculateNewArgument x // Instead of recursion, run next iteration
calculateResult x // Final calculation in the branch that returns
答案 2 :(得分:2)
基本上,不是创建像
这样的链findstart(findstart(findstart(findstart.....
编译器转换为一个消除函数调用的循环。
这是Tail call elimination,一个非常标准的函数式编程优化。这是因为重新设置函数的argignments的开销低于调用生成新堆栈帧的新函数。