代码之家  ›  专栏  ›  技术社区  ›  dtech

了解std::函数的开销并捕获同步Lambda

  •  1
  • dtech  · 技术社区  · 10 月前

    考虑以下琐碎的例子:

    #include <QCoreApplication>
    #include <QDebug>
    #include <QElapsedTimer>
    #include <QRandomGenerator>
    #include <QDateTime>
    #include <functional>
    
    inline void reader(int * d, int c, std::function<void(int *)> foo) {
      while (c--) foo(d++);
    }
    
    template <class FOO>
    inline void readerT(int * d, int c, FOO foo) {
      while (c--) foo(d++);
    }
    
    inline int summer(int * d, int c) {
      int s = 0;
      reader(d, c, [&s](int * dd){ s += *dd; });
      return s;
    }
    
    inline int summerT(int * d, int c) {
      int s = 0;
      readerT(d, c, [&s](int * dd){ s += *dd; });
      return s;
    }
    
    int main(int argc, char* argv[]) {
      QCoreApplication a(argc, argv);
      int tcount = 100000000;
    
      QVector<int> vec; vec.resize(tcount);
      QRandomGenerator gen = QRandomGenerator::securelySeeded();  
      gen.generate(vec.begin(), vec.end());
    
      QElapsedTimer t;
      t.restart();
      qDebug() << summer(vec.data(), tcount) << t.elapsed(); // 110 ms
      t.restart();
      qDebug() << summerT(vec.data(), tcount) << t.elapsed(); // 15 ms
    
      t.restart();
      int sum = 0;
      for (const auto & i : vec) { sum += i; }
      qDebug() << sum << t.elapsed(); // 15 ms
    
      return a.exec();
    }
    

    测试表明,非模板解决方案的速度要慢得多,其中模板解决方案似乎与常规循环一样快,或者零开销。

    为什么编译器无法达到与非模板版本相同的优化级别?

    注:用GCC11 O3编制

    1 回复  |  直到 10 月前
        1
  •  3
  •   Jan Schultke Luc Touraille    10 月前

    问题归根结底是 std::function 的调用机制过于复杂,因为它可以可靠地内联。 在一个有效的 std::函数 实施是:

    • 小对象优化,使较小的函数对象不会存储在堆中
    • 类型擦除,通常使用多态类
    • 函数指针的特殊情况,以避免过多的间接级别

    编译器要撤消所有这些操作,需要应用

    • 内联
    • 堆省略
    • 控制流分析/数据流分析

    ……非常完美。现代编译器在优化与函数指针有关的任何事情方面仍然相当糟糕。 你只是期望过高。

    另一方面 template 版本非常容易内联,如程序集输出中所示( https://godbolt.org/z/vvG3fnT5f ). 如果你能内联 reader ,实现了部分循环展开、自动矢量化、数据流分析和其他优化,这些优化将性能提高了一个数量级。 内衬决定成败。

    遗漏优化示例

    只是为了说明现代编译器在这个领域有多糟糕:

    int fls() { return 0; }
    int tru() { return 1; }
    
    int get(int i) {
        static constexpr int(*f[])() = { fls, tru };
        return f[i]();
    }
    

    Clang和GCC都不在此列( https://godbolt.org/z/zqjeG5xff ),并且两者都沿着以下路线发射一些东西:

    get(int):
            movsx   rdi, edi
            jmp     [QWORD PTR get(int)::f[0+rdi*8]]
    

    在这个非常琐碎的例子中,理论上的最佳值是

    get(int):
            mov eax, edi
            ret
    

    这个琐碎的例子要求太多,并且内联了 std::函数 呼叫调度要求更高。

    堆省略和函数指针内联通常不值得花费太多精力,因为您可以假设,如果开发人员正在使用函数指针和动态分配,他们可能需要这些,而您无法直接优化它们。