引用传递阻碍了gcc消除尾部调用



参见BlendingTable::createBlendingTable::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参数值的解决方案有效。

最新更新