【问题标题】:Why is it clear that a template function instantiation will not be inlined?为什么模板函数实例化不会被内联?
【发布时间】:2012-11-20 10:16:16
【问题描述】:

关于Function passed as template argument,Ben Supnik 提供的社区 wiki 回答讨论了内联实例化函数模板的问题。

在那个答案中是以下代码:

template<typename OP>
int do_op(int a, int b, OP op)
{
  return op(a,b,);
}

int add(int a, b) { return a + b; }

int (* func_ptr)(int, int) = add;

int c = do_op(4,5,func_ptr);

答案继续这样说(关于最后一行,它实例化了函数模板do_op):

显然这不是内联。

我的问题是:为什么它没有被内联?

【问题讨论】:

    标签: c++ templates


    【解决方案1】:

    我认为,到目前为止的讨论中遗漏了要点。首先,由于语法错误,测试代码甚至无法编译。大概意思如下:

    template<typename OP>
    int do_op(int a, int b, OP op) { return op(a,b); }
    int add(int a, int b) { return a + b; }
    int (*func_ptr)(int, int) = add;
    int c = do_op(4, 5, func_ptr);
    // int c = (*func_ptr)(4, 5);
    

    编译时,编译器会发出代码来实际调用 add() 函数。但是,当没有模板编写为 int c = (*func_ptr)(4, 5); 时,编译器也会发出对 add() 的调用。这是因为func_ptr 在此示例代码中是全局定义的,并且编译器必须注意另一个线程中的某些代码在其初始化和后续使用之间修改 func_ptr 的可能性。但这是全局可见函数指针的属性,与模板无关!除了一些本地标签的名称外,带有优化器的 GCC 为通过 func_ptr 对 add() 的模板化和非模板化调用产生完全相同的汇编器输出。不同的标签名称意味着,由于模板,优化器必须多转一轮,因此编译时间增加(就像所有模板一样),但代码和代码运行时间是相同的。

    如果将 func_ptr 移动到函数内部的局部变量中,如下例所示,编译器可以肯定地跟踪对 func_ptr 的所有访问,从而优化所有内容,甚至不再调用 add() 函数,既不直接也不通过函数指针:

    int testf(void) {
      int (*func_ptr)(int, int) = add;
      return do_op(4, 5, func_ptr);
    }
    

    所以,总结一下:通过模板调用函数不会停止优化器的工作。如果函数指针的值不能在编译时安全地确定,那么函数指针可能会造成伤害,但如果添加了模板,该问题不会恶化。

    【讨论】:

      【解决方案2】:

      当通过函数指针调用函数时,编译器极不可能避免通过函数指针调用。只有当编译器能够证明它知道函数指针被初始化的是什么并且它不能被改变时,它才有可能避免通过函数指针调用函数,从而内联函数。在引用的设置中,即,

      int (* func_ptr)(int, int) = add;
      

      函数指针func_ptr 是可修改的,因此编译器不能保证它永远不会改变。因此,它不可能内联对add 的调用。

      如果代码的 sn-p 确实完整,则在初始化期间会发生一些事情,编译器实际上可以知道 func_ptr 已初始化为包含 add

      【讨论】:

        【解决方案3】:

        为什么它没有被内联?

        不是。编译器没有理由不能内联该 sn-p 中的所有代码。

        【讨论】:

        • 没有理由不能这样做,但有很多理由甚至可能不会尝试这样做。如果我没有误会,那么直到最近,C(和 C++)编译器甚至都没有尝试跟踪函数指针以进行内联。
        • @KonradRudolph - 当然,但声称很明显这没有被内联。一点都不清楚。
        【解决方案4】:

        他的意思是(我认为)add 函数没有被内联。换句话说,编译器可能会像这样内联do_op

        int c = func_ptr(4, 5);
        

        但它也不会像这样内联add

        int c = 4 + 5;
        

        但是,在这个简单的例子中,他可能错了。

        通常,当您通过指针调用函数时,编译器无法(在编译时)知道您将调用什么函数,因此它无法内联该函数。示例:

        void f1() { ... }
        void f2() { ... }
        
        void callThroughPointer() {
            int i = arc4random_uniform(2);
            void (*f)() = i ? f2 : f1;
            f();
        }
        

        这里,编译器无法知道callThroughPointer 是否会调用f1f2,因此无法将f1f2 内联到callThroughPointer

        但是,如果编译器可以在编译时证明将调用哪个函数,则允许内联该函数。示例:

        void f1() { ... }
        void f2() { ... }
        
        void callThroughPointer2() {
            int i = arc4random_uniform(2);
            void (*f)() = i ? f2 : f1;
            f = f1;
            f();
        }
        

        在这里,编译器可以证明f 将永远是f1,因此允许将f1 内联到callThroughPointer2。 (这并不意味着它内联f1...)

        同样,在您在帖子中引用的示例中,编译器可以证明在对do_op 的调用中func_ptr 始终为add,因此允许内联add。 (这并不意味着它内联add...)

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-07-02
          • 1970-01-01
          • 1970-01-01
          • 2021-02-17
          相关资源
          最近更新 更多