gcc优化递归函数吗?怎么做

Does gcc optimize recursive functions? How to do it?

本文关键字:优化 递归函数 gcc      更新时间:2023-10-16

今天我发现了一个关于gcc的有趣测试http://ridiculousfish.com/blog/posts/will-it-optimize.html

这段代码是怎么来的

int factorial(int x) {
   if (x > 1) return x * factorial(x-1);
   else return 1;
}

可以被编译器翻译成

int factorial(int x) {
   int result = 1;
   while (x > 1) result *= x--;
   return result;
}

这是真的吗?gcc是如何做到的?

您已经知道,gcc可以将尾递归函数优化为循环。gcc可以做的另一件事(在您的链接中提到)是尝试将非尾部递归函数优化为尾部递归函数。

你的阶乘函数在这里:

int factorial(int x) {
   if (x > 1) return x * factorial(x-1);
   else return 1;
}

现在我将尝试做尽可能少的更改,并将其重写为尾部递归。首先,我将翻转if测试:

int factorial(int x) {
   if (!(x > 1)) return 1;
   else return x * factorial(x-1);
}
接下来,我将删除不需要的else:
int factorial(int x) {
   if (!(x > 1)) return 1;
   return x * factorial(x-1);
}

这几乎是尾部递归的,但它返回的是x * factorial()而不仅仅是factorial()。使尾部递归的典型方法是包含第二个参数,它是一个累加器。

int factorial(int x, int accumulator = 1) {
   if (!(x > 1)) return accumulator;
   return factorial(x - 1, x * accumulator);
}

现在这是一个尾递归函数,它可以被优化成一个循环。

编译器可以通过将乘法放在递归函数调用之前将该代码转换为尾部调用可优化的代码:

int factorial(int x) {
    return factorial_tail_call(x, 1);
}
int factorial_tail_call(int x, int result) {
    if (x > 1) return factorial_tail_call(x-1, result*x);
    return result;
}

通过在递归调用factorial_tail_call之前执行result*x的求值,编译器可以确定不再需要xresult。因此,它可以从堆栈中弹出它们。这就证明了堆栈不需要增长。

你能看出转换后的代码之间有什么相似之处吗?1在同一个地方,条件x > 1在同一个地方,return result;在同一个地方。这只是表达相同算法的不同方式,提供了编译器实现尾部调用优化。通过将乘法表达式移动到一个参数中,并在帖子右侧的代码中放入注释,您可能能够看到功能的一些相似之处,以及编译器如何设法完成转换的其余部分:

int factorial(int x) {
    return factorial_tail_call(x, 1);                     // int result = 1;
}
int factorial_tail_call(int x, int result) {
    if (x > 1) return factorial_tail_call(x-1, result*x); // while (x > 1) result *= x--;
    return result;                                        // return result;
}

§5.1.2.3p4 of n1570.pdf

在抽象机器中,所有表达式都按照的语义。实际的实现不需要计算An的一部分表达式,如果它可以推断出它的值没有被使用,并且没有产生所需的副作用(包括调用函数或访问易失性对象)。

编译器是聪明的东西,由比我们大多数人优秀得多的程序员编写。如果编译器可以找出两段代码是等价的,那么它可以选择它所希望的两段代码中的任何一段(有一些限制,在下面的引语中描述)。例如,它可以用单个printf表达式替换计算并打印前1000个素数的循环。

§5.1.2.3p6 of n1570.pdf

一致性实现的最低要求是:

-对易失性对象的访问严格按照抽象机器的规则。

-在程序终止时,所有写入文件的数据都应该是与执行程序的结果相同抽象语义会产生。

-交互设备的输入和输出动态应采取按7.21.3规定放置。这些要求的目的是未缓冲或行缓冲的输出尽可能快地出现确保提示消息实际出现在程序之前等待输入

这是程序的可观察行为。

这就是为什么微优化是徒劳的原因之一。

如果另一个线程修改了strlen正在处理的字符串,这是一个竞争条件。竞态条件是未定义的行为。您需要使用互斥锁来保护字符串,以确保不会发生这种情况,或者学习更好的多线程范例。你在读哪本书?

§5.1.2.4p25 of n1570.pdf

如果程序包含两个数据竞争,则该程序的执行包含一个数据竞争不同线程中的冲突操作,其中至少有一个不是原子的,两者都不发生在对方之前。任何这样的数据竞赛导致未定义的行为。