引用传递阻碍了gcc消除尾部调用
Pass-by-reference hinders gcc from tail call elimination
参见BlendingTable::create
和BlendingTable::print
。两者都有相同形式的尾部递归,但create
将被优化为循环,print
不会,并且会导致堆栈溢出。
向下查看修复,我从一个gcc开发人员在我的这个问题的bug报告中得到了一个提示。
#include <cstdlib>
#include <iostream>
#include <memory>
#include <array>
#include <limits>
class System {
public:
template<typename T, typename... Ts>
static void print(const T& t, const Ts&... ts) {
std::cout << t << std::flush;
print(ts...);
}
static void print() {}
template<typename... Ts>
static void printLine(const Ts&... ts) {
print(ts..., 'n');
}
};
template<typename T, int dimension = 1>
class Array {
private:
std::unique_ptr<T[]> pointer;
std::array<int, dimension> sizes;
int realSize;
public:
Array() {}
template<typename... Ns>
Array(Ns... ns):
realSize(1) {
checkArguments(ns...);
create(1, ns...);
}
private:
template<typename... Ns>
static void checkArguments(Ns...) {
static_assert(sizeof...(Ns) == dimension, "dimension mismatch");
}
template<typename... Ns>
void create(int d, int n, Ns... ns) {
realSize *= n;
sizes[d - 1] = n;
create(d + 1, ns...);
}
void create(int) {
pointer = std::unique_ptr<T[]>(new T[realSize]);
}
int computeSubSize(int d) const {
if (d == dimension) {
return 1;
}
return sizes[d] * computeSubSize(d + 1);
}
template<typename... Ns>
int getIndex(int d, int n, Ns... ns) const {
return n * computeSubSize(d) + getIndex(d + 1, ns...);
}
int getIndex(int) const {
return 0;
}
public:
template<typename... Ns>
T& operator()(Ns... ns) const {
checkArguments(ns...);
return pointer[getIndex(1, ns...)];
}
int getSize(int d = 1) const {
return sizes[d - 1];
}
};
class BlendingTable : public Array<unsigned char, 3> {
private:
enum {
SIZE = 0x100,
FF = SIZE - 1,
};
public:
BlendingTable():
Array<unsigned char, 3>(SIZE, SIZE, SIZE) {
static_assert(std::numeric_limits<unsigned char>::max() == FF, "unsupported byte format");
create(FF, FF, FF);
}
private:
void create(int dst, int src, int a) {
(*this)(dst, src, a) = (src * a + dst * (FF - a)) / FF;
if (a > 0) {
create(dst, src, a - 1);
} else if (src > 0) {
create(dst, src - 1, FF);
} else if (dst > 0) {
create(dst - 1, FF, FF);
} else {
return;
}
}
void print(int dst, int src, int a) const {
System::print(static_cast<int>((*this)(FF - dst, FF - src, FF - a)), ' ');
if (a > 0) {
print(dst, src, a - 1);
} else if (src > 0) {
print(dst, src - 1, FF);
} else if (dst > 0) {
print(dst - 1, FF, FF);
} else {
System::printLine();
return;
}
}
public:
void print() const {
print(FF, FF, FF);
}
};
int main() {
BlendingTable().print();
return EXIT_SUCCESS;
}
修改System
的类定义
class System {
public:
template<typename T, typename... Ts>
static void print(const T& t, const Ts&... ts) {
std::cout << t << std::flush;
print(ts...);
}
static void print() {}
template<typename... Ts>
static void printLine(const Ts&... ts) {
print(ts..., 'n');
}
};
class System {
public:
template<typename T, typename... Ts>
static void print(T t, Ts... ts) {
std::cout << t << std::flush;
print(ts...);
}
static void print() {}
template<typename... Ts>
static void printLine(Ts... ts) {
print(ts..., 'n');
}
};
神奇地允许GCC消除尾部调用。
为什么"是否通过引用传递函数参数"会在gcc的行为中产生如此大的差异?在这种情况下,它们在语义上看起来是一样的。
正如@jxh所指出的,强制转换static_cast<int>()
创建了一个临时对象,其引用被传递给print
函数。如果没有这样的强制转换,尾部递归将被正确优化。
这个问题与旧的情况非常相似,为什么在gcc优化的同时n't g++尾部调用在优化?解决方法类似于https://stackoverflow.com/a/31793391/4023446。
如果对System::print
的调用将被移动到单独的私有辅助函数SystemPrint
:
System
与引用传递的参数一起使用。class BlendingTable : public Array<unsigned char, 3> {
//...
private:
void SystemPrint(int dst, int src, int a) const
{
System::print(static_cast<int>((*this)(FF - dst, FF - src, FF - a)), ' ');
}
void print(int dst, int src, int a) const {
SystemPrint(dst, src, a);
if (a > 0) {
print(dst, src, a - 1);
} else if (src > 0) {
print(dst, src - 1, FF);
} else if (dst > 0) {
print(dst - 1, FF, FF);
} else {
System::printLine();
return;
}
}
// ...
}
现在尾部调用优化工作(g++ (Ubuntu/Linaro 4.7.2-2ubuntu1) 4.7.2与优化选项-O2)和print
不会导致堆栈溢出
我用其他编译器验证过了:
- 原始代码没有任何改变,完美地优化了clang++苹果LLVM版本5.1 (clang-503.0.40)(基于LLVM 3.4svn)与-O1优化
- g++ (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4即使没有强制转换或使用包装函数
SystemPrint
解决方案也无法执行TCO;这里只有System::print
参数值的解决方案有效。
相关文章:
- 什么时候调用组成单元对象的析构函数
- 对RValue对象调用的LValue ref限定成员函数
- 为什么使用 "this" 指针调用派生成员函数?
- 函数调用中参数的顺序重要吗
- OpenGL - 在抛出"__gnu_cxx::recursive_init_error"实例后终止调用?
- 基于另一个成员参数将函数调用从类传递给它的一个成员
- 为什么我的C#代码在调用回C++COM直到Task时会暂停.等待/线程.加入
- 在c++类上调用void函数
- 为什么 std::unique 不调用 std::sort?
- 调用专用模板时出错"no matching function for call to [...]"
- 选择要调用的构造函数
- 将尾部调用void(i32,..)位转换为llvm::函数以获取FnAttribute
- 如何应用尾部调用优化
- 尾部调用优化是否适用于此功能?
- 如何防止递归功能调用(尾部回复)堆积在堆栈中
- 为什么 noreturn/__builtin_unreachable 会阻止尾部调用优化
- 尾部调用优化似乎略微降低了性能
- 这可以优化尾部调用吗?如果是这样,它没有发生的特殊原因是什么?
- 局部变量和尾部调用优化
- 引用传递阻碍了gcc消除尾部调用