【问题标题】:Pass-by-reference hinders gcc from tail call elimination引用传递阻碍 gcc 消除尾调用
【发布时间】:2015-09-25 13:59:48
【问题描述】:

BlendingTable::createBlendingTable::print。两者都有相同形式的尾递归,但是create 将被优化为循环,print 不会并导致堆栈溢出。

下去查看修复,这是我从一个 gcc 开发人员那里得到的关于这个问题的错误报告的提示。

#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 的行为产生如此大的影响?在这种情况下,它们在语义上看起来都一样。

【问题讨论】:

  • 我真的很好奇 perfect forwarding 是否会出现同样的问题。
  • 一般来说,通过引用传递会使编译器的程序分析更加困难,因此更难确定哪些转换是合法的。无论这是这里的实际问题还是有其他原因(比如标准中的一个怪癖,这使得这种优化是非法的)我不能说
  • 一个假设是强制转换创建了一个临时文件,其引用被传递给print 函数,现在编译器可能觉得它需要挂在临时文件上直到print 函数返回。

标签: c++ gcc recursion functional-programming tail-call-optimization


【解决方案1】:

正如@jxh 所指出的,演员static_cast&lt;int&gt;() 创建了一个临时对象,其引用被传递给print 函数。如果没有这种强制转换,尾递归将得到正确优化。

问题与旧案例Why isn't g++ tail call optimizing while gcc is? 非常相似,解决方法可能类似于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 不会导致堆栈溢出。

更新

我用其他编译器验证过:

  • 原代码未做任何改动,通过-O1优化的clang++ Apple LLVM version 5.1 (clang-503.0.40)(基于LLVM 3.4svn)完美优化
  • g++ (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4 无法执行 TCO,即使没有强制转换或使用包装函数 SystemPrint 解决方法;这里只有 System::print 参数值的解决方法有效。

所以,这个问题是特定于编译器版本的。

【讨论】:

  • 我还是觉得这有点奇怪。 print 递归调用 print 两次。首先,使用一组参数,其中包括static_cast 演员表,以及来自三个候选者的第二个调用(通过if...else if... 选择)。只有第二个与高调用优化相关。所以我根本看不出第一个会如何影响这一点,即使它确实有一个临时的?
  • @Aaron McDaid 递归与BlendingTable::print 有关,但是在递归退出之前调用了System::print。在该调用期间,会在 BlendingTable::print 的堆栈上创建一个临时文件。该临时堆栈的地址作为参数传递给System::print。因此,对于当前的 TCO 实现来说,堆栈帧可以被重用于尾递归优化并不明显。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-13
  • 1970-01-01
  • 2010-10-29
  • 1970-01-01
相关资源
最近更新 更多