【问题标题】:How smart is the g++ template instantiator (avoiding template bloat)g++ 模板实例化器有多聪明(避免模板膨胀)
【发布时间】:2012-02-19 05:06:31
【问题描述】:

如果我有一个包含一堆其他代码的模板。 g++ 会为模板的每个版本重新生成所有相同的代码吗?

例如:

template <typename> T
T parseSomething(const std::string& data) {
    // Some state variables go here
    enum State {state1,state2,state3} state;
    for(std::string::const_iterator i=data.begin();i!=data.end();++i) {
        // Some big testy stuff to see if we got in the right place
        switch (state) {
            case state1: {
                switch (*i) {
                    case f:  // ...
        // ... lots of switchy stuff here ..
        return T(*i);
    }
}

所以在那个 func 中.. 唯一真正需要模板的是 return T(*i) 行。

假设我用 4 个不同的 T 实例化它,例如。

parseSomething<float>(data);
parseSomething<int>(data);

等等

g++ 是否会为每个 T 单独生成所有其他代码(循环和开关部分)?

或者只生成一次开关和循环是否足够聪明.. 然后为每个 T.. 生成返回 T(*i);行吗?

我尝试过测试,使用 -O0 肯定会在任何地方复制开关,但使用 -O2 及以上则很难判断;它看起来更聪明..但它太聪明了,我无法破译 ASM :)


这是我试图用来测试的示例程序。

编译:

g++ -std=c++0x -fverbose-asm -ggdb3 -fvar-tracking-assignments  -O6 -march=native  codegen.cpp

运行:

gdb --args ./a.out asdf1111

偏执代码版本:

#include <iostream>
#include <string>

using namespace std;

char getSomething(const string& myString) {
    for(auto myPlase=myString.begin();myPlase!=myString.end();++myPlase) {
        if (*myPlase == 'f') {
            return *(myPlase+1);
        }
    }
}

template <typename T>
T getSomething(const string& myString) {
    return T(getSomething(myString));
}

int main(int argc, char** argv) {
    string base = argv[1];
    float myFloat = getSomething<float>(base);
    int myInt = getSomething<int>(base);
    char myChar = getSomething<char>(base);
    //string newString = getSomething<string>(base);
    cout << myFloat << " " << myInt << " " << myChar << endl;
}

我想使用的代码版本:

#include <iostream>
#include <string>

using namespace std;

template <typename T>
T getSomething(const string& myString) {
    for(auto myPlace=myString.begin();myPlace!=myString.end();++myPlace) {
        if (*myPlace == 'f') {
            return T(*(myPlace+1));
        }
    }
}

int main(int argc, char** argv) {
    string base = argv[1];
    float myFloat = getSomething<float>(base);
    int myInt = getSomething<int>(base);
    char myChar = getSomething<char>(base);
    //string newString = getSomething<string>(base);
    cout << myFloat << " " << myInt << " " << myChar << endl;
}

【问题讨论】:

  • 在现实世界的程序中,我使用 ragel 生成代码,大约有 1200 行。我想把它变成一个模板函数,主要的模板位是返回类型,但我不希望看到为每种类型重新生成这 1200 行。
  • 如果你是 GCC,你会如何生成有问题的代码?
  • 当然真正的问题是:在偏执的代码示例中,对getsomething的调用是否内联:)?
  • 为什么不直接从解析中提取转换?这将保证一个函数,你甚至可以现在转发声明它并且只在一个翻译单元中实现它。为什么不能只返回 *i 返回的任何内容并在调用站点进行转换?
  • 是的,我想这就是我要走的路。解析可以从 *i 中生成类型,或者通过解析一系列字符来构建结果。我要看看我是否可以将解析分成不同的函数并采用“偏执狂”的方式。

标签: c++ templates c++11 metaprogramming


【解决方案1】:

我认为编译器不够聪明,无法合并独立于模板参数的代码。也就是说,函数会被实例化4次,每个T一次。

在 linux 上,您可以在生成的目标文件上使用 readelf -s 转储公共符号并使用 readelf -S 转储部分;每个非内联非静态函数在符号表中都有一个(损坏的)条目。 AFAIK,模板实例位于其自己的部分中,因此它们可以在链接时合并。

【讨论】:

  • 谢谢@zvrba .. 是的,问题是只要我打开任何东西 >= -O1 它就会内联所有内容,而且 ASM 看起来真的很复杂,所以我真的不知道如果它正在取出循环并切换东西。我倾向于它可能不会把它拿出来;并使用我认为的偏执代码。
【解决方案2】:

在模板中包含大量非参数化代码是相当少见的,无论是类还是函数。

检测一大段不依赖参数的代码听起来并不难。引擎只需要在相关的 AST 节点中记录一些子树大小的指标,以及是否有任何子节点是模板参数。

但是您建议的优化本质上需要将内部范围与外部范围分开,这意味着将它们重构为一个新函数。如果你有一个命名变量而不是一个临时变量,它的生命周期包括内部switch 怎么办?堆栈将被重新排列,内部范围将取决于变量,尽管可能没有引用它,并且局部变量必须作为引用参数传递给switch。这将是一个脆弱而复杂的优化。

如果模板膨胀是一个问题,我强烈推荐“偏执”版本,它将模板关注点分离到一个包装函数中。这样的包装器永远不应该非常复杂,因为关键是要避免臃肿!

如果元膨胀是另一个问题(我刚刚读到您正在使用代码生成器并且担心成千上万个此类模板包装器的源代码大小),您可能会考虑稍微改变界面:

template< typename T, char (*func)( std::string const & ) >
T get_anything( std::string const &s ) {
    return T( func( s ) );
}

这样,可以有很多get_something()函数,它们都可以作为get_anything的第二个模板参数。您还可以使用指向成员的指针而不是函数指针作为模板参数。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-06-02
    • 1970-01-01
    • 2014-10-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-12
    相关资源
    最近更新 更多