跟我学C++中级篇——尾调优化

一、尾调优化

Tail Call Optimization, TCO,尾调优化。名字看起来有点高大上,其实真正明白一就发现它并不是一个多么高深的技术。要想弄明白尾调优化,先得扯开来把算法学习中的迭代(Iterative)和递归(Recursive)搞清楚。可能大家对这两个概念都很清楚。但为了对比,看一下例子:

//Iterative
for(int num = 0;num < 100;num++)
{
   int d = vec[num];
}
//Recursive
int factorial(int n) {
    if (n == 0) return 1;
    return n * factorial(n - 1); 
}

对于迭代常用的可能就是各种迭代器,说的再直白一些就是循环处理;而对于递归来说就更简单了,常见的斐波那切数列估计学习过C/C++的都知道。
尾调用优化 ,其实就是一种编译器优化技术,旨在减少函数调用时的栈空间消耗,从而提升性能并避免栈溢出错误。更简单的来说,就是指函数的最后一个操作是调用函数的操作,如果编译器发现这种情况,则会利用当前函数的栈帧,从而避免生成新的栈帧,减少栈的消耗。看下面的例子:

int demo(int x);

int test(int x) {
    return demo(x); // 尾调用
}

编译器是否处理TCO,需要几个条件:
1、函数最后调用的是一个函数,即其为最后一步操作且返回值直接传递(不能有附加的其它计算等)
2、函数本身的栈帧不再需要,即无内部相关变量等的处理和计算等
3、编译器必须支持TCO优化并打开相关优化选项(-O2等,不同编译器略有不同)

二、内部机制和原理

尾调优化其实特别适合递归,更精确的说尾递归,因为尾递归更容易触发尾调用优化。大家都知道,递归最麻烦的就在于栈空间的消耗,所以在一些需要较深递归计算场合下,一般都不建议使用递归。毕竟栈的空间在哪个系统上都不算大,弄不好就会栈溢出,导致程序出现问题。先看一下代码:

//尾调用斐波那切数列
int factorial(int n, int a = 1) {
    if (n == 0) return a;
    return factorial_tail(n - 1, c * n); // 注意尾调用
}
//编译器可优化代码类似:
int factorial(int n) {
    int a = 1;
    while (n > 0) {
        a *= n;
        n--;
    }
    return a;
}

在第一个尾调用函数中,编译器如果发现可以进行TCO,则会将代码优化为类似第二个循环处理的代码,这样,就不会有栈溢出的风险,从而能更好的处理递归调用的情况。也就是说,尾调用优化的结果就是将递归转化为循环处理。
但一般情况下,还是建议开发者如果遇到类似的情况,尽量直接以迭代的方式进行处理代码,而不是去依赖编译器的自动处理。另外,在某些情况下,即使符合TCO的条件,编译器也有可能不对其进行处理如在Debug模式下(也包括如虚函数或函数指针以及有本地数组等较特殊的情况下等)。

三、应用场景

在上面已经分析过了,TCO最适合递归特别是尾递归的应用场景下。除了递归算法,在一些特殊程序处理,如栈大小安全控制、状态控制以及协程的跳转中,都可以用到TCO。但对于大多数的开发者来说,递归调用算是经典的应用了,其它情况可能极少遇到。
其实对于C++这类语言来说可能尾调用优化并不多么显眼,但如果换成函数式编程的语言中,可能这种TCO就非常必要了。

四、实例分析

下面无法TCO的例子:

// 普通递归
int factorial(int n) {
    if (n == 0) return 1;
    return n * factorial(n - 1); // 非尾调用
}
int (*func)(int); // 函数指针

int test(int x) {
    return func(x); // 可能无法优化
}
int demo(int x) {
    if (x == 0) return 1;
    int result = demo(x - 1); // 非尾调用
    return result * x;       // 需要调用者栈保留
}

再看可TCO的例子(除前面已列举外):

//多分支尾调用 
int fibonacci(int n, int a = 0, int b = 1) {
    if (n == 0) return a;
    if (n == 1) return b;
    return fibonacci(n - 1, b, a + b); // 多分支尾调用
}
//条件判断
int demo(int x) {
    if (x < 0) return 0;
    return (x % 3 == 0) ? demo(x / 3) : demo(2 * x + 1); // 条件尾调用
}

代码的示例都非常简单,大家一看就明白。那么,如何判断TCO最终有没有实现呢?可以有两种小方法:
1、利用编译器在编译时生成的汇编代码,看调用者中使用的指令是call还是jmp,跳转说明栈帧被复用了。
2、另外一个就是打印一下相关的栈地址即可,地址不变,也说明栈被重用了。

五、总结

通过上面的分析可以发现,TCO是需要条件进行触发的。换句话说,它和inline有些类似,开发者并不能完全决定是否最终会调用TCO,而是由编译器根据情况来确定的。所以,这就给开发者一个提醒,能自己优化的就不要依赖编译器,特别是那些跨平台的开发者。
只有开发者自己确定的才是真正可以确定的。依赖其它的情况都可能导致意外的发生,一定要注意!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值