无法使用带有 -O3 选项的 clang 在 C++ 中执行尾递归
Unable to do tail recursion in C++ using clang with -O3 option
我无法告诉 clang 编译器在我的 C++ 代码中执行尾递归优化。我看到了这个 post Which, if any, C++ compilers do tail-recursion optimization?,其中的建议是将 -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++编译器都会进行尾调用优化。您的代码应该会从中受益。
查看了从您在 Godbolt with Clang 11 上的代码获得的程序集,看来 -O3 标志确实导致了尾代码优化。您的递归函数编译为:
smallest_divisor(test, test, test, test): # @smallest_divisor(test, test, test, test)
push rax
.LBB1_1: # =>This Inner Loop Header: Depth=1 <------ TCO Loop
mov edi, 2
call mpz_pow_ui(test, test, unsigned int)
call mpz_cmp(test, test)
test eax, eax
jg .LBB1_4 <------ go to return sequence
call mpz_init(test)
call mpz_tdiv_r(test, test, test)
xor edi, edi
call mpz_cmp_si(test, int)
test eax, eax
je .LBB1_4
mov edi, 1
call mpz_add_ui(test, test, unsigned int)
jmp .LBB1_1 <--- no recursive call but loop
.LBB1_4: <--- this is the end of recursion
pop rax
jmp mpz_set(test, test) # TAILCALL <-- no ret, but another tail call :-)
我无法测试您的代码,因为我没有库,也没有时间花时间获取和安装它。
根据您描述的症状,我可以看出以下可能的根本原因:
- 库中有一些UB,导致段错误
- 由于编译器设置,优化被禁用。例如,如果您使用 XCode,则需要转到菜单“产品”>“方案”>“编辑方案...”,然后选择“发布”作为构建配置。
尝试了 XCode 中的程序集(产品 > 执行操作 > Assemble),生成的程序集代码显示了 TCO 的证据:
__Z16smallest_divisor4testS_S_S_: ## @_Z16smallest_divisor4testS_S_S_
Lfunc_begin1:
.loc 67 29 0 ## Test0105-1/main.cpp:29:0
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
LBB1_1: ## =>This Inner Loop Header: Depth=1
Ltmp3:
.loc 67 24 4 prologue_end ## Test0105-1/main.cpp:24:4
movl , %edi
callq __Z10mpz_pow_ui4testS_j
....
movl , %edi
callq __Z10mpz_add_ui4testS_j
Ltmp13:
.loc 67 0 3 is_stmt 0 ## Test0105-1/main.cpp:0:3
jmp LBB1_1 <<<<<<<<<<<< Tail call optimization (loop)
LBB1_4:
popq %rbp
Ltmp14:
jmp __Z7mpz_set4testS_ ## TAILCALL
mpz_is_divisible
有点奇怪。其一,您忘记释放 rem
占用的内存。即使您添加 mpz_clear(rem)
调用,您也不会在 smallest_divisor
中获得 TCO。您需要做的是将其标记为 __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(从 trunk GCC 窃取 GMP,可能必须用当前日期更新一些路径才能使其工作)
我还大量清理了代码以生成新版本。特别是,我的 FP 直觉告诉我 smallest_divisor
实际上不应该是它自己的函数。它是属于 mpz_prime_test_basic
内部的递归工作者。我们可以使用 this answer 来编写局部递归定义。我在 Godbolt 上找不到 gmpxx.h
,所以我也写了一个 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(同样的警告)
我无法告诉 clang 编译器在我的 C++ 代码中执行尾递归优化。我看到了这个 post Which, if any, C++ compilers do tail-recursion optimization?,其中的建议是将 -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++编译器都会进行尾调用优化。您的代码应该会从中受益。
查看了从您在 Godbolt with Clang 11 上的代码获得的程序集,看来 -O3 标志确实导致了尾代码优化。您的递归函数编译为:
smallest_divisor(test, test, test, test): # @smallest_divisor(test, test, test, test)
push rax
.LBB1_1: # =>This Inner Loop Header: Depth=1 <------ TCO Loop
mov edi, 2
call mpz_pow_ui(test, test, unsigned int)
call mpz_cmp(test, test)
test eax, eax
jg .LBB1_4 <------ go to return sequence
call mpz_init(test)
call mpz_tdiv_r(test, test, test)
xor edi, edi
call mpz_cmp_si(test, int)
test eax, eax
je .LBB1_4
mov edi, 1
call mpz_add_ui(test, test, unsigned int)
jmp .LBB1_1 <--- no recursive call but loop
.LBB1_4: <--- this is the end of recursion
pop rax
jmp mpz_set(test, test) # TAILCALL <-- no ret, but another tail call :-)
我无法测试您的代码,因为我没有库,也没有时间花时间获取和安装它。
根据您描述的症状,我可以看出以下可能的根本原因:
- 库中有一些UB,导致段错误
- 由于编译器设置,优化被禁用。例如,如果您使用 XCode,则需要转到菜单“产品”>“方案”>“编辑方案...”,然后选择“发布”作为构建配置。
尝试了 XCode 中的程序集(产品 > 执行操作 > Assemble),生成的程序集代码显示了 TCO 的证据:
__Z16smallest_divisor4testS_S_S_: ## @_Z16smallest_divisor4testS_S_S_
Lfunc_begin1:
.loc 67 29 0 ## Test0105-1/main.cpp:29:0
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
LBB1_1: ## =>This Inner Loop Header: Depth=1
Ltmp3:
.loc 67 24 4 prologue_end ## Test0105-1/main.cpp:24:4
movl , %edi
callq __Z10mpz_pow_ui4testS_j
....
movl , %edi
callq __Z10mpz_add_ui4testS_j
Ltmp13:
.loc 67 0 3 is_stmt 0 ## Test0105-1/main.cpp:0:3
jmp LBB1_1 <<<<<<<<<<<< Tail call optimization (loop)
LBB1_4:
popq %rbp
Ltmp14:
jmp __Z7mpz_set4testS_ ## TAILCALL
mpz_is_divisible
有点奇怪。其一,您忘记释放 rem
占用的内存。即使您添加 mpz_clear(rem)
调用,您也不会在 smallest_divisor
中获得 TCO。您需要做的是将其标记为 __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(从 trunk GCC 窃取 GMP,可能必须用当前日期更新一些路径才能使其工作)
我还大量清理了代码以生成新版本。特别是,我的 FP 直觉告诉我 smallest_divisor
实际上不应该是它自己的函数。它是属于 mpz_prime_test_basic
内部的递归工作者。我们可以使用 this answer 来编写局部递归定义。我在 Godbolt 上找不到 gmpxx.h
,所以我也写了一个 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(同样的警告)