• C++经验(十一)-- (inlining)内联函数的使用


    不管是我们以前使用面向过程还是面向对象编程,编写代码过程中使用函数,能够降低代码的重复率,特别是在一些重复性比较高的代码中,也能够用降低运行程序的体积。但不可避免的也会带来一些程序运行上的时间消耗。

    这个主要的时间消耗在这儿。函数执行的时候,首先要在栈上为形参和局部变量申请内存,同时也要将实参的值复制给形参,最后还需要将函数的返回地址写入栈,以便函数结束运行后,程序能够知道应该回到哪儿继续执行。最后才跳转到函数内部进行执行。

    函数中 return 语句执行之后,前面我们在栈上为形参和局部变量申请的内存需要回收,然后从栈中取出写入的函数返回地址,记性执行。

    上面的这整个过程是比较耗时的。

    如果函数的逻辑比较复杂,上面的这个时间消耗可以忽略不计。但如果函数只有简单的几行代码,甚至不涉及到比较复杂逻辑运算的时候,上面的整个时间消耗在变量的对比下就会显得比较高了。

    如果你的代码中有很多个地方都调了这个函数呢?

    那函数调用的时间消耗可不是一笔小的开销。在C++中,有一种函数被叫做内联函数。函数如下,在C++中,一般是在函数声明的时候在函数前面加关键字 inline

    inline int max(int a, int b)
    {
        return a < b ? b : a;
    }
    
    • 1
    • 2
    • 3
    • 4

    如果是在类中,函数的如果在类定义的时候进行定义,那么,加不加关键字 inline 并没有什么异样,因为,这样的函数都会被默认为是内联函数。

    class A
    {
    public:
        int max(int a, int b)
        {
            return a < b ? b : a;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如果函数的定义实在类的定义之外,那么就需要在函数声明的时候或者定义的时候在其前面加上关键字 inline 。

    class A
    {
    public:
        inline int max(int a, int b);
    }
    
    int A::max(int a, int b)
    {
        return a < b ? b : a;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    他们是函数,动作也像函数,相对于宏来说有不可超越的好处。可以调用他们又不需要管函数调用所带来的额外开销。内联函数不仅仅是避免了在函数调用上的开销,同时编译器的最优化机制也会对不含有函数的代码进行优化。

    但是需要注意的一点,编译器对内联函数的调用都是在对应的函数调用的地方进行函数体的替换,这也就是说,如果我们代码中大量的使用了或者定义了内联函数,可能并不会减少代码量,相反地可能还会增加代码量。代码的膨胀也会带来额外的换页行为,这会有效率上的损失。

    template<typename _Tp, typename _Compare>
    _GLIBCXX14_CONSTEXPR
    inline const _Tp&
    max(const _Tp& __a, const _Tp& __b, _Compare __comp)
    {
        if (__comp(__a, __b))
            return __b;
        return __a;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    上面的这个函数我是摘自stl算法头文件的,我们可以发现,这个函数被定义在头文件中,是一个内联函数,同时也是一个模版函数。 他们为什么通常被定义在头文件中呢?

    其实前面我们已经说过了,内联函数一般是由编译器在编译阶段进行了代码段的拷贝和复制。那也就说,在编译阶段,编译器需要知道函数长什么样子。而 template 一旦被使用,编译阶段,进行具现化的时候也必须要知道模版的类型。并且 template 的具化是跟 inline 无关的。

    大部分的编译器会拒绝将一些逻辑比较复杂的函数进行内联化。现在大多数的编译器都会在编译的时候生成一条警告信息。所以有些函数,就算函数体很小,编译器也会拒绝 inline ,比如虚函数。因为虚函数是在运行的时候才知道要执行的具体代码。

    所以一个表面上看起来像是 inline 的函数并不一定是内联函数,这个主要取决于编译器。

    再看下下面这个例子:

    
    inline void func(){...}
    void (*pf)() = func;
    
    func();
    pf();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    上面的这两个函数调用,第一个在编译器阶段会被 inline,第二个不会,因为inline的过程是需要找到具体的函数进行函数体的替换,而编译器并不知道函数指针指向的具体函数。

    同样的,有时候,看起来是肯定会被inline的函数也不一定会被 inline 的。比如下面这个:

    class A
    {
    public:
        A(){};
    
    private:
        std::string m_name{ "" };
        std::string m_addr{ "" };
    }
    
    class B : public A
    {
    public:
        B(){};
    
    private:
        std::string m_school{ "" };
        std::string m_company{ "" };
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    上面类 B 的构造函数是在类的定义式中被定义的,它会被隐式的 inline 吗?

    不会的,虽然表面上看着 类 B 的构造函数没有函数体,是非常适合被 inline 的一类函数,但实际上并不是这样的。C++做了一些保证,来确保,当对象被创建和销毁的时候进行一些必要的操作,比如,派生类构造的时候,基类也会被构造,同时包括派生类及其基类的成员也会被构造。对象析构的时候恰好相反。

    所以上面的 类 B 的构造函数被执行的时候,会有一些隐形的代码被调用,而这些代码是编译器帮我们默认生成的,并不会显现的让我们看到。

    B::B()
    {
        A::A()
        {
            try{ m_name.std::string::string(); }
            catch{A::~A();}
            ....
        }
    
        try{ m_company.std::string::string(); }
        catch
        {
            m_name.std::string::~string();
            m_addr.std::string::~string();
            A::~A();
            m_school.std::string::~string();
            B::~B();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    是不是,看似一个简单的并且没有什么函数体的函数,其实在经过编译器之后,会被编译器生成很多的代码。

    除了上述的这些,内联函数还有一个问题就是,如果改变了内联函数的函数体,那么所有调用该内联函数的文件都需要被重新构建,因为内联是进行了代码的替换。

    总结一下:

    • 将那些函数体较小,计算逻辑并不复杂的函数声明为 inline 函数。
    • 函数模版的定义跟内联没有关系,因此不要因为在头文件中定义了template函数而将其声明为 inline 。
    • 表面上看起来可以内联的函数,并不一定一定会被 inline,因此在申明内联函数的时候一定要注意。
    • 通过函数指针调用的函数一定不会被编译器inlineing。

    80% 的 运行时间是花费在 20% 的代码上的,因此,我们需要的是找到那20% 的代码并进行优化。

  • 相关阅读:
    论文精度 —— 2017 ACM《Globally and Locally Consistent Image Completion》
    【操作系统】磁盘物理地址怎么表示
    [HD2006.X1] 打印图形(菱形换壳)——海淀区赛
    python爬虫:多线程收集/验证IP从而搭建有效IP代理池
    Epoxy:跨不同数据存储的 ACID 事务
    uniapp企业微信web-view父子通信问题
    中医-通过舌象判断身体状况
    现代PCB生产工艺——加成法、减成法与半加成法
    stm32工程中的DebugConfig、Listings和Objects 3个文件夹
    .NET8中的Microsoft.Extensions.Http.Resilience库
  • 原文地址:https://blog.csdn.net/tax10240809163com/article/details/125513204