【问题标题】:How does modern compiler optimize function object in c++?现代编译器如何优化 C++ 中的函数对象?
【发布时间】:2020-04-21 08:19:32
【问题描述】:

正如我从《Effective C++》一书中了解到的,如果我通过 Function Object 的值而不是 C++ 中的函数引用或函数指针传递一个 Function Object,它的性能会更好。那么现代编译器如何优化这种场景呢?

或者说通常我们不建议通过值传递我们自定义类的对象,但作为函数对象实际上与普通对象相同,只是实现了“operator()”在课堂上。那么,在通过值传递这两个东西时,编译器必须有一些不同的东西,对吧?

下面是一个比较函数对象和函数指针的例子。

#include <algorithm>
#include <vector>
#include <ctime>
#include <iostream>

bool cmp(int a, int b) { return a < b; }

int main() {
    std::vector<int> v(10000000);
    for (size_t i = 0; i < 10000000; ++i)
        v.push_back(rand());
    std::vector<int> v2(v);
    std::sort(v.begin(), v.end(), std::less<int>()); // This way would be faster than below;
    std::sort(v2.begin(), v2.end(), cmp);
}

【问题讨论】:

    标签: c++ c++11


    【解决方案1】:

    在函数指针的情况下,编译器很可能会传递函数指针并执行间接函数调用,而不是进行直接函数调用甚至内联。

    相比之下,函数对象的operator()很可能是内联的,或者至少是直接调用的,因为它没有被传递,只有data给它传递(通过值或通过引用) )。在没有数据的函数对象的情况下,你什么也不传递(这将编译为一个虚拟整数,甚至什么也不传递)。

    尤其是std::function,在函数指针的情况下,从实现方面几乎没有办法避免双重间接函数调用。

    lambda 是进行这种优化的最简单方法。这是您的示例,其中有一个字符差异:

    #include <algorithm>
    #include <vector>
    #include <ctime>
    #include <iostream>
    
    int main() {
        std::vector<int> v(10000000);
        for (size_t i = 0; i < 10000000; ++i)
            v.push_back(rand());
        std::vector<int> v2(v);
        std::sort(v.begin(), v.end(), [] (int a, int b) { return a < b; }); // This way would be faster than below;
        std::sort(v2.begin(), v2.end(), +[] (int a, int b) { return a < b; });
    }
    

    在这方面,现代编译器并没有比旧编译器走得更远。尽管您可以在不同的现代编译器上尝试您的示例来确定(您可以使用https://godbolt.org/ 并检查反汇编)

    【讨论】:

      【解决方案2】:

      在 gcc 7.5 的情况下,std::sort 在内部使用 __gnu_cxx::__ops::_Iter_comp_iter 模板,如下所示:

      template<typename _Compare>
      struct _Iter_comp_iter
      {
        _Compare _M_comp;
        explicit _GLIBCXX14_CONSTEXPR
        _Iter_comp_iter(_Compare __comp) : _M_comp(_GLIBCXX_MOVE(__comp)) { }
      
        template<typename _Iterator1, typename _Iterator2>
        _GLIBCXX14_CONSTEXPR bool
        operator()(_Iterator1 __it1, _Iterator2 __it2)
        { return bool(_M_comp(*__it1, *__it2)); }
      }
      

      在第一种情况下_Comparestd::less&lt;int&gt;,在第二种情况下——bool (*)(int, int)

      在第一种情况下,gcc 内联比较,而在第二种情况下,它会生成类似 callq *%r13 的内容来调用存储在 _M_comp 中的指针。

      更新:

      在 cmets 提示的更多挖掘之后,事实证明问题不在于 _Compare 的类型——gcc 7.5 也可以用函数指针内联小的纯函数,即使没有 inline 修饰符——而是在std::sort 的内部工作中存在递归。这会使编译器关闭并生成间接调用。好消息是 gcc 8+ 似乎没有这个缺点。

      【讨论】:

      • 如果你声明 cmp() 内联怎么办?即inline bool cmp(int a, int b) { return a &lt; b; } don't function ptr 和 functor 具有相同的性能(因为 gcc 都内联)?
      • @SPD _Compare 仍然是 bool (*)(int, int) 并且 gcc 仍然生成 callq *%r13。我认为从它使用指针的那一刻起,它就拒绝了自己的选项。此外,没有 const 修饰符,因此编译器必须考虑 _M_comp 更改的可能性。
      • 在这里查看我的示例godbolt.org/z/WWFyzq:使用内联,gcc 不会为函数 ptr 生成 callq。
      • @SPD 那是因为您没有将指针存储在成员变量中而是立即使用它,因此编译器可以内联它。从上面的清单中复制 _Iter_comp_iter 并使用它,你会看到 gcc 将停止内联。
      • @SPD 嗯,实际上它仍然内联它。但在 std::sort 的情况下则不然。感谢您的意见,可以玩的东西。
      猜你喜欢
      • 2021-07-31
      • 1970-01-01
      • 1970-01-01
      • 2011-02-25
      • 1970-01-01
      • 1970-01-01
      • 2017-10-03
      • 2016-02-14
      • 2013-03-11
      相关资源
      最近更新 更多