首页 > 解决方案 > 无法在 C++ 中使用带有 -O3 选项的 clang 进行尾递归

问题描述

我无法告诉 clang 编译器在我的 C++ 代码中执行尾递归优化。我看到了这篇文章,如果有的话,C++ 编译器会进行尾递归优化吗?,建议将 -O3 标志与 clang 一起使用,它会做到这一点。但尽管如此,对于下面的素性测试代码的大量输入,我遇到了堆栈溢出。

#include <iostream>
#include <chrono>
#include <gmp.h>
#include <string>

bool mpz_is_divisible(mpz_t n, mpz_t d);

void mpz_sqr(mpz_t res, mpz_t x) {
   //computes square of x and puts the result into res

   mpz_pow_ui(res, x, 2);
}

//the below function causes stack overflow for large inputs

void smallest_divisor(mpz_t n, mpz_t div, mpz_t div_sqr, mpz_t smallest_div) {
 //finds the smallest number which divides n and puts the result into smallest_div

  mpz_sqr(div_sqr, div);
  
  if (mpz_cmp(div_sqr, n) > 0) {
     mpz_set(smallest_div, n);
     return;
  }

  if (mpz_is_divisible(n, div)) {
    mpz_set(smallest_div, div);
    return;
  }

  mpz_add_ui(div, div, 1);
  smallest_divisor(n, div, div_sqr, smallest_div); //<-- should do tail recursion optimisation?
}

bool mpz_prime_test_basic(mpz_t n) {
  //checks if n is prime

  mpz_t div, div_sqr, smallest_div;

  mpz_inits(div, div_sqr, smallest_div, NULL);
  mpz_set_ui(div, 2);

  smallest_divisor(n, div, div_sqr, smallest_div);

  if (mpz_cmp(smallest_div, n) == 0) {
    mpz_clear(div);
    mpz_clear(div_sqr);
    mpz_clear(smallest_div);
    return true;
  }
  mpz_clear(div);
  mpz_clear(div_sqr);
  mpz_clear(smallest_div);
  return false;
}

bool mpz_is_divisible(mpz_t n, mpz_t d) {
  //checks if n is divisible by d
  mpz_t rem;
  mpz_init(rem);
  mpz_tdiv_r(rem, n, d);
  int cmp = mpz_cmp_si(rem, 0); //checks if remainder is equal to 0
  if (cmp == 0) return true;
  return false;
}

int main() {
  std::string num_str;
  mpz_t num;
  mpz_init(num);
  std::cout << "Enter number to check" << '\n';
  std::cin >> num_str;
  mpz_set_str(num, num_str.c_str(), 10);

  bool is_prime = mpz_prime_test_basic(num);

  if (is_prime) {
    std::cout << num_str << " is a prime\n";
  } else {
    std::cout << num_str << " is not a prime\n";
  }

  return 0;
}

我现在正在阅读 SICP 的第 1 章,并且有一个关于使用 O(√n) 算法检查素数的练习 (1.22)。尾递归由 Scheme 标准强制执行,因此对于大输入没有问题,但我在 C++ 中做同样的问题,对于大数,最小除数函数会消耗堆栈。我正在使用 GMP 库进行大整数运算。

是否可以在这样的程序中强制执行尾递归?

谢谢

标签: c++tail-recursiongmp

解决方案


有点奇怪mpz_is_divisible。一方面,您忘记释放rem. 即使您添加mpz_clear(rem)呼叫,您也不会在smallest_divisor. 您需要做的是标记它__attribute__((noinline))以让 Clang 进行优化(尽管 GCC 在没有它的情况下会这样做)。很奇怪,也没有实际意义,因为mpz_is_divisible它只是mpz_divisible_p.

void smallest_divisor(mpz_t n, mpz_t div, mpz_t div_sqr, mpz_t smallest_div) {    
    mpz_sqr(div_sqr, div);
    if (mpz_cmp(div_sqr, n) > 0) {
        mpz_set(smallest_div, n);
        return;
    }
    if (mpz_divisible_p(n, div)) {
        mpz_set(smallest_div, div);
        return;
    }
    mpz_add_ui(div, div, 1);
    smallest_divisor(n, div, div_sqr, smallest_div); // gets TCO
}

Godbolt(从主干 GCC 窃取 GMP,可能必须使用当前日期更新某些路径才能使其工作)

我还清理了很多代码以生成新版本。特别是,我的 FP 直觉告诉我,这smallest_divisor真的不应该是它自己的功能。它是一个递归工作者,属于mpz_prime_test_basic. 我们可以使用这个答案来编写本地递归定义。我gmpxx.h在 Godbolt 上找不到,所以我也写了一个mpz_class. 然后我们可以写

bool is_prime(mpz_t n) noexcept {
    mpz div(2L);
    fix{[](auto rec, mpz_t n, mpz_t div, mpz_t div_sqr) noexcept -> void {
        mpz_mul(div_sqr, div, div);
        if(mpz_cmp(div_sqr, n) > 0) mpz_set(div, n);
        else if(!mpz_divisible_p(n, div)) {
            mpz_add_ui(div, div, 1);
            rec(n, div, div_sqr);
        }
    }}(n, div, mpz());
    return mpz_cmp(div, n) == 0;
}

由于我们实际上并没有改变调用之间的参数,只是在它们后面发生变异,我们也可以只写

bool is_prime(mpz_t n) noexcept {
    mpz div(2L), div_sqr;
    fix{[&](auto rec) noexcept -> void {
        mpz_mul(div_sqr, div, div);
        if(mpz_cmp(div_sqr, n) > 0) mpz_set(div, n);
        else if(!mpz_divisible_p(n, div)) {
            mpz_add_ui(div, div, 1);
            rec();
        }
    }}();
    return mpz_cmp(div, n) == 0;
}

Godbolt(同样的警告)


推荐阅读